Android 组件化架构 个人笔记

前言说明

以下内容均为 Android 组件化架构知识点的总结归纳、修正错误和完善扩展,非系统知识集,个人笔记,仅供参考。

组件化基础

1. 引入库的三种方式

compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':base')
compile 'com.dji.dpush:core:1.0.0'

2. AndroidManifest

当有多个 Module 时,最终 apk 会将多个 AndroidManifest 合为一个。

可在 <app>/build/intermediates/manifests/full/debug 目录下查看合成的 AndroidManifest。

3. module

每个子 module 都会在 <module>/build/out/aar 下生成 aar 文件。
主 module 会在编译时重新编译子 module,并将这些 module 引用进来。

主 module 的合成 manifest 会补全子 module manifest 中配置的全限定名,如下示例:

主 module 包名:com.app.dixon.module_study
子 module 包名:com.app.dixon.base
子 module activity 在 manifest 中的配置:android:name=".LibraryActivity"
子 module activity 在合成 manifest 中的配置:android:name="com.app.dixon.base.LibraryActivity"

4. Application

application 的引用规则:

主 module 子 module 最终结果 要求
主 application 主 module 可以同时配置 tools:replace="android:name",无影响
子 application 主 module 不能配置 tools:replace="android:name"
解决冲突后,主 application 主 module 需要配置 tools:replace="android:name",子 module 配置与否没影响

解决冲突:
module 的 application 中配置 tools:replace="android:name"
多个可替换项用逗号分隔,如 tools:replace="android:name,android:theme"

application 的常用方法:

onConfigurationChanged:仅在 activity 不销毁、旋转屏幕下调用。

registerActivityLifecycleCallbacks:对 App 内所有生命周期事件的监听,还可获取栈顶端的 activity 对象。利用该特性可以做全局弹窗、生命周期管理。

组件化编程

1. 组件化通信

原生事件通信推荐 LocalBroadcastReceiver,太过重量级、不方便、对解耦不利,所以使用事件总线 EventBus

Event 3.0 与 2.0 区别

2.0 采用运行时注解,利用反射,对整个注册的类的所有方法进行扫描完成注册,效率有一定影响。
3.0 采用编译时注解,Java 编译成 class 文件时,就创建出索引关系,并编入 apk 中。(使用 EventBusAnnimationProcessor 注解处理器处理)

组件化架构图

初级架构

依赖特性

implementation:A 依赖 B,B 依赖 C,则 A 不能直接调用 C 中的类。因为 implementation 不能传递依赖。(优势在于,底层代码变更不需要修改上层依赖,跨模块完全隔离代码依赖,是 Gradle 4.1、AS 3.0 新增,不是 Android Gradle 插件新增)

api | compile:A 依赖 B,B 依赖 C,则 A 可以直接调用 C 中的类。因为 api 可以依赖传递。
需要注意,api 需要配置在子 module 里,表示子 module 的某个依赖可以被向上传递。
如 a 依赖 b,b 依赖 c,如果 a 想引用 c,则应该在 b 的 build.gradle 里配置 c 的依赖方式为 api,则 c 可以向上传递(依赖传递)。
如果在 a 的 build.gradle 里配置 b 的依赖方式为 api,则表示 b 中代码可以被依赖传递,c 仍然不能被依赖传递。

所以上述架构图依赖关系代码为:

module 依赖 module
主 module implementation project(':login')
implementation project(':base')
login implementation project(':base')
base api project(':bus')
bus api 'org.greenrobot:eventbus:3.1.1'

bus 是对事件通信的解耦,抽离所有 event 事件实体到该 module 中。

可以看出,各模块必然包含对 event 事件实体的耦合,删除某一 event 必将影响到所有关联模块。

2. 组件化跳转

显式启动,将会在主 module 中引入子 module 中的类,未来拆卸子 module,将会导致主 module 编译异常。如何解耦呢?

原生实现推荐隐式启动,可以使用下面安全代码:

Intent intent = new Intent();
//intent.setClassName(getPackageName(), "com.app.dixon.login.LoginActivity"); //or this
intent.setComponent(new ComponentName(getPackageName(), "com.app.dixon.login.LoginActivity"));
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

其中 intent.setClassName 的第一个参数是主 module 的包名,因为 manifest 在合并后子 module 的包名会被覆盖(抹除)掉。

更好的实现方式:ARouter 路由

ARouter 介绍

原生 ARouter
跳转依赖类 跳转通过 url 索引
AndroidManifest 注册 注解注册
系统控制跳转 AOP 切面编程支持
失败无法降级 灵活降级
- 有拦截过滤机制(如在跳转前进行登录判断)

官方 Github

实现原理:

ARouter 的编译时注解框架会将页面索引的三个文件生成到 module/build/generated/source/apt/debug/com/alibaba.android.arouter.routes 目录下。

Application 加载时,会初始化调用 init 方法,将文件中的索引保存到 HashMap 中,这样就保存了全部模块的跳转关系。

跳转时会先查询是否存在跳转对象、然后经过层层拦截器、最终调用 ActivityCompat.startActivity() 跳转。

其它实现方式

如果项目中引入 RxJava,则推荐使用 OkDeepLink。

3. 动态创建

动态创建的作用是解耦。

