当前位置:首页 > 网站源码 > 正文内容

apk生成器软件(生成apk的软件)

网站源码1年前 (2023-11-05)246

本文作者

作者: xiangcman

昨晚加班睡得较晚,早上补觉中,有问题可以留言,睡醒了放出回复。

1

概述

arouter是android实现组件化的路由框架,涉及到的功能有activity、fragment的跳转、跳转带参数、自定义服务、自定义拦截器、拦截下沉、重定向url都是Arouter里面定义的功能,可能用过Arouter的小伙伴们只用过Arouter的跳转以及跳转功能带参数的功能,像它的自定义服务、拦截器、全局降级策略、重定向功能都是很不错的功能,下面我会一一介绍这些功能该怎么使用。

arouter是android实现组件化的路由框架,涉及到的功能有activity、fragment的跳转、跳转带参数、自定义服务、自定义拦截器、拦截下沉、重定向url都是Arouter里面定义的功能,可能用过Arouter的小伙伴们只用过Arouter的跳转以及跳转功能带参数的功能,像它的自定义服务、拦截器、全局降级策略、重定向功能都是很不错的功能,下面我会一一介绍这些功能该怎么使用。

目录

基础依赖

初始化

添加注解

发起路由

添加混淆

使用Gradle实现路由表自动加载

使用IDE插件通过导航的形式到目标类

基础依赖

初始化

添加注解

发起路由

添加混淆

使用Gradle实现路由表自动加载

使用IDE插件通过导航的形式到目标类

展开全文

2

使用介绍

1.基础依赖

1.1.java版本的依赖

在需要使用Arouter的module中添加如下代码:

android {

defaultConfig {

...

javaCompileOptions {

annotationProcessorOptions {

//arouter编译的时候需要的 module名字

arguments = [ AROUTER_MODULE_NAME:project.getName]

}

}

}

}

dependencies {

...

implementation 'com.alibaba:arouter-api:1.5.1'

annotationProcessor 'com.alibaba:arouter-compiler:1.5.1'

}

这里一般习惯的做法是把arouter-api的依赖放在基础服务的module里面,因为既然用到了组件化,那么肯定是所有的module都需要依赖arouter-api库的,而arouter-compiler的依赖需要放到每一个module里面。

1.2.kotlin版本的依赖

plugins {

...

id 'kotlin-kapt'

}

dependencies {

...

implementation 'com.alibaba:arouter-api:1.5.1'

kapt 'com.alibaba:arouter-compiler:1.5.1'

}

注意上面定义plugin的写法是新的androidStudio的写法了,其实kotlin的写法与java的写法就是在编译时注解的依赖形式不一样,其余的都是一样的。

2.初始化

这个很简单,在Application中初始化就可以了:

if(isDebug) { // 这两行必须写在init之前,否则这些配置在init过程中将无效

ARouter.openLog; // 打印日志

ARouter.openDebug; // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)

}

ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

这里简单提一句,当时我们项目为了做Arouter启动优化,Arouter的初始化是花了4秒多,结果把Arouter的初始化放到了欢迎页,结果在线上版本发现了有坑,在进程被kill的情况下,回到当前Activity的时候,会发现Arouter的初始化标记为false,所以思来想去,Arouter的初始化工作还是得放到Application。

因为即使进程被kill掉了,Application还是会初始化Arouter的,所以初始化工作还是得放在Application中,不过官方实现了Gradle插件路由的自动加载功能,后面会说到。

3.添加注解

3.1@Route注解

Route注解是作用在类上面,里面会携带path路径,这里列举Route注解使用的几种情况:

Route注解添加Activity的路由

@Route(path = "/login/loginActivity")

classLoginActivity: AppCompatActivity{

...

}

Route注解添加全局序列化方式

@Route(path = "/yourservicegroupname/json")

classJsonServiceImpl: SerializationService {

lateinitvargson: Gson

overridefun<T : Any?>json2Object(input: String?, clazz: Class< T>?) : T {

returngson.fromJson(input, clazz)

}

overridefuninit(context: Context?) {

gson = Gson

}

overridefunobject2Json(instance: Any?) : String {

returngson.toJson(instance)

}

overridefun<T : Any?>parseObject(input: String?, clazz: Type?) : T {

returngson.fromJson(input, clazz)

}

}

这里用了Route注解定义了SerializationService的序列化的方式,在使用withObject的时候会使用该SerializationService,后面会讲到该情况。

Route注解定义了全局降级策略

@Route(path = "/yourservicegroupname/DegradeServiceImpl")

classDegradeServiceImpl: DegradeService {

overridefunonLost(context: Context, postcard: Postcard) {

Log.d( "DegradeServiceImpl", "没有找到该路由地址: ${postcard.path}" )

}

overridefuninit(context: Context?) {

}

}

上面也是用了Route注解定义了全局降级策略,也就是在找不到的路由表的时候,做相应的处理。

Route注解实现提服务

interfaceHelloService: IProvider{

funsayHello(name: String) :String

}

// 实现接口

@Route(path = "/common/hello", name = "测试服务")

classHelloServiceImpl: HelloService {

overridefunsayHello(name: String) : String {

Log.d( "HelloServiceImpl", "hello, $name" )

return"hello, $name"

}

overridefuninit(context: Context) {}

}

这个例子是官网的写法,意思是通过Route注解实现提供服务,那怎么实现接收服务呢,下面会在另外一种注解的时候讲到。

3.2@Interceptor注解

这个注解可以说非常强大,它能拦截你的路由,什么时候让路由通过什么时候让路由不通过,完全靠该Interceptor注解可以控制。比如我有一个需求,在跳分享的时候,我想看有没有登录,如果没有登录做登录的操作,如果登了了才让分享。如果之前是不是在每一个路由的地方都得判断有没有登录,很繁琐,有了路由拦截器不用在跳转的地方判断。

@Interceptor(priority = 8, name = "登录拦截")

classLoginInterceptor: IInterceptor {

overridefunprocess(postcard: Postcard, callback: InterceptorCallback) {

valpath = postcard.path

if(path == "/share/shareActivity") {

valuserInfo = DataSource.getInstance(ArouterApplication.application).getUserInfo

if(TextUtils.isEmpty(userInfo)) {

callback.onInterrupt(Throwable( "还没有登录,去登陆"))

} else{

callback.onContinue(postcard)

}

} else{

callback.onContinue(postcard)

}

}

overridefuninit(context: Context?) {

// 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次

Log.d( "LoginInterceptor", "LoginInterceptor初始化了")

}

}

