1. 程式人生 > >《Android徹底元件化方案實踐》讀後分析

《Android徹底元件化方案實踐》讀後分析

《得到》App相關開發成員開源關於Android元件話的實踐方案及demo,看似實現程式碼簡單,但核心是元件化的實現想法以及自動整合、單獨執行的構建實現。現對其中的核心思想和gradle外掛寫些自己的理解以及Gradle外掛開發學習筆記,填補這方面的知識以及拓展到自己的專案中。

元件化核心思想

作者在文中提到對專案程式碼元件化成各個模組,這些模組相互獨立,不耦合,從主專案中抽離出來後,主專案依然編譯通過,且各模組能進行單位測試,測試完成了又可以釋出元件,並在主專案中自動完成編譯整合打包,用作者的話來講就是有元件的生命週期,能加入-onCreate(),且能剝離-onStop()。其原理實現程式碼會比較簡單,思想架構設計還是挺值得學習的:

  • 類BinderC/S模式-把介面和實現分離,主專案僅僅通過元件的暴露的介面服務來程式設計,對元件的具體實現不管,各個抽離出的元件都暴露一個介面服務,這樣也同時解決資料傳遞和程式碼隔離,解耦。如下例子
public interface ReadBookService {
      //提供介面服務供主專案的程式設計
    Fragment getReadBookFragment();
}

//主專案統計介面程式設計
        Router router = Router.getInstance();
        if (router.getService(ReadBookService.class
.getSimpleName()) != null) { ReadBookService service = (ReadBookService) router.getService(ReadBookService.class.getSimpleName()); fragment = service.getReadBookFragment(); ft = getSupportFragmentManager().beginTransaction(); ft.add(R.id
.tab_content, fragment).commitAllowingStateLoss(); }
  • 類Activity生命週期思想來管理維護各元件新增和刪除

    每個元件都暴露一個介面服務,通過生命週期的方式來動態新增、刪除元件,這樣很方便管理整合或單元測試各個元件。故作者設計了個類似管理的角色-Router來註冊,登出這些元件的生命週期介面類-IApplicationLike,例子-ReaderAppLike 程式碼如下:

//各個元件都實現這個介面來現實對元件的新增和刪除管理
public interface IApplicationLike {

    void onCreate();

    void onStop();
}


public class AppApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //在Application中註冊,趕在使用元件之前,
        Router.registerComponent("com.mrzhang.reader.applike.ReaderAppLike");
    }
}

public class Router {

