阿里ARouter使用及源碼解析(一)


 

在app的開發中,頁面之間的相互跳轉是最基本常用的功能。在Android中的跳轉一般通過顯式intent和隱式intent兩種方式實現的,而Android的原生跳轉方式會存在一些缺點:

  • 顯式intent的實現方式,因為會存在直接的類依賴的問題,導致耦合嚴重;
  • 隱式intent的實現方式,則會出現規則集中式管理,導致協作變得困難;
  • 可配置性較差,一般而言配置規則都是在Manifest中的,這就導致了擴展性較差;
  • 跳轉過程無法控制,一旦使用了StartActivity()就無法插手其中任何環節了,只能交給系統管理;
  • 當多組件化開發,使用原生的路由方式很難實現完全解耦;

而阿里的ARouter路由框架具有解耦、簡單易用、支持多模塊項目、定制性較強、支持攔截邏輯等諸多優點,很好的解決了上述的問題。關於ARouter具體實現功能,典型應用以及相應技術方案實現的介紹不在這詳細介紹,具體可參見開源最佳實踐:Android平台頁面路由框架ARouter

阿里ARouter的分析計划

基本功能使用

1.添加依賴和配置

android {
    defaultConfig {
    ...
    javaCompileOptions {
        annotationProcessorOptions {
        arguments = [ moduleName : project.getName() ] } } } } dependencies { compile 'com.alibaba:arouter-api:1.2.1.1' annotationProcessor 'com.alibaba:arouter-compiler:1.1.2.1' ... } 

2.添加注解

// 在支持路由的頁面上添加注解(必選) // 這里的路徑需要注意的是至少需要有兩級,/xx/xx @Route(path = "/test/test1") public class Test1Activity extends AppCompatActivity{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test1); } } 

3.初始化SDK

public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button btn1,btn2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn1 = (Button) findViewById(R.id.btn1); btn2 = (Button) findViewById(R.id.btn2); btn1.setOnClickListener(this); btn2.setOnClickListener(this); } @Override public void onClick(View v) { if (v.getId() == R.id.btn1) { // 如果使用了InstantRun,必須在初始化之前開啟調試模式,但是上線前需要關閉,InstantRun僅用於開發階段, // 線上開啟調試模式有安全風險,可以使用BuildConfig.DEBUG來區分環境 ARouter.openDebug(); ARouter.init(getApplication()); // 盡可能早,推薦在Application中初始化 } } } 

4.發起跳轉操作

public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button btn1,btn2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn1 = (Button) findViewById(R.id.btn1); btn2 = (Button) findViewById(R.id.btn2); btn1.setOnClickListener(this); btn2.setOnClickListener(this); } @Override public void onClick(View v) { if (v.getId() == R.id.btn1) { .... } else if (v.getId() == R.id.btn2){ ARouter.getInstance().build("/test/test1").navigation(); } } } 

以上相關代碼就是ARouter的最基本功能使用的步驟,下面來分析跳轉功能是如何實現的。

原理分析
1.ARouter編譯的過程

ARouter在編譯期的時候,利用自定義注解完成了頁面的自動注冊。相關注解源碼參見arouter-annotation,編譯處理器源碼參見arouter-compiler

下面是注解@Route的源碼介紹:

@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface Route { /** *路由的路徑,標識一個路由節點 */ String path(); /** * 將路由節點進行分組,可以實現按組動態加載 */ String group() default ""; /** * 路由節點名稱,可用於生成javadoc文檔 */ String name() default "undefined"; /** * 用32位int類型標示,可用於頁面的一些配置 */ int extras() default Integer.MIN_VALUE; /** * 路由的優先級 */ int priority() default -1; } 

Route中的extra值是個int值,由32位表示,即轉換成二進制后,一個int中可以配置31個1或者0,而每一個0或者1都可以表示一項配置(排除符號位),如果從這31個位置中隨便挑選出一個表示是否需要登錄就可以了,只要將標志位置為1,就可以在聲明的攔截器中獲取到這個標志位,通過位運算的方式判斷目標頁面是否需要登錄。所以可以通過extra給頁面配置30多個屬性,然后在攔截器中去進行處理。
ARouter在攔截器中會把目標頁面的信息封裝一個類Postcard,這個類就包含了目標頁面注解上@Route標識的各種信息。關於攔截器的使用以及源碼分析,后續會有介紹。

將代碼編譯一遍,可以看到ARouter生成下面幾個源文件:

上面三個文件均是通過注解處理器RouteProcessor生成的,關於如何自定義注解處理器,可以閱讀Android編譯時注解APT實戰(AbstractProcessor),同時也需要學習JavaPoet的基本使用。下面我們看RouteProcessor是如何生成相關文件的。

    @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { //判斷被注解了的元素集合是否為空 if (CollectionUtils.isNotEmpty(annotations)) { //獲取所有被@Route注解的元素集合,Element可以是類、方法、變量等 Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class); try { logger.info(">>> Found routes, start... <<<"); //具體處理注解,生成java文件的方法 this.parseRoutes(routeElements); } catch (Exception e) { logger.error(e); } return true; } return false; } 

process()方法相當於處理器的主函數main(),可以在這個方法中掃描、評估和處理注解的代碼,以及生成Java文件。RouteProcessor中調用了parseRoutes(),用來處理所有被@Route注解的元素。在分析上述三個java文件如何生成之前,先看看生成文件的具體代碼。

  • ARouter$$Root$$app類
public class ARouter$$Root$$app implements IRouteRoot { @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("test", ARouter$$Group$$test.class); } } 
  • ARouter$$Group$$test類
public class ARouter$$Group$$test implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/test/test1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/test1", "test", null, -1, -2147483648)); } } 
  • ARouter$$Providers$$app類
public class ARouter$$Providers$$app implements IProviderGroup { @Override public void loadInto(Map<String, RouteMeta> providers) { } } 

我們接着分析上述三個文件是如何生成的

