Android 組件化 —— 路由設(shè)計(jì)最佳實(shí)踐

引子

這篇文章會(huì)告訴你

  • 什么是路由,是為了解決什么問題才產(chǎn)生的
  • 業(yè)界現(xiàn)狀是怎么樣的,我們可以做什么來優(yōu)化當(dāng)前的問題
  • 路由設(shè)計(jì)思路是怎么樣的,該怎么設(shè)計(jì)比較好
  • 如何用注解實(shí)現(xiàn)路由表
  • URL的參數(shù)如何依賴注入到Activity、Fragement
  • 如何HookOnActivityResult,不需要再進(jìn)行requstCode判斷
  • 如何異步攔截路由,實(shí)現(xiàn)線程切換,不阻塞頁面跳轉(zhuǎn)
  • 如何用Apt實(shí)現(xiàn)Retrofit接口式調(diào)用
  • 如何找到Activity的調(diào)用方
  • 如何實(shí)現(xiàn)路由的安全調(diào)用
  • 如何避開Apt不能匯總所有Module路由的問題

前言

當(dāng)前Android的路由庫實(shí)在太多了,剛開始的時(shí)候想為什么要用路由表的庫,用Android原生的Scheme碼不就好了,又不像iOS只能類依賴,后面越深入就越發(fā)現(xiàn)當(dāng)時(shí)想的太簡單了,后面看到Retrofit和OkHttp,才想到頁面請(qǐng)求本質(zhì)和網(wǎng)絡(luò)請(qǐng)求不是一樣嗎,終于業(yè)界最簡單高效的路由方案1.0出來了
開源的庫后面會(huì)放在公司github地址上面

背景

什么是路由

根據(jù)路由表頁面請(qǐng)求分發(fā)到指定頁面

使用場(chǎng)景

  1. App接收到一個(gè)通知,點(diǎn)擊通知打開App的某個(gè)頁面
  2. 瀏覽器App中點(diǎn)擊某個(gè)鏈接打開App的某個(gè)頁面
  3. 運(yùn)營活動(dòng)需求,動(dòng)態(tài)把原生的頁面替換成H5頁面
  4. 打開頁面需要某些條件,先驗(yàn)證完條件,再去打開那個(gè)頁面
  5. 不合法的打開App的頁面被屏蔽掉
  6. H5打開鏈接在所有平臺(tái)都一樣,方便統(tǒng)一跳轉(zhuǎn)
  7. App存在就打開頁面,不存在就去下載頁面下載,只有Google的App Link支持

為什么要有路由

Android原生已經(jīng)支持AndroidManifest去管理App跳轉(zhuǎn),為什么要有路由庫,這可能是大部分人接觸到Android各種Router庫不太明白的地方,這里我講一下我的理解

  • 顯示Intent:項(xiàng)目龐大以后,類依賴耦合太大,不適合組件化拆分
  • 隱式Intent:協(xié)作困難,調(diào)用時(shí)候不知道調(diào)什么參數(shù)
  • 每個(gè)注冊(cè)了Scheme的Activity都可以直接打開,有安全風(fēng)險(xiǎn)
  • AndroidMainfest集中式管理比較臃腫
  • 無法動(dòng)態(tài)修改路由,如果頁面出錯(cuò),無法動(dòng)態(tài)降級(jí)
  • 無法動(dòng)態(tài)攔截跳轉(zhuǎn),譬如未登錄的情況下,打開登錄頁面,登錄成功后接著打開剛才想打開的頁面
  • H5、Android、iOS地址不一樣,不利于統(tǒng)一跳轉(zhuǎn)

怎么樣的路由才算好路由

路由說到底還是為了解決開發(fā)者遇到的各種奇葩需求,使用簡單、侵入性低、維護(hù)方便是首要條件,不影響你原來的代碼,寫入代碼也很少,這里就要說說我的OkDeepLink的五大功能了,五大功能瞬間擊中你的各種痛點(diǎn),早點(diǎn)下班不是夢(mèng)。

  • 編譯時(shí)注解,實(shí)現(xiàn)靜態(tài)路由表,不再需要在臃腫的AndroidManifest中找到那個(gè)Actvity寫Scheme和Intent Filter
  • 異步攔截器,實(shí)現(xiàn)動(dòng)態(tài)路由,安全攔截、動(dòng)態(tài)降級(jí)難不倒你
  • 模仿Retrofit接口式調(diào)用,實(shí)現(xiàn)方式用apt,不耗性能,參數(shù)調(diào)用不再是問題
  • HookOnActivityResult,支持RxJava響應(yīng)式調(diào)用,不再需要進(jìn)行requestCode判斷
  • 參數(shù)依賴注入,自動(dòng)保存,不再需要手動(dòng)寫onSaveInstanceonCreate(SaveInstace)onNewIntent(Intent)getQueryParamer
注冊(cè)路由
路由結(jié)構(gòu)圖

詳細(xì)比較

大部分路由庫都用Apt(編譯時(shí)注解)生成路由表,然后用路由表轉(zhuǎn)發(fā)到指定頁面

方案對(duì)比 OkDeepLink Airbnb DeepLinkDispatch 阿里 ARouter 天貓 統(tǒng)跳協(xié)議 ActivityRouter
路由注冊(cè) 注解式接口注冊(cè) 每個(gè)module都要手動(dòng)注冊(cè) 每個(gè)module的路由表都要類查找 AndroidManiFest配置 每個(gè)module都要手動(dòng)注冊(cè)
路由查找 路由表 路由表 路由表 系統(tǒng)Intent 路由表
路由分發(fā) Activity轉(zhuǎn)發(fā) Activity轉(zhuǎn)發(fā) Activity轉(zhuǎn)發(fā) Activity轉(zhuǎn)發(fā) Activity轉(zhuǎn)發(fā)
動(dòng)態(tài)替換 Rxjava實(shí)現(xiàn)異步攔截器 不支持 線程等待 不支持 不支持
動(dòng)態(tài)攔截 Rxjava實(shí)現(xiàn)異步攔截器 不支持 線程等待 不支持 主線程
安全攔截 Rxjava實(shí)現(xiàn)異步攔截器 不支持 線程等待 不支持 主線程
方法調(diào)用 接口 手動(dòng)拼裝 手動(dòng)拼裝 手動(dòng)拼裝 手動(dòng)拼裝
參數(shù)獲取 Apt依賴注入,支持所有類型,不需要在Activity的onCreate中手動(dòng)調(diào)用get方法 參數(shù)定義在path,不利于多人協(xié)作 Apt依賴注入,但是要手動(dòng)調(diào)用get方法 手動(dòng)調(diào)用 手動(dòng)調(diào)用
結(jié)果返回 Rxjava回調(diào) onActivityResult onActivityResult onActivityResult onActivityResult
Module接入不同App 支持 不支持 支持 不支持 支持

其實(shí)說到底,路由的本質(zhì)就是注冊(cè)再轉(zhuǎn)發(fā),圍繞著轉(zhuǎn)發(fā)可以進(jìn)行各種操作,攔截,替換,參數(shù)獲取等等,其他Apt、Rxjava說到底都只是為了方便使用出現(xiàn)的,這里你會(huì)發(fā)現(xiàn)各種路由庫反而為了修復(fù)各種工具帶來的問題,出現(xiàn)了原來沒有的問題,譬如DeepLinkDispatch為了解決Apt沒法匯總所有Module路由,每個(gè)module都要手動(dòng)注冊(cè),ARouter為了解決Apt沒法匯總所有Module路由,通過類操作耗時(shí),才出現(xiàn)分組的概念。

原理分析

原理流程圖

定義路由

路由定義

對(duì)應(yīng)路由的定義,業(yè)界有兩種做法

  1. 參數(shù)放在path里面
  2. 參數(shù)放在query里面

參數(shù)定義在path里面的做法,有不需要額外傳參數(shù)的好處,但是沒有那么靈活,調(diào)試起來也沒有那么方便。

路由注冊(cè)

AndroidManifest里面的acitivity聲明scheme碼是不安全的,所有App都可以打開這個(gè)頁面,這里就產(chǎn)生有三種方式去注冊(cè),

  • 注解產(chǎn)生路由表,通過DispatchActivity轉(zhuǎn)發(fā)
  • AndroidManifest注冊(cè),將其export=fasle,但是再通過DispatchActivity轉(zhuǎn)發(fā)Intent,天貓就是這么做的,比上面的方法的好處是路由查找都是系統(tǒng)調(diào)用,省掉了維護(hù)路由表的過程,但是AndroidManifest配置還是比較不方便的
  • 注解自動(dòng)修改AndroidManifest,這種方式可以避免路由表匯總的問題,方案是這樣的,用自定義Lint掃描出注解相關(guān)的Activity,然后在processManifestTask后面修改Manifest

我現(xiàn)在還是采用了注解,第三種不穩(wěn)定

生成路由表

思路都是用Apt生成URL和activity的對(duì)應(yīng)關(guān)系

Airbnb

@DeepLink("foo://example.com/deepLink/{id}")
public class MainActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}

生成

public final class SampleModuleLoader implements Parser {
  public static final List<DeepLinkEntry> REGISTRY = Collections.unmodifiableList(Arrays.asList(
    new DeepLinkEntry("foo://example.com/deepLink/{id}", DeepLinkEntry.Type.METHOD, MainActivity.class, null)
    ));

  @Override
  public DeepLinkEntry parseUri(String uri) {
    for (DeepLinkEntry entry : REGISTRY) {
      if (entry.matches(uri)) {
        return entry;
      }
    }
    return null;
  }
}

阿里Arouter

@Route(path = "/deepLink")
public class MainActivity extends Activity {
 @Autowired
    String id;
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}

生成


public class ARouter$$Group$$m2 implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/deepLink", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/deepLink", null, null, -1, -2147483648));
  }
}

Activity Router

@Router("deeplink")
public class ModuleActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

生成

public final class RouterMapping_sdk {
  public static final void map() {
    java.util.Map<String,String> transfer = null;
    com.github.mzule.activityrouter.router.ExtraTypes extraTypes;

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("deeplink", ModuleActivity.class, null, extraTypes);

  }
}

OkDeepLink

public interface SampleService {


    @Path("/main")
    @Activity(MainActivity.class)
    void startMainActivity(@Query("key") String key);
}

生成

@After("execution(* okdeeplink.DeepLinkClient.init(..))")
  public void init() {
    DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
  }

初始化路由表

匯總路由表

這里就要提一下使用Apt會(huì)造成每個(gè)module都要手動(dòng)注冊(cè),因?yàn)锳PT是在javacompile任務(wù)前插入了一個(gè)task,所以只對(duì)自己的moudle處理注解

DeepLinkDispatch是這么做的

@DeepLinkModule
public class SampleModule {
}
@DeepLinkHandler({ SampleModule.class, LibraryDeepLinkModule.class })
public class DeepLinkActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate(
        new SampleModuleLoader(), new LibraryDeepLinkModuleLoader());
    deepLinkDelegate.dispatchFrom(this);
    finish();
  }
}

ARouter是通過類查找,就比較耗時(shí)了,所以他又加入了分組的概念,按需加載

/**
     * 通過指定包名,掃描包下面包含的所有的ClassName
     *
     * @param context     U know
     * @param packageName 包名
     * @return 所有class的集合
     */
    public static List<String> getFileNameByPackageName(Context context, String packageName) throws PackageManager.NameNotFoundException, IOException {
        List<String> classNames = new ArrayList<>();
        for (String path : getSourcePaths(context)) {
            DexFile dexfile = null;

            try {
                if (path.endsWith(EXTRACTED_SUFFIX)) {
                    //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                    dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                } else {
                    dexfile = new DexFile(path);
                }
                Enumeration<String> dexEntries = dexfile.entries();
                while (dexEntries.hasMoreElements()) {
                    String className = dexEntries.nextElement();
                    if (className.contains(packageName)) {
                        classNames.add(className);
                    }
                }
            } catch (Throwable ignore) {
                Log.e("ARouter", "Scan map file in dex files made error.", ignore);
            } finally {
                if (null != dexfile) {
                    try {
                        dexfile.close();
                    } catch (Throwable ignore) {
                    }
                }
            }
        }

        Log.d("ARouter", "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
        return classNames;
    }

ActivityRouter就比較巧妙了,通過Stub項(xiàng)目,其他地方都是provide的,只有主工程里面用Apt生成RouterInit類,雖然還是要寫module的注解

        // RouterInit
        if (hasModules) {
            debug("generate modules RouterInit");
            generateModulesRouterInit(moduleNames);
        } else if (!hasModule) {
            debug("generate default RouterInit");
            generateDefaultRouterInit();
        }

美柚路由是通過生成每個(gè)module的路由表,然后復(fù)制到app的assets目錄,運(yùn)行的時(shí)候遍歷asset目錄,反射對(duì)應(yīng)的activity

//拷貝生成的 assets/目錄到打包目錄
android.applicationVariants.all { variant ->
    def variantName = variant.name
    def variantNameCapitalized = variantName.capitalize()
    def copyMetaInf = tasks.create "copyMetaInf$variantNameCapitalized", Copy
    copyMetaInf.from project.fileTree(javaCompile.destinationDir)
    copyMetaInf.include "assets/**"
    copyMetaInf.into "build/intermediates/sourceFolderJavaResources/$variantName"
    tasks.findByName("transformResourcesWithMergeJavaResFor$variantNameCapitalized").dependsOn copyMetaInf
}

Metis是一個(gè)android中解決服務(wù)發(fā)現(xiàn)的庫,他是這么解決的,在app主工程中transfomer的時(shí)候去掃描所有modlue和jar帶注解的文件去生成路由表,然后把這個(gè)java文件編譯,但是這種方式需要掃描整個(gè)app會(huì)慢一點(diǎn),而且手動(dòng)去編譯java感覺不太穩(wěn)定的感覺

 def destDir
        List<String> classpaths = new ArrayList<>()
        transformInvocation.inputs.each { input ->

            input.jarInputs.each { jarInput ->

                def jarName = jarInput.name
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }

                def dest = transformInvocation.outputProvider.getContentLocation(jarName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

                classpaths.add(dest)
                mAction.loadJar(new JarFile(jarInput.file), jarInput.status)
                FileUtils.copyFile(jarInput.file, dest)

                mProject.logger.info("scan file:\t ${jarInput.file} status:${jarInput.status}")
            }

            input.directoryInputs.each { dirInput ->

                // 測(cè)試發(fā)現(xiàn): 如果目錄下的文件沒有任何改變,不會(huì)進(jìn)入到這個(gè) transform
                Map<File, Status> changedFiles = dirInput.changedFiles
                if (changedFiles == null || changedFiles.isEmpty()) {
                    // clean 后進(jìn)入, changed 為空
                    mAction.loadDirectory(dirInput.file)
                    mProject.logger.info("scan dir:\t ${dirInput.file}")
                } else {
                    mAction.loadChangedFiles(changedFiles)
                }

                destDir = transformInvocation.outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
                classpaths.add(destDir)
                FileUtils.copyDirectory(dirInput.file, destDir)
            }
        }

天貓 統(tǒng)跳協(xié)議 是最簡單的,轉(zhuǎn)發(fā)一下Intent就可以,但是這樣就沒法享受注解的好處了。

而OkDeepLink用aspectj解決了這個(gè)問題,會(huì)自動(dòng)匯總所有module的路由省略了這些多余的代碼。

@After("execution(* okdeeplink.DeepLinkClient.init(..))")
  public void init() {
    DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
  }

路由查找

路由查找就是查找路由表對(duì)應(yīng)的頁面,值得提起的就是因?yàn)橐m應(yīng)Module接入不同App,Scheme要自動(dòng)適應(yīng),路由表其實(shí)是Path---》Activity,這樣的話內(nèi)部跳轉(zhuǎn)的時(shí)候ARouterUri是沒有的。而我這邊是有的,我組裝了一個(gè)內(nèi)部的Uri,這樣攔截器不會(huì)有影響。

public Request buildRequest(Intent sourceIntent) {
        if (sourceIntent == null) {
            return null;
        }
        Intent newIntent = new Intent(sourceIntent);
        Uri uri = newIntent.getData();

        addNewTaskFlag(newIntent);

        if (uri != null) {
            addBundleQuery(newIntent, uri);

            Address entry = new DeepLinkClient(context).matchUrl(uri.toString());
            if (entry == null || entry.getActivityClass() == null) {
                return new Request(newIntent, this).setDeepLink(false);
            }
            newIntent.setComponent(new ComponentName(context, entry.getActivityClass()));

            return new Request(newIntent, this);
        }
        return new Request(newIntent, this).setDeepLink(false);

    }

路由分發(fā)

現(xiàn)在所有路由方案分發(fā)都是用Activity做分發(fā)的,這樣做會(huì)有這幾個(gè)缺點(diǎn)

  1. 每次都要啟動(dòng)一個(gè)Activity,而Activity就算不寫任何代碼啟動(dòng)都要0.1秒
  2. 如果是異步等待的話,Activiy要在合適時(shí)間finish,不然會(huì)有一層透明的頁面阻擋操作

對(duì)于第一個(gè)問題,有兩個(gè)方法

  1. QQ音樂是把DispatchActivity設(shè)為SingleInstacne,但是這樣的話,動(dòng)畫會(huì)奇怪,堆棧也會(huì)亂掉,后退會(huì)有一層透明的頁面阻擋操作
  2. DispatchActivity只在外部打開的時(shí)候調(diào)用

我選擇了第二種

對(duì)于第二個(gè)問題,有兩個(gè)方法

  1. DispatchActivity再把Intent轉(zhuǎn)發(fā)到Service,再finish,這種方法唯一的缺陷是攔截器里面的context是Servcie的activity,就沒發(fā)再攔截器里面彈出對(duì)話框了。
  2. DispatchActivity在打開和錯(cuò)誤的時(shí)候finish,如果activity已經(jīng)finish了,就用application的context去轉(zhuǎn)發(fā)路由

我選擇了第二種

  public void dispatchFrom(Intent intent) {
        new DeepLinkClient(this)
                .buildRequest(intent)
                .dispatch()
                .subscribe(new Subscriber<Request>() {
                    @Override
                    public void onCompleted() {
                        finish();
                    }

                    @Override
                    public void onError(Throwable e) {
                        finish();
                    }

                    @Override
                    public void onNext(Request request) {
                        Intent dispatchIntent = request.getIntent();
                        startActivity(dispatchIntent);
                    }
                });
    }

其實(shí)處理透明Activity阻擋操作可以采用取消所有事件變成無感頁面的方法
我找到一種方式解決這個(gè)問題解決透明Activity點(diǎn)擊不影響用戶操作

結(jié)果返回

這里我封裝了一個(gè)庫RxActivityResult去捕獲onActivityResult,這樣能保正流式調(diào)用

譬如拍照可以這樣寫,先定義一個(gè)接口

    public interface ImageCaptureService {


    @Action(MediaStore.ACTION_IMAGE_CAPTURE)
    Observable<Response> startImageCapture();
}

然后這樣調(diào)用

public class MainActivity extends AppCompatActivity {

    @Service
    ImageCaptureService imageCaptureService;
  
    public void captureImage(){
        imageCaptureService
                .startImageCapture()
                .subscribe(new Action1<Response>() {
                    @Override
                    public void call(Response response) {
                        Intent data = response.getData();
                        int resultCode = response.getResultCode();
                        if (resultCode == RESULT_OK) {
                            Bitmap imageBitmap = (Bitmap) data.getExtras().get("data");
                        }
                    }
                });
    }
}
}

是不是很簡單,原理是這樣的,通過封裝一個(gè)RxResultHoldFragment去處理onActivityResult

 private IActivityObservable buildActivityObservable() {

            T target = targetWeak.get();

            if (target instanceof FragmentActivity) {
                FragmentActivity activity = (FragmentActivity) target;
                android.support.v4.app.FragmentManager fragmentManager = activity.getSupportFragmentManager();
                IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);
                return activityObservable;
            }

            if (target instanceof Activity) {
                Activity activity = (Activity) target;
                FragmentManager fragmentManager = activity.getFragmentManager();
                IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);
                return activityObservable;
            }
            if (target instanceof Context) {
                final Context context = (Context) target;
                IActivityObservable activityObservable = new RxResultHoldContext(context);
                return activityObservable;
            }

            if (target instanceof Fragment) {
                Fragment fragment = (Fragment) target;
                FragmentManager fragmentManager = fragment.getFragmentManager();
                if (fragmentManager != null) {
                    IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);
                    return activityObservable;
                }
            }
            if (target instanceof android.support.v4.app.Fragment) {
                android.support.v4.app.Fragment fragment = (android.support.v4.app.Fragment) target;
                android.support.v4.app.FragmentManager fragmentManager = fragment.getFragmentManager();
                if (fragmentManager != null) {
                    IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);
                    return activityObservable;
                }
            }
            return new RxResultHoldEmpty();
        }

動(dòng)態(tài)攔截

攔截器是重中之重,有了攔截器可以做好多事情,可以說之所以要做頁面路由,就是為了要實(shí)現(xiàn)攔截器。ARouter是用線程等待實(shí)現(xiàn)的,但是現(xiàn)在有Rxjava了,可以實(shí)現(xiàn)更優(yōu)美的方式。
先來看一下我做的攔截器的效果.

@Intercept(path = "/second")
public class SecondInterceptor extends Interceptor {
    @Override
    public void intercept(final Call call) {

        Request request = call.getRequest();
        final Intent intent = request.getIntent();
        Context context = request.getContext();

        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("Intercept\n");
        stringBuffer.append("URL: " + request.getUrl() + "\n");

        AlertDialog.Builder builder = new AlertDialog.Builder(context,R.style.Theme_AppCompat_Dialog_Alert);
        builder.setTitle("Notice");
        builder.setMessage(stringBuffer);
        builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                call.cancel();
            }
        });
        builder.setPositiveButton("ok", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                intent.putExtra("key1", "value3");
                call.proceed();
            }
        });
        builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                call.cancel();
            }
        });
        builder.show();
    }
}

是不是很簡單,參考了部分OkHttp的實(shí)現(xiàn)思路,加入Rxjava,實(shí)現(xiàn)異步攔截。

首先將請(qǐng)求轉(zhuǎn)換成責(zé)任鏈模式RealCallChain,RealCallChain的call方法實(shí)際不會(huì)執(zhí)行路由跳轉(zhuǎn),只有Interceptor里面調(diào)用了call.proceed或者call.cancel才會(huì)執(zhí)行.

    private Observable<Request> buildRequest() {
        RealCallChain chain = new RealCallChain(interceptors, 0, request);
        chain.setTimeout(interceptTimeOut);
        chain.call();
        return chain
                .getRequestObservable()
                .map(new Func1<Request, Request>() {
                    @Override
                    public Request call(Request request) {
                        if (interceptors != null) {
                            for (Interceptor interceptor : interceptors) {
                                interceptor.onCall(request);
                            }
                        }
                        return request;
                    }
                });
    }

接著處理異步的問題,這里用到了Rxjava的AsyncSubject和BehaviorSubject,

  1. AsyncSubject具有僅釋放Observable釋放的最后一個(gè)數(shù)據(jù)的特性,作為路由請(qǐng)求的發(fā)送器
  2. BehaviorSubject具有一開始就會(huì)釋放最近釋放的數(shù)據(jù)的特性,作為路由攔截器的發(fā)送器

具體實(shí)現(xiàn)看核心代碼

    @Override
    public void proceed() {


        if (index >= interceptors.size()) {
            realCall();
            return;
        }
        final Interceptor interceptor = interceptors.get(index);
        Observable
                .just(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        interceptor.intercept(RealCallChain.this);
                    }
                });

        interceptorSubject.onNext(interceptor);
        index = index + 1;
    }

方法調(diào)用

大部分路由庫都是手動(dòng)拼參數(shù)調(diào)用路由的,這里模仿了Retrofit接口式調(diào)用,受了LiteRouter的啟發(fā),不過Retrofit使用了動(dòng)態(tài)代理,我使用的Apt沒有性能損耗。

通過Apt生成每個(gè)接口的實(shí)際方法

譬如把SecondService接口

public interface SecondService {

    @Path("/second")
    @Activity(SecondActivity.class)
    void startSecondActivity();
}

生成

@Aspect
public final class SecondService$$Provider implements SecondService {
  public DeepLinkClient deepLinkClient;

  public SecondService$$Provider(DeepLinkClient deepLinkClient) {
    this.deepLinkClient= deepLinkClient;
  }
  @Override
  public void startSecondActivity() {
    Intent intent = new Intent();
    intent.setData(Uri.parse("app://deeplink/second"));
    Request request = deepLinkClient.buildRequest(intent);
    if (request != null) {
      request.start();
    }
  }
  
  @Around("execution(* okdeeplink.DeepLinkClient.build(..))")
  public Object aroundBuildMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    DeepLinkClient target = (DeepLinkClient)joinPoint.getTarget();
    if (joinPoint.getArgs() == null || joinPoint.getArgs().length != 1) {
      return joinPoint.proceed();
    }
    Object arg = joinPoint.getArgs()[0];
    if (arg instanceof Class) {
      Class buildClass = (Class) arg;
      if (buildClass.isAssignableFrom(getClass())) {
        return new SecondService$$Provider(target);
      }
    }
    return joinPoint.proceed();
  }
}

然后調(diào)用

SecondService secondServicenew = DeepLinkClient(target).build(SecondService.class);

SecondService就生成了。
為了調(diào)用方便,直接在Activity或者fragement寫這段代碼,sampleServive就自動(dòng)生成了

  @Service
  SampleService sampleService;

但是如果用到MVP模式,不是在Activity里面調(diào)用路由,后面會(huì)支持在這些類里面自動(dòng)注入SampleService,現(xiàn)在先用java代碼build

參數(shù)獲取

大部分路由庫都是手動(dòng)獲取參數(shù)的,這樣還要傳入?yún)?shù)key比較麻煩,有三種做法

  1. Hook掉InstrumentationnewActivity方法,注入?yún)?shù)
  2. 注冊(cè)ActivityLifecycleCallbacks方法,注入?yún)?shù)
  3. Apt生成注入代碼,onCreate的時(shí)候bind一下

Hook掉InstrumentationnewActivity方法是這么實(shí)現(xiàn)的

@Deprecated
public class InstrumentationHook extends Instrumentation {
    /**
     * Hook the instrumentation's newActivity, inject
     * <p>
     * Perform instantiation of the process's {@link Activity} object.  The
     * default implementation provides the normal system behavior.
     *
     * @param cl        The ClassLoader with which to instantiate the object.
     * @param className The name of the class implementing the Activity
     *                  object.
     * @param intent    The Intent object that specified the activity class being
     *                  instantiated.
     * @return The newly instantiated Activity object.
     */
    public Activity newActivity(ClassLoader cl, String className,
                                Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {

//        return (Activity)cl.loadClass(className).newInstance();

        Class<?> targetActivity = cl.loadClass(className);
        Object instanceOfTarget = targetActivity.newInstance();

        if (ARouter.canAutoInject()) {
            String[] autoInjectParams = intent.getStringArrayExtra(ARouter.AUTO_INJECT);
            if (null != autoInjectParams && autoInjectParams.length > 0) {
                for (String paramsName : autoInjectParams) {
                    Object value = intent.getExtras().get(TextUtils.getLeft(paramsName));
                    if (null != value) {
                        try {
                            Field injectField = targetActivity.getDeclaredField(TextUtils.getLeft(paramsName));
                            injectField.setAccessible(true);
                            injectField.set(instanceOfTarget, value);
                        } catch (Exception e) {
                            ARouter.logger.error(Consts.TAG, "Inject values for activity error! [" + e.getMessage() + "]");
                        }
                    }
                }
            }
        }

        return (Activity) instanceOfTarget;
    }
}

業(yè)界的統(tǒng)一做法都是用apt,其他方式不穩(wěn)定,ARouterandroidannotationsJet, 思路都是一樣的,這里拿ARouter的代碼說明一下是怎么實(shí)現(xiàn)的

Autowired生成Test1Activity$$ARouter$$Autowired類,用inject方法找到AutowiredServiceImpl方法,AutowiredServiceImpl調(diào)用到Test1Activity$$ARouter$$Autowired

@Route(path = "/test/activity1")
public class Test1Activity extends AppCompatActivity {

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

        ARouter.getInstance().inject(this);
        }
    
    }

@Route(path = "/arouter/service/autowired")
public class AutowiredServiceImpl implements AutowiredService {
    private LruCache<String, ISyringe> classCache;
    private List<String> blackList;

    @Override
    public void init(Context context) {
        classCache = new LruCache<>(66);
        blackList = new ArrayList<>();
    }

    @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.
        }
    }
}
public class Test1Activity$$ARouter$$Autowired implements ISyringe {

  @Override
  public void inject(Object target) {
    Test1Activity substitute = (Test1Activity)target;
    substitute.name = substitute.getIntent().getStringExtra("name");
  }
}

