可能是最好理解的ARouter源碼分析

ARouter是什么

ARouter是阿里巴巴開源的Android平臺中對頁面、服務提供路由功能的中間件,提倡的是簡單且夠用。
Github: https://github.com/alibaba/ARouter
介紹: https://yq.aliyun.com/articles/71687

簡單使用示例

Activtiy跳轉

ARouter.getInstance().build("/test/activity2").withString("name", "老王").navigation();

獲取服務功能

ARouter.getInstance().navigation(HelloService.class).sayHello("mike");

((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

主要代碼結構

  • arouter-api: 上層主要代碼,包括入口類ARouter,主要邏輯代碼類LogisticsCenter,相關輔助類ClassUtils等

  • arouter-annotation: ARouter中主要支持的annotation(包括Autowired, Route, Interceptor)的定義,以及RouteMeta等基礎model bean的定義

  • arouter-compiler: ARouter中annotation對應的annotation processor代碼(注解處理器:讓這些注解代碼起作用,Arouter中主要是生成相關代碼,關于annotation processor,詳細了解可參考 Java注解處理器, 這篇譯文介紹的非常完整。

  • arouter-gradle-plugin: 一個gradle插件,目的是在arouter中插入相關注冊代碼(代替在Init時掃描dex文件獲取到所有route相關類)

  • app: Demo代碼,包括Activity跳轉,面向接口服務使用,攔截器的使用等詳細使用示例

核心源碼分析

ARouter.getInstance().build("/test/activity2").withString("name", "老王").navigation();

從最常用的跳轉開始分析,基本可了解到ARouter的運轉原理。這行完成跳轉的代碼最終效果是攜帶參數跳轉到對應的Activity,在Android層面來說最后一定是通過調用startActivity或是startActivityForResult來完成跳轉。

分為幾步來看:

  1. ARouter調用Build生成Postcard,過程是怎樣的
  2. Postcard是什么
  3. Postcard調用navigation是怎樣執行到startActivity的

生成Postcard過程

打開ARouter類,發現基本都是調用_ARouter類的方法。

ARouter類非常簡單,只是通過Facade pattern包裝了_ARouter類的相關方法,方便調用和閱讀。

_ARouter類真正實現了相關入口功能,包括初始化和銷毀等方法,另外主要包括生成Postcard以及通過Postcard完成navigation的代碼 (這里先介紹生成Postcard生成部分,_ARouter詳細介紹見下方)

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));
        }
    }

這過程主要完成了2個步驟:

  1. 查找是否存在PathReplaceService,如果有調用PathReplaceService的forString方法獲取新的path (后面介紹完Service功能會更好理解這部分)
  2. 從path中解析出group值,調用帶有group參數的build方法
    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);
        }
    }

在這個方法中就可以看到Postcard的構造。

Postcard是什么

首先Postcard繼承RouteMeta,RouteMeta中存儲的是關于route的一些基礎信息,只定位于存儲route基礎信息。

public class RouteMeta {
    private RouteType type;         // Type of route
    private Element rawType;        // Raw type of route
    private Class<?> destination;   // Destination
    private String path;            // Path of route
    private String group;           // Group of route
    private int priority = -1;      // The smaller the number, the higher the priority
    private int extra;              // Extra data
    private Map<String, Integer> paramsType;  // Param type
    private String name;

    private Map<String, Autowired> injectConfig;  // Cache inject config.
  
    ...
    
}

Postcard類則還包括了"明信片"的"寄出"(navigation方法),"是否支持綠色通道"(isGreenChannel方法),以及支持"寄出效果"(withTransition方法)等具體的功能。

public final class Postcard extends RouteMeta {
    // Base
    private Uri uri;
    private Object tag;             // A tag prepare for some thing wrong.
    private Bundle mBundle;         // Data to transform
    private int flags = -1;         // Flags of route
    private int timeout = 300;      // Navigation timeout, TimeUnit.Second
    private IProvider provider;     // It will be set value, if this postcard was provider.
    private boolean greenChannel;
    private SerializationService serializationService;

    // Animation
    private Bundle optionsCompat;    // The transition animation of activity
    private int enterAnim = -1;
    private int exitAnim = -1;
  
    ...
      
    public void navigation(Activity mContext, int requestCode, NavigationCallback callback){
        ARouter.getInstance().navigation(mContext, this, requestCode, callback);
    }
  
    ...
      