反射

反射可获取属性的修饰符

方法 本 Class SuperClass
getField public public
getDeclaredField public protected private default no
getMethod public public
getDeclaredField public protected private default no
getConstructor public no
getDeclaredConstructor public protected private default no

获取父类 Class 的任何属性:

cl.getSupperclass().getDeclaredField("name");

反射泛型:

cl.getDeclaredMethod("test", Object.class);

反射提供了动态代理的实现:

参考 HookJava基础

反射框架

jOOR:链式调用、支持动态代理

Fragment 组件化:动态创建

方案1:使用反射获取 Fragment module 加载。(这样 Fragment module 移除时会抛出异常,而不是 crash)
方案2:使用 ARouter 路由,所有 Fragment module 提供 newInstance 方法返回实例。

Application 组件化:动态初始化子 module

方案1:子 module 在 Application 中反射引入,再手动调用初始化。
方案2:以接口形式,抽象初始化方法到 base module 中,子 module 继承并实现接口,主 module application 则负责添加需要初始化的子 module 类。

4. 组件化存储

greenDao

greenDAO:对象关系映射框架,可以通过操作对象的方式去操作数据库。

因为对象关系,与 EventBus 有同样的解耦问题,推荐如下架构解决:

带数据存储的项目结构

如图,不论 bus 还是 data,都是从 base 基础层中分离出来的组件模块,属于更低的框架层。

5. 组件化权限

Android 的所有权限定义在 frameworks/base/core/res/AndroidManifest.xml 中,源码参考此链接。

权限申请流程图

启动 App 正确的权限申请流程。

启动权限申请

权限配置

方案1:

normal 权限放到 base module 中,dangerous 权限放到各个 module 里。

好处是当添加删除某一模块时,隐私权限也将跟着移除。

方案2:

将所有权限包括 normal 全部移到子 module 中。

好处是最大程度解耦,缺点是增加了编译时 AndroidManifest 合并检测的消耗。(个人倾向这种)

权限组件化框架:AndPermission

链式操作、国内厂商适配、注解回调

路由拦截实现模块权限控制

组件化微 Demo Git 地址,临时编写,初级结构,仅供参考(后续会上线较完整的私人中小组件化项目,并更新在文章中)。

架构说明:

base 层实现 AndPermission 库的引入,以便于各个模块均能使用;
base 层提供返回 TopActivity 的接口,由 app module 实现,因为组件 module 不依赖 app module,所以通过接口曲线救国。
function(Demo 中不够严谨,临时起名 save) 层。即组件 module,实现权限定义、ARouter 拦截器等功能,方便后续移除模块时一并移除。
function 与 app 均依赖 ARouter,以实现路由跳转。
单独抽离 bus 层,作为比 base 更底层的基础层,EventBus 依赖、Event 类均定义于此。

另外还可以利用 ARouter 的拦截功能做登录前、支付前验证。

6. 静态常量与资源冲突

基本规则

1.
主 Module 编译的静态常量: public static final int
子 Module 编译的静态常量: public static int

因为子 Module 的特殊性,导致某些必须为常量的代码不能使用,如下:

//id 不是 final,不能用于 switch-case
@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.tv:
            //TODO
            break;
    }
}

解决方法是 Mac 上将光标点至 switch,使用 option + return 可将代码专为 if-else。

2.
R.java 目录:build/generated/source/r/debug(release)/包名/R.java

3.
编译时,aar 文件汇总到主 Module,在解决冲突后,Module 中的 R.java 文件会合并成一份。
主 Module 与子 Module 有同名资源,则保留主 Module 同名资源。所有资源均是如此,不论 R.string 还是 R.layout。 所以布局上有替代风险。

ButterKnife

1. 配置

annotationProcessor 是编译时执行依赖的库,不会打包进 apk 中。它和每个 Module 的编译息息相关,必须配置在每个 Module 的 build.gradle 中。

而不论是 ARouter 还是 ButterKnife 的项目依赖,只需要在 base module 配置一个传递依赖即可。

所以对于 ButterKnife 的配置:

base module:api 'com.jakewharton:butterknife:8.4.0'
app module & function module:annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'

2. ButterKnife module 使用

ButterKnife 生成的文件在 module/build/generated/source/apt/debug(release)/包名/ 下。

ButterKnife 通过注解 findViewById,而注解中只能使用常量,对此 ButterKnife 提供了生成 R2 final 资源的方式,但是最好的方式还是通过 findViewById(),而不是使用注解。

依赖树

详情参考 Gradle 依赖树

*号表示依赖被忽略。

默认会选用较新的依赖,如果想指定某一依赖,除上面链接中强制指定的方式,还有下面的排除方式:

complie('com.facebook.fresco:fresco:10.10.0'){
    exclude group:'com.android.support',module:'support-v4'
}

资源名冲突

上面 基本规则-3 说到主 Module 会覆盖子 Module 中的同名资源,实际规则是:

后编译的模块会覆盖之前编译模块的资源字段中的内容,而编译遵循从底层(base)到顶层(app)的顺序。

解决办法是:模块中资源命名以模块名为前缀,尽量保证不同模块间的资源命名不一样。

7. 组件化混淆

混淆,将类名、方法名、成员变量等重命名为无意义的简短名称,增加逆向工程的难度。