 public static void registerComponent(String classname) {
        try {
            Class clazz = Class.forName(classname);
            IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
            applicationLike.onCreate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}


public class ReaderAppLike implements IApplicationLike {

    Router router = Router.getInstance();

    @Override
    public void onCreate() {
    //在元件初始化時,完成新增元件具體實現
        router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
    }

    @Override
    public void onStop() {
        router.removeService(ReadBookService.class.getSimpleName());
    }
}
  • 類路由方式實現UI跳轉:每個元件可以註冊自己所能處理的短鏈的scheme和host,並定義傳輸資料的格式。然後註冊到統一的UIRouter中,UIRouter通過scheme和host的匹配關係負責分發路由。這個也有開源的庫,作者借鑑了來實現。

元件化實現

元件如何拆分?

每個大功能叢集為一個模組,如作者文中提到的分享模組和閱讀模組,這些模組能夠單獨執行起來,作為一個可以單獨執行的單元,又能向外暴露單獨的唯一功能。這樣拆分下來即可以分為如下幾類:

1、RunaloneModule:在AS中對每個模組開發建一個單獨的Module,通過自定義的外掛實現其既可以是library又可以是application。這樣才能釋出單獨的元件又能單獨執行除錯。

2、ComponentServicesModule:各個元件之間以及主專案對各元件的引用通過各元件的外露的介面服務來實現,從而形成一個單獨的Module-元件介面服務library。

3、BasicComponentModule:對各個元件以及主專案中需要使用的公共基礎工具類以及公共資源單獨模組化形成單獨的元件Module:BasicLib 以及Module:BasicRes

4、ComponentProtocolModule:各個元件之間的以及主專案和元件之間的資料傳輸、UI跳轉、元件生命週期管理介面。資料傳輸以及引用都通過介面保留的方式完成,UI跳轉通過UIRouter的形式來統一對註冊的UI進行處理。

引用原文的一張圖來看之間的依賴關係:
這裡寫圖片描述

自動構建整合開發

上述分析了作者解耦、整合、元件生命週期管理的思想,在實踐demo中,如果有很多元件的話,如何靈活的新增元件以及元件單獨除錯,這些幕後的工作若是通過手動來修改主專案完成配置,那會顯得很麻煩,且風險很大,故作者提供了外掛來完成元件由module和application切換,主專案通過gradle.properties配置來自動完成元件的新增打包到apk中,並完成Application中注入register元件程式碼。這一起工作都由plugin: ‘com.dd.comgradle’來完成,這個外掛具體如何開發不多說,可以自行看程式碼,看下作者如何自動新增程式碼:

  //根據配置新增各種元件依賴,並且自動化生成元件載入程式碼
        if (isRunAlone) {
            project.apply plugin: 'com.android.application'
            System.out.println("apply plugin is " + 'com.android.application');
            if (assembleTask.isAssemble && module.equals(compilemodule)) {
                compileComponents(assembleTask, project)
                project.android.registerTransform(new ComCodeTransform(project))
            }
        } else {//元件不單獨除錯即為library,併發包aar到指定路徑
            project.apply plugin: 'com.android.library'
            System.out.println("apply plugin is " + 'com.android.library');
            project.afterEvaluate {
                Task assembleReleaseTask = project.tasks.findByPath("assembleRelease")
                if (assembleReleaseTask != null) {
                    assembleReleaseTask.doLast {
                        File infile = project.file("build/outputs/aar/$module-release.aar")
                        File outfile = project.file("../componentrelease")
                        File desFile = project.file("$module-release.aar");
                        project.copy {
                            from infile
                            into outfile
                            rename {
                                String fileName -> desFile.name
                            }
                        }
                        System.out.println("$module-release.aar copy success ");
                    }
                }
            }
        }

不論是主專案還是元件 通過獲取當前編譯的project 是否是單獨執行,若是則增加所需要的元件並在application中新增程式碼。但是runalone時並且重新配置了資源路徑,很好的隔離了測試程式碼和元件分包時的正式程式碼。AS這點直接通過配置改變路徑還是很讚的,都不是手動拷貝打包中。

    resourcePrefix "readerbook_"

    sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
                java.srcDirs = ['src/main/java', 'src/main/runalone/java']
                res.srcDirs = ['src/main/res', 'src/main/runalone/res']
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

還有一個就是Gradle提供Transform監聽,作者利用這個在位元組碼轉成dx時注入程式碼,實現如下:

public class ComCodeTransform extends Transform {

    private Project project
    ClassPool classPool
    String applicationName;

     ComCodeTransform(Project project) {
        this.project = project
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        getRealApplicationName(transformInvocation.getInputs());
        classPool = new ClassPool()
      。。。。。。。。
            //對型別為“資料夾”的input進行遍歷
            input.directoryInputs.each { DirectoryInput directoryInput ->
                boolean isRegisterCompoAuto = project.extensions.combuild.isRegisterCompoAuto
                if (isRegisterCompoAuto) {
                    String fileName = directoryInput.file.absolutePath
                    File dir = new File(fileName)
                    dir.eachFileRecurse { File file ->
                        String filePath = file.absolutePath

                        String classNameTemp = filePath.replace(fileName, "").replace("\\", ".").replace("/", ".")
                        if (classNameTemp.endsWith(".class")) {
                            String className = classNameTemp.substring(1, classNameTemp.length() - 6)
                            if (className.equals(applicationName)) {
                            //注入程式碼
                                injectApplicationCode(applications.get(0), activators, fileName);
                            }

                        }
                    }
                }

        }
    }

利用javassist來實現注入依賴元件的註冊。
 private void injectApplicationCode(CtClass ctClassApplication, List<CtClass> activators, String patch) {
        System.out.println("injectApplicationCode begin");
        ctClassApplication.defrost();
        try {
            CtMethod attachBaseContextMethod = ctClassApplication.getDeclaredMethod("onCreate", null)
            attachBaseContextMethod.insertAfter(getAutoLoadComCode(activators))
        } catch (CannotCompileException | NotFoundException e) {
            StringBuilder methodBody = new StringBuilder();
            methodBody.append("protected void onCreate() {");
            methodBody.append("super.onCreate();");
            methodBody.
                    append(getAutoLoadComCode(activators));
            methodBody.append("}");
            ctClassApplication.addMethod(CtMethod.make(methodBody.toString(), ctClassApplication));
        } catch (Exception e) {

        }
        ctClassApplication.writeFile(patch)
        ctClassApplication.detach()

        System.out.println("injectApplicationCode success ");
    }

簡單外掛這塊 就這麼多了。