    public Postcard withString(@Nullable String key, @Nullable String value) {
        mBundle.putString(key, value);
        return this;
    }
  
    ...
      
    public Postcard withTransition(int enterAnim, int exitAnim) {
        this.enterAnim = enterAnim;
        this.exitAnim = exitAnim;
        return this;
    }
  
    ...
}

回到前面的介紹,在_ARouter的的build方法中通過path和group參數構造出了一張可以"寄出"的"明信片"(new Postcard)。

Postcard.navigation到startActivity

接下來就是調用Postcard的navigation方法

public void navigation(Activity mContext, int requestCode, NavigationCallback callback) {
    ARouter.getInstance().navigation(mContext, this, requestCode, callback);
}

可以看到其實是調用了ARouter的navigation, 內部調用了_ARouter的navigation。接下來看看navigation的主要過程是怎樣的。

protected Object navigation(final Context context, final Postcard postcard, final int
    requestCode, final NavigationCallback callback) {
    try {
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());

        if (debuggable()) {
            // Show friendly tips for user.
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(mContext, "There's no route matched!\n" +
                            " Path = [" + postcard.getPath() + "]\n" +
                            " Group = [" + postcard.getGroup() + "]",
                            Toast.LENGTH_LONG).show();
                }
            });
        }

        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;
    }

    if (null != callback) {
        callback.onFound(postcard);
    }

    if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor 
            // cost too mush time made ANR.
        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;
}

這個方法有點長,但主要包括3步流程。

  1. 調用LogisticsCenter.completion

    主要是為Postcard找到對應router,并且用router中信息填充Postcard對象。

    如果該方法拋出NoRouteFoundException,則調用對應Callback的onLost,如果沒有配置Callback則嘗試獲取是否存在DegradeService,如果存在調用DegradeService的onLost方法。(DegradeService的實現可以理解為兜底方案,比如native頁面沒有找到,用相應H5頁面替代展示)

    LogisticsCenter類的詳細介紹見下方。

  2. 調用Callback的onFound方法,然后判斷是否可以走“綠色通道”(即不支持攔截)。

    不能走“綠色通道”的都需要經過攔截器攔截(后面會介紹攔截器原理),攔截器會返回繼續和中斷2種結果,繼續則會繼續執行調用navigation方法(相當于在跳轉前做了點額外動作);

    如果走了“綠色通道”的則直接調用_navigation方法。

  3. _navigation方法中則可以看到,如果 postcard.getType() 是activity則調用startActivity或startActivityForResult,且支持動畫和啟動flags的設置,接著調用callback.onArrival。
    如果種類是provider則提供提供provider對象,如果是Fragment則生成對象且賦值參數。

ARouter.getInstance().build("/test/activity2").withString("name", "老王").navigation();

至此,已梳理完上方代碼完成Activity跳轉的主要流程。

獲取接口對象

((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

大體流程和上面完成Activity跳轉流程代碼類似,主要區別在于,

  1. LogisticsCenter.completion中給Postcard填充type時,對應的是RouteType.PROVIDER,并且在Warehouse中找到對應的Provider賦值給Postcard。
  2. 在ARouter的_navigation方法中,發現Postcard類型是RouteType.PROVIDER則直接返回對應Provider。

ARouter中Annotation的使用

Annotation在ARouter中有著非常重要作用,Annotation基本原理這里不做詳談。
ARouter中主要的Annotation有3個:

Autowired
Route
Interceptor

對應的Annotation定義代碼位置如下圖所示,位于arouter-annotation module中。

annotation代碼位置.jpg

Autowired

Autowired主要完成界面跳轉過程中,Intent參數的自動填充。

先看看Autowired是如何使用的,再看看Autowired是如何做到的。

如何使用

@Route(path = "/test/activity1", name = "測試用 Activity")
public class Test1Activity extends AppCompatActivity {

    @Autowired(desc = "姓名")
    String name = "jack";

    @Autowired
    int age = 10;

    ...
      
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test1);

        ARouter.getInstance().inject(this);
      
        ...
          
        //inject方法被調用后,被Autowired注解的變量則已從Intent中取出對應參數賦值
    }
}

如何實現

在arouter-compiler module中,查看對應的AutowiredProcessor代碼,這是一個用來處理Autowired注解的注解處理器,主要目的是生成Java代碼,生成怎樣的Java代碼(如何生成的,可基于注解處理器原理詳細查看AutowiredProcessor),如下:

public class Test1Activity$$ARouter$$Autowired implements ISyringe {
  private SerializationService serializationService;

  @Override
  public void inject(Object target) {
    serializationService = ARouter.getInstance().navigation(SerializationService.class);
    Test1Activity substitute = (Test1Activity)target;
    substitute.name = substitute.getIntent().getExtras() == null ? substitute.name : 
            substitute.getIntent().getExtras().getString("name", substitute.name);
    substitute.age = substitute.getIntent().getIntExtra("age", substitute.age);
    substitute.height = substitute.getIntent().getIntExtra("height", substitute.height);
    substitute.girl = substitute.getIntent().getBooleanExtra("boy", substitute.girl);
    
    ...
  }

代碼路徑:


autowired路徑.jpg

梳理下Autowired注解整個流程:

在Activity的變量上標注,注解處理器會在編譯階段掃描有被Autowired注解標注的變量,根據這個類和變量的情況生成一份Java代碼,其中最主要會有一個inject方法,完成對相關變量的解析賦值,然后在Activity的onCreate靠前位置調用對應inject方法即可。這個注解主要目的在于省去了手動編寫解析Intent參數的代碼。
(但為什么代碼是調用 ARouter.getInstance().inject(this); 后面會有說明)

Route

Route注解標注的類(頁面或服務類),可通過path跳轉到對應界面或是獲取到具體服務。

如何使用

@Route(path = "/test/activity1", name = "測試用 Activity")
public class Test1Activity extends AppCompatActivity {
  ...
}

@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
  ...
}

如何實現

在arouter-compiler module中,查看對應的RouteProcessor代碼,用來處理Route注解的注解處理器,主要完成以下幾部分的生成代碼工作(主要為RouteProcessor中的parseRoutes方法):

  1. 獲取到所有的被注解Route的類,生成對應的RouteMeta,分組放到groupMap中,key為group name, value為支持排序放入的的Set<RouteMeta>中。

  2. 遍歷groupMap中所有的Set<RouteMeta>的所有RouteMeta (所以是個雙層for循環)生成對應代碼。

  for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
      String groupName = entry.getKey();
       ...
      Set<RouteMeta> groupData = entry.getValue();
      for (RouteMeta routeMeta : groupData) {
        ...
      }
    ...
  }

生成的代碼如下:

public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    ...
    atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, 
                                 "/test/activity4", "test", null, -1, -2147483648));
    atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, 
                                 "/test/fragment", "test", null, -1, -2147483648));
    atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, 
                                "/test/webview", "test", null, -1, -2147483648));
    ...
  }
}
  1. 生成rootMap,key為group name,value為剛才每個group對應生成的java類的類名,根據rootMap生成對應Java文件。
   public class ARouter$$Root$$app implements IRouteRoot {
     @Override
     public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
       routes.put("test", ARouter$$Group$$test.class);
       routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
     }
   }

詳細關于這些生成類是如何被使用的,在后面LogisticsCenter中會有詳細介紹。

Interceptor

Interceptor 主要用于在跳轉過程中插入一些功能。

如何使用

@Interceptor(priority = 7)
public class Test1Interceptor implements IInterceptor {
    Context mContext;

    @Override
    public void process(final Postcard postcard, final InterceptorCallback callback) {
        if ("/test/activity4".equals(postcard.getPath())) {

            // 這里的彈窗僅做舉例,代碼寫法不具有可參考價值
            final AlertDialog.Builder ab = new AlertDialog.Builder(MainActivity.getThis());
            ab.setCancelable(false);
            ab.setTitle("溫馨提醒");
            ab.setMessage("想要跳轉到Test4Activity么?(觸發了\"/inter/test1\"攔截器,攔截了本次跳
                          轉)");
            ab.setNegativeButton("繼續", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callback.onContinue(postcard);
                }
            });
            ab.setNeutralButton("算了", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callback.onInterrupt(null);
                }
            });
            ab.setPositiveButton("加點料", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    postcard.withString("extra", "我是在攔截器中附加的參數");
                    callback.onContinue(postcard);
                }
            });

            MainLooper.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    ab.create().show();
                }
            });
        } else {
            callback.onContinue(postcard);
        }
    }

這個例子是想在跳轉到 "/test/activity4" 對應的Activity過程中彈出對話框,在用戶做出了相關動作后再繼續跳轉,點擊"繼續"則繼續執行,點擊"算了"則取消跳轉,點擊"加點料"則在跳轉過程中添加參數。

如何實現

上面在分析ARouter的navigation的跳轉過程中,分析_ARouter的navigation方法中有提到會去判斷是否走"綠色通道",如果沒有的話則要調用 interceptorService.doInterceptions 方法來經過攔截器的處理。

來看看攔截器是如何處理的,主要代碼在InterceptorServiceImpl類中。主要方法有:

doInterceptions:供外部調用接口方法,會在異步線程中開啟攔截器的逐個調用,會去調用_excute方法,完成對攔截器IInterceptor的process方法的調用,通過CountDownLatch來控制是否所有攔截器都調用完成,且在超時時間內。

_excute: 調用IInterceptor的process方法,在callback的onContinue方法中遞歸調用自己,但調整index值,使用下一個攔截器。

_ARouter介紹

ARouter類只是使用Facade patten對_ARouter類進行了封裝,并沒有太多實際功能代碼,所以看看_ARouter中的具體實現代碼。

首先_ARouter是個單例類,但會在getInstance方法中判斷是否有進行初始化(必須要先初始化)。

其次幾個主要的方法:

init: 這里面最重要是調用了LogisticsCenter.init,完成相關路由信息填充。

Inject: 獲取AutowiredService的實體,會調用到AutowiredServiceImpl的autowire方法,在這個方法中會根據傳入參數Activity的Name再加上注解處理器中約定的后綴字段獲得新的類名(也就是注解處理器生成的對應的Java文件對應的類名),利用反射方式生成新對象,調用其inject方法。

(如上面分析的類 Test1Activity$$ARouter$$Autowired的inject方法)。

@Route(path = "/arouter/service/autowired")
public class AutowiredServiceImpl implements AutowiredService {
    
    ...

    @Override
    public void autowire(Object instance) {
        String className = instance.getClass().getName();
        try {
            if (!blackList.contains(className)) {
                ISyringe autowiredHelper = classCache.get(className);
                if (null == autowiredHelper) {  // No cache.
                    autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() 
                                            + SUFFIX_AUTOWIRED).getConstructor().newInstance();
                }
                autowiredHelper.inject(instance);
                classCache.put(className, autowiredHelper);
            }
        } catch (Exception ex) {
            blackList.add(className);    // This instance need not autowired.
        }
    }
}

build: 生成對應Postcard

navigation: 跳轉到對應頁面或獲取對應Service等

arouter-gradle-plugin module

這個module 是一個gradle plugin, 目的是幫助在com.alibaba.android.arouter.core.LogisticsCenter類的loadRouterMap方法中插入各個module注冊router代碼。(關于gradle plugin的機制,可通過深入理解Android之Gradle詳細了解)

/**
     * arouter-auto-register plugin will generate code inside this method
     * call this method to register all Routers, Interceptors and Providers
     * @author billy.qi <a href="mailto:qiyilike@163.com">Contact me.</a>
     * @since 2017-12-06
     */
    private static void loadRouterMap() {
        registerByPlugin = false;
        //auto generate register code by gradle plugin: arouter-auto-register
        // looks like below:
        // registerRouteRoot(new ARouter..Root..modulejava());
        // registerRouteRoot(new ARouter..Root..modulekotlin());
    }

    /**
     * method for arouter-auto-register plugin to register Routers
     * @param routeRoot IRouteRoot implementation class in the package: com.alibaba.android.arouter.core.routers
     * @author billy.qi <a href="mailto:qiyilike@163.com">Contact me.</a>
     * @since 2017-12-06
     */
    private static void registerRouteRoot(IRouteRoot routeRoot) {
        markRegisteredByPlugin();
        if (routeRoot != null) {
            routeRoot.loadInto(Warehouse.groupsIndex);
        }
    }

    /**
     * method for arouter-auto-register plugin to register Interceptors
     * @param interceptorGroup IInterceptorGroup implementation class in the package: com.alibaba.android.arouter.core.routers
     * @author billy.qi <a href="mailto:qiyilike@163.com">Contact me.</a>
     * @since 2017-12-06
     */
    private static void registerInterceptor(IInterceptorGroup interceptorGroup) {
        markRegisteredByPlugin();
        if (interceptorGroup != null) {
            interceptorGroup.loadInto(Warehouse.interceptorsIndex);
        }
    }