OkDeepLink這里模仿了ARouter,不過支持類型更全一些,支持Bundle支持的所有類型,而且不需要在Acitivty的onCreate調(diào)用獲取代碼。
通過Apt把這段代碼

public class MainActivity extends AppCompatActivity {

    @Query("key")
    String key;
}

生成


@Aspect
public class MainActivity$$Injector {
  @Around("execution(* okdeeplink.sample.MainActivity.onCreate(..))")
  public void onCreate(ProceedingJoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Bundle dataBundle = new Bundle();
    Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
    Bundle targetBundle = BundleCompact.getSupportBundle(target);
    if(targetBundle != null) {
      dataBundle.putAll(targetBundle);
    }
    if(saveBundle != null) {
      dataBundle.putAll(saveBundle);
    }
    try {
      target.key= BundleCompact.getValue(dataBundle,"key",String.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    joinPoint.proceed();
  }

  @After("execution(* okdeeplink.sample.MainActivity.onSaveInstanceState(..))")
  public void onSaveInstanceState(JoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
    Intent intent = new Intent();
    intent.putExtra("key",target.key);
    saveBundle.putAll(intent.getExtras());
  }

  @Around("execution(* okdeeplink.sample.MainActivity.onNewIntent(..))")
  public void onNewIntent(ProceedingJoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Intent targetIntent = (Intent)joinPoint.getArgs()[0];
    Bundle dataBundle = targetIntent.getExtras();
    try {
      target.key= BundleCompact.getValue(dataBundle,"key",String.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    joinPoint.proceed();
  }
}

Module接入不同App

這里是參考ARouter把path作為key對(duì)應(yīng)activity,這樣接入到其他app中,就自動(dòng)替換了scheme碼

DeepLinkClient.addAddress(new Address("/main", MainActivity.class));

安全

現(xiàn)在有好多人用腳本來打開App,然后干壞事,其實(shí)時(shí)可以用路由來屏蔽掉.

有三種方法供君選擇,不同方法適合不同場(chǎng)景

簽名屏蔽

就是把所有參數(shù)加密成一個(gè)數(shù)據(jù)作為sign參數(shù),然后比對(duì)校驗(yàn),但是這要求加密方法不變,要不然升級(jí)了以前的app就打不開了

adb打開屏蔽

在android5.1手機(jī)上,用adb打開的app它的mReferrer為空

 public boolean isStartByAdb(android.app.Activity activity){
        if (Build.VERSION.SDK_INT >= 22) {
            android.net.Uri uri = ActivityCompat.getReferrer(activity);
            return uri == null | TextUtils.isEmpty(uri.toString()) ;
        }
        return false;
    }

包名過濾

在Android 4.4手機(jī)上, 寫了android:ssp的組件,只有特定應(yīng)用可以打開

<activity
            android:name="okdeeplink.DeepLinkActivity"
            android:noHistory="true"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:ssp="com.app.test"
                    android:host="app"
                    android:scheme="odl" />
            </intent-filter>
        </activity>

這三種方法,比較適合的還是簽名校驗(yàn)為主,adb過濾為副

如何解決路由造成的Activity堆棧錯(cuò)亂的問題

activity的launchMode使用不當(dāng)會(huì)照成閃屏頁面打開多次的問題,可以參考我這篇文章

未來展望

路由是一個(gè)基礎(chǔ)模塊,技術(shù)難度雖然不是很大,但是如果每個(gè)開發(fā)都重新踩一遍,性價(jià)比就比較低,我希望能把路由相關(guān)的所有鏈路都替你弄好,你可以留著時(shí)間去干其他更重要的事情,譬如陪陪家人,逗逗狗什么的。
接下來我會(huì)在這幾個(gè)方面努力,把整條鏈路補(bǔ)全。

  • 做一個(gè)像Swagger的平臺(tái),支持一鍵導(dǎo)出所有路由、二維碼打開路由
  • 注解修改AndroidManifest,不再需要路由表
  • 支持路由方法接收器,Url直接打開某個(gè)方法,不再局限Activity已實(shí)現(xiàn)

如果大家有意見,歡迎聯(lián)系我kingofzqj@gmail.com

參考文獻(xiàn)

業(yè)界做法

設(shè)計(jì)方案

個(gè)人開發(fā)

安全討論

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容