1.首先獲取生成方法的參數的類型和參數名稱

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException { if (CollectionUtils.isNotEmpty(routeElements)) { logger.info(">>> Found routes, size is " + routeElements.size() + " <<<"); rootMap.clear(); // TypeElement 表示一個類或接口元素 // public static final String ACTIVITY = "android.app.Activity"; //得到類activity元素 TypeElement type_Activity = elementUtil.getTypeElement(ACTIVITY); // public static final String SERVICE = "android.app.Service"; //得到類service的元素 TypeElement type_Service = elementUtil.getTypeElement(SERVICE); // public static final String SERVICE = "android.app.Fragment"; TypeMirror fragmentTm = elements.getTypeElement(FRAGMENT).asType(); // public static final String SERVICE = "android.support.v4.app.Fragment"; TypeMirror fragmentTmV4 = elements.getTypeElement(Consts.FRAGMENT_V4).asType(); // public static final String IROUTE_GROUP = "com.alibaba.android.arouter.facade.template.IRouteGroup"; //得到接口IRouteGroup元素 TypeElement type_IRouteGroup = elementUtil.getTypeElement(IROUTE_GROUP); // public static final String IROUTE_GROUP = "com.alibaba.android.arouter.facade.template.IProviderGroup"; //得到接口IProviderGroup元素 TypeElement type_IProviderGroup = elementUtil.getTypeElement(IPROVIDER_GROUP); //獲取RouteMeta,RouteType類名 ClassName routeMetaCn = ClassName.get(RouteMeta.class); ClassName routeTypeCn = ClassName.get(RouteType.class); //下面代碼是獲取生成java文件中方法的參數類型名稱和參數名稱。 /* 獲取獲取ARouter$$Root$$app 類中方法參數Map<String, Class<? extends IRouteGroup>>類型的名稱 */ ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ParameterizedTypeName.get( ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup)) ) ); /* 獲取ARouter$$Group$$test,ARouter$$Providers$$app類中方法參數 Map<String, RouteMeta>類型的名稱 */ ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ClassName.get(RouteMeta.class) ); /* 獲取相關的參數 */ //獲取ARouter$$Root$$app 類中方法的參數Map<String, Class<? extends IRouteGroup>> routes ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build(); //獲取ARouter$$Group$$test類中方法的參數Map<String, RouteMeta> atlas ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build(); //獲取ARouter$$Providers$$app類中方法的參數Map<String, RouteMeta> providers ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build(); ..... } } 

2.獲取了方法的參數的類型和參數名稱后,下面便是生成相應的方法

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException { if (CollectionUtils.isNotEmpty(routeElements)) { ........ /* 首先創建ARouter$$Root$$xxx 類中的loadInto()方法 @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {} */ MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(rootParamSpec); // 遍歷所有被@Route注解的元素 for (Element element : routeElements) { TypeMirror tm = element.asType(); Route route = element.getAnnotation(Route.class); RouteMeta routeMete = null; //判斷該元素否為 Activity 、IProvider 、 Service 的子類,然后創建相應的RouteMeta 對象 if (typeUtil.isSubtype(tm, type_Activity.asType())) { // Activity logger.info(">>> Found activity route: " + tm.toString() + " <<<"); // 如果是acitiviy類型,獲取所有被@Autowired的屬性 //關於@Autowired的注解,我們之后再進行分析 Map<String, Integer> paramsType = new HashMap<>(); for (Element field : element.getEnclosedElements()) { if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !typeUtil.isSubtype(field.asType(), iProvider)) { // It must be field, then it has annotation, but it not be provider. Autowired paramConfig = field.getAnnotation(Autowired.class); paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), TypeUtils.typeExchange(field.asType())); } } // ACTIVITY類型節點 routeMete = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType); } else if (typeUtil.isSubtype(tm, iProvider)) { // IProvider logger.info(">>> Found provider route: " + tm.toString() + " <<<"); //從該判斷可看出,如果要想成功注冊一個 PROVIDER 類型的路由節點, //一定要實現 com.alibaba.android.arouter.facade.template.IProvider 這個接口 routeMete = new RouteMeta(route, element, RouteType.PROVIDER, null); } else if (typeUtil.isSubtype(tm, type_Service.asType())) { // Service logger.info(">>> Found service route: " + tm.toString() + " <<<"); //SERVICE類型節點 routeMete = new RouteMeta(route, element, RouteType.parse(SERVICE), null); } else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) { logger.info(">>> Found fragment route: " + tm.toString() + " <<<"); //FRAGMENT類型節點 routeMete = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null); } //routeMete包含了每個路由節點的各種信息,下面的方法的主要功能就是根據@Route注解信息對節點進行分組,保存在groupMap集合中。 //關於方法的具體實現,后面會有解析 categories(routeMete); } ......... } } 

以上代碼主要功能就是遍歷所有被@Route注解的元素,然后將每個路由節點的信息按照類型(ACTIVITY類型,實現了IProvider 接口類型以及SERVICE類型)封裝到RouteMeta中,最后調用categories(routeMete)方法將節點分組,保存在groupMap集合。

繼續往下分析

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException { if (CollectionUtils.isNotEmpty(routeElements)) { ........ /* 然后創建ARouter$$Providers$$xxx 類中的loadInto()方法 @Override public void loadInto(Map<String, RouteMeta> providers) {} */ MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(providerParamSpec); //遍歷分組的集合,生成相應的java文件 //因為本文使用的例子沒有對頁面進行分組,所以只生成了一個組文件ARouter$$Group$$xxx for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) { String groupName = entry.getKey(); /* 創建ARouter$$Group$$xxx 類中的loadInto()方法 @Override public void loadInto(Map<String, RouteMeta> atlas) {} */ MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(groupParamSpec); // 生成loadInto()方法體 Set<RouteMeta> groupData = entry.getValue(); //遍歷每個組里面的路由節點 for (RouteMeta routeMeta : groupData) { switch (routeMeta.getType()) { //如果節點類型是PROVIDER, case PROVIDER: //獲取路由節點元素的接口集合 List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces(); for (TypeMirror tm : interfaces) { if (types.isSameType(tm, iProvider)) { // Its implements iProvider interface himself. //路由節點元素其中一個接口是 com.alibaba.android.arouter.facade.template.IProvider //給ARouter$$Providers$$xxx 類中的loadInto()添加方法體 loadIntoMethodOfProviderBuilder.addStatement( "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", (routeMeta.getRawType()).toString(),//路由節點元素的全名 routeMetaCn, routeTypeCn, ClassName.get((TypeElement) routeMeta.getRawType()), routeMeta.getPath(), routeMeta.getGroup()); } else if (types.isSubtype(tm, iProvider)) { //路由節點元素其中一個接口是com.alibaba.android.arouter.facade.template.IProvider 接口的子類型 loadIntoMethodOfProviderBuilder.addStatement( "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", tm.toString(), //IProvider子類型的全名 routeMetaCn, routeTypeCn, ClassName.get((TypeElement) routeMeta.getRawType()), routeMeta.getPath(), routeMeta.getGroup()); } //上面方法體的代碼為: //providers.put("實現接口的名稱", RouteMeta.build(RouteType.PROVIDER, 類名.class, "@Route.path", "@Route.group", null, @Route.priority, @Route.extras)); } break; default: break; } // 將路由節點中被@Autowired注解的屬性集合轉換成字符串 StringBuilder mapBodyBuilder = new StringBuilder(); //獲取路由節點中被@Autowired注解的屬性集合 Map<String, Integer> paramsType = routeMeta.getParamsType(); if (MapUtils.isNotEmpty(paramsType)) { for (Map.Entry<String, Integer> types : paramsType.entrySet()) { mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); "); } } String mapBody = mapBodyBuilder.toString(); //給ARouter$$Group$$xxx 類中的loadInto()添加方法體 //注意:有多個分組就會創建多個組文件 loadIntoMethodOfGroupBuilder.addStatement( "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", routeMeta.getPath(), routeMetaCn, routeTypeCn, ClassName.get((TypeElement) routeMeta.getRawType()), routeMeta.getPath().toLowerCase(), routeMeta.getGroup().toLowerCase()); } // 真正生成ARouter$$Group$$test JAVA文件 //NAME_OF_GROUP = ARouter$$Group$$ // groupName = test; 關於groupname的值在方法categories(routeMete)中會有講解 String groupFileName = NAME_OF_GROUP + groupName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(groupFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_IRouteGroup)) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfGroupBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated group: " + groupName + "<<<"); //將生成的組文件放在rootmap集合中去,為下面生成ARouter$$Root$$xxx文件做准備 rootMap.put(groupName, groupFileName); } ....... } } 

以上代碼主要功能由幾點:

  • 遍歷groupmap集合給ARouter$$Group$$xxx類中的loadInto()添加方法體,並且生成ARouter$$Group$$xxx JAVA文件,而文件命名為ARouter$$Group$$+groupname,其中有多個分組就會創建多個組文件。比如AROUTER源碼中的樣例就生成了多個分組文件
兩個分組文件

關於生成的loadInto()中的方法體的例子,來自 AROUTER源碼中的樣例:

public class ARouter$$Group$$test implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { //存在被@Autowired注解參數生成的代碼 atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("name", 18); put("boy", 0); put("age", 3); put("url", 18); }}, -1, -2147483648)); ..... //沒有被@Autowired注解參數生成的代碼 atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648)); .... } } 
  • 遍歷每個組里面的路由節點,查找節點類型是否為PROVIDER類型,如果是就向給ARouter$$Providers$$xxx類中的loadInto()添加方法,其文件命名ARouter$$Providers$$+modulename。關於生成的loadInto()中的方法體的例子,來自 AROUTER源碼中的樣例:
public class ARouter$$Providers$$app implements IProviderGroup { @Override public void loadInto(Map<String, RouteMeta> providers) { providers.put("com.alibaba.android.arouter.demo.testservice.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648)); //路由節點元素其中一個接口是IProvider的子類型 providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, -1, -2147483648)); //路由節點元素其中一個接口是IProvider接口 providers.put("com.alibaba.android.arouter.demo.testservice.SingleService", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single", "service", null, -1, -2147483648)); } } 
  • 將生成的組文件放在rootmap集合中去,為下面生成ARouter$$Root$$xxx文件做准備,其文件命名ARouter$$Root$$+modulename。

我們接着分析parseRoutes()方法最后一段代碼,這段代碼其實很簡單,主要目的就是給ARouter$$Root$$xxx的loadInto()添加方法體,最后生成Router$$Providers$$xxx,ARouter$$Root$$xxx文件

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException { if (CollectionUtils.isNotEmpty(routeElements)) { ........ //遍歷rootMap集合,給ARouter$$Root$$xxx的`loadInto()`添加方法體 if (MapUtils.isNotEmpty(rootMap)) { // Generate root meta by group name, it must be generated before root, then I can findout the class of group. for (Map.Entry<String, String> entry : rootMap.entrySet()) { loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue())); } } // 生成Router$$Providers$$xxx文件 String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(providerMapFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_IProviderGroup)) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfProviderBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated provider map, name is " + providerMapFileName + " <<<"); // 生成ARouter$$Root$$xxx文件 String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(rootFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(elementUtil.getTypeElement(ITROUTE_ROOT))) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfRootBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated root, name is " + rootFileName + " <<<"); } } 

關於生成的loadInto()中的方法體的例子,來自 AROUTER源碼中的樣例:

public class ARouter$$Root$$app implements IRouteRoot { @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("service", ARouter$$Group$$service.class); routes.put("test", ARouter$$Group$$test.class); } } 

上面分析的便是parseRoutes()方法所有代碼的解析

3.最后我們看下categories()方法是如何分組的

   private void categories(RouteMeta routeMete) { //如果路由路徑合法,且有groupname進行執行 if (routeVerify(routeMete)) { logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<"); //根據groupname獲取該組的路由節點集合,如果集合為空,則創建一個新的組,將該節點添加進去,並將組集合保存在groupmap中; //不為空,則添加到所屬的組集合中去 Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup()); if (CollectionUtils.isEmpty(routeMetas)) { Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() { @Override public int compare(RouteMeta r1, RouteMeta r2) { try { return r1.getPath().compareTo(r2.getPath()); } catch (NullPointerException npe) { logger.error(npe.getMessage()); return 0; } } }); routeMetaSet.add(routeMete); groupMap.put(routeMete.getGroup(), routeMetaSet); } else { routeMetas.add(routeMete); } } else { logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<"); } } //判斷路由路徑是否合法,並且設置groupname private boolean routeVerify(RouteMeta meta) { String path = meta.getPath(); //如果路徑為空,或者不是由'/'開頭,返回false if (StringUtils.isEmpty(path) || !path.startsWith("/")) { // The path must be start with '/' and not empty! return false; } //如果在@Route注解中沒有設置group標識,那么就默認取path路徑第一段路徑名作為groupname if (StringUtils.isEmpty(meta.getGroup())) { // Use default group(the first word in path) try { String defaultGroup = path.substring(1, path.indexOf("/", 1)); if (StringUtils.isEmpty(defaultGroup)) { return false; } meta.setGroup(defaultGroup); return true; } catch (Exception e) { logger.error("Failed to extract default group! " + e.getMessage()); return false; } } return true; } 

通過分析,如果@Route注解中有設置group標識,作為groupname,如果沒有就取/xxx1/xxx2,xxx1作為groupname,並將同一組的路由節點放到同一個集合中去。

至此關於@Route注解在編譯期時生成ARouter$$Root$$xxx,Router$$Providers$$xxx,ARouter$$Group$$xxx三種映射文件的源碼分析完畢。

2.ARouter初始化過程

ARouter經過代碼編譯后,生成了相應的映射文件,我們可以斷定,ARouter 的初始化會將這些文件加載到內存中去,形成一個路由表,以供后面路由查找跳轉之用。其相關源碼可參見 arouter-api

  • ARouterinit()方法
public static void init(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."); } } 

由上面代碼可以看出,其初始化實際上是調用了_ARouterinit ()方法,而且其他的跳轉方法最終調用的也是_ARouter 種的方法。

  • _ARouterinit()方法
  protected static synchronized boolean init(Application application) { mContext = application; LogisticsCenter.init(mContext, executor); logger.info(Consts.TAG, "ARouter init success!"); hasInit = true; return true; } 

_ARouter中又調用了LogisticsCenter.init(),繼續追蹤下去,其中傳入了一個線程池executor,這個線程池在攔截器的時候會使用到。

    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException { mContext = context; executor = tpe; try { //ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes" // 獲取ROUTE_ROOT_PAKCAGE 包里面的所有文件 List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); //遍歷所有ROUTE_ROOT_PAKCAGE 包里的文件 for (String className : classFileNames) { //文件名以“com.alibaba.android.arouter.routes.ARouter$$Root”開頭執行下面代碼 if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { // 通過反射實例化,並且調用loadInto(),目的即是將編譯生成的ARouter$$Group$$xxx文件加載到內存中,保存在Warehouse.groupsIndex; ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) { //文件名以“com.alibaba.android.arouter.routes.ARouter$$Interceptors”開頭執行下面代碼 // 執行編譯生成的ARouter$$Interceptors$$xxx的loadInto(),將自定義攔截器類存放在Warehouse.interceptorsIndex中 ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) { //文件名以“com.alibaba.android.arouter.routes.ARouter$$Providers”開頭執行下面代碼 // 執行編譯生成的ARouter$$Interceptors$$xxx的loadInto() ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex); } } if (Warehouse.groupsIndex.size() == 0) { logger.error(TAG, "No mapping files were found, check your configuration please!"); } if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size())); } } catch (Exception e) { throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]"); } } 
  • _ARouterafterInit()方法
static void afterInit() { // 通過路由機制,初始化路由攔截機制。關於路由攔截機制的使用和原理,后續文章會有分析 interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation(); } 

以上就是ARouter初始化的所有代碼,關於如何查找到com.alibaba.android.arouter.routes包內所有文件這里便不做過多分析,大家可以去閱讀 arouter-apiClassUtils這個類的源碼。
總結下來,其實ARouter 的初始化只做了一件事,找到自己編譯期產生的清單文件,把 Group 、Interceptor 、Provider 三種清單加載到 Warehouse 內存倉庫中。即下面這些文件,來源自AROUTER源碼中的樣例

值得注意的是,在初始化階段,ARouter 僅載入了 Group 清單,並沒有具體載入每個 Group 中包含的具體的路由節點清單,只有當使用到具體的 Group 時,才會加載對應的 Group 列表。這種分組管理,按需加載,大大的降低了初始化時的內存壓力。並且Warehouse類中保存了路由清單,並且將使用過的路由對象緩存起來,之后查找都是直接使用緩存的對象 。

3.ARouter調用過程分析

頁面跳轉最基本方法

ARouter.getInstance().build("/test/activity2").navigation();

獲取Provider服務(實現了IProvider接口以及IProvider子類接口的服務類)的方法有兩種:

1.byName方式
ARouter.getInstance().build("/service/hello").navigation()

2.byType方式
ARouter.getInstance().navigation(HelloService.class)

ARouter路由跳轉采用鏈式調用,ARouter.getInstance()其中采用的單例模式,獲取ARouter的實例,這個就不作過多分析,主要分析build()navigation()

build()方法
ARouter的build(String path)init()方法一樣,調用的是_ARouterbuild(String path)方法。

  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); } return build(path, extractGroup(path)); } } 

其中extractGroup(String path)就是根據path獲取分組名,即path第一段“/”符號之間的值

  private String extractGroup(String path) { if (TextUtils.isEmpty(path) || !path.startsWith("/")) { throw new HandlerException(Consts.TAG + "Extract the default group failed, the path must be start with '/' and contain more than 2 '/'!"); } try { // /xxx1/xxx2 ===> defaulGroup = xxx1 String defaultGroup = path.substring(1, path.indexOf("/", 1)); if (TextUtils.isEmpty(defaultGroup)) { throw new HandlerException(Consts.TAG + "Extract the default group failed! There's nothing between 2 '/'!"); } else { return defaultGroup; } } catch (Exception e) { logger.warning(Consts.TAG, "Failed to extract default group! " + e.getMessage()); return null; } } 

build(String path)方法最終調用的是build(String path, String group)

    protected Postcard build(String path, String group) { if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) { throw new HandlerException(Consts.TAG + "Parameter is invalid!"); } else { PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null != pService) { path = pService.forString(path); } return new Postcard(path, group); } } 

值得注意的是其中ARouter.getInstance().navigation(PathReplaceService.class)就是得到實現PathReplaceService接口的一個服務對象,對原始path進行處理后,生成新的path路徑。而這個類需要我們自己自定義去實現,如果沒有實現,pService=null,原始path不做任何處理。
下面是PathReplaceService接口,我們可以通過實現forString()forUri()方法,對某些url進行替換處理,跳轉到其他的目標頁面。

public interface PathReplaceService extends IProvider { /** * For normal path. * * @param path raw path */ String forString(String path); /** * For uri type. * * @param uri raw uri */ Uri forUri(Uri uri); } 

最后返回一個Postcard實例對象,里面封裝了路由節點的路徑,分組等節點信息。其實build()方法的目的只有一個就是根據路由,封裝成Postcard對象,其對象貫穿之后整個路由過程。Postcard 包含了眾多的屬性值,提供了路由過程中所有的控制變量。

public final class Postcard extends RouteMeta { private Uri uri; private Object tag; // A tag prepare for some thing wrong. private Bundle mBundle; // 傳遞的參數 private int flags = -1; // intent 的flag標志 private int timeout = 300; // Navigation timeout, TimeUnit.Second ! private IProvider provider; // IProvider服務對象 private boolean greenChannal; private SerializationService serializationService;//序列化服務對象 // 跳轉動畫 private Bundle optionsCompat; // The transition animation of activity private int enterAnim; private int exitAnim; // copy from RouteMeta private RouteType type; // 路由節點類型 private Element rawType; private Class<?> destination; //需要跳轉到的頁面 private String path; // 路徑 private String group; // 分組 private int priority = -1; // 優先級 private int extra; // 配置標識 private Map<String, Integer> paramsType; // 路由頁面被@Autowired注解屬性 // ...... } 

navigation()方法
關於頁面跳轉的navigation()方法有多個重載的方法,但最終都會調用_ARouter下面這個方法

    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { try { //首先對postcard進行一些處理,設置postcard的destination,type,priority 等一些屬性值,completion()后面會有分析 LogisticsCenter.completion(postcard); } catch (NoRouteFoundException ex) { logger.warning(Consts.TAG, ex.getMessage()); if (debuggable()) { // Show friendly tips for user. Toast.makeText(mContext, "There's no route matched!\n" + " Path = [" + postcard.getPath() + "]\n" + " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show(); } // 如果處理postcard失敗,通過 callback 回調失敗結果 // callback為空的情況下,如果有定義全局的降級處理(DegradeService),則使用全局處理 //降級處理也需要我們自己實現DegradeService接口 if (null != callback) { callback.onLost(postcard); } else { // No callback for this invoke, then we use the global degrade service. DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class); if (null != degradeService) { degradeService.onLost(context, postcard); } } return null; } //路由處理成功,回調callback.onFound() if (null != callback) { callback.onFound(postcard); } //目前來說,PROVIDER服務類型,以及FRAGMENT類型不需要通過攔截器外,其他類型均需要通過攔截器 //關於攔截器相關用法及原理分析在后續的文章中會講解到,大家去可以關注下 if (!postcard.isGreenChannel()) { interceptorService.doInterceptions(postcard, new InterceptorCallback() { /** * Continue process * * @param postcard route meta */ @Override public void onContinue(Postcard postcard) { _navigation(context, postcard, requestCode, callback); } /** * Interrupt process, pipeline will be destory when this method called. * * @param exception Reson of interrupt. */ @Override public void onInterrupt(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); } return null; } 

值得注意的是,當跳轉路由處理失敗的時候,會獲取一個降級服務,我們可以實現DegradeService接口,實現onLost()方法,對路由處理失敗的情況進行處理,比如跳轉到一個信息提示頁面,讓用戶去更新版本等操作等。 下面是DegradeService接口:

public interface DegradeService extends IProvider { /** * Router has lost. * * @param postcard meta */ void onLost(Context context, Postcard postcard); } 

通過上面代碼的分析,不管是否通過攔截器進行處理,最后都會調用_navigation()達到路由的目的:

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { final Context currentContext = null == context ? mContext : context; switch (postcard.getType()) { case ACTIVITY: //下面就是最基本的使用intent進行activity進行跳轉 // 創建intent final Intent intent = new Intent(currentContext, postcard.getDestination()); //設置傳參 intent.putExtras(postcard.getExtras()); //activity啟動標志 int flags = postcard.getFlags(); if (-1 != flags) { intent.setFlags(flags); } else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } // 在主線程中進行跳轉 new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { //新版本帶轉場動畫的啟動方式 if (requestCode > 0) { // Need start for result ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle()); } else { ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle()); } if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version. //老版本的跳轉動畫 ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim()); } //跳轉成功,回調callback.onArrival() if (null != callback) { // Navigation over. callback.onArrival(postcard); } } }); break; case PROVIDER: return postcard.getProvider(); case BOARDCAST: case CONTENT_PROVIDER: case FRAGMENT: Class fragmentMeta = postcard.getDestination(); try { //實例化fragment,並傳遞參數 Object instance = fragmentMeta.getConstructor().newInstance(); if (instance instanceof Fragment) { ((Fragment) instance).setArguments(postcard.getExtras()); } else if (instance instanceof android.support.v4.app.Fragment) { ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras()); } return instance; } catch (Exception ex) { logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace())); } case METHOD: case SERVICE: default: return null; } return null; } 

目前僅ARouter實現了 ACTIVITY , PROVIDER ,FRAGMENT三種種類型。上面關於postcard的provider,destination的值都是在completion()中設置的。我們接着看LogisticsCentercompletion(Postcard postcard)

    public synchronized static void completion(Postcard postcard) { if (null == postcard) { throw new NoRouteFoundException(TAG + "No postcard!"); } // 查找Warehouse倉庫的路由節點緩存,看是否已在緩存中 RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath()); if (null == routeMeta) { // 如果沒有,查找倉庫的組別清單中是否存在該組別,組別清單已經在初始化的時候加載到倉庫中去了 Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); //如果沒有拋出異常 if (null == groupMeta) { throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]"); } else { // Load route and cache it into memory, then delete from metas. try { if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath())); } // 實例化個組別的類,調用loadInto(),將組別中所有的路由節點加載進倉庫Warehouse.routes,緩存 IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); iGroupInstance.loadInto(Warehouse.routes); // 從組別清單中刪除已加載的組別,防止重復加載 Warehouse.groupsIndex.remove(postcard.getGroup()); if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath())); } } catch (Exception e) { throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]"); } //當路由節點加載到緩存中去后,重新查找執行else代碼,對postcard進行處理 completion(postcard); // Reload } } else { //給postcard設置destination,type,priority等值,供上面講解到的_navigation()進行使用 // 其中routeMeta是在ARouter$$Group$$xxx的loadInto中創建的 postcard.setDestination(routeMeta.getDestination()); postcard.setType(routeMeta.getType()); postcard.setPriority(routeMeta.getPriority()); postcard.setExtra(routeMeta.getExtra()); //如果通過build(Uri url) 進行跳轉的話 通過解析url ,將傳參保存進bundle中 Uri rawUri = postcard.getUri(); if (null != rawUri) { //splitQueryParameters()就是在uri中攜帶的參數進行解析 Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri); Map<String, Integer> paramsType = routeMeta.getParamsType(); if (MapUtils.isNotEmpty(paramsType)) { // Set value by its type, just for params which annotation by @Param for (Map.Entry<String, Integer> params : paramsType.entrySet()) { setValue(postcard, params.getValue(), params.getKey(), resultMap.get(params.getKey())); } // Save params name which need autoinject. postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{})); } // Save raw uri postcard.withString(ARouter.RAW_URI, rawUri.toString()); } //從這里也可以看出PROVIDER,FRAGMENT不需要通過攔截器 switch (routeMeta.getType()) { case PROVIDER: // 如果是PROVIDER節點類型,從服務節點列表中獲取,如果沒有,則實例化,並保存在服務節點列表Warehouse.providers中 //並將實例化的對象設置給postcard的provider屬性 Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination(); IProvider instance = Warehouse.providers.get(providerMeta); if (null == instance) { // There's no instance of this provider IProvider provider; try { provider = providerMeta.getConstructor().newInstance(); provider.init(mContext); Warehouse.providers.put(providerMeta, provider); instance = provider; } catch (Exception e) { throw new HandlerException("Init provider failed! " + e.getMessage()); } } postcard.setProvider(instance); postcard.greenChannel(); // Provider should skip all of interceptors break; case FRAGMENT: postcard.greenChannel(); // Fragment needn't interceptors default: break; } } } 

分析到這里,關於頁面基本跳轉的原理分析就已經結束了。最后就是關於獲取Provider服務兩種方法的源碼分析。其中byName方式,和頁面跳轉是一模一樣的。我們只需要看看byType方式即可。byType方式最后調用的是_ARouternavigation(Class<? extends T> service)

  protected <T> T navigation(Class<? extends T> service) { try { // 通過 className 獲取 Postcard 對象 Postcard postcard = LogisticsCenter.buildProvider(service.getName()); // 兼容1.0.5 compiler sdk版本. if (null == postcard) { // No service, or this service in old version. postcard = LogisticsCenter.buildProvider(service.getSimpleName()); } // 對 Postcard 對象進行處理 LogisticsCenter.completion(postcard); //返回 Postcard 中的 provider 屬性值 return (T) postcard.getProvider(); } catch (NoRouteFoundException ex) { logger.warning(Consts.TAG, ex.getMessage()); return null; } } 

上面代碼中的completion()方法之前已經分析過了,只需要看下LogisticsCenter.buildProvider(service.getName())即可。

  public static Postcard buildProvider(String serviceName) { RouteMeta meta = Warehouse.providersIndex.get(serviceName); if (null == meta) { return null; } else { return new Postcard(meta.getPath(), meta.getGroup()); } } 

這個方法非常的簡單,就是根據服務類名去倉庫Warehouse.providersIndex中獲去路由節點元素,然后封裝在Postcard對象中。服務類清單列表Warehouse.providersIndex中的值是在初始化時緩存的。值得注意的是,PROVIDER 類型的路由節點既存在於對應的分組中,也存在於服務類清單列表中。所以,ARouter 可通過byType,byName兩種方式來獲取

補充

關於ARouter的基本用法上面只有最基本跳轉的介紹,下面對其他一些基本使用進行下補充

  • 帶參數跳轉
//1.傳遞參數 ARouter.getInstance().build("/test/activity1") .withString("name", "老王") .withInt("age", 18) .withBoolean("boy", true) .withLong("high", 180) .withString("url", "https://a.b.c") .withParcelable("pac", testParcelable) .withObject("obj", testObj) .navigation(); //2.直接傳遞Bundle Bundle params = new Bundle(); ARouter.getInstance() .build("/test/activity1") .with(params) .navigation(); 

這些傳參都是保存在生成的postcard對象中的mBundle屬性里,然后在跳轉的時候通過intent.putExtras(postcard.getExtras())達到傳送參數的目的。
值得注意的是,關於對象的傳遞有兩種,一種是withParcelable()方法,不過此方法需要傳遞的對象實現Parcelable接口,達到序列化的目的;另外一種是withObject()方法,此方法的原理是將實體類轉換成json字符串,通過String的方式進行傳遞,而且使用這種方式需要實現 SerializationService,並使用@Route注解標注,下面是ARouter樣例:

@Route(path = "/service/json") public class JsonServiceImpl implements SerializationService { @Override public void init(Context context) { } @Override public <T> T json2Object(String text, Class<T> clazz) { return JSON.parseObject(text, clazz); } @Override public String object2Json(Object instance) { return JSON.toJSONString(instance); } } 

而且,需要在跳轉到的頁面獲取JsonServiceImpl服務,將json字符串轉換成對象。

SerializationService serializationService = ARouter.getInstance().navigation(SerializationService.class); TestObj obj = serializationService.json2Object(getIntent().getString("obj"), TestObj.class); 
  • 帶返回結果跳轉
ARouter.getInstance().build("/test/activity2").navigation(this, 666); 

值得注意的是,這時候的 navigation需要傳遞activit和requestCode。

  • 獲取Fragment的實例

定義一個fragment

@Route(path = "/test/fragment") public class BlankFragment extends Fragment { public BlankFragment() { //必須要一個空的構造器 } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { TextView textView = new TextView(getActivity()); return textView; } } 

獲取frament

Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation(); 
  • 帶轉場動畫跳轉
// 轉場動畫(常規方式) ARouter.getInstance() .build("/test/activity2") .withTransition(R.anim.slide_in_bottom, R.anim.slide_out_bottom) .navigation(this); // 轉場動畫(API16+) ActivityOptionsCompat compat = ActivityOptionsCompat.makeScaleUpAnimation(v, v.getWidth() / 2, v.getHeight() / 2, 0, 0); ARouter.getInstance().build("/test/activity2").withOptionsCompat(compat) .navigation(); 
  • 獲取服務

服務是全局單例的,只有在第一次使用到的時候才會被初始化。
暴露服務,必須實現IProvider 接口 或者其子類型

// 聲明接口,其他組件通過接口來調用服務 public interface HelloService extends IProvider { String sayHello(String name); } // 實現接口 @Route(path = "/service/hello", name = "測試服務") public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return "hello, " + name; } @Override public void init(Context context) { } } 

獲取服務

//bytype HelloService helloService1 = ARouter.getInstance().navigation(HelloService.class); //byname HelloService helloService2 = (HelloService) ARouter.getInstance().build("/service/hello").navigation(); 
  • 多模塊結構

app中可能存在多個模塊,每個模塊下面都有一個root結點,每個root結點都會管理整個模塊中的group節點,每個group結點則包含了該分組下的所有頁面,而每個模塊允許存在多個分組,每個模塊中都會有一個攔截器節點就是Interceptor結點,除此之外每個模塊還會有控制攔截反轉的provider結點

最后

到此,關於ARouter的基本用法以及原理分析的就全部結束了,如果有不清楚或者錯誤的地方,希望各位同學指出。關於ARouter攔截器,各種服務,依賴注入等更多進階用法及源碼分析會更新在后續的文章。

如果各位同學認為本文對你有一些幫助,希望能點個喜歡,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM