編輯:Android資訊
這篇文章會告訴你
OnActivityResult,不需要再進行requstCode判斷Retrofit接口式調用Activity的調用方安全調用Apt不能匯總所有Module路由的問題當前Android的路由庫實在太多了,剛開始的時候想為什麼要用路由表的庫,用Android原生的Scheme碼不就好了,又不像iOS只能類依賴,後面越深入就越發現當時想的太簡單了,後面看到Retrofit和OKHttp,才想到頁面請求本質和網絡請求不是一樣嗎,終於業界最簡單高效的路由方案1.0出來了
OkDeepLink
根據路由表將頁面請求分發到指定頁面
Android原生已經支持AndroidManifest去管理App跳轉,為什麼要有路由庫,這可能是大部分人接觸到Android各種Router庫不太明白的地方,這裡我講一下我的理解
路由說到底還是為了解決開發者遇到的各種奇葩需求,使用簡單、侵入性低、維護方便是首要條件,不影響你原來的代碼,寫入代碼也很少,這裡就要說說我的OkDeepLink的五大功能了,五大功能瞬間擊中你的各種痛點,早點下班不是夢。
AndroidManifest中找到那個Actvity寫Scheme和Intent FilterRetrofit接口式調用,實現方式用apt,不耗性能,參數調用不再是問題OnActivityResult,支持RxJava響應式調用,不再需要進行requestCode判斷onSaveInstance、onCreate(SaveInstace)、onNewIntent(Intent)、getQueryParamer
具體使用見OkDeepLink

大部分路由庫都用Apt(編譯時注解)生成路由表,然後用路由表轉發到指定頁面
onCreate中手動調用get方法
參數定義在path,不利於多人協作
Apt依賴注入,但是要手動調用get方法
手動調用
手動調用
結果返回
Rxjava回調
onActivityResult
onActivityResult
onActivityResult
onActivityResult
Module接入不同App
支持
不支持
支持
不支持
支持
其實說到底,路由的本質就是注冊再轉發,圍繞著轉發可以進行各種操作,攔截,替換,參數獲取等等,其他Apt、Rxjava說到底都只是為了方便使用出現的,這裡你會發現各種路由庫反而為了修復各種工具帶來的問題,出現了原來沒有的問題,譬如DeepLinkDispatch為了解決Apt沒法匯總所有Module路由,每個module都要手動注冊,ARouter為了解決Apt沒法匯總所有Module路由,通過類操作耗時,才出現分組的概念。