解决混淆冲突

每个 module 都有自己的混淆规则,会造成重复混淆,导致资源找不到。(报错为 transformClassesAndResourcesWithProguardForRelease

解决办法:只在主 module 混淆,其余子 module 均不混淆(实测可行,但要注意混淆配置)(即只在主 Module 配置 minifyEnabled true)。

存在问题:主 module 混淆耦合,如果移除主 module 时没有删除相应混淆文件,虽然不会导致编译不通过,但是会影响编译效率。另外多 module 开发可能涉及协作问题,主 module 开发人员可能不了解子 module 的内部逻辑(如调用反射),导致混淆错误,需要子 module 同步混淆代码,存在沟通成本问题。

其余方案:1.利用 Gradle 插件重构混淆逻辑;2.consumerProguardFiles 方案。但书中 consumerProguardFiles 'proguard-rules.pro' 的方式测试无效(我实现有问题?)

混淆基础知识

详情参考 混淆基础知识

上面链接包括混淆简介、基本语法、Android 注意事项等。

资源混淆

AndResGuard 略

8. 多渠道模块

详情参考 多渠道打包

多渠道模块配置

以应用的免费版和收费版为例,收费版依赖 vip 模块、并使用不同的包名。

flavorDimensions "version" //1.定义维度

productFlavors {
    //free
    free {
        dimension "version" //2.选定维度
        manifestPlaceholders.put('app_name', '免费版') //3.添加维度下特定变量 下一步转Manifest
        manifestPlaceholders.put('ver_num', '1')
        manifestPlaceholders.put('ver_name', name) //将编译时的变种命名生成为Manifest变量
    }
    //vip
    vip {
        dimension "version"
        applicationId project.android.defaultConfig.applicationId + '.vip'  //这样因为包名不同可以同时安装 但是要注意Provider-auth不能同名
        //applicationIdSuffix 'vip'  //或者这样简写
        manifestPlaceholders.put('app_name', 'vip付费版')
        manifestPlaceholders.put('ver_num', '2')
        manifestPlaceholders.put('ver_name', name)
    }
}

    
dependencies {
    ...
    //vip版本引入vip模块
    vipImplementation project(':vip')
}

<!-- 4.将特定变量定义到具体占位符 -->
<meta-data
    android:name="ver_num"
    android:value="${ver_num}" />

免费版因为没有 vip 模块,所以编写、调用 vip 模块代码时需使用反射、ARouter 等无耦合调用方式,避免直接 import。(上述即是组件化要求解耦的应用场景之一,而解耦是组件化的目标方向之一

9. 总结

效率和适配,是选型的关键。

组件化优化

基础

每个 build.gradle 自身是一个 Project 对象,project.apply() 会加载某个工具库到 project 对象中。

apply plugin:"xx" 的方法会将 project 对象传递入工具库,然后通过插件中的 Groovy 文件来操作 project 对象的属性,以完善配置初始化信息。

android{} 等调用相当于 project.android(){} 方法,方法中会设置 project 属性。

每个 Project 中包含很多 Task 构建任务,每个 Task 中包含很多 Action 动作,每个 Action 相当于代码块,包含很多需要被执行的代码。

Gradle 优化

Gradle 基础

Gradle 基础流程
  1. 读取根目录 settings.gradle 中的 include 信息,决定哪些工程会加入构建,并创建 project 实例。
  2. 按引用树执行所有工程的 build.gradle 脚本,配置 project 对象,一个对象由多个任务组成,此阶段也会创建、配置 Task 及相关信息。
  3. 运行阶段会根据 Gradle 命令传递过来的 Task 名称,执行相关依赖任务。

Gradle 参数优化

每个 module 的 build.gradle 有一些必要的属性,且同一个 Android 工程中要求属性值一致,如 compileSdkVersionbuildToolVersion等。(如果不同,虽然能编译通过,但是会 bug 告警,且存在安全风险。)

为了使用统一的、基础的 Gradle 配置,提供以下优化方案。

方案一 给 project 添加自定义属性

1.根目录创建 config.gradle 文件,如下添加自定义属性。

project.ext {
    compileSdkVersion = 28
    minSdkVersion = 15
    targetSdkVersion = 28
    applicationId = "com.example.plugdemo"
}

project.ext{} 可以直接简写为 ext{}ext = xx

2.build.gradle 引入config.gradle

apply from: "${rootProject.rootDir}/config.gradle"

3.应用属性

android {
    compileSdkVersion project.ext.compileSdkVersion //全称
    defaultConfig {
        applicationId project.ext.applicationId
        minSdkVersion project.ext.minSdkVersion
        targetSdkVersion project.ext.targetSdkVersion
        versionCode versionCode //也可以这样简写
        versionName versionName
        ...

lib module 也需要上述配置。

方案二 使用闭包设置属性

1.方案一中project.ext内多添加如下代码:

    setDefaultConfig = {
            //定义setDefaultConfig方法
        extension -> //extension相当于是闭包的参数 后续android对象会作为参数传入
            extension.compileSdkVersion project.ext.compileSdkVersion
            extension.defaultConfig {
                minSdkVersion project.ext.minSdkVersion
                targetSdkVersion project.ext.targetSdkVersion

                testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
            }
    }

书中配置方式为minSdkVersion minSdkVersion,但是实测第二个minSdkVersion的值会丢失,所以修改为如上配置。

2.调用setDefaultConfig方法。

除方案一引入外,如下调用:

android {
    project.ext.setDefaultConfig android //关键代码 调用配置函数

    defaultConfig {
        versionCode versionCode
        versionName versionName

        //ARouter 编译生成路由 放在具体功能模块里
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
    ...
方案三 project + 闭包配置属性

上述方案二中创建了一个方法,并将 android 对象作为参数传入,同理,project 对象(一个 build.gradle 文件)也可以类似操作,下面是完整的代码。

新建 config_project.gradle,添加如下代码:

apply from: "${rootProject.rootDir}/version.gradle"

project.ext {

    //主module(app)配置
    setAppDefaultConfig = {
        extension -> //extension后续会传入project替代
            extension.apply plugin: 'com.android.application'
            extension.description "app"
            //设置通用Android配置
            setAndroidConfig extension.android
            //设置通用依赖配置
            setDependencies extension.dependencies
    }

    //设置lib配置
    setLibDefaultConfig = {
        extension ->
            extension.apply plugin: 'com.android.library'
            extension.description "lib"
            //设置通用Android配置
            setAndroidConfig extension.android
            //设置通用依赖配置
            setDependencies extension.dependencies
    }

    //设置android配置
    setAndroidConfig = {
        extension -> //extension 即 android 对象
            extension.compileSdkVersion 28
            extension.defaultConfig {
                minSdkVersion 15
                targetSdkVersion 28
                versionCode project.ext.versionCode
                versionName project.ext.versionName

                testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

                //ARouter 编译生成路由
                javaCompileOptions {
                    annotationProcessorOptions {
                        arguments = [AROUTER_MODULE_NAME: extension.project.getName()]
                    }
                }

            }
    }

    //设置依赖
    setDependencies = {
        extension ->
            extension.implementation fileTree(dir: 'libs', include: ['*.jar'])
            extension.implementation 'com.android.support:appcompat-v7:28.0.0'
            extension.testImplementation 'junit:junit:4.12'
            extension.androidTestImplementation 'com.android.support.test:runner:1.0.2'
            extension.androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

            //ARouter 路由apt插件,用于生成相应代码,每个module都需要
            extension.annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
    }
}

下面是使用了 config_project.gradle 后的简化版 build.gradle 配置:

apply from: "${rootProject.rootDir}/config_project.gradle"
project.ext.setAppDefaultConfig project  //将 project 作为参数传入方法

android {

    defaultConfig {
        applicationId "com.example.plugdemo"
        signingConfigs {
            release {
                keyAlias 'xx'
                keyPassword 'xx'
                storeFile file('/Users/xx/Desktop/xx')
                storePassword 'xx'
                v2SigningEnabled false
            }
        }
    }

    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }

    flavorDimensions "version" //1.定义维度

    productFlavors {
        //free
        free {
            dimension "version" //2.选定维度
            manifestPlaceholders.put('app_name', '免费版') //3.添加维度下特定变量 下一步转Manifest
            manifestPlaceholders.put('ver_num', '1')
            manifestPlaceholders.put('ver_name', name) //将编译时的变种命名生成为Manifest变量
        }
        //vip
        vip {
            dimension "version"
            applicationId project.android.defaultConfig.applicationId + '.vip'
            //这样因为包名不同可以同时安装 但是要注意Provider-auth不能同名
            //applicationIdSuffix 'vip'  //或者这样简写
            manifestPlaceholders.put('app_name', 'vip付费版')
            manifestPlaceholders.put('ver_num', '2')
            manifestPlaceholders.put('ver_name', name)
        }
    }
}


dependencies {
    //依赖最底层基础模块
    implementation project(':base')
    //依赖下一层功能模块
    implementation project(':login')
    implementation project(':pay')

    //vip版本引入vip模块
    vipImplementation project(':vip')
}

当然,上述简化结果还可以按需继续抽离、精简。

config_project.gradle 的意义是为了抽出多个 build.gradle 文件重复的部分,简化代码的同时,方便管理和维护,对于各模块不同的部分无需抽出。

调试优化

优化目的

子模块作为 App 单独启动,分离调试。

优化方案

以下为具体优化步骤:

1. 创建 isXXDebug 变量,控制子模块是否转变为分离模块。

project.ext {
    isLoginDebug = false
    isVipDebug = false
}

2. 根据 isXXDebug 变量,转变以下变量:

<1.library → application

if (project.ext.isVipDebug) { //app 模式
    project.ext.setAppDefaultConfig project
} else {
    project.ext.setLibDefaultConfig project
}

<2.配置 applicationId

if (project.ext.isVipDebug) { //app 模式
    applicationIdSuffix 'vipdebug'
}

<3.配置 AndroidManifest 文件

main 文件夹同级目录下创建 debug 文件夹,并将 main 中的 AndroidManifest 复制到这里。

指定 debug 文件夹下 AndroidManifest 文件中的某 Activity 为启动 Activity(记得配置 theme)。

    <application>
        <activity android:name=".VipActivity"
            android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

最后使用 sourceSets 指定目标 AndroidManifest:

sourceSets {
    //all 表示所有 type,包括 debug 和 release。
    all {
        if (project.ext.isVipDebug) {
            manifest.srcFile 'src/debug/AndroidManifest.xml'
            res.srcDir 'src/main/res'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            resources {
                exclude 'src/debug/*'
            }
        }
    }
}

3. App module 移除分离调试的模块依赖。

if (!project.ext.isVipDebug) {
    vipImplementation project(':vip')
}

综上配置后,当 isVipDebug 为 false,只有 app 一个启动项;当 isVipDebug 为 true,则有 app、vip 俩个启动项,分别安装后,vip 仅能运行其模块的功能,而 app 则仅能运行除 vip 模块外的其他功能。

意义总结

从上面可以看出 组件化 的一部分意义:

1.首先,子模块分离调试、应用,独立性很高,调试快速方便;

2.有同事说,使用 ARouter (或页面跳转使用反射、不引入子模块的 Activity 类)的强行解耦合是不必要的,他的理由是存在一定的跳转耦合,可以在移除模块并编译时,快速定位相关联的页面、代码,进而彻底移除冗余代码
他的说法有一定的道理,但是结合上述例子可以一窥,子模块分离是一件很频繁的事情,不仅是只有子模块没用时才抛弃移除,它可以应用在快速调试、分离协作开发、或用于检测子模块独立性等诸多用途,所以假如如同事所说编写代码,当每次调试时,都需要上述删除冗余代码操作,在反复修改增加不必要工作量的同时、将不能有效保证主模块的正常运行(耦合的通病)。所以对于初学者,有时看似没必要的强行解耦,实际在开发、调试、应用中因为频繁移出模块而很重要。

资源引用配置

资源引用的多种方式

1. sourceSets

sourceSets

2. resValue

可以在 buildType、productFlavor 等变种里动态添加资源

注意:
<1.是资源不是变量;
<2.只能添加,不能替换,资源名重复 Gradle 会提示。

resValue

3. resConfigs

指定特定尺寸资源,同样在变种中定义。

resConfigs

4. manifestPlaceholders、buildConfigField

顾名思义,manifestPlaceholders 是 manifest 占位符,buildConfigField 是 BuildConfig 类中的成员变量。同样变种中定义。

buildConfigField
资源引用的优先级

优先级高的会在优先级低的 之后 合成。

资源引用优先级

模块依赖架构

分析经过上面组件化编程后,可行的多种模块依赖结构。

我的依赖关系图
my-dependencies

依赖关系表(省略部分依赖)

module 依赖 module
主 module implementation project(':login')
implementation project(':base')
login implementation project(':base')
base api project(':bus')
bus api 'org.greenrobot:eventbus:3.1.1'
书籍依赖关系图

方案一 上述结构中,子模块也用 api。

优点:省去调用封装,同时保证兼容 Gradle 4.1 以下的组件化项目。
缺点:牺牲编译速度,并且存在子模块全部移除时主模块不能运行的风险。

架构如图:

书籍推荐1

依赖关系表:

module 依赖 module
主 module implementation project(':login')
login api project(':base')
base api project(':bus')
bus api 'org.greenrobot:eventbus:3.1.1'

方案二 主模块与 Base 完全解耦,需要定义封装 Base 的独立 module 供主模块调用。

书籍推荐2

依赖关系表:

module 依赖 module
主 module implementation project(':login')
implementation project(':app-core')
app-core implementation project(':base')
login implementation project(':base')
base api project(':bus')
bus api 'org.greenrobot:eventbus:3.1.1'

选型没有硬性要求,选择符合项目需求的合适架构即可。

Git 组件化部署

暂略,后续出组件化部署 Blog。

组件化编译

Gradle 编译

Android 基础编译流程

基础编译流程

命令行编译生成 apk 一文中,可以知道 Gradle 编译大概有如下几步:

1.生成 R.java 文件 → 2.生成 class 文件 → 3.生成 dex 文件 → 4.打包资源文件 → 5.生成 apk → 6.签名对齐

按照功能划分,编译构建分为四个步骤:

代码编译 → 代码合成 → 资源打包 → 签名对齐

代码编译:Java 编译器对工程代码资源编译。包括 App 源代码、apt 编译生成的 R 文件、AIDL,最终生成为 class 文件。对应上述 1、2 步。

代码合成:通过 dex 工具,将编译后所有的 class 文件、依赖库的 class 文件合成为虚拟机可执行的 .dex 文件。如果使用了 MultiDex,会产生多个 dex 文件。对应上述 3 步。

资源打包:通过 apkbuilder 工具,将 .dex 文件、apt 编译后的资源文件、依赖库中的资源文件打包生成 apk 文件。对应上述 4、5 步。

签名对齐:使用 Jarsigner 和 Zipaligin 对 apk 进行签名对齐。对应上述 6 步。

完整流程图:

Android 基础编译流程
Task 编译链

在下图位置查看 Gradle 编译流程与耗时情况:

Gradle 工具栏
  • Run init scripts:初始化描述
  • Configure build:检查 build.gradle 中引入的 classpath
  • Calculate task graph:计算出每个模块的依赖
  • Run tasks:开始构建任务

由于组件化编译时,开启了并行编译,所以上述 tasks 任务存在并行操作的情况,顺序是乱的。网上查的关闭并行编译的方式也不生效。为了查看 Task 依赖树,使用 Gradle 框架 gradle-task-tree:

配置方式:

buildscript {
    repositories {
        ...
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        ...
        classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.3.1"
    }
}

allprojects {
    ...
    apply plugin: TaskTreePlugin
}

调用:

./gradlew assembleDebug taskTree --no-repeat

下面是结果的部分截图:

编译链

组件化架构一书的配置方式不可行,原因是因为 class 找不到,比较奇怪(也可能是我的问题...)。

知道了上述顺序关系,就可以在编译任务中嵌入额外任务操作。

Instant Run

部分见热更新、插件化部分,暂略。

热部署:不需要重启应用,也不需要重建当前 Activity。适合简单修改,如方法实现修改、变量值修改。
温部署:需要 Activity 重启。场景多为变更了当前页面的资源文件。
冷部署:App 需要重启,但不是重装。场景为一些继承规则、方法签名变更等情况。市场上的 App 热更新框架多参照冷部署。

三种部署原理详见 Android组件化架构 P167。

调试技巧

开启了 MultiDex 之后,minSdkVersion 要求最小为 21,为了能在 instant run 调试时使用 21 版本,打包成 app 时使用低版本,需要下面的配置方式(未测试,instant run 暂略):

android {
    productFlavors {
        instant {
            minSdkVersion 21
        }
        app {
            minSdkVersion 17
        }
    }
}

其他 Gradle 构建优化

目的:加快编译速度

Properties 配置
配置 用途
org.gradle.parallel=true 开启并行编译
android.enableBuildCache=true 使用编译缓存
org.gradle.daemon=true 守护进程中编译apk,可以大大减少加载 JVM 和 classes 的时间
org.gradle.configureondemand=true 大型多项目快速构建
org.gradle.jvmargs=-Xmx3072M -XX\:MaxPermSize\=512m 加大编译时使用的内存空间
Task 任务过滤

选择性去除不需要运行的 Gradle Task 任务:

tasks.whenTaskAdded {
    task ->
        if (task.name.contains("lint") //不扫描潜在bug
                || task.name == "clean"
                || task.name.contains("Aidl") // 项目中不适用 Aidl,则关闭
                || task.name.contains("mockableAndroidJar") //用不到测试可以先关闭
                || task.name.contains("UnitTest") //用不到测试可以先关闭
                || task.name.contains("AndroidTest") //用不到测试可以先关闭
                || task.name.contains("Ndk") //用不到NDK和JNI可以先关闭
                || task.name.contains("Jni")) {
            task.enabled = false
        }
}
不执行重复任务

[Android 组件化架构] 一书,该章节说默认情况下 Library 只发布并被依赖 Release 版本,但是 Debug 和 Release 的 Task 都要执行。

经测试后发现并没有只依赖 Release 版本(App module 为 debug 时依赖的子模块是 debug library 而不是上面说的 release library),后来确认书中描述的现象是 Gradle 的一个问题,在 AndroidStudio 3.0 上已经修复,而我用的版本就是 3.x。新版本 AS 已经没有问题,所以不需要再配置。

增量 build

在 module 中减少使用 Annotation processor 有助于提升编译速度,因为 project 不支持 Annotation processor 增量 build。

使用 Gradle 新特性

implementation 的依赖隔离保证了模块间解耦,之前 compile 机制因为底层向上暴露,为了安全起见,Gradle 会完全编译整个 App。而依赖隔离则可以准确定位编译模块。

设置 API 版本

Android 5.0 以下因为 .dex 合并时超过方法数限制的原因会多执行部分 Task 任务,所以 debug 设置 minSdkVersion > 5.0 可以跳过不必要的 Task 任务。

buildTypes {
    debug {
        defaultConfig {
            minSdkVersion 21
        }
    }
    release {
        defaultConfig {
            minSdkVersion 14
        }
    }
}

Freeline 极速增量编译框架

暂略

总结

编写业务代码是对用户的优化,编写环境代码是对自身工作的优化。

所谓组件化编译,实际和组件化关联不大,更直接的方向是提升编译速度,优化工作流程。

组件化分发

Activity 分发

详见 Activity 组件化分发结构

Fragment 分发

Fragment 生命周期

onAttach → onCreate → onCreateView → onActivityCreated → onStart → onResume
→ onPause → onStop → onDestroyView → onDestroy → onDetach

onAttach:Fragment 与 Activity 建立关联时调用,用于获得 Activity 传递的值。
onDetach:Fragment 与 Activity 关联被取消时调用。
onCreateView:创建 Fragment 视图时调用。
onActivityCreated:初始化 onCreateView 方法的视图后返回时被调用。
onDestroyView:Fragment 视图被移除时调用。

Fragment 分发技术

和 Activity 分发基本没有区别。

View 分发

View 生命周期

完整生命周期图

View 生命周期图

View 的构造函数有俩种加载情况:

  1. View 代码创建时;
  2. layout 资源文件加载时;(onFinishInflate 前调用)

生命周期调用顺序

调用顺序

View 与 Activity 生命周期关联关系

生命周期关联关系

注意:
1.onSizeChanged 因为在 onResume 之后执行,其顺序晚于 setContentView XML 加载时机,所以当期间大小发生变化就会回调。
2.onPause 和 onStop 会触发 onWindowFocusChanged,告知外界 Activity 已失去焦点。
3.Activity 销毁调用 onDestroy 时,View 才会从 Activity 解绑并调用 onDetachedFromWindow
4.View 自身也存在 onSaveInstanceStateonRestoreInstanceState 来保存、恢复视图状态。
5.View 也有 onConfigurationChange 函数来触发视图配置变更。

View 分发技术

分发目的:将业务模块割离,抽成有生命周期的独立模块

分发做法:Activity 分发中,是直接创建与 Activity 同生命周期的 Manager 进行生命控制分发。而 View 分发,目的不变,也是抽离业务模块(而不是 View 的模块开发),做法是在 Activity 内创建一个 View,因为该 View 与 Activity 存在关联关系(部分生命周期存在同步关系,不同步的函数则需要额外调用),所以可以利用 View 来给 ModuleManager 做生命周期分发。

View 的分发虽然解耦更高(书中还说消耗资源少,没看出来,因为 Activity 分发使用 Manager,View 分发使用 View + Manager,看起来反而增大了),但逻辑不够直白、配置量增大(View 与 Activity 生命周期兼容导致)、且会引入大量 module 导致增加编译配置问题,所以不推荐使用。

部分关键代码及截图详见 <Android 组件化架构> P205

从同步关系看出,View 不能分发以下 Activity 生命周期函数:
onResume:虽然 onResume 之后会调用 View 的 onAttachedToWindow,但是该函数每个 View 只调用一次。
onPauseonStop

依赖倒置

依赖倒置原则:程序要依赖于抽象接口,不依赖于具体实现。核心是面向接口编程。

高层不依赖底层,应该依赖抽象,抽象不应该依赖于细节,细节应该依赖于抽象。

依赖倒置分发

实际实现是 module 不再需要指定 ViewGroup,多个 ViewGroup 移交给实体 Module 父类管理(经由父 Activity 调用 Module.init 将 ModuleContext 传给实体 Module,ModuleContext 内包含多个非指定的布局信息),而实体 Module 可以自由选择 ViewGroup 加载。这样的好处的解除了 Module 对视图的依赖,实际也是 Activity 分发结构的变种。

代码有一定参考价值,可使用 RxJava 实现。

组件化列表配置

暂时来看将 Activity 子模块配置为'列表文件顺序加载'较'直接代码顺序加载'而言意义不够明显,可能是个人理解不够,以后继续深入组件化知识再学习,暂略。

加载优化

线程加载

利用 Handler 或 RxJava 将 Module 创建的工作移交给工作线程,且工作线程使用 newSingleThreadExecutor 来保证 Module 创建、初始化的有序性,这样既保证了模块 init 顺序,又不会因多模块初始化而阻塞主线程。

原先需要等每个模块依次初始化结束后才能执行下一步。
现在将初始化完整序列交给了单个线程池,然后直接执行下一步。由 MessageQueue 决定什么时候回到主线程。
init 函数记得回归主线程。(eg:handler.post)

代码

首先,MessageQueue 也是阻塞队列,本质是管道。当队列无数据的情况下,消费端线程(即主线程)进入等待,直到有数据放入队列,MessageQueue 重新唤醒消费端线程使其继续执行,以此实现跨线程。

Looper 会执行死循环,从 MessageQueue 中取出消息。当 MessageQueue 中没有待处理消息时,主线程就会被 MessageQueue 阻塞,所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

那么主线程处于被阻塞状态,如何保证生命周期函数及其它正常流程呢?原来,四大组件的运行中,最终也是通过 binder 线程(用于进程间通信的线程,在 Looper.loop() 前创建)调度 Handler 唤醒主线程,并执行生命周期函数(可参考 ActivityThread.H,它有很多响应函数)。

handleMessage 的执行顺序与主线程原本的代码流没有关联,实际会交叉进行。

代码详见 YNotes Demo(即上面的私人组件化中小型项目,暂未上传)。

模块懒加载

思想:利用 ViewStub 占位,需要时再唤醒视图。

public class RealModule extends AbsModule {

    private Activity activity;
    private ViewGroup parentViewGroup;

    //布局懒加载 1
    private View mRoot;
    private ViewStub stub;

    @Override
    public void init(ModuleContext moduleContext) {
        activity = moduleContext.getContext();
        parentViewGroup = moduleContext.getViewGroups().get(0);
        //布局懒加载 2
        stub = new ViewStub(activity);
        parentViewGroup.addView(stub);
    }

    //布局懒加载 3
    private void initView() {
        stub.setLayoutResource(R.layout.note_content_note_home);
        mRoot = stub.inflate();
        //TODO mRoot.findViewById
    }
    ...

层级限制

Activity 内子模块的顺序加载(层级加载)。有如下方式实现:

代码列表控制、编写模块加载列表(方便清晰阅读模块层级顺序)、编译时注解调序、懒加载调序、全部使用 ViewStub 然后通过'模块加载列表'调序等多种方式。

多模板设计

暂略(需 Javapoet编译时注解组件化列表配置 等前置知识点)。

组件化流通

远程仓库与本地仓库

详见 Android 仓库解析

SDK 知识

AAR 资源合并

SDK 指的是软件开发工具,包括 JNI 的 so 库、Gradle 的插件、Android Studio 的运行脚本、jar、aar 等。

模块依赖通过 implementation project(':library'),它和直接引用 implementation 'com.app.xz.library:librarytest:1.0.0' 的不同是,当这俩个 module 都依赖其它库实现部分功能时,前者可以通过 api 等属性传递依赖,而后者打包成 aar 时并不能将其依赖库同时打进 aar,当主工程没有 aar 需要的依赖时,项目就会报 NoClassDefFoundError。

除在主工程引入 aar 需要的依赖外,也可以通过其它方式将依赖库资源打包到 aar 中以解决问题。

书中 fat-aar 测试已过时,新版本似乎不能用(也可能是配置有误?)。

后续会单独出博客研究如何将依赖打进 aar 并阐明原理。

架构模板

组件化模板

类模板

工程模板路径:/Applications/Android Studio.app/Contents/plugins/android/lib/templates

文件目录

文件目录说明

activities:Activity 模板;
gradle:默认的 gradle-wrapper 文件;
other:Fragment、View、Service 等其它模板;

类模板即创建工程时的 EmptyActivity or 其它 Activity 类型的模板,制作成本需要学习 FreeMarker 语法,暂略。

实时模板

即使用快捷键生成代码,类似代码补全。

注释模板

顾名思义。主要用于统一注释信息。

注解检测

注解基础

让代码在使用过程中获取提示信息,可以借助特殊的注解。

类型 效果 用途
RetentionPolicy.Source 源码注解,Java 编译成 class 时注解被遗弃。 编码时检测,如 @Null
RetentionPolicy.CLASS 注解保留到 class 文件,但 JVM 加载 class 时遗弃,是默认生命周期。 编译时处理,如编译时注解
RetentionPolicy.RunTime 注解在运行时仍然存在。 动态注解,如 EventBus 2.0,或枚举替代

Android 注解库依赖

Android support library 引入了新的注解库,包含很多有用的元注解,以此修饰代码提示。

implementation 'com.android.support:support-annotations:23.1.1'

如果使用了 v4、v7、appcompat 的库,则内部已经引用过该库了。

注解库元注解说明

详见 Android support-annotations 注解使用详解

总结

模板和提示目的在于引导协作者,只有规则稳定高效,才能引导更多协作者完成任务。

架构演化

下面分析项目从小到大架构演化的过程。

基础架构

以文件夹作为业务区分的低级架构,Base 则负责引入多种工具库。

基础结构

基础组件化

每个组件代表一个业务,Base 封装工具库和框架。适合中小项目。

基础组件化

模块化

应用层:仅负责生成 App、加载初始化。
模块层:独立业务模块。
基础层:基础组件的整合,提供基础组件能力给业务层用。基础层目的是为了隔离模块层与组件层入口,所以可以是空壳。
组件层:三方库、基础功能层。

适合中型 App,要求模块能不依赖于其它模块实现,所以考虑重点是业务之间如何进行信息交互和转发。

模块化

多模板化

融合组件化分发以后的架构,目的是在单页面中承载多个独立的业务,可以实现业务的自由组合。

多模板化

插件化

每个模块是以业务是否独立作为划分条件,对于基础业务如登陆、支付等需要账号的模块最好集成到宿主 App 里(?)。

插件化

小组负责独立业务存在的问题是:通信机制、页面跳转、资源冗余、资源冲突、混淆等合作问题。

进程化

大型 App 架构,Android 的进程开发以四大组件为基础,进程定义需要以四大组件为入口

进程化

进程化注意的问题:

1.静态成员和单例失效;
2.线程同步机制失效,因为 Java 同步机制基于虚拟机,而多进程会有多个虚拟机;
3.SharedPerferences 可靠性下降;
4.并发访问文件;
5.Application 多次创建,只能通过进程名区分不同进程以进行不同进程的初始化操作。

总结

架构最重要的是对未来的思考、未来的把控,以此才能明白遵循严格的开发规则的重要性。

目录

[TOC]

简书不识别 [toc] ... 简要目录(仅包含二级标题,部分知识点以外链方式给出):

  • 前言说明
  • 组件化基础
  • 组件化编程
    • 组件化通信
    • 组件化跳转
    • 动态创建
    • 组件化存储
    • 组件化权限
    • 静态变量与资源冲突
    • 组件化混淆
    • 多渠道模块
    • 总结
  • 组件化优化
    • 基础
    • Gradle 优化
    • Git 组件化部署
  • 组件化编译
    • Gradle 编译
    • Freeline 极速增量编译框架
    • 总结
  • 组件化分发
    • Activity 分发
    • Fragment 分发
    • View 分发
    • 依赖倒置
    • 组件化列表配置
    • 加载优化
    • 层级限制
    • 多模板设计
  • 组件化流通
    • 远程仓库与本地仓库
    • SDK 知识
  • 架构模板
    • 组件化模板
    • 注解检测
    • 总结
  • 架构演化
    • 基础架构
    • 基础组件化
    • 模块化
    • 多模板化
    • 插件化
    • 进程化
    • 总结
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345