拦截器可以定义优先级,如果有多个拦截器,会依次执行拦截器。

3.3@Autowired注解

Autowired注解是定义在目标页的属性上,通常用来定义目标页接收的值,还可以定义上面说到的接收服务方:

3.3.1Autowired注解接收值

@Autowired(name = "username")

lateinitvarusername: String

@Autowired

lateinitvartestBean: TestBean

@Autowired(name = "listBean")

lateinitvarlistBean: List<TestBean>

上面定义基本类型的值接收,还有自定义bean和集合的接收。

3.3.2Autowired注解接收服务

@Autowired

lateinitvarhelloService: HelloService

@Autowired(name = "/common/hello")

lateinitvarhelloService1: HelloService

lateinitvarhelloService2: HelloService

lateinitvarhelloService3: HelloService

可以看到,上面定义了前面提供的服务,helloService可以直接指向HelloServiceImpl,如果HelloService有多个服务,那Autowired注解需要指定name路由属性,指明是哪一个服务的实例。有人好奇helloService2和helloService3没有用@Autowired注解定义服务的来源,别急,下面会提供服务的来源的:

helloService2 =

ARouter.getInstance.build( "/common/hello").navigation as HelloService

helloService3 = ARouter.getInstance.navigation(HelloService::class.java)

//使用服务

helloService.sayHello( "helloService")

helloService1.sayHello( "helloService1")

helloService2.sayHello( "helloService2")

helloService3.sayHello( "helloService3")

helloService2是通过build指定路由地址的形式,helloService3是通过navigation指定HelloService的class类也能拿到HelloServiceImpl的服务。

上面的@Autowired注解使用都得在类的初始化中使用ARouter.getInstance.inject(this),否则@Autowired注解不会被执行到

3.3预处理服务

预处理服务意思是在路由navigation之前进行干扰路由,通过实现PretreatmentService接口,比如我想干扰在分享之前判断有没有登录,如果没有登录,自行判断逻辑:

@Route(path = "/yourservicegroupname/pretreatmentService")

classPretreatmentServiceImpl: PretreatmentService {

overridefunonPretreatment(context: Context, postcard: Postcard) : Boolean{

if(postcard.path == "/share/ShareActivity") {

valuserInfo = DataSource.getInstance(ArouterApplication.application).getUserInfo

if(TextUtils.isEmpty(userInfo)) {

Toast.makeText(ArouterApplication.application, "你还没有登录", Toast.LENGTH_SHORT).show

returnfalse// 跳转前预处理,如果需要自行处理跳转,该方法返回 false 即可

}

}

returntrue

}

overridefuninit(context: Context) {}

}

其实在这个例子中我演示的拦截器功能和预处理服务功能是一样的,只不过预处理服务是早于拦截器的,等到分析源码的时候我们分析他们的具体区别。

3.4重定义URL跳转

重新定以URL的跳转

// 实现PathReplaceService接口,并加上一个Path内容任意的注解即可

@Route(path = "/yourservicegroupname/pathReplaceService") // 必须标明注解

classPathReplaceServiceImpl: PathReplaceService {

/**

* For normal path.

*

* @parampath raw path

*/

overridefunforString(path: String) : String {

if(path == "/login/loginActivity") {

return"/share/shareActivity"

}

returnpath // 按照一定的规则处理之后返回处理后的结果

}

/**

* For uri type.

*

* @paramuri raw uri

*/

overridefunforUri(uri: Uri?) : Uri? {

returnnull// 按照一定的规则处理之后返回处理后的结果

}

overridefuninit(context: Context?) {

}

}

上面我把登录的路由改成分享的路由,在实际项目中大家看看有什么适用的场景?

4.发起路由

我们先来个最简单的方式:

ARouter.getInstance.build( "/test/activity").navigation

主要是通build方法生成postCard对象,最后调用postCard的navigation方法。

传值写法:

ARouter.getInstance.build( "/test/1")

.withLong( "key1", 666L)

.withString( "key3", "888")

.withObject( "key4", newTest( "Jack", "Rose"))

.navigation

上面能用withObject方法传object是因为在上面定义了JsonServiceImpl序列化方式的路由类。withObejct还可以传集合、map等:

ARouter.getInstance.build( "/share/shareActivity").withString( "username", "zhangsan")

.withObject( "testBean", TestBean( "lisi", 20))

apk生成器软件(生成apk的软件)

.withObject(

"listBean",

listOf<TestBean>(TestBean( "wanger", 20), TestBean( "xiaoming", 20))

)

.navigation

这里注意了在路由目标类里面定义接收list、map的时候,接收对象的地方不能标注具体的实现类类型,应仅标注为list或map,否则会影响序列化中类型的判断,其他类似情况需要同样处理其他几种序列化的方式也带了,大家自行查看postCard的with** 相关方法:

跳转写法:跳转方法主要指navigation方法,其实说是跳转方法不太准确,因为它不仅仅是跳转用的,比如生成一个interceptor、service等都是通过navigation方法实现的,下一节介绍源码的时候会说到navigation有哪些具体作用

navigation主要有下面几个方法,我们说下NavigationCallback对象,一看就是个回调:

ARouter.getInstance.build( "/share/shareActivity").withString( "username", "zhangsan")

.withObject( "testBean", TestBean( "lisi", 20))

.withObject(

"listBean",

listOf<TestBean>(TestBean( "wanger", 20), TestBean( "xiaoming", 20))

)

.navigation( this, object: NavigationCallback {

overridefunonLost(postcard: Postcard?) {

}

overridefunonFound(postcard: Postcard?) {

}

overridefunonInterrupt(postcard: Postcard?) {

Log.d( "LoginActivity", "还没有登录")

}

overridefunonArrival(postcard: Postcard?) {

}

})

实现了四个方法,onLost是找不到路由,onFound是找到路由,onInterrupt表示路由挂了,默认路由设置的超时时间是300s,onArrival表示路由跳转成功的回调,目前只在startActivity的回调,这个后面源码部分会讲到。

5.混淆

混淆部分就没什么好说的了,因为Arouter是通过反射创建arouter的注解类,所以大部分需要加混淆:

-keep publicclasscom. alibaba. android. arouter. routes.** {*;}

-keep publicclasscom. alibaba. android. arouter. facade.** {*;}

-keep class* implementscom. alibaba. android. arouter. facade. template. ISyringe{*;}

