編輯: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
如果大家有意見,歡迎聯系我[email protected]
Android平台有三種網絡接口可以使用,他們分別是:java.net.*(標准Java接口)、Org.apache接口和Android.net.*(Androi
整個框架式不同於androidannotations,Roboguice等ioc框架,這是一個類似spring的實現方式。在整應用的生命周期中找到切入點,然後對a
一、WebService介紹 WebService是基於SOAP協議可實現web服務器與web服務器之間的通信,因采用SOAP協議傳送XML數據具有平台無關性,也
本文由碼農網 – 小峰原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! Android PullToRefresh是一款可以再Android系