引子
這篇文章會(huì)告訴你
- 什么是路由涩蜘,是為了解決什么問題才產(chǎn)生的
- 業(yè)界現(xiàn)狀是怎么樣的,我們可以做什么來優(yōu)化當(dāng)前的問題
- 路由設(shè)計(jì)思路是怎么樣的熏纯,該怎么設(shè)計(jì)比較好
- 如何用注解實(shí)現(xiàn)路由表
- URL的參數(shù)如何依賴注入到Activity同诫、Fragement
- 如何Hook
OnActivityResult
,不需要再進(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ā)到指定頁面
使用場景
- App接收到一個(gè)通知蝗罗,點(diǎn)擊通知打開App的某個(gè)頁面
- 瀏覽器App中點(diǎn)擊某個(gè)鏈接打開App的某個(gè)頁面
- 運(yùn)營活動(dòng)需求艇棕,動(dòng)態(tài)把原生的頁面替換成H5頁面
- 打開頁面需要某些條件蝌戒,先驗(yàn)證完條件,再去打開那個(gè)頁面
- 不合法的打開App的頁面被屏蔽掉
- H5打開鏈接在所有平臺(tái)都一樣沼琉,方便統(tǒng)一跳轉(zhuǎn)
- 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)用不再是問題 - Hook
OnActivityResult
,支持RxJava響應(yīng)式調(diào)用,不再需要進(jìn)行requestCode判斷 -
參數(shù)依賴注入楔敌,自動(dòng)保存,不再需要手動(dòng)寫
onSaveInstance
驻谆、onCreate(SaveInstace)
卵凑、onNewIntent(Intent)
庆聘、getQueryParamer
詳細(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è)界有兩種做法
- 參數(shù)放在path里面
- 參數(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)
- 每次都要啟動(dòng)一個(gè)Activity听系,而Activity就算不寫任何代碼啟動(dòng)都要0.1秒
- 如果是異步等待的話贝奇,Activiy要在合適時(shí)間
finish
虹菲,不然會(huì)有一層透明的頁面阻擋操作
對(duì)于第一個(gè)問題,有兩個(gè)方法
- QQ音樂是把
DispatchActivity
設(shè)為SingleInstacne
,但是這樣的話掉瞳,動(dòng)畫會(huì)奇怪毕源,堆棧也會(huì)亂掉浪漠,后退會(huì)有一層透明的頁面阻擋操作 -
DispatchActivity
只在外部打開的時(shí)候調(diào)用
我選擇了第二種
對(duì)于第二個(gè)問題,有兩個(gè)方法
-
DispatchActivity
再把Intent轉(zhuǎn)發(fā)到Service
,再finish霎褐,這種方法唯一的缺陷是攔截器里面的context是Servcie的activity址愿,就沒發(fā)再攔截器里面彈出對(duì)話框了。 -
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胎围,
- AsyncSubject具有僅釋放Observable釋放的最后一個(gè)數(shù)據(jù)的特性,作為路由請(qǐng)求的發(fā)送器
- 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比較麻煩颤介,有三種做法
- Hook掉
Instrumentation
的newActivity
方法,注入?yún)?shù) - 注冊(cè)
ActivityLifecycleCallbacks
方法,注入?yún)?shù) -
Apt
生成注入代碼滚朵,onCreate
的時(shí)候bind一下
Hook掉Instrumentation
的newActivity
方法是這么實(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è)方法姨夹,不再局限已實(shí)現(xiàn)Activity
如果大家有意見,歡迎聯(lián)系我kingofzqj@gmail.com
參考文獻(xiàn)
業(yè)界做法
設(shè)計(jì)方案
- UrlRouter路由框架的設(shè)計(jì)
- 移動(dòng)端路由層設(shè)計(jì)
- 客戶端路由動(dòng)態(tài)配置
- 移動(dòng)端基于動(dòng)態(tài)路由的架構(gòu)設(shè)計(jì)
- Android組件化通信(多進(jìn)程)
- iOS 組件化 —— 路由設(shè)計(jì)思路分析
- QQ音樂首頁Activity的單例實(shí)現(xiàn)
個(gè)人開發(fā)
- LiteRouter 模仿retrofit矾策,各個(gè)業(yè)務(wù)分根據(jù)需求約定好接口,就像一份接口文檔一樣
- ActivityRouter
- ActivityRouter2
- AndRouter
- Router
- Router2
- router-android
安全討論
- 如何在Activity中獲取調(diào)用者 討論了android里面原生支持找到路由來源的可能性峭沦,分析了referrer是如何產(chǎn)生的
-
LauncherFrom
提供了一種hook activitythread找到launchedFromPackage的方法贾虽,不過也只支持5.0以上 -
高效過濾Intents
只有包含特定Package URL的 intent 才會(huì)喚起頁面