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ā)到指定頁面

使用場景

  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)寫onSaveInstance驻谆、onCreate(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 ->

                // 測試發(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)定,ARouter辕近、androidannotations韵吨、Jet, 思路都是一樣的,這里拿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í)可以用路由來屏蔽掉.

有三種方法供君選擇,不同方法適合不同場景

簽名屏蔽

就是把所有參數(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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吼鱼,隨后出現(xiàn)的幾起案子蓬豁,更是在濱河造成了極大的恐慌,老刑警劉巖菇肃,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件地粪,死亡現(xiàn)場離奇詭異,居然都是意外死亡琐谤,警方通過查閱死者的電腦和手機(jī)蟆技,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斗忌,“玉大人质礼,你說我怎么就攤上這事≈簦” “怎么了眶蕉?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長唧躲。 經(jīng)常有香客問我造挽,道長,這世上最難降的妖魔是什么弄痹? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任饭入,我火速辦了婚禮,結(jié)果婚禮上肛真,老公的妹妹穿的比我還像新娘圣拄。我一直安慰自己,他們只是感情好毁欣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布庇谆。 她就那樣靜靜地躺著,像睡著了一般凭疮。 火紅的嫁衣襯著肌膚如雪饭耳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天执解,我揣著相機(jī)與錄音寞肖,去河邊找鬼纲酗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛新蟆,可吹牛的內(nèi)容都是我干的觅赊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼琼稻,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼吮螺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起帕翻,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤鸠补,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后嘀掸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體紫岩,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年睬塌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泉蝌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡揩晴,死狀恐怖梨与,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情文狱,我是刑警寧澤粥鞋,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站瞄崇,受9級(jí)特大地震影響呻粹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苏研,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一等浊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧摹蘑,春花似錦筹燕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至大渤,卻和暖如春制妄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泵三。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工耕捞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衔掸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓俺抽,卻偏偏與公主長得像敞映,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子磷斧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,070評(píng)論 25 707
  • 作者:Alon Zakai 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/pos...
    胡子大哈閱讀 3,034評(píng)論 0 2
  • 一 過度自信是我們的天性 我們習(xí)慣性的低估別人振愿,高估自己,已經(jīng)高估自己擁有的東西瞳抓。需要用客觀的科學(xué)來展示出來,然后...
    劉子逸閱讀 255評(píng)論 0 0
  • 2017年十一中秋假期伏恐,我在禹州了五天孩哑,來運(yùn)城了三天,完成了假期作業(yè)翠桦。 1横蜒、數(shù)學(xué)作業(yè) 2、我的寫繪 3销凑、我的閱讀 ...
    小寶巖松閱讀 240評(píng)論 0 0