《Android彻底组件化方案实践》读后分析

《得到》App相关开发成员开源关于Android组件话的实践方案及demo,看似实现代码简单,但核心是组件化的实现想法以及自动集成、单独运行的构建实现。现对其中的核心思想和gradle插件写些自己的理解以及Gradle插件开发学习笔记,填补这方面的知识以及拓展到自己的项目中。

原文:请移步原著地址:Android彻底组件化方案实践
作者又出一篇文章具体的demo:Android彻底组件化demo发布

组件化核心思想

作者在文中提到对项目代码组件化成各个模块,这些模块相互独立,不耦合,从主项目中抽离出来后,主项目依然编译通过,且各模块能进行单位测试,测试完成了又可以发布组件,并在主项目中自动完成编译集成打包,用作者的话来讲就是有组件的生命周期,能加入-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 ");
    }

简单插件这块 就这么多了。

猜你喜欢

转载自blog.csdn.net/u010019468/article/details/76547798