Android 元件化最佳實踐 ARetrofit 原理
本文首發於 vivo網際網路技術 微信公眾號 https://mp.weixin.qq.com/s/TXFt7ymgQXLJyBOJL8F6xg
作者:朱壹飛
ARetrofit 是一款針對Android元件之間通訊的路由框架,實現快速元件化開發的利器。本文主要講述 ARetrofit 實現的原理。
簡介
ARetrofit 是一款針對Android元件之間通訊的路由框架,實現快速元件化開發的利器。
原始碼連結:
元件化架構 APP Demo, ARetrofit 使用例項:
元件化
Android元件化已經不是一個新鮮的概念了,出來了已經有很長一段時間了,大家可以自行Google,可以看到一堆相關的文章。
簡單的來說,所謂的元件就是Android Studio中的Module,每一個Module都遵循高內聚的原則,透過ARetrofit 來實現無耦合的程式碼結構,如下圖:
每一個 Module 可單獨作為一個 project 執行,而打包到整體時 Module 之間的通訊透過 ARetrofit 完成。
ARetrofit 原理
講原理之前,我想先說說為什麼要ARetrofit。開發ARetrofit 這個專案的思路來源其實是 Retrofit,Retrofit 是Square公司開發的一款針對 Android 網路請求的框架,這裡不對Retrofit展開來講。主要是 Retrofit 框架使用非常多的設計模式,可以說 Retrofit 這個開源專案將Java的設計模式運用到了極致,當然最終提供的API也是非常簡潔的。如此簡潔的API,使得我們APP中的網路模組實現變得非常輕鬆,並且維護起來也很舒服。因此我覺得有必要將Android元件之間的通訊也變得輕鬆,使用者可以優雅的透過簡潔的API就可以實現通訊,更重要的是維護起來也非常的舒服。
ARetrofit 基本原理可以簡化為下圖所示:
1.透過註解宣告需要通訊的Activity/Fragment或者Class
2.每一個module透過annotationProcessor在編譯時生成待注入的RouteInject的實現類和AInterceptorInject的實現類。
這一步在執行app[build]時會輸出日誌,可以直觀的看到,如下圖所示:
注: AInjecton::Compiler >>> Apt interceptor Processor start... <<< 注: AInjecton::Compiler enclosindClass = null注: AInjecton::Compiler value = 3注: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$eCGVmTMvXG$$AInterceptorInject 注: AInjecton::Compiler add path= 3 and class= LoginInterceptor .... 注: AInjecton::Compiler >>> Apt route Processor start... <<< 注: AInjecton::Compiler enclosindClass = null注: AInjecton::Compiler value = /login-module/ILoginProviderImpl 注: AInjecton::Compiler enclosindClass = null注: AInjecton::Compiler value = /login-module/LoginActivity 注: AInjecton::Compiler enclosindClass = null注: AInjecton::Compiler value = /login-module/Test2Activity 注: AInjecton::Compiler enclosindClass = null注: AInjecton::Compiler value = /login-module/TestFragment 注: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$VWpdxWEuUx$$RouteInject 注: AInjecton::Compiler add path= /login-module/TestFragment and class= null注: AInjecton::Compiler add path= /login-module/LoginActivity and class= null注: AInjecton::Compiler add path= /login-module/Test2Activity and class= null注: AInjecton::Compiler add path= /login-module/ILoginProviderImpl and class= null注: AInjecton::Compiler >>> Apt route Processor succeed <<<
3.將編譯時生成的類注入到RouterRegister中,這個類主要用於維護路由表和攔截器,對應的[build]日誌如下:
TransformPluginLaunch >>> ========== Transform scan start =========== TransformPluginLaunch >>> ========== Transform scan end cost 0.238 secs and start inserting =========== TransformPluginLaunch >>> Inserting code to jar >> /Users/yifei/as_workspace/ARetrofit/app/build/intermediates/transforms/TransformPluginLaunch/release/8.jar TransformPluginLaunch >>> to class >> com/sjtu/yifei/route/RouteRegister.classInjectClassVisitor >>> inject to class:InjectClassVisitor >>> com/sjtu/yifei/route/RouteRegister{ InjectClassVisitor >>> public *** init() { InjectClassVisitor >>> register("com.sjtu.yifei.FBQWNfbTpY.com$$sjtu$$yifei$$FBQWNfbTpY$$RouteInject") InjectClassVisitor >>> register("com.sjtu.yifei.klBxerzbYV.com$$sjtu$$yifei$$klBxerzbYV$$RouteInject") InjectClassVisitor >>> register("com.sjtu.yifei.JmhcMMUhkR.com$$sjtu$$yifei$$JmhcMMUhkR$$RouteInject") InjectClassVisitor >>> register("com.sjtu.yifei.fpyxYyTCRm.com$$sjtu$$yifei$$fpyxYyTCRm$$AInterceptorInject") InjectClassVisitor >>> } InjectClassVisitor >>> } TransformPluginLaunch >>> ========== Transform insert cost 0.017 secs end ===========
4.Routerfit.register(Class<T> service) 這一步主要是透過動態代理模式實現介面中宣告的服務。
前面講的是整體的框架設計思想,便於讀者從全域性的覺得來理解ARetrofit的框架的架構。接下來,將待大家個個擊破上面提到的annotationProcessor、 transform在專案中如何使用,以及動態代理、攔截器功能的實現等細節。
一、annotationProcessor生成程式碼
annotationProcessor(註解處理器)是javac內建的一個用於編譯時掃描和處理註解(Annotation)的工具。簡單的說,在原始碼編譯階段,透過註解處理器,我們可以獲取原始檔內註解(Annotation)相關內容。Android Gradle 2.2 及以上版本提供annotationProcessor的外掛。
在ARetrofit中annotationProcessor對應的module是auto-complier,在使用annotationProcessor之前首先需要宣告好註解。關於註解不太瞭解或者遺忘的同學可直接參考我之前寫的Java註解這篇文章,本專案中宣告的註解在auto-annotation這個module中,主要有:
-
路由引數
-
intent flags
-
路由路徑key
-
宣告自定義攔截器
-
@RequestCode 路由引數
-
@Route路由
-
@Uri
-
@IMethod 用於標記註冊程式碼將插入到此方法中(transform中使用)
-
@Inject 用於標記需要被注入類,最近都將插入到標記了#com.sjtu.yifei.annotation.IMethod的方法中(transform中使用)
建立自定義的註解處理器,具體使用方法可參考利用註解動態生成程式碼,本專案中的註解處理器如下所示:
//這是用來註冊註解處理器要處理的原始碼版本。@SupportedSourceVersion(SourceVersion.RELEASE_8)//這個註解用來註冊註解處理器要處理的註解型別。有效值為完全限定名(就是帶所在包名和路徑的類全名@SupportedAnnotationTypes({ANNOTATION_ROUTE, ANNOTATION_GO})//來註解這個處理器,可以自動生成配置資訊@AutoService(Processor.class)public class IProcessor extends AbstractProcessor { }
生成程式碼的關鍵部分在GenerateAInterceptorInjectImpl 和 GenerateRouteInjectImpl中,以下貼出關鍵程式碼:
public void generateAInterceptorInjectImpl(String pkName) { try { String name = pkName.replace(".",DECOLLATOR) + SUFFIX; logger.info(String.format("auto generate class = %s", name)); TypeSpec.Builder builder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(Inject.class) .addSuperinterface(AInterceptorInject.class); ClassName hashMap = ClassName.get("java.util", "HashMap"); //Map<String, Class<?>> TypeName wildcard = WildcardTypeName.subtypeOf(Object.class); TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard); TypeName string = ClassName.get(Integer.class); TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny); MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getAInterceptors") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(map) .addStatement("$T interceptorMap = new $T<>()", map, hashMap); for (Map.Entry<Integer, ClassName> entry : interceptorMap.entrySet()) { logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().simpleName()); injectBuilder.addStatement("interceptorMap.put($L, $T.class)", entry.getKey(), entry.getValue()); } injectBuilder.addStatement("return interceptorMap"); builder.addMethod(injectBuilder.build()); JavaFile javaFile = JavaFile.builder(pkName, builder.build()) .build(); javaFile.writeTo(filer); } catch (Exception e) { e.printStackTrace(); } } public void generateRouteInjectImpl(String pkName) { try { String name = pkName.replace(".",DECOLLATOR) + SUFFIX; logger.info(String.format("auto generate class = %s", name)); TypeSpec.Builder builder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(Inject.class) .addSuperinterface(RouteInject.class); ClassName hashMap = ClassName.get("java.util", "HashMap"); //Map<String, String> TypeName wildcard = WildcardTypeName.subtypeOf(Object.class); TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard); TypeName string = ClassName.get(String.class); TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny); MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getRouteMap") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(map) .addStatement("$T routMap = new $T<>()", map, hashMap); for (Map.Entry<String, ClassName> entry : routMap.entrySet()) { logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().enclosingClassName()); injectBuilder.addStatement("routMap.put($S, $T.class)", entry.getKey(), entry.getValue()); } injectBuilder.addStatement("return routMap"); builder.addMethod(injectBuilder.build()); JavaFile javaFile = JavaFile.builder(pkName, builder.build()) .build(); javaFile.writeTo(filer); } catch (Exception e) { e.printStackTrace(); } }
二、Transform
Android Gradle 工具在 1.5.0 版本後提供了 Transfrom API, 允許第三方 Plugin在打包dex檔案之前的編譯過程中操作 .class 檔案。這一部分面向高階Android工程師的,面向位元組碼程式設計,普通工程師可不做了解。
寫到這裡也許有人會有這樣一個疑問,既然annotationProcessor這麼好用為什麼還有Transform面向位元組碼注入呢?這裡需要解釋以下,annotationProcessor具有侷限性,annotationProcessor只能掃描當前module下的程式碼,且對於第三方的jar、aar檔案都掃描不到。而Transform就沒有這樣的侷限性,在打包dex檔案之前的編譯過程中操作.class 檔案。
關於Transfrom API在Android Studio中如何使用可以參考Transform API — a real world example,順便提供一下位元組碼指令方便我們讀懂ASM。
本專案中的Transform外掛在AInject中,實現原始碼TransformPluginLaunch如下,貼出關鍵部分:
/** * * 標準transform的格式,一般實現transform可以直接複製一份重新命名即可 * * 兩處todo實現自己的位元組碼增強/最佳化操作 */class TransformPluginLaunch extends Transform implements Plugin<Project> { @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) //todo step1: 先掃描 transformInvocation.inputs.each { TransformInput input -> input.jarInputs.each { JarInput jarInput -> ... } input.directoryInputs.each { DirectoryInput directoryInput -> //處理完輸入檔案之後,要把輸出給下一個任務 ... } } //todo step2: ...完成程式碼注入 if (InjectInfo.get().injectToClass != null) { ... } } /** * 掃描jar包 * @param jarFile */ static void scanJar(File jarFile, File destFile) { } /** * 掃描檔案 * @param file */ static void scanFile(File file, File dest) { ... } }
注入程式碼一般分為兩個步驟:
-
第一步:掃描
這一部分主要是掃描的內容有:
注入類和方法的資訊,是AutoRegisterContract的實現類和其中@IMethod,@Inject的方法。
待注入類的和方法資訊,是RouteInject 和 AInterceptorInject實現類且被@Inject註解的。 -
第二步:注入
以上掃描的結果,將待注入類注入到注入類的過程。這一過程面向ASM操作,可參考位元組碼指令來讀懂以下的關鍵注入程式碼:
class InjectClassVisitor extends ClassVisitor { ... class InjectMethodAdapter extends MethodVisitor { InjectMethodAdapter(MethodVisitor mv) { super(Opcodes.ASM5, mv) } @Override void visitInsn(int opcode) { Log.e(TAG, "inject to class:") Log.e(TAG, own + "{") Log.e(TAG, " public *** " + InjectInfo.get().injectToMethodName + "() {") if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { InjectInfo.get().injectClasses.each { injectClass -> injectClass = injectClass.replace('/', '.') Log.e(TAG, " " + method + "(\"" + injectClass + "\")") mv.visitVarInsn(Opcodes.ALOAD, 0) mv.visitLdcInsn(injectClass) mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, own, method, "(Ljava/lang/String;)V", false) } } Log.e(TAG, " }") Log.e(TAG, "}") super.visitInsn(opcode) } ... } ... }
三、動態代理
定義:為其它物件提供一種代理以控制對這個物件的訪問控制;在某些情況下,客戶不想或者不能直接引用另一個物件,這時候代理物件可以在客戶端和目標物件之間起到中介的作用。
Routerfit.register(Class<T> service) 這裡就是採用動態代理的模式,使得ARetrofit的API非常簡潔,使用者可以優雅定義出路由介面。關於動態代理的學習難度相對來說還比較小,想了解的同學可以參考這篇文章java動態代理。
本專案相關原始碼:
public final class Routerfit { ... private <T> T create(final Class<T> service) { RouterUtil.validateServiceInterface(service); return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } ServiceMethod<Object> serviceMethod = (ServiceMethod<Object>) loadServiceMethod(method, args); if (!TextUtils.isEmpty(serviceMethod.uristring)) { Call<T> call = (Call<T>) new ActivityCall(serviceMethod); return call.execute(); } try { if (serviceMethod.clazz == null) { throw new RouteNotFoundException("There is no route match the path \"" + serviceMethod.routerPath + "\""); } } catch (RouteNotFoundException e) { Toast.makeText(ActivityLifecycleMonitor.getApp(), e.getMessage(), Toast.LENGTH_SHORT).show(); e.printStackTrace(); } if (RouterUtil.isSpecificClass(serviceMethod.clazz, Activity.class)) { Call<T> call = (Call<T>) new ActivityCall(serviceMethod); return call.execute(); } else if (RouterUtil.isSpecificClass(serviceMethod.clazz, Fragment.class) || RouterUtil.isSpecificClass(serviceMethod.clazz, android.app.Fragment.class)) { Call<T> call = new FragmentCall(serviceMethod); return call.execute(); } else if (serviceMethod.clazz != null) { Call<T> call = new IProviderCall<>(serviceMethod); return call.execute(); } if (serviceMethod.returnType != null) { if (serviceMethod.returnType == Integer.TYPE) { return -1; } else if (serviceMethod.returnType == Boolean.TYPE) { return false; } else if (serviceMethod.returnType == Long.TYPE) { return 0L; } else if (serviceMethod.returnType == Double.TYPE) { return 0.0d; } else if (serviceMethod.returnType == Float.TYPE) { return 0.0f; } else if (serviceMethod.returnType == Void.TYPE) { return null; } else if (serviceMethod.returnType == Byte.TYPE) { return (byte)0; } else if (serviceMethod.returnType == Short.TYPE) { return (short)0; } else if (serviceMethod.returnType == Character.TYPE) { return null; } } return null; } }); } ... }
這裡ServiceMethod是一個非常重要的類,使用了外觀模式,主要用於解析方法中的被註解所有資訊並儲存起來。
四、攔截器鏈實現
本專案中的攔截器鏈設計,使得使用者可以非常優雅的處理業務邏輯。如下:
@Interceptor(priority = 3)public class LoginInterceptor implements AInterceptor { private static final String TAG = "LoginInterceptor"; @Override public void intercept(final Chain chain) { //Test2Activity 需要登入 if ("/login-module/Test2Activity".equalsIgnoreCase(chain.path())) { Routerfit.register(RouteService.class).launchLoginActivity(new ActivityCallback() { @Override public void onActivityResult(int i, Object data) { if (i == Routerfit.RESULT_OK) {//登入成功後繼續執行 Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登入成功", Toast.LENGTH_LONG).show(); chain.proceed(); } else { Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登入取消/失敗", Toast.LENGTH_LONG).show(); } } }); } else { chain.proceed(); } } }
這一部分實現的思想是參考了okhttp中的攔截器,這裡使用了java設計模式責任鏈模式,具體實現歡迎閱讀原始碼。
總結
基本上讀完本文可以對 ARetrofit 的核心原理有了很清晰的理解.簡單來說 ARetrofit 透過 annotationProcessor 在編譯時獲取路由相關內容,透過 ASM 實現了可跨模組獲取物件,最終透過動態代理實現面向切面程式設計(AOP)。
ARetrofit 相對於其他同型別的路由框架來說,其優點是提供了更加簡潔的 API,其中高階用法對開發者提供了更加靈活擴充套件方式,開發者還可以結合 RxJava 完成複雜的業務場景。具體可以參考 ARetrofit 的基本用法,以及 Issues。
———— 參考資料 ————
-
Java註解:
-
利用註解動態生成程式碼: https://blog.csdn.net/Gaugamela/article/details/79694302
-
Transform API — a real world example:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2652804/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android元件化最佳實踐-ARetrofitAndroid元件化
- Android 元件化最佳實踐Android元件化
- iOS 解藕、元件化最佳實踐iOS元件化
- Mysql事務原理與最佳化最佳實踐MySql
- Android元件化探索與實踐Android元件化
- Android元件化開發實踐Android元件化
- Rocketmq原理&最佳實踐MQ
- Android元件化實踐專案分享Android元件化
- Android徹底元件化方案實踐Android元件化
- Android Emoji 最佳實踐Android
- Android MVP 最佳實踐AndroidMVP
- Android客戶端專案元件化實踐Android客戶端元件化
- Android元件化開發實踐和案例分享Android元件化
- iOS元件化實踐iOS元件化
- Android最佳效能實踐(4):佈局優化技巧Android優化
- React 進階二-元件最佳實踐React元件
- ?vue元件釋出npm最佳實踐Vue元件NPM
- 編寫 React 元件的最佳實踐React元件
- Android SharedPreferences最佳實踐Android
- 使用Android API最佳實踐AndroidAPI
- Android SharedPreference最佳實踐Android
- Guava Cache 原理分析與最佳實踐Guava
- 基於CocoaPods的元件化原理及私有庫實踐元件化
- Android篇 | 愛奇藝App啟動最佳化實踐分享AndroidAPP
- 分散式鎖實現原理與最佳實踐分散式
- 多Target、元件化實踐元件化
- iOS 元件化實踐思考iOS元件化
- 基於 MVP 的 Android 元件化開發框架實踐MVPAndroid元件化框架
- 微服務快取原理與最佳實踐微服務快取
- Android元件化開發實踐(一):為什麼要進行元件化開發?Android元件化
- Android 路由設計最佳實踐Android路由
- Android許可權最佳實踐Android
- 前端工程化最佳實踐前端
- Vue工程化最佳實踐Vue
- Vue 工程化最佳實踐Vue
- Gulp 結構化最佳實踐
- Golang效能最佳化實踐Golang
- 元件化工具BeeHive(二):元件化實踐元件化Hive