# 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口

-keep interface* implementscom. alibaba. android. arouter. facade. template. IProvider

# 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现

# - keepclass* implementscom. alibaba. android. arouter. facade. template. IProvider

6.使用Gradle实现路由表自动加载

可以说这个功能虽然是选项配置,但是对于arouter启动优化有很大的作用,我们项目在没使用这个gradle自动加载路由插件的时候初始化sdk需要4秒多,用了这个插件之后基本没消耗时间。

它主要是在编译期通过gradle插装把需要依赖arouter注解的类自动扫描到arouter的map管理器里面,在下一章我们通过反编译工具查看它是怎么插装代码的,而传统的是通过扫描dex文件来过滤arouter注解类来添加到map中。

具体使用

//app的module的build.gradle

apply plugin: 'com.alibaba.arouter'

//工程的build.gradle

build {

repositories {

jcenter

}

dependencies {

classpath "com.alibaba:arouter-register:1.0.2"

}

}

7.使用IDE插件通过导航的形式到目标类

在 Android Studio 插件市场中搜索 ARouter Helper, 或者直接下载文档上方 最新版本 中列出的 arouter-idea-plugin zip 安装包手动安装,安装后 插件无任何设置,可以在跳转代码的行首找到一个图标 (navigation) 点击该图标,即可跳转到标识了代码中路径的目标类目前不支持kotlin的图标样式,大家自己尝试下java跳转。

示例代码

https://github.com/xiangcman/ArouterApp

更多文档请走这里arouter官网:

https://github.com/alibaba/ARouter

3

ARouter 源码分析

这节主要围绕Arouter源码的设计,以及通过源码我们能学习到什么,以及如何应对面试过程中Arouter的问题。

目的

源码分析

Arouter路由跳转的设计

Arouter拦截器的设计

Arouter的服务怎么设计的

Arouter的注解属性怎么获取的

Arouter自动加载路由表

思考源码的设计

源码分析

在使用Arouter过程中,我们的module依赖了arouter-api、arouter-compiler,在编译期arouter-compiler通过扫描项目中用到的Route、Autowired、Interceptor等注解来生成对应的class文件,大家如果想学习学习编译期扫描注解生成class文件可以学习apt相关的技术,或者看Arouter官网的arouter-compiler模块怎么扫描注解生成class文件的。

在使用Arouter过程中,我们的module依赖了arouter-api、arouter-compiler,在编译期arouter-compiler通过扫描项目中用到的Route、Autowired、Interceptor等注解来生成对应的class文件,大家如果想学习学习编译期扫描注解生成class文件可以学习apt相关的技术,或者看Arouter官网的arouter-compiler模块怎么扫描注解生成class文件的。

比如我在login模块的LoginActivity中定义如下的注解:

@Route(path = "/login/loginActivity")

classLoginActivity: AppCompatActivity{

}

结果在module的build目录下生成了ARouter$$Root$$loginclass文件,它是实现IRouteRoot接口:

public classARouter$$ Root$$ loginimplementsIRouteRoot{

@Override

public voidloadInto( Map< String, Class<? extendsIRouteGroup>> routes) {

routes.put( "login", ARouter$$Group$$login. class);

}

}

而此处的ARouter$$Group$$login类它是实现了IRouteGroup接口:

publicclassARouter$$ Group$$ loginimplementsIRouteGroup{

@Override

publicvoidloadInto(Map<String, RouteMeta> atlas){

atlas.put( "/login/loginActivity", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/login/loginactivity", "login", null, - 1, - 2147483648));

}

}

里面把loginActivity的信息通过RouteMeta存到了传进来的map中了。其实我们的ARouter$$Group$$组名类不只是存放了activity的RouteMeta信息,还会有IProvider类型。

如果用Interceptor注解的话,会生成对应的ARouter$$Interceptors$$模块名的class类,我们的JsonServiceImpl它最终是一个IProvider接口,还有DegradeServiceImpl类也是一样的,都会在ARouter$$Interceptors$$yourservicegroupname类中保存了一个RouteMeta信息,而定义的LoginInterceptor最终是被ARouter$$Providers$$app管理的:

publicclassARouter$$ Interceptors$$ appimplementsIInterceptorGroup{

@Override

publicvoidloadInto(Map<Integer, Class<? extends IInterceptor>> interceptors){

interceptors.put( 8, LoginInterceptor.class);

}

}

IInterceptor就没有被包装成RouteMeta对象,上面在ARouter$$Group$$yourservicegroupname中定义的IProvider信息还会在ARouter$$Providers$$app被定义了一遍:

public classARouter$$ Providers$$ appimplementsIProviderGroup{

@Override

public voidloadInto( Map< String, RouteMeta> providers) {

providers.put( "com.alibaba.android.arouter.facade.service.DegradeService", RouteMeta.build(RouteType.PROVIDER, DegradeServiceImpl. class, "/yourservicegroupname/DegradeServiceImpl", "yourservicegroupname", null, -1, -2147483648));

providers.put( "com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl. class, "/yourservicegroupname/json", "yourservicegroupname", null, -1, -2147483648));

providers.put( "com.alibaba.android.arouter.facade.service.PathReplaceService", RouteMeta.build(RouteType.PROVIDER, PathReplaceServiceImpl. class, "/yourservicegroupname/pathReplaceService", "yourservicegroupname", null, -1, -2147483648));

providers.put( "com.alibaba.android.arouter.facade.service.PretreatmentService", RouteMeta.build(RouteType.PROVIDER, PretreatmentServiceImpl. class, "/yourservicegroupname/pretreatmentService", "yourservicegroupname", null, -1, -2147483648));

}

}

其实都是我们在app模块中定义的IProvider类型的service,那为什么在ARouter$$Group$$组名定义了IProvider类型,那为什么还需要在ARouter$$Providers$$模块名中还要定义一遍呢,看官莫急,听我细细道来。

小节

所以在apt阶段,生成的class类有Arouter$$Root$$模块名,模块名是在gradle中配置的arg("AROUTER_MODULE_NAME", "${project.getName}")属性,把所有组的信息放到传进来的map中,这个组是通过我们在Route注解的path属性拆分的,比如定义/login/loginActivity,会认为组名是login,Arouter$$Root$$组名放的是该组下,所有的路由表信息,包括route、provider注解通过RouteMeta包装对应的class类信息,provider注解会放在Arouter$$Providers$$模块名下面。

所以在apt阶段,生成的class类有Arouter$$Root$$模块名,模块名是在gradle中配置的arg("AROUTER_MODULE_NAME", "${project.getName}")属性,把所有组的信息放到传进来的map中,这个组是通过我们在Route注解的path属性拆分的,比如定义/login/loginActivity,会认为组名是login,Arouter$$Root$$组名放的是该组下,所有的路由表信息,包括route、provider注解通过RouteMeta包装对应的class类信息,provider注解会放在Arouter$$Providers$$模块名下面。

初始化

我们在上一面使用部分,初始化部分代码如下:

ARouter.init( this)

就一句,那下面追随源码看下:

publicstaticvoidinit(Application application){

if(!hasInit) {

logger = _ARouter.logger;

_ARouter.logger.info(Consts.TAG, "ARouter init start.");

hasInit = _ARouter.init(application);

if(hasInit) {

_ARouter.afterInit;

}

_ARouter.logger.info(Consts.TAG, "ARouter init over.");

}

}

很简单的几句,我们主要_Arouter.init方法以及_ARouter.afterInit方法,其实我们的入口虽然是ARouter类的,但是真正调用的还是_ARouter类的方法,废话不多说,直接看_ARouter.init方法:

_ARouter.init

protectedstaticsynchronizedbooleaninit(Application application){

mContext = application;

LogisticsCenter.init(mContext, executor);

logger.info(Consts.TAG, "ARouter init success!");

hasInit = true;

mHandler = newHandler(Looper.getMainLooper);

returntrue;

}

最终还是调用了LogisticsCenter的init方法,并且拿到主线的looper给了mHandler,顺着看LogisticsCenter的init方法:

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {

mContext = context;

executor = tpe;

try{

//如果通过插件获取路由表信息,则该方法registerByPlugin= false

loadRouterMap;

if(registerByPlugin) {

} else{

Set<String> routerMap;

//如果是debuggable= true或者有新的版本

if(ARouter.debuggable || PackageUtils.isNewVersion(context)) {

logger.info(TAG, "Run with debug mode or new install, rebuild router map.");

//加载前缀为com.alibaba.android.arouter.routes的 class类,放到 set集合里面

routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

//将过滤到的 class类放到 sp中,方便下次直接取

if(!routerMap.isEmpty) {

context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit.putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply;

}

//更新下最新的版本

PackageUtils.updateVersion(context); //Save newversion name whenrouter map update finishes.

} else{

//直接从sp中取过滤到的 class类

routerMap = newHashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, newHashSet<String>));

}

for(String className : routerMap) {

if(className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {

//将com.alibaba.android.arouter.routes.ARouter$$Root前缀的 class类放到 Warehouse. groupsIndex中

((IRouteRoot) (Class.forName(className).getConstructor.newInstance)).loadInto(Warehouse.groupsIndex);

} elseif(className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {

//将com.alibaba.android.arouter.routes.ARouter$$Interceptors前缀的 class类放到 Warehouse. interceptorsIndex中

((IInterceptorGroup) (Class.forName(className).getConstructor.newInstance)).loadInto(Warehouse.interceptorsIndex);

} elseif(className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {

//将com.alibaba.android.arouter.routes.ARouter$$Providers前缀的 class类放到 Warehouse. providersIndex中

((IProviderGroup) (Class.forName(className).getConstructor.newInstance)).loadInto(Warehouse.providersIndex);

}

}

}

} catch(Exception e) {

thrownewHandlerException(TAG + "ARouter init logistics center exception! ["+ e.getMessage + "]");

}

}

在上面init方法中做了几件事:

通过loadRouterMap方法判断是不是通过arouter-register自动加载路由表,如果是通过自动加载的则registerByPlugin=true,这里我们先不管通过arouter-register自动加载的方式,

紧接着通过ClassUtils.getFileNameByPackageName(此处用到了线程池、CountDownLatch面试高频考点)获取到apk中前缀为com.alibaba.android.arouter.routes的类,这里面主要是通过判断是不是支持MultiDex,如果不支持MultiDex,扫描所有的dex文件,然后压缩成zip文件,然后通过DexFile.loadDex转化成DexFile对象,如果支持MultiDex,直接new DexFile,然后循环DexFile拿里面的class文件,然后过滤出com.alibaba.android.arouter.routes前缀的class并返回。

拿到了需要的class类后,放到sp里面,方便下次不去扫描apk拿class,更新版本号

将com.alibaba.android.arouter.routes.ARouter$$Root前缀的class类放到Warehouse.groupsIndex中

将com.alibaba.android.arouter.routes.ARouter$$Interceptors前缀的class类放到Warehouse.interceptorsIndex中

将com.alibaba.android.arouter.routes.ARouter$$Providers前缀的class类放到Warehouse.providersIndex中

_ARouter.afterInit

该方法里面会去拿InterceptorServiceImpl,怎么去拿的,以及里面做了些啥,我们一一来看:

interceptorService= (InterceptorService) ARouter.getInstance.build( "/arouter/service/interceptor").navigation;

ARouter.build

ARouter.getInstance.build("/arouter/service/interceptor")会返回PostCard对象,并给PostCard的group和path属性赋值为arouter、/arouter/service/interceptor:

protected Postcard build(String path) {

if(TextUtils.isEmpty( path)) {

throw new HandlerException(Consts.TAG + "Parameter is invalid!");

} else{

PathReplaceService pService = ARouter.getInstance.navigation(PathReplaceService.class);

if(null != pService) {

path= pService.forString( path);

}

returnbuild( path, extractGroup( path), true);

}

}

先判断有没有设置PathReplaceService类型的路由,如果有会调用forString方法返回新的path,这也就是我们的path替换的IProvider,在第一篇文章我们定义过PathReplaceServiceImpl类:

@Route(path = "/yourservicegroupname/pathReplaceService") // 必须标明注解

classPathReplaceServiceImpl: PathReplaceService {

overridefunforString(path: String) : String {

if(path == "/login/loginActivity") {

return"/share/shareActivity"

}

returnpath // 按照一定的规则处理之后返回处理后的结果

}

overridefunforUri(uri: Uri?) : Uri? {

returnnull// 按照一定的规则处理之后返回处理后的结果

}

overridefuninit(context: Context?) {

}

}

如果有该PathReplaceService会替换掉我们的/login/loginActivity为/share/shareActivity,这里先不管怎么拿到PathReplaceServiceImpl,通过extractGroup方法拿到group:

private StringextractGroup( Stringpath) {

if(TextUtils.isEmpty(path) || !path.startsWith( "/")) {

thrownewHandlerException(Consts.TAG + "Extract the default group failed, the path must be start with '/' and contain more than 2 '/'!");

}

try{

StringdefaultGroup = path.substring( 1, path.indexOf( "/", 1));

if(TextUtils.isEmpty(defaultGroup)) {

thrownewHandlerException(Consts.TAG + "Extract the default group failed! There's nothing between 2 '/'!");

} else{

returndefaultGroup;

}

} catch(Exception e) {

logger.warning(Consts.TAG, "Failed to extract default group! "+ e.getMessage);

returnnull;

}

}

很简单,按照第一个'/'和第二个'/'的字符作为group信息,所以这也是为什么我们在定义path的时候需要两级的path目录。最终会走另一个重载的build方法:

protectedPostcard build( String path, String group, Boolean afterReplace ) {

if(TextUtils.isEmpty(path) || TextUtils.isEmpty( group)) {

thrownewHandlerException(Consts.TAG + "Parameter is invalid!");

} else{

if(!afterReplace) {

PathReplaceService pService = ARouter.getInstance.navigation(PathReplaceService.class);

if( null!= pService) {

path = pService.forString(path);

}

}

returnnewPostcard(path, group);

}

}

所以build过程把传过来的path构造出Postcard对象,给path和group赋值。

PostCard.navigation

navigation有很多重载的方法,最终都会走_Arouter.navigation,其中navigation里面也有两种形式获取到路由表类,我们先介绍activity常规的形式:

protectedObject navigation( finalContext context, finalPostcard postcard, finalint requestCode, finalNavigationCallback callback) {

//这也是个路由表,通过另外一种形式获取PretreatmentService的实例

PretreatmentService pretreatmentService = ARouter.getInstance.navigation(PretreatmentService. class);

//如果onPretreatment返回false就是自己处理路由逻辑,不往下走了

if( null!= pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {

returnnull;

}

try{

//最终会走这里

LogisticsCenter.completion(postcard);

} catch(NoRouteFoundException ex) {

logger.warning(Consts.TAG, ex.getMessage);

...

if( null!= callback) {

callback.onLost(postcard);

} else{

// 获取DegradeService的实例

DegradeService degradeService = ARouter.getInstance.navigation(DegradeService. class);

if( null!= degradeService) {

degradeService.onLost(context, postcard);

}

}

returnnull;

}

//省略provider部分逻辑和_navigation部分代码

returnnull;

}

由于navigation代码比较长,我把代码分块来说,上面首先获取PretreatmentService类型的路由表,我们先只说上面传入/arouter/service/interceptor,怎么得到InterceptorService实例的,我们直接看LogisticsCenter.completion:

public synchronized staticvoidcompletion(Postcard postcard) {

//第一次进来是拿不到RouteMeta信息的,因为routes是空的

RouteMeta routeMeta = Warehouse.routes. get(postcard.getPath);

if( null== routeMeta) {

//我们传过来的postcard的group是arouter、path是/arouter/service/interceptor

//我们在groupIndex中找对应的groupMeta,其实看到这的时候,我们默认是没有root为arouter的组,只能去arouter默认提供的root中找

Class<? extendsIRouteGroup> groupMeta = Warehouse.groupsIndex. get(postcard.getGroup); // Load route meta.

if( null== groupMeta) {

} else{

try{

//反射拿到ARouter$$Group$$arouter

IRouteGroup iGroupInstance = groupMeta.getConstructor.newInstance;

//所以最终把InterceptorServiceImpl放到了Warehouse.routes中

iGroupInstance.loadInto(Warehouse.routes);

//用完groupsIndex对应的IRouteGroup信息后,从map中移除掉,下次就直接从routes中去拿了

Warehouse.groupsIndex.remove(postcard.getGroup);

} catch(Exception e) {

thrownewHandlerException(TAG + "Fatal exception when loading group meta. ["+ e.getMessage + "]");

}

//继续走一遍completion,下次会走下面的else

completion(postcard);

}

} else{

//对postCard属性赋值

postcard.setDestination(routeMeta.getDestination);

postcard.setType(routeMeta.getType);

postcard.setPriority(routeMeta.getPriority);

postcard.setExtra(routeMeta.getExtra);

UrirawUri = postcard.getUri;

//默认uri为空

if( null!= rawUri) {

Map< String, String> resultMap = TextUtils.splitQueryParameters(rawUri);

Map< String, Integer> paramsType = routeMeta.getParamsType;

if(MapUtils.isNotEmpty(paramsType)) {

for( Map.Entry< String, Integer> params : paramsType.entrySet) {

setValue(postcard,

params.getValue,

params.getKey,

resultMap. get(params.getKey));

}

// Save params name which need auto inject.

postcard.getExtras.putStringArray(ARouter.AUTO_INJECT, paramsType.keySet.toArray( newString[]{}));

}

// Save raw uri

postcard.withString(ARouter.RAW_URI, rawUri.toString);

}

switch(routeMeta.getType) {

//由于InterceptorServiceImpl是provider类型的

casePROVIDER:

Class<? extendsIProvider> providerMeta = (Class<? extendsIProvider>) routeMeta.getDestination;

//拿对应的provider

IProvider instance = Warehouse.providers. get(providerMeta);

if( null== instance) {

IProvider provider;

try{

//反射创建InterceptorServiceImpl

provider = providerMeta.getConstructor.newInstance;

//调用InterceptorServiceImpl的init方法

provider.init(mContext);

Warehouse.providers.put(providerMeta, provider);

instance = provider;

} catch(Exception e) {

thrownewHandlerException( "Init provider failed! "+ e.getMessage);

}

}

//给postcard赋值

postcard.setProvider(instance);

postcard.greenChannel;

break;

caseFRAGMENT:

postcard.greenChannel;

default:

break;

}

}

}

上面代码还是很清晰的,首先从Warehouse.routes去拿对应的RouteMeta信息,如果没有,就先去Warehouse.groupsIndex中拿,而此时的postCard的group是arouter,我们从下面图看下,拿到的是ARouter$$Group$$arouter的class:

所以此时的iGroupInstance是ARouter$$Group$$arouter,通过反射创建ARouter$$Group$$arouter,紧接着把Warehouse.routes传进它的loadInto方法:

所以我们最终能确定把AutowiredServiceImpl和InterceptorServiceImpl的RouteMeta放进了Warehouse.routes的map中。

注意:上面用完了Warehouse.groupsIndex中对应的group信息后,会从Warehouse.groupsIndex中移除该group的信息。

最后又走一遍completion方法,所以会走else分支,由于我们还在获取InterceptorServiceImpl过程中,它的RouteType=PROVIDER,所以providerMeta是InterceptorServiceImpl类型的,然后去Warehouse.providers拿,此时是空的,所以通过反射创建InterceptorServiceImpl对象,创建完调用InterceptorServiceImpl调用init方法:

@Override

publicvoidinit( finalContext context) {

LogisticsCenter.executor.execute( newRunnable {

@Override

publicvoidrun{

if(MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {

//遍历我们自己代码里面定义的interceptorsIndex

for(Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet) {

Class<? extends IInterceptor> interceptorClass = entry.getValue;

try{

//拿到对应的iInterceptor后

IInterceptor iInterceptor = interceptorClass.getConstructor.newInstance;

//调用init

iInterceptor.init(context);

//把iInterceptor放到interceptors中

Warehouse.interceptors.add(iInterceptor);

} catch(Exception ex) {

thrownewHandlerException(TAG + "ARouter init interceptor error! name = ["+ interceptorClass.getName + "], reason = ["+ ex.getMessage + "]");

}

}

interceptorHasInit = true;

logger.info(TAG, "ARouter interceptors init over.");

synchronized(interceptorInitLock) {

interceptorInitLock.notifyAll;

}

}

}

});

}

上面获取interceptorsIndex,还记得上一节我们定义的LoginInterceptor吗?它会放到interceptorsIndex的map里面,所以这里是拿到所有的IInterceptor,反射创建每一个IInterceptor,调用init方法,添加到Warehouse.interceptors中。

到这里,我们已经清楚了Warehouse.routes存放了AutowiredServiceImpl和InterceptorServiceImpl类型的RouteMeta,Warehouse.providers存放了InterceptorServiceImpl,Warehouse.interceptors存放了所有的IInterceptor。

到这里,我们已经清楚了Warehouse.routes存放了AutowiredServiceImpl和InterceptorServiceImpl类型的RouteMeta,Warehouse.providers存放了InterceptorServiceImpl,Warehouse.interceptors存放了所有的IInterceptor。

创建完InterceptorServiceImpl,我们一下子要回到_ARouter的navigation方法的下半部分代码,上面没有贴出代码:

//如果是`InterceptorServiceImpl`类型的postcard.isGreenChannel是true,除非是activity或fragment类型的

if(!postcard.isGreenChannel) {

interceptorService.doInterceptions(postcard, newInterceptorCallback {

@Override

publicvoidonContinue(Postcard postcard){

_navigation(context, postcard, requestCode, callback);

}

@Override

publicvoidonInterrupt(Throwable exception){

if( null!= callback) {

callback.onInterrupt(postcard);

}

logger.info(Consts.TAG, "Navigation failed, termination by interceptor : "+ exception.getMessage);

}

});

} else{

return_navigation(context, postcard, requestCode, callback);

}

所以上面代码我们直接看_navigation方法:

privateObject _navigation( finalContext context, finalPostcard postcard, finalintrequestCode, finalNavigationCallback callback) {

finalContext currentContext = null== context ? mContext : context;

switch(postcard.getType) {

casePROVIDER:

returnpostcard.getProvider;

}

returnnull;

}

这里先把其他类型给省略掉了,如果是PROVIDER类型的,直接把postCard的provider直接返回,所以我们上面怎么拿到的InterceptorServiceImpl就迎刃而解了。

PostCard. T navigation(Class<? extends T> service)

上面分析的是通过path获取到路由表的实例,还有另外通过传进来的class类型也可以获取,我们就拿上一节用到的JsonServiceImpl怎么拿到的,在PostCard中可以通过withObject传值,其实归功于JsonServiceImpl,可以看下面代码:

publicPostcard withObject( @NullableString key, @NullableObject value) {

serializationService = ARouter.getInstance.navigation(SerializationService. class);

mBundle.putString(key, serializationService.object2Json(value));

returnthis;

}

所以直接看_Arouter.navigation(Class<? extends T> service) 方法:

protected<T> T navigation( Class<? extendsT> service) {

try{

Postcard postcard = LogisticsCenter.buildProvider(service.getName);

if( null== postcard) {

// No service, or this service in old version.

postcard = LogisticsCenter.buildProvider(service.getSimpleName);

}

if( null== postcard) {

returnnull;

}

LogisticsCenter.completion(postcard);

return(T) postcard.getProvider;

} catch(NoRouteFoundException ex) {

logger.warning(Consts.TAG, ex.getMessage);

returnnull;

}

}

直接看LogisticsCenter.buildProvider方法:

publicstaticPostcard buildProvider( String serviceName) {

RouteMeta meta = Warehouse.providersIndex. get(serviceName);

if( null== meta) {

returnnull;

} else{

returnnewPostcard(meta.getPath, meta.getGroup);

}

}

此处是从providersIndex中去拿对应的RouteMeta信息,而providersIndex是在初始化sdk中通过加载ARouter$$Providers$$模块名的loadInto方法,好吧,为了大家能顺着阅读,我把上面的代码重新复制了一份:

public classARouter$$ Providers$$ appimplementsIProviderGroup{

@Override

public voidloadInto( Map< String, RouteMeta> providers) {

providers.put( "com.alibaba.android.arouter.facade.service.DegradeService", RouteMeta.build(RouteType.PROVIDER, DegradeServiceImpl. class, "/yourservicegroupname/DegradeServiceImpl", "yourservicegroupname", null, -1, -2147483648));

providers.put( "com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl. class, "/yourservicegroupname/json", "yourservicegroupname", null, -1, -2147483648));

providers.put( "com.alibaba.android.arouter.facade.service.PathReplaceService", RouteMeta.build(RouteType.PROVIDER, PathReplaceServiceImpl. class, "/yourservicegroupname/pathReplaceService", "yourservicegroupname", null, -1, -2147483648));

providers.put( "com.alibaba.android.arouter.facade.service.PretreatmentService", RouteMeta.build(RouteType.PROVIDER, PretreatmentServiceImpl. class, "/yourservicegroupname/pretreatmentService", "yourservicegroupname", null, -1, -2147483648));

}

}

看到了没,SerializationService作为的key时候,指向的是JsonServiceImpl的RouteMeta,所以在navigation方法传class对象的时候,是在providersIndex中先去找,没找到,最终通过navigation传path去找,这也是为什么在ARouter$$Group$$组名和ARouter$$Providers$$模块名中都定义了provider的routeMeta信息,一种通过path来找provider,一种通过class来找provider。

4

Arouter路由跳转的设计

由于activity路由的postCard的isGreenChannel为false,因此在_Arouter.navigation方法中会走如下代码:

上面已经分析了interceptorService是interceptorServiceImpl,因此看doInterceptions方法:

@Override

publicvoiddoInterceptions( finalPostcard postcard, finalInterceptorCallback callback) {

//省略了拦截器的流程,直接看callback.onContinue

callback.onContinue(postcard);

}

如果默认没有拦截器,直接会走callback.onContinue,回到_Arouter的navigation,会走_navigation方法:

privateObject _navigation( finalContext context, finalPostcard postcard, finalintrequestCode, finalNavigationCallback callback) {

finalContext currentContext = null== context ? mContext : context;

switch(postcard.getType) {

caseACTIVITY:

// Build intent

finalIntent intent = newIntent(currentContext, postcard.getDestination);

runInMainThread( newRunnable {

@Override

publicvoidrun{

startActivity(requestCode, currentContext, intent, postcard, callback);

}

});

break;

}

returnnull;

}

所以最终也是通过routeMeta的destination作为目标activity的class跳转。

所以activity最终跳转也是先获取routeMeta,最终完成跳转。

5

Arouter拦截器的设计

这个我们直接看InterceptorServiceImpl的doInterceptions方法:

@Override

publicvoiddoInterceptions( finalPostcard postcard, finalInterceptorCallback callback) {

//如果有interceptors

if( null!= Warehouse.interceptors && Warehouse.interceptors.size > 0) {

//判断拦截器有没有初始化成功

checkInterceptorsInitStatus;

if(!interceptorHasInit) {

callback.onInterrupt( newHandlerException( "Interceptors initialization takes too much time."));

return;

}

LogisticsCenter.executor.execute( newRunnable {

@Override

publicvoidrun{

CancelableCountDownLatch interceptorCounter = newCancelableCountDownLatch(Warehouse.interceptors.size);

try{

_execute( 0, interceptorCounter, postcard);

//拦截器如果超时会回调callback.onInterrupt

interceptorCounter.await(postcard.getTimeout, TimeUnit.SECONDS);

if(interceptorCounter.getCount > 0) {

callback.onInterrupt( newHandlerException( "The interceptor processing timed out."));

} elseif( null!= postcard.getTag) {

callback.onInterrupt( newHandlerException(postcard.getTag.toString));

} else{

callback.onContinue(postcard);

}

} catch(Exception e) {

callback.onInterrupt(e);

}

}

});

}

}

创建CancelableCountDownLatch作为所有拦截器处理完成的标志,处理IInterceptor通过触发process方法,如果想继续处理下一个拦截通过触发InterceptorCallback的onContinue方法,如果想拦截掉路由的处理,通过触发InterceptorCallback的onInterrupt方法。如果所有的拦截都不拦截,则会回调到doInterceptions方法的callback.onContinue(postcard);这一句,而这一句最终回到了_Arouter._navigation方法,走正常的路由处理了。

6

Arouter的服务怎么设计的

上一节我们通过定义了HelloServiceImpl的服务:

@Route(path = "/common/hello", name = "测试服务")

classHelloServiceImpl: HelloService {

overridefunsayHello(name: String) : String {

Log.d( "HelloServiceImpl", "hello, $name" )

return"hello, $name"

}

overridefuninit(context: Context) {}

}

然后我们可以通过传服务的path或class类都可以获取:

helloService2=ARouter.getInstance.build( "/common/hello").navigation as HelloService

helloService3= ARouter.getInstance.navigation(HelloService::class.java)

这也就是我们上面说的Iprovider可以通过两种形式获取,他们分别定义在了ARouter$$Group$$组名、ARouter$$Providers$$模块名中。

7

Arouter的注解属性怎么获取的

我们在定义属性的时候通过Autowired注解赋值,比如我上一节在shareActivity中定义如下属性:

@Autowired(name = "username")

lateinitvarusername: String

@Autowired

lateinitvartestBean: TestBean

@Autowired(name = "listBean")

lateinitvarlistBean: List<TestBean>

会在build下生成如下代码:

publicclassShareActivity$$ ARouter$$ AutowiredimplementsISyringe{

privateSerializationService serializationService;

@Override

publicvoidinject(Object target){

serializationService = ARouter.getInstance.navigation(SerializationService.class);

ShareActivity substitute = (ShareActivity)target;

substitute.username = substitute.getIntent.getExtras == null? substitute.username : substitute.getIntent.getExtras.getString( "username", substitute.username);

if( null!= serializationService) {

substitute.testBean = serializationService.parseObject(substitute.getIntent.getStringExtra( "testBean"), newcom.alibaba.android.arouter.facade.model.TypeWrapper<TestBean>{}.getType);

} else{

}

if( null!= serializationService) {

substitute.listBean = serializationService.parseObject(substitute.getIntent.getStringExtra( "listBean"), newcom.alibaba.android.arouter.facade.model.TypeWrapper<List<TestBean>>{}.getType);

} else{

}

}

}

可以看到在inject方法中,获取了基本类型和对象类型,如果是基本类型,直接给属性赋值,如果是对象类型,先获取SerializationService对应的实例,所以我们想定义对象类型的属性,需要实例化SerializationService类型,那什么时候调用的inject方法,在定义属性的类中有这么一句:

ARouter.getInstance.inject( this)

最终到_Arouter.inject方法:

staticvoidinject( Object thiz) {

AutowiredService autowiredService = ((AutowiredService) ARouter.getInstance.build( "/arouter/service/autowired").navigation);

if( null!= autowiredService) {

autowiredService.autowire(thiz);

}

}

这里拿的AutowiredService是AutowiredServiceImpl,这跟InterceptorServiceImpl获取方式是一样的,这里就不介绍了,直接看AutowiredServiceImpl的doInject方法:

privatevoiddoInject( Object instance, Class<?> parent) {

Class<?> clazz = null== parent ? instance.getClass : parent;

ISyringe syringe = getSyringe(clazz);

//上面就拿到了ShareActivity$$ARouter$$Autowired类,调用inject方法

if( null!= syringe) {

syringe.inject(instance);

}

Class<?> superClazz = clazz.getSuperclass;

if( null!= superClazz && !superClazz.getName.startsWith( "android")) {

doInject(instance, superClazz);

}

}

最终在这里调用了ShareActivity$$ARouter$$Autowired的inject方法。

8

Arouter自动加载路由表

自动加载路由表是通过arouter-register来实现的,主要通过在编译期给LogisticsCenter的loadRouterMap方法插入register方法调用的代码:

我们可以通过反编译工具查看下apk下面的该类:

反编译出来的代码确实在loadRouterMap方法处插入了register方法调用的代码,并且把registerByPlugin置为true,所以最终不会通过扫描dex文件来加载路由表类装载到map中。

那大家想想和普通的通过扫描dex文件加载class有什么区别呢,我们在上面普通加载dex文件可以看到在初始化Arouter sdk的时候是非常慢的,因为它得扫描dex文件,然后加载dex文件里面的所有class,然后过滤出arouter需要的class文件,这还不是算慢得,如果虚拟机不支持MultiDex还会更慢,它会通过压缩所有的dex文件,然后压缩成zip,然后通过DexFile.loadDex转话成dex文件的集合,我们知道在DexFile.loadDex过程中会把普通的dex文件抓话成odex,这个过程是很慢的,关于dex文件转成odex做了些啥大家可以查查,具体我也不是很清楚,哈哈哈。最后通过遍历dex文件,拿到里面的class文件,最后过滤拿到Arouter需要的class,在我们项目中亲测arouter普通初始化是花了4秒多,所以我们可以看到Arouter自动加载路由表的插件对启动优化还是有很大改善的,Arouter自动加载路由表的插件是使用的通过gradle插装技术在编译期插入代码来达到自动加载路由表信息。

那大家想想和普通的通过扫描dex文件加载class有什么区别呢,我们在上面普通加载dex文件可以看到在初始化Arouter sdk的时候是非常慢的,因为它得扫描dex文件,然后加载dex文件里面的所有class,然后过滤出arouter需要的class文件,这还不是算慢得,如果虚拟机不支持MultiDex还会更慢,它会通过压缩所有的dex文件,然后压缩成zip,然后通过DexFile.loadDex转话成dex文件的集合,我们知道在DexFile.loadDex过程中会把普通的dex文件抓话成odex,这个过程是很慢的,关于dex文件转成odex做了些啥大家可以查查,具体我也不是很清楚,哈哈哈。最后通过遍历dex文件,拿到里面的class文件,最后过滤拿到Arouter需要的class,在我们项目中亲测arouter普通初始化是花了4秒多,所以我们可以看到Arouter自动加载路由表的插件对启动优化还是有很大改善的,Arouter自动加载路由表的插件是使用的通过gradle插装技术在编译期插入代码来达到自动加载路由表信息。

关于字节码插装技术我也不是很懂,所以我也会去恶补相关知识去了。

源码总结

在初始化阶段把所有的root、interceptor、provider信息分别存储到groupsIndex、interceptorsIndex、providersIndex中

然后初始化interceptorServiceImpl实例,顺便将interceptor加入到interceptors中。

在加载路由的时候,先去routes中取,如果没取到则去groupsIndex中拿group信息,再拿对应的metaMeta信息,将属性封装到postCard中,最后通过判断metaType是那种类型,做相应类型的处理。

navigation有两种形式获取Iprovider,通过传class或path,如果传的是class则去interceptorsIndex找对应的Iprovder,如果是path则去groupsIndex中找,找到后,最终会保存在providers中。

源码总结

在初始化阶段把所有的root、interceptor、provider信息分别存储到groupsIndex、interceptorsIndex、providersIndex中

然后初始化interceptorServiceImpl实例,顺便将interceptor加入到interceptors中。

在加载路由的时候,先去routes中取,如果没取到则去groupsIndex中拿group信息,再拿对应的metaMeta信息,将属性封装到postCard中,最后通过判断metaType是那种类型,做相应类型的处理。

navigation有两种形式获取Iprovider,通过传class或path,如果传的是class则去interceptorsIndex找对应的Iprovder,如果是path则去groupsIndex中找,找到后,最终会保存在providers中。

扫描二维码推送至手机访问。

版权声明:本文由我的模板布,如需转载请注明出处。


本文链接:http://sdjcht.com/post/38149.html

分享给朋友:

“apk生成器软件(生成apk的软件)” 的相关文章

女生房间装修设计图片大全(女生房间装修设计图片大全农村)

女生房间装修设计图片大全(女生房间装修设计图片大全农村)

今天给各位分享女生房间装修设计图片大全的知识,其中也会对女生房间装修设计图片大全农村进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!本文目录一览: 1、女生的房间设计简约 2...

17173游戏交易平台代理(17173手游交易平台)

17173游戏交易平台代理(17173手游交易平台)

本篇文章给大家谈谈17173游戏交易平台代理,以及17173手游交易平台对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。 本文目录一览: 1、17173淘金城网络游戏交易平台,交易安全吗?他会人...

怎么查看国外网站平台访问量(怎么查看国外网站平台访问量多少)

怎么查看国外网站平台访问量(怎么查看国外网站平台访问量多少)

本篇文章给大家谈谈怎么查看国外网站平台访问量,以及怎么查看国外网站平台访问量多少对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。 本文目录一览: 1、怎么查一个网站的ip访问数? 2、如何查...

香港虚拟资产交易平台(香港虚拟资产交易平台牌照)

香港虚拟资产交易平台(香港虚拟资产交易平台牌照)

本篇文章给大家谈谈香港虚拟资产交易平台,以及香港虚拟资产交易平台牌照对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。 本文目录一览: 1、解密加密货币第一股Coinbase:风险、机遇和未来展望...

游戏多源码网(游戏源码网站)

游戏多源码网(游戏源码网站)

本篇文章给大家谈谈游戏多源码网,以及游戏源码网站对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。 本文目录一览: 1、求一个游戏官方网站模版源码 2、一直想做个属于自己的网站,但是网上倒卖游...

虚拟资源网站源码(虚拟交易网站源码)

虚拟资源网站源码(虚拟交易网站源码)

本篇文章给大家谈谈虚拟资源网站源码,以及虚拟交易网站源码对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。 本文目录一览: 1、如何修改上传到虚拟空间的网站源码? 2、购买一个网站一般给源码不...