在ARouter 1.4版本前,是通過掃描ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes"這個packagename下獲取所有route相關類(包括分布在不同的module中的),然后遍歷分別調用對應的loadInto方法加載到 Warehouse 的各自Index中(包括Warehouse.providersIndex,Warehouse.interceptorsIndex等),構成不同類別的全局路由總表。
這段代碼目前仍存在,主要對應LogisticsCenter.init方法中registerByPlugin變量為false時對應的分支代碼。
這種方式的缺點在于是在運行時去掃描dex文件中所有類找到需要的類反射來完成映射表的注冊。

從ARouter 1.4開始,引入AutoRegister機制(關于AutoRegister,可以在AutoRegister:一種更高效的組件自動注冊方案了解詳情)。基本原理是:在編譯時,掃描所有類,將符合條件的類收集起來,并通過修改字節碼生成注冊代碼到指定的管理類中,從而實現編譯時自動注冊的功能,不用再關心項目中有哪些組件類了。不會增加新的class,不需要反射,運行時直接調用組件的構造方法。
在ARouter中具體的實現如下:

  1. arouter-gradle-plugin實現為gradle plugin, plugin name 是 PLUGIN_NAME = "com.alibaba.arouter",定義在ScanSetting中。在app module中的build.gradle中加入 apply plugin: 'com.alibaba.arouter',表示使用該plugin。

  2. RegisterTransform中完成掃描以ROUTER_CLASS_PACKAGE_NAME = 'com/alibaba/android/arouter/routes/' 開頭的類,然后調用RegisterCodeGenerator類的方法完成向LogisticsCenter::loadRouterMap插入代碼。

通過反編譯apk出來的實際出來的代碼如下:

  private static void loadRouterMap() {
    registerByPlugin = false;
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$app");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$app");
  }

這樣在編譯階段即可完成相關類的掃描工作,在ARouter初始化時只是完成路由總表的加載,省去在Dex文件中掃描類的步驟。

這里也貼下代碼README.md中給予的說明

使用 Gradle 插件實現路由表的自動加載 (可選)

    apply plugin: 'com.alibaba.arouter'

    buildscript {
        repositories {
            jcenter()
        }

        dependencies {
            // Replace with the latest version
            classpath "com.alibaba:arouter-register:?"
        }
    }

可選使用,通過 ARouter 提供的注冊插件進行路由表的自動加載(power by AutoRegister), 默認通過掃描 dex 的方式。
進行加載通過 gradle 插件進行自動注冊可以縮短初始化時間解決應用加固導致無法直接訪問dex 文件,初始化失敗的問題,需要注意的是,該插件必須搭配 api 1.3.0 以上版本使用!

LogisticsCenter介紹

本想再詳細介紹下LogisticsCenter類,但發現經過上面的一些介紹,LogisticsCenter的主要幾個方法已經都解釋過。

loadRouteMap: 由arouter-gradle-plugin 插件module在編譯階段插入注冊Route相關代碼。

init: 調用loadRouteMap完成Warehouse中各個路由Index的加載,如果有plugin幫忙插入代碼則直接使用,如果沒有則通過運行時掃描dex文件方式加載。

completion:前面分析navigation時有詳細介紹,在Route路由表中根據postcard中的path找到對應的路由信息RouteMeta,利用RouteMeta中信息為Postcard賦值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 組件化被越來越多的Android項目采用,而作為組件化的基礎——路由也是重中之重。本篇文章將詳細的分析阿里巴巴開源...
    胡奚冰閱讀 14,852評論 8 32
  • ARouter探究(一) 前言 ARouter 是 Alibaba 開源的一款 Android 頁面路由框架,特別...
    Jason騎蝸牛看世界閱讀 1,347評論 1 3
  • 前言 隨著項目業務邏輯和功能點日益遞增, 邏輯的耦合程度也逐漸升高, 組件化技術可以很好的解決這個問題, 公司大佬...
    SharryChoo閱讀 1,107評論 0 9
  • 組件化被越來越多的Android項目采用,而作為組件化的基礎——路由也是重中之重。本篇文章將詳細的分析阿里巴巴開源...
    小小的coder閱讀 313評論 0 0
  • 此間花香 不在天堂 在家鄉 家鄉勝過天堂 銀鈴聲聲 聲聲在耳旁 那一眼 白馬,青衫,少年 后面跟著一條小狗 少年說...
    三人生閱讀 61評論 0 0