当前位置:网站首页>Source code analysis of activityrouter
Source code analysis of activityrouter
2022-07-28 09:39:00 【Yu Qirong】
ActivityRouter :https://github.com/mzule/ActivityRouter
Header
In today's Android Component based development , A good routing framework is indispensable . For example, Alibaba's ARouter 、 US mission WMRouter etc. . The routing framework can reduce Activity The coupling between , Thus, there is no need to care about the goal Activity Implementation class of , Use the protocol to complete the jump .
ActivityRouter Usage method
stay AndroidManifest.xml To configure
<activity
android:name="com.github.mzule.activityrouter.router.RouterActivity"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mzule" /><!-- Change to your own scheme-->
</intent-filter>
</activity>Where configuration is required Activity Add comment on
@Router("main")
public class MainActivity extends Activity {
...
}Want to jump to MainActivity , Just call the following code
Routers.open(context, "mzule://main")If you want to use @Router To call methods
@Router("logout")
public static void logout(Context context, Bundle bundle) {
Toast.makeText(context, "logout", Toast.LENGTH_SHORT).show();
}The source code parsing
ActivityRouter The structure of the project is as follows
20181216160032.png
- activityrouter: The specific implementation code of route jump
- annotaition: Route annotation
- app: route demo
- app_module: route demo module
- compiler: Annotation Processing
- stub: shell module
annotation
First look at it. Router Annotations
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface Router {
String[] value();
String[] stringParams() default "";
String[] intParams() default "";
String[] longParams() default "";
String[] booleanParams() default "";
String[] shortParams() default "";
String[] floatParams() default "";
String[] doubleParams() default "";
String[] byteParams() default "";
String[] charParams() default "";
String[] transfer() default "";
}@Router Defines the Activity The name of the route and some parameters , It can be noted here that @Retention yes CLASS , So it must be used later during compilation Processor Parsing @Router Generate routing table .
in addition , notice @Target yes ElementType.TYPE and ElementType.METHOD , Actually @Router Except jump Activity outside , Another function is to execute methods , Just add @Router that will do .
The source code of routing table generation will be discussed later , Let's first look at the agreement ,Routers How to realize jump Activity Of .
activityrouter
public class Routers {
...
public static boolean open(Context context, String url) {
return open(context, Uri.parse(url));
}
public static boolean open(Context context, String url, RouterCallback callback) {
return open(context, Uri.parse(url), callback);
}
public static boolean open(Context context, Uri uri) {
return open(context, uri, getGlobalCallback(context));
}
public static boolean open(Context context, Uri uri, RouterCallback callback) {
return open(context, uri, -1, callback);
}
public static boolean openForResult(Activity activity, String url, int requestCode) {
return openForResult(activity, Uri.parse(url), requestCode);
}
public static boolean openForResult(Activity activity, String url, int requestCode, RouterCallback callback) {
return openForResult(activity, Uri.parse(url), requestCode, callback);
}
public static boolean openForResult(Activity activity, Uri uri, int requestCode) {
return openForResult(activity, uri, requestCode, getGlobalCallback(activity));
}
public static boolean openForResult(Activity activity, Uri uri, int requestCode, RouterCallback callback) {
return open(activity, uri, requestCode, callback);
}
...
} You can see different open openForResult Method overloading , Finally, it calls open(Context context, Uri uri, int requestCode, RouterCallback callback) . Then follow up :
private static boolean open(Context context, Uri uri, int requestCode, RouterCallback callback) {
boolean success = false;
// If there is callback Callback before jump
if (callback != null) {
if (callback.beforeOpen(context, uri)) {
return false;
}
}
// Perform route jump
try {
success = doOpen(context, uri, requestCode);
} catch (Throwable e) {
e.printStackTrace();
if (callback != null) {
// Wrong callback
callback.error(context, uri, e);
}
}
// Success or failure callback
if (callback != null) {
if (success) {
callback.afterOpen(context, uri);
} else {
callback.notFound(context, uri);
}
}
return success;
}open Many of the methods are in different states callback The callback , The logic of the real jump is doOpen In the method .
private static boolean doOpen(Context context, Uri uri, int requestCode) {
// If not initialized , call Router.init Initialize the routing table
initIfNeed();
// analysis uri Get the corresponding path
Path path = Path.create(uri);
// according to path To find the corresponding matching mapping , And then jump
for (Mapping mapping : mappings) {
if (mapping.match(path)) {
// If activity It's empty. , It means that the method is executed
if (mapping.getActivity() == null) {
mapping.getMethod().invoke(context, mapping.parseExtras(uri));
return true;
}
// Otherwise, it's using intent To jump activity
Intent intent = new Intent(context, mapping.getActivity());
intent.putExtras(mapping.parseExtras(uri));
intent.putExtra(KEY_RAW_URL, uri.toString());
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (requestCode >= 0) {
if (context instanceof Activity) {
((Activity) context).startActivityForResult(intent, requestCode);
} else {
throw new RuntimeException("can not startActivityForResult context " + context);
}
} else {
context.startActivity(intent);
}
return true;
}
}
return false;
} Let's analyze... Step by step doOpen The specific steps in . First from Path path = Path.create(uri); Start looking at .
public static Path create(Uri uri) {
Path path = new Path(uri.getScheme().concat("://"));
String urlPath = uri.getPath();
if (urlPath == null) {
urlPath = "";
}
if (urlPath.endsWith("/")) {
urlPath = urlPath.substring(0, urlPath.length() - 1);
}
parse(path, uri.getHost() + urlPath);
return path;
}
private static void parse(Path scheme, String s) {
String[] components = s.split("/");
Path curPath = scheme;
for (String component : components) {
Path temp = new Path(component);
curPath.next = temp;
curPath = temp;
}
}After reading the above code, some students may feel very confused , Simply explain . The main thing that the above code does is to pass in uri analysis , It generates a Path object . The Path Objects mainly contain uri Medium scheme 、host 、path These three parts , Use the characteristics of single linked list to connect these three parts . This Path It is used to match the routing table later .
Maybe some students are right uri Of scheme 、 host Wait and don't understand , Simply popularize it here .
For example, now there is a uri
mzule://main/home/login?username=tomThis uri It can be broken down into
scheme :mzule , Namely “://” The preceding string host :main ,“://” String after path :home and login All belong to path, Namely “/” And “/” String between query : Parameters , It can be understood as a key value pair , Use... Between multiple & Connect . obtain username This parameter , The corresponding value is tom
Generate good Path after , Just traverse the routing table to match .
The so-called routing table is actually a List
private static List<Mapping> mappings = new ArrayList<>();Calling RouterInit.init The routing data will be added to List in . Accurately speaking , RouterInit.init Called in Router.map Method to implement the added .
static void map(String format, Class<? extends Activity> activity, MethodInvoker method, ExtraTypes extraTypes) {
mappings.add(new Mapping(format, activity, method, extraTypes));
}that , Let's see Mapping Structure
public class Mapping {
private final String format;
private final Class<? extends Activity> activity;
private final MethodInvoker method;
private final ExtraTypes extraTypes;
private Path formatPath;
...
}- format It's what we introduced uri
- activity It is the corresponding route activity
- method Indicates whether it is an execution method
- extraTypes Is the parameter type carried
- formatPath Namely uri Corresponding Path
Concrete Mapping Initialization is in Processor Completed in the generated code , Let's talk later .
Looking back doOpen Method , stay mapping.match(path) Method is used to judge the path Is there a route matching in the route table
public boolean match(Path fullLink) {
if (formatPath.isHttp()) {
return Path.match(formatPath, fullLink);
} else {
// fullLink without host
boolean match = Path.match(formatPath.next(), fullLink.next());
if (!match && fullLink.next() != null) {
// fullLink with host
match = Path.match(formatPath.next(), fullLink.next().next());
}
return match;
}
}Mapping Of match The way is to put your own formatPath and fullLink Compare , The final call is Path.match Method , The essence is to Path Compare each item in the linked list , To judge two Path Whether it is equal or not . I won't show the specific source code here , Interested students can go back and see by themselves .
Then there is judgment activity , If it's empty , It is considered as the implementation method , Otherwise, we will construct Intent To make a jump , recycling requestCode To judge is startActivity still startActivityForResult . The execution method mainly calls MethodInvoker.invoke Method
public interface MethodInvoker {
void invoke(Context context, Bundle bundle);
}Pay more attention to mapping.parseExtras(uri) This code . The main thing to do here is to construct Bundle Pass in uri Parameters of .
public Bundle parseExtras(Uri uri) {
Bundle bundle = new Bundle();
// path segments // ignore scheme
Path p = formatPath.next();
Path y = Path.create(uri).next();
while (p != null) {
// Whether it is path Middle pass parameter
if (p.isArgument()) {
put(bundle, p.argument(), y.value());
}
p = p.next();
y = y.next();
}
// analysis uri Parameters in , Put in bundle in
Set<String> names = UriCompact.getQueryParameterNames(uri);
for (String name : names) {
String value = uri.getQueryParameter(name);
put(bundle, name, value);
}
return bundle;
}
// The main thing this method does is to judge the parameter type according to the parameter name
private void put(Bundle bundle, String name, String value) {
int type = extraTypes.getType(name);
name = extraTypes.transfer(name);
if (type == ExtraTypes.STRING) {
type = extraTypes.getType(name);
}
switch (type) {
case ExtraTypes.INT:
bundle.putInt(name, Integer.parseInt(value));
break;
case ExtraTypes.LONG:
bundle.putLong(name, Long.parseLong(value));
break;
case ExtraTypes.BOOL:
bundle.putBoolean(name, Boolean.parseBoolean(value));
break;
case ExtraTypes.SHORT:
bundle.putShort(name, Short.parseShort(value));
break;
case ExtraTypes.FLOAT:
bundle.putFloat(name, Float.parseFloat(value));
break;
case ExtraTypes.DOUBLE:
bundle.putDouble(name, Double.parseDouble(value));
break;
case ExtraTypes.BYTE:
bundle.putByte(name, Byte.parseByte(value));
break;
case ExtraTypes.CHAR:
bundle.putChar(name, value.charAt(0));
break;
default:
bundle.putString(name, value);
break;
}
}The code is simple , Basically, they are annotated , I believe everyone can understand , Don't talk .
Come here , Whole ActivityRouter That's the end of the process .
The rest , Namely Processor Parsing annotations generates code .
compiler
First tell the comments supported by the processor
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> ret = new HashSet<>();
ret.add(Modules.class.getCanonicalName());
ret.add(Module.class.getCanonicalName());
ret.add(Router.class.getCanonicalName());
return ret;
}The rest mainly depends on RouterProcessor Of process Method .
The code of the method is as follows :
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
debug("process apt with " + annotations.toString());
if (annotations.isEmpty()) {
return false;
}
boolean hasModule = false;
boolean hasModules = false;
// module
String moduleName = "RouterMapping";
Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);
if (moduleList != null && moduleList.size() > 0) {
Module annotation = moduleList.iterator().next().getAnnotation(Module.class);
moduleName = moduleName + "_" + annotation.value();
hasModule = true;
}
// modules
String[] moduleNames = null;
Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
if (modulesList != null && modulesList.size() > 0) {
Element modules = modulesList.iterator().next();
moduleNames = modules.getAnnotation(Modules.class).value();
hasModules = true;
}
// RouterInit
if (hasModules) {
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) {
debug("generate default RouterInit");
generateDefaultRouterInit();
}
// RouterMapping
return handleRouter(moduleName, roundEnv);
}process The logic in the method can be divided into three parts :
- To determine if there is @module and @modules , That is, whether it is component-based development
- Generate RouterInit
- Generate RouterMapping
Let's analyze it slowly , Let's look at the first part
// module
String moduleName = "RouterMapping";
Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);
if (moduleList != null && moduleList.size() > 0) {
Module annotation = moduleList.iterator().next().getAnnotation(Module.class);
// If it is more module Component development , Every module Label required @module , So each of them module Will generate their own RouterMapping , Prevent duplication
// such as @Module("abc") moduleName Namely RouterMapping_abc
moduleName = moduleName + "_" + annotation.value();
hasModule = true;
}
// @Modules The function of is to put the above generated RouterMapping Put it all together , Unified to RouterInit Inside , So just call RouterInit.init Method completes the route initialization of each module
String[] moduleNames = null;
Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
if (modulesList != null && modulesList.size() > 0) {
Element modules = modulesList.iterator().next();
// such as @Modules("abc","def") , moduleNames Namely [“abc”, "def"]
moduleNames = modules.getAnnotation(Modules.class).value();
hasModules = true;
}Next is generation RouterInit class
if (hasModules) {
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) {
debug("generate default RouterInit");
generateDefaultRouterInit();
}If it is more module Component development , Will eventually call generateModulesRouterInit , Otherwise, what is called is the default generateDefaultRouterInit .
Here we will see generateModulesRouterInit The code of .
private void generateModulesRouterInit(String[] moduleNames) {
MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
for (String module : moduleNames) {
initMethod.addStatement("RouterMapping_" + module + ".map()");
}
TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(initMethod.build())
.build();
try {
JavaFile.builder("com.github.mzule.activityrouter.router", routerInit)
.build()
.writeTo(filer);
} catch (Exception e) {
e.printStackTrace();
}
}You can see , Take advantage of javapoet To generate java Code , The code is simple , There is no need to talk more , Let's take a look at the final generation RouterInit Class code
package com.github.mzule.activityrouter.router;
public final class RouterInit {
public static final void init() {
RouterMapping_app.map();
RouterMapping_sdk.map();
}
}RouterInit After generation , The final job is to generate the corresponding RouterMapping_app and RouterMapping_sdk These two classes .
The generated entry is handleRouter(moduleName, roundEnv) Method .
private boolean handleRouter(String genClassName, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Router.class);
// Define methods public static final void map()
MethodSpec.Builder mapMethod = MethodSpec.methodBuilder("map")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
.addStatement("java.util.Map<String,String> transfer = null")
.addStatement("com.github.mzule.activityrouter.router.ExtraTypes extraTypes")
.addCode("\n");
// Traverse @Router Embellished element
for (Element element : elements) {
Router router = element.getAnnotation(Router.class);
// Judge @Router Is there a transfer
String[] transfer = router.transfer();
if (transfer.length > 0 && !"".equals(transfer[0])) {
mapMethod.addStatement("transfer = new java.util.HashMap<String, String>()");
for (String s : transfer) {
String[] components = s.split("=>");
if (components.length != 2) {
error("transfer `" + s + "` not match a=>b format");
break;
}
mapMethod.addStatement("transfer.put($S, $S)", components[0], components[1]);
}
} else {
mapMethod.addStatement("transfer = null");
}
// Resolve the routing parameter type
mapMethod.addStatement("extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()");
mapMethod.addStatement("extraTypes.setTransfer(transfer)");
addStatement(mapMethod, int.class, router.intParams());
addStatement(mapMethod, long.class, router.longParams());
addStatement(mapMethod, boolean.class, router.booleanParams());
addStatement(mapMethod, short.class, router.shortParams());
addStatement(mapMethod, float.class, router.floatParams());
addStatement(mapMethod, double.class, router.doubleParams());
addStatement(mapMethod, byte.class, router.byteParams());
addStatement(mapMethod, char.class, router.charParams());
// Traverse @Router Generate resolution codes for all routes
for (String format : router.value()) {
ClassName className;
Name methodName = null;
if (element.getKind() == ElementKind.CLASS) {
className = ClassName.get((TypeElement) element);
} else if (element.getKind() == ElementKind.METHOD) {
className = ClassName.get((TypeElement) element.getEnclosingElement());
methodName = element.getSimpleName();
} else {
throw new IllegalArgumentException("unknow type");
}
if (format.startsWith("/")) {
error("Router#value can not start with '/'. at [" + className + "]@Router(\"" + format + "\")");
return false;
}
if (format.endsWith("/")) {
error("Router#value can not end with '/'. at [" + className + "]@Router(\"" + format + "\")");
return false;
}
// If @Router It's a modifier class Is the route jump
if (element.getKind() == ElementKind.CLASS) {
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);
} else {
// Otherwise, it is the route calling method , The third parameter is passed in MethodInvoker object
mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, null, " +
"new MethodInvoker() {\n" +
" public void invoke(android.content.Context context, android.os.Bundle bundle) {\n" +
" $T.$N(context, bundle);\n" +
" }\n" +
"}, " +
"extraTypes)", format, className, methodName);
}
}
mapMethod.addCode("\n");
}
TypeSpec routerMapping = TypeSpec.classBuilder(genClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(mapMethod.build())
.build();
// Generate RouterMapping_xxx class
try {
JavaFile.builder("com.github.mzule.activityrouter.router", routerMapping)
.build()
.writeTo(filer);
} catch (Throwable e) {
e.printStackTrace();
}
return true;
}
// Generate extraTypes Parameter type setting code
// such as
// extraTypes.setLongExtra("id,updateTime".split(","));
// extraTypes.setBooleanExtra("web".split(","));
private void addStatement(MethodSpec.Builder mapMethod, Class typeClz, String[] args) {
String extras = join(args);
if (extras.length() > 0) {
String typeName = typeClz.getSimpleName();
String s = typeName.substring(0, 1).toUpperCase() + typeName.replaceFirst("\\w", "");
mapMethod.addStatement("extraTypes.set" + s + "Extra($S.split(\",\"))", extras);
}
}Take a look at the last generated RouterMapping_xxx Code for :
public final class RouterMapping_app {
public static final void map() {
java.util.Map<String,String> transfer = null;
com.github.mzule.activityrouter.router.ExtraTypes extraTypes;
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("user/:userId", UserActivity.class, null, extraTypes);
com.github.mzule.activityrouter.router.Routers.map("user/:nickname/city/:city/gender/:gender/age/:age", UserActivity.class, null, extraTypes);
transfer = new java.util.HashMap<String, String>();
transfer.put("web", "fromWeb");
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
extraTypes.setLongExtra("id,updateTime".split(","));
extraTypes.setBooleanExtra("web".split(","));
com.github.mzule.activityrouter.router.Routers.map("http://mzule.com/main", MainActivity.class, null, extraTypes);
com.github.mzule.activityrouter.router.Routers.map("main", MainActivity.class, null, extraTypes);
com.github.mzule.activityrouter.router.Routers.map("home", MainActivity.class, null, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("with_host", HostActivity.class, null, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("home/:homeName", HomeActivity.class, null, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("logout", null, new MethodInvoker() {
public void invoke(android.content.Context context, android.os.Bundle bundle) {
NonUIActions.logout(context, bundle);
}
}, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("upload", null, new MethodInvoker() {
public void invoke(android.content.Context context, android.os.Bundle bundle) {
NonUIActions.uploadLog(context, bundle);
}
}, extraTypes);
transfer = null;
extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
extraTypes.setTransfer(transfer);
com.github.mzule.activityrouter.router.Routers.map("user/collection", UserCollectionActivity.class, null, extraTypes);
}
}thus ,ActivityRouter All the processes have been finished !!!
RouterActivity
Yes , And a little bit more ,ActivityRouter Support external arousal Activity .
stay AndroidManifest.xml In a statement RouterActivity , Fill in corresponding scheme and host .
<activity
android:name="com.github.mzule.activityrouter.router.RouterActivity"
android:theme="@android:style/Theme.NoDisplay">
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="mzule.com" />
</intent-filter>
</activity>In fact, the first thing to arouse is RouterActivity , And then in RouterActivity According to the uri Then jump to the corresponding Activity , This can be done from RouterActivity The code of .
public class RouterActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RouterCallback callback = getRouterCallback();
Uri uri = getIntent().getData();
if (uri != null) {
Routers.open(this, uri, callback);
}
finish();
}
private RouterCallback getRouterCallback() {
if (getApplication() instanceof RouterCallbackProvider) {
return ((RouterCallbackProvider) getApplication()).provideRouterCallback();
}
return null;
}
}This is really the end of the story
That's it
It's over
La
Footer
In fact, the current routing framework is basically this routine , Knowing the profound meaning of it can make better use of it .
Interested students can go and have a look again ARouter Source code like that , I believe the harvest will be greater !
bye
边栏推荐
- ARouter源码解析(二)
- ARouter源码解析(三)
- LinkedList内部原理解析
- What is it like to use gbase C API to execute stored procedures?
- LeetCode - 哈希表专题
- 什么是跨域?如何解决请跨域问题?
- golang升级到1.18.4版本 遇到的问题
- Problems encountered in upgrading golang to version 1.18.4
- 【日志】日志干什么的?日志工厂是什么?log4j 的配置和使用? log4j.properties 文件配置、log4j jar包坐标
- LeetCode(剑指 Offer)- 50. 第一个只出现一次的字符
猜你喜欢

matlab基本操作

Final keyword and enumeration type

【广西大学】考研初试复试资料分享

Conditions and procedures of stock index futures account opening

ECCV 2022 | 无需微调即可推广!基于配准的少样本异常检测框架

Activiti startup error: cannot create poolableconnectionfactory (could not create connection to database server

JDBC连接数据库

Personal blog applet

What is cross domain? How to solve the cross domain problem?

2022 high voltage electrician examination simulated 100 questions and simulated examination
随机推荐
Oracle creates users with query permission only
Rgb-t tracking: [multimodal fusion] visible thermal UAV tracking: a large scale benchmark and new baseline
就这么一个简单的校验,80%的程序员却做不到,更不理解!
matlab基本操作
opencv安装配置测试
【高数】高数平面立体几何
Alibaba cloud server setup and pagoda panel connection
技术分享| 快对讲综合调度系统
With frequent data leakage and deletion events, how should enterprises build a security defense line?
MATLAB的符号运算
oracle 创建用户且只有查询权限
Common tool functions are constantly updated
树上启发式合并
5 operators, expressions, and statements
ShardingSphere之分库分表概念介绍(二)
MySQL 8.0.30 GA
Dn-detr paper accuracy, and analyze its model structure & 2022 CVPR paper
Inside database system distributed system
[Guangxi University] information sharing of postgraduate entrance examination and re examination
F - jealous two-dimensional reverse order pair