我這邊是完全按照URL規范了,這裡要說一下,現在好多方法是把參數定義在path裡面的,雖然這樣做,有不需要額外傳參數的好處,但是這樣路由就沒有那麼靈活,調試起來就沒有那麼方便了。
建議有好幾款app的公司,host都一樣,只有scheme不一樣,這樣只要替換Scheme就能實現降級,維護也簡單。
AndroidManifest裡面的acitivity聲明scheme碼是不安全的,所有App都可以打開這個頁面,這裡就產生有兩種方式去注冊,
DispatchActivity轉發AndroidManifest注冊,將其export=fasle,但是再通過DispatchActivity轉發Intent,天貓就是這麼做的,比上面的方法的好處是路由查找都是系統調用,省掉了維護路由表的過程,但是AndroidManifest配置還是比較不方便的我現在還是采用了注解,後面我會結合兩種方法,將注解自動修改AndroidManifest,對於接入方是沒有變動的,方法已經找到了,用自定義Lint掃描出注解相關的Activity,然後用processManifestTask修改Manifest,有個demo了,後面會接入。
譬如通過Apt把這段代碼
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會造成每個module都要手動注冊
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是通過類查找,就比較耗時了,所以他又加入了分組的概念,按需加載
/**
* 通過指定包名,掃描包下面包含的所有的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項目,其他地方都是provide的,只有主工程裡面用Apt生成RouterInit類,雖然還是要寫module的注解
// RouterInit
if (hasModules) {
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) {
debug("generate default RouterInit");
generateDefaultRouterInit();
}
天貓 統跳協議 是最簡單的,轉發一下Intent就可以,但是這樣就沒法享受注解的好處了。
而我用aspectj解決了這個問題,會自動匯總所有module的路由省略了這些多余的代碼,或者有誰知道用Apt自生怎麼解決,請聯系我一下。
@After("execution(* okdeeplink.DeepLinkClient.init(..))")
public void init() {
DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
}
路由查找就是查找路由表對應的頁面,值得提起的就是因為要適應Module接入不同App,Scheme要自動適應,路由表其實是Path—》Activity,這樣的話內部跳轉的時候ARouterUri是沒有的。而我這邊是有的,我組裝了一個內部的Uri,這樣攔截器不會有影響。
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);
}
現在所有路由方案分發都是用Activity做分發的,這樣做會有這幾個缺點
finish,不然會有一層透明的頁面阻擋操作對於第一個問題,有兩個方法
DispatchActivity設為SingleInstacne,但是這樣的話,動畫會奇怪,堆棧也會亂掉,後退會有一層透明的頁面阻擋操作DispatchActivity只在外部打開的時候調用我選擇了第二種
對於第二個問題,有兩個方法
DispatchActivity再把Intent轉發到Service,再finish,這種方法唯一的缺陷是攔截器裡面的context是Servcie的activity,就沒發再攔截器裡面彈出對話框了。DispatchActivity在打開和錯誤的時候finish,如果activity已經finish了,就用application的context去轉發路由我選擇了第二種
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);
}
});
}
其實處理透明Activity阻擋操作可以采用取消所有事件變成無感頁面的方法,但是還是覺得會影響activity堆棧沒有采用這種方案
getwindow().addflags( windowmanager.layoutparams.flag_not_focusable | windowmanager.layoutparams.flag_not_touch_modal | windowmanager.layoutparams.flag_not_touchable);
這裡我封裝了一個庫RxActivityResult去捕獲onActivityResult,這樣能保正流式調用
譬如拍照可以這樣寫,先定義一個接口
public interface ImageCaptureService {
@Action(MediaStore.ACTION_IMAGE_CAPTURE)
Observable<Response> startImageCapture();
}
然後這樣調用
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");
}
}
});
}
}
}
是不是很簡單,原理是這樣的,通過封裝一個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();
}
攔截器是重中之重,有了攔截器可以做好多事情,可以說之所以要做頁面路由,就是為了要實現攔截器。ARouter是用線程等待實現的,但是現在有Rxjava了,可以實現更優美的方式。
先來看一下我做的攔截器的效果.
@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的實現思路,加入Rxjava,實現異步攔截。
首先將請求轉換成責任鏈模式RealCallChain,RealCallChain的call方法實際不會執行路由跳轉,只有Interceptor裡面調用了call.proceed或者call.cancel才會執行.
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,
具體實現看核心代碼
@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;
}
大部分路由庫都是手動拼參數調用路由的,這裡模仿了Retrofit接口式調用,受了LiteRouter的啟發,不過Retrofit使用了動態代理,我使用的Apt沒有性能損耗。
通過Apt生成每個接口的實際方法
譬如把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();
}
}
然後調用
SecondService secondServicenew = DeepLinkClient(target).build(SecondService.class);
SecondService就生成了。
為了調用方便,直接在Activity或者fragement寫這段代碼,sampleServive就自動生成了
@Service SampleService sampleService;
但是如果用到MVP模式,不是在Activity裡面調用路由,後面會支持在這些類裡面自動注入SampleService,現在先用java代碼build
大部分路由庫都是手動獲取參數的,這樣還要傳入參數key比較麻煩,這裡模仿了ARouter,不過我支持類型更全一些,支持Bundle支持的所有類型,而且不需要在Acitivty的onCreate調用獲取代碼。
通過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();
}
}
這裡是參考ARouter把path作為key對應activity,這樣接入到其他app中,就自動替換了scheme碼了
DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
現在有好多人用腳本來打開App,然後干壞事,其實時可以用路由來屏蔽掉.
有三種方法供君選擇,不同方法適合不同場景
就是把所有參數加密成一個數據作為sign參數,然後比對校驗,但是這要求加密方法不變,要不然升級了以前的app就打不開了
在android5.1手機上,用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手機上, 寫了android:ssp的組件,只有特定應用可以打開
<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>
這三種方法,比較適合的還是簽名校驗為主,adb過濾為副
activity的launchMode使用不當會照成閃屏頁面打開多次的問題,可以參考我這篇文章。
路由是一個基礎模塊,技術難度雖然不是很大,但是如果每個開發都重新踩一遍,性價比就比較低,我希望能把路由相關的所有鏈路都替你弄好,你可以留著時間去干其他更重要的事情,譬如陪陪家人,逗逗狗什麼的。
接下來我會在這幾個方面努力,把整條鏈路補全。
Swagger的平台,支持一鍵導出所有路由、二維碼打開路由Activity如果大家有意見,歡迎聯系我kingofzqj@gmail.com
Android 三大網絡通訊方式詳解
Android平台有三種網絡接口可以使用,他們分別是:java.net.*(標准Java接口)、Org.apache接口和Android.net.*(Androi
高質量 Android 開發框架 LoonAndroid 詳解
整個框架式不同於androidannotations,Roboguice等ioc框架,這是一個類似spring的實現方式。在整應用的生命周期中找到切入點,然後對a
Android中使用Android Ksoap2調用WebService
一、WebService介紹 WebService是基於SOAP協議可實現web服務器與web服務器之間的通信,因采用SOAP協議傳送XML數據具有平台無關性,也
Android下拉刷新組件Android PullToRefresh
本文由碼農網 – 小峰原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! Android PullToRefresh是一款可以再Android系