Android組件化工程結(jié)構(gòu)以及項(xiàng)目具體實(shí)施方案

組件化優(yōu)點(diǎn)

1、代碼解耦

2待牵、方便多人協(xié)作開(kāi)發(fā)

3其屏、可復(fù)用性高,不同的APP可復(fù)用不同組件偎行,提高開(kāi)發(fā)效率

4、每個(gè)組件可獨(dú)立運(yùn)行贰拿,減少編譯時(shí)間珍德,方便開(kāi)發(fā)調(diào)試

組件化工程結(jié)構(gòu)

Android組件化架構(gòu).png

第一層:空殼app虑稼。應(yīng)用的入口,可存放啟動(dòng)頁(yè)且改,依賴所有業(yè)務(wù)組件

第二層:業(yè)務(wù)組件。根據(jù)不同業(yè)務(wù)橫向拆分出來(lái)的業(yè)務(wù)組件犀忱。任何一個(gè)業(yè)務(wù)組件都可以獨(dú)立出來(lái)成為一個(gè)應(yīng)用

第三層:功能組件未斑。通用業(yè)務(wù)是從應(yīng)用業(yè)務(wù)中抽取出來(lái)的交集钠糊,從應(yīng)用上說(shuō)攀甚,他屬于業(yè)務(wù)刑赶,而針對(duì)應(yīng)用業(yè)務(wù)而言則更像是一種功能捏浊,好比登錄這種業(yè)務(wù)功能,不需要關(guān)心有沒(méi)有界面撞叨,當(dāng)中是怎樣的邏輯金踪,只需要提供結(jié)果即可

第四層:公共業(yè)務(wù)組件和公共服務(wù)組件公共業(yè)務(wù)組件:業(yè)務(wù)相關(guān)Base代碼牵敷。公共服務(wù)組件:可存放各個(gè)組件對(duì)外暴露的接口胡岔,接口實(shí)現(xiàn)在組件內(nèi)部,可通過(guò)ARouter或者DI(依賴注入)實(shí)現(xiàn)跨組件服務(wù)調(diào)用枷餐;可存放路由跳轉(zhuǎn)等信息和路由服務(wù)靶瘸。

第五層:業(yè)務(wù)無(wú)關(guān)基礎(chǔ)組件。網(wǎng)絡(luò)請(qǐng)求毛肋、圖片加載怨咪、存儲(chǔ)、utils润匙、通用View的封裝

項(xiàng)目組件化

1诗眨、代碼解耦

代碼解耦主要是從兩個(gè)方面,其一是公共代碼的抽取和歸納趁桃,其二是面向接口編程辽话,接口下沉。

公共代碼的抽取和歸納:

部分通用的功能性的代碼抽出成utils卫病,上層只關(guān)心結(jié)果油啤,不關(guān)心具體的實(shí)現(xiàn)邏輯

面向接口編程:

當(dāng)上層需要底層的某項(xiàng)服務(wù)時(shí),將服務(wù)抽象成一個(gè)接口蟀苛,上層持有這個(gè)接口益咬,而不是具體的類,那么當(dāng)?shù)讓影l(fā)生了改變或是實(shí)現(xiàn)的時(shí)候帜平,上層只需要實(shí)例化對(duì)應(yīng)的新實(shí)現(xiàn)類即可幽告,如果把這層實(shí)例化也作為接口去作梅鹦,那么上層完全不用改變就能擁抱變化。

依賴注入:

橫向的業(yè)務(wù)代碼或者功能實(shí)現(xiàn)可以進(jìn)行依賴注入的方式來(lái)達(dá)到解耦的目的冗锁。

工程結(jié)構(gòu)解耦:

結(jié)構(gòu)的解耦其實(shí)一般針對(duì)應(yīng)用的整體業(yè)務(wù)而言進(jìn)行的一個(gè)"分Module"操作齐唆,根據(jù)業(yè)務(wù)類型的橫向拆分,業(yè)務(wù)屬性的縱向拆分以及功能SDK下沉冻河。

2箍邮、組件module gradle管理

  • 在根目錄下建立一個(gè)config.gradle文件
  • 編寫對(duì)應(yīng)的依賴常量代碼
  • 在app module 的build.gradle中引用
  • 注意,如果想要在別的.gradle中使用聲明的這些常量叨叙,一定要在抽取的xx.gradle文件中將對(duì)應(yīng)的代碼塊用"ext"進(jìn)行包裹

config.gradle

ext {
    android = [
            compileSdkVersion: 28,
            targetSdkVersion : 28,
            minSdkVersion    : 21,
    ]

    version = [
            retrofitSdkVersion      : "2.4.0",
            androidSupportSdkVersion: "28.0.0",
            butterknifeSdkVersion   : "8.8.1",
            espressoSdkVersion      : "3.0.1",
            canarySdkVersion        : "1.5.4",
            glideSdkVersion         : "4.8.0"
    ]


    dependencies = [
            //support
            "appcompat-v7"                : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
            "design"                      : "com.android.support:design:${version["androidSupportSdkVersion"]}",
            "support-v4"                  : "com.android.support:support-v4:${version["androidSupportSdkVersion"]}",
            "cardview-v7"                 : "com.android.support:cardview-v7:${version["androidSupportSdkVersion"]}",
            "annotations"                 : "com.android.support:support-annotations:${version["androidSupportSdkVersion"]}",
    ]
}

在工程根目錄的build.gradle中加上如下代碼:

apply from: "config.gradle"

在app module中的build.gradle中引用(其他組件module中引用類似):

apply plugin: 'com.android.application'


android {

    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    defaultConfig {
        applicationId "cn.com.xxx"
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionName "1.0.0"
        versionCode getVersionCode(versionName)
    }
}


dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation rootProject.ext.dependencies["appcompat-v7"]
    implementation rootProject.ext.dependencies["cardview-v7"]
    implementation rootProject.ext.dependencies["support-v4"]
    implementation rootProject.ext.dependencies["design"]
    implementation rootProject.ext.dependencies["annotations"]
    implementation rootProject.ext.dependencies["constraint-layout"]
    implementation rootProject.ext.dependencies["arch-lifecycle"]
    implementation rootProject.ext.dependencies["FlycoTabLayout_Lib"]
    implementation rootProject.ext.dependencies["FlycoPageIndicator_Lib"]
    implementation rootProject.ext.dependencies["nineoldandroids"]
    implementation rootProject.ext.dependencies["jiecaovideoplayer"]
    implementation rootProject.ext.dependencies["SmartRefreshLayout"]
}

如此以后在查看或者更換依賴的時(shí)候也方便查看和維護(hù)锭弊,注意在進(jìn)行依賴的過(guò)程中,因?yàn)橐蕾嚥煌娜嚼薮恚赡軙?huì)出現(xiàn)重復(fù)依賴相同庫(kù)而版本不一致的情況味滞,這里有兩種解決辦法,一種是在對(duì)應(yīng)依賴的三方中剔除對(duì)應(yīng)的pom依賴钮呀,如:

api('com.facebook.fresco:fresco:0.10.0') {
       exclude module: 'support-v4'
}

另外一種是強(qiáng)制依賴的相同庫(kù)的版本剑鞍,如:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (requested.name.startsWith("support-") ||
                    requested.name.startsWith("animated") ||
                    requested.name.startsWith("cardview") ||
                    requested.name.startsWith("design") ||
                    requested.name.startsWith("gridlayout") ||
                    requested.name.startsWith("recyclerview") ||
                    requested.name.startsWith("transition") ||
                    requested.name.startsWith("appcompat")) {
                details.useVersion SUPPORT_LIB_VERSION
            } else if (requested.name.startsWith("multidex")) {
                details.useVersion OTHER_VERSION.multiDex
            }
        }
    }
}

3、組件路由

路由其實(shí)是組件化的核心組件行楞,網(wǎng)上也有很多優(yōu)秀的開(kāi)源庫(kù)攒暇,這里就直接使用阿里的開(kāi)源庫(kù)ARouter,地址如下:

https://github.com/alibaba/ARouter

ARouter配置

1.每一個(gè)模塊都必須引入compiler sdk子房,只需在base模塊中依賴 api sdk

base module

api   "com.alibaba:arouter-api:1.4.1"

其他組件 module

annotationProcessor "com.alibaba:arouter-compiler:1.2.2"

2.每一個(gè)module 的分組都必須不同,分組就是path的第一個(gè)"/"與第二個(gè)"/"之間就轧。

3.每個(gè)module中的build.gradle中都要加入如下配置

android {
    defaultConfig {
        
        javaCompileOptions {
            annotationProcessorOptions {
                includeCompileClasspath = true
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

ARouter使用

在common-service模塊下建一個(gè)router包和一個(gè)service包证杭,router包存放路由相關(guān)代碼,service包存放各模塊對(duì)外提供的服務(wù)相關(guān)代碼妒御,不同的模塊提供的服務(wù)代碼放在service包下面不同的子包中

image.png

1.ModulePath用來(lái)存在組件的路由地址解愤,一級(jí)路由用模塊名,二級(jí)路由用“module”(無(wú)特別含義乎莉,ARouter要求至少要有二級(jí)路由)送讲,根據(jù)實(shí)際情況確定是否需要三級(jí)路由

@StringDef(
       ModulePath.LOGIN,
       ModulePath.MAIN
)
annotation class ModulePath {

    companion object {

        /**
         * 登錄模塊
         */
        const val LOGIN = "/login/module"

        /**
         * 首頁(yè)模塊
         */
        const val MAIN = "/main/module"
    }
}

2.RouterManager用來(lái)提供各模塊間的路由和路由參數(shù)的解析,每個(gè)模塊的開(kāi)發(fā)者應(yīng)該將該模塊的路由跳轉(zhuǎn)的代碼寫在RouterManager來(lái)提供別的模塊跳轉(zhuǎn)到該模塊

object RouterManager {

    fun goMain(index: Int) {
        val params = HashMap<String, Any>()
                .apply {
                    put("position", index)
                }
        postcard(ModulePath.MAIN, "tabSelect", params.toJson())
                .navigation()
    }

    private fun postcard(@ModulePath module: String, target: String, params: String): Postcard {
        return ARouter.getInstance().build(module)
                .withString(TARGET, target)
                .withString(PARAMS, params)
    }
}

3.ServiceName用來(lái)存放組件對(duì)外提供服務(wù)的地址

@StringDef(
        ServiceName.SERVICE_LOGIN,
        ServiceName.SERVICE_SHARE
)
annotation class ServiceName {
    companion object {
        /**
         * 登錄信息服務(wù)
         */
        const val SERVICE_LOGIN = "/login/user"
        /**
         * 分享服務(wù)
         */
        const val SERVICE_SHARE = "/share/service"
    }
}

4.ServiceManager用來(lái)存放組件對(duì)外提供服務(wù)的方法惋啃,若模塊開(kāi)發(fā)者對(duì)其他模塊需暴露自己的服務(wù)哼鬓,需要將服務(wù)相關(guān)代碼寫在ServiceManager中,并加上注釋

object ServiceManager {

    /**
     * 獲取通用服務(wù)
     */
    fun <T> getService(service: Class<out T>): T? {
        return ARouter.getInstance().navigation(service)
    }

    /**
     * 獲取個(gè)人信息服務(wù)
     */
    fun getUserInfo(): IAppUserInfo? {
        return getService(IAppUserInfo::class.java)
    }

    /**
     * 獲取main模塊服務(wù)
     */
    fun getMainService(): IMainService? {
        return getService(IMainService::class.java)
    }
}

5.若一個(gè)模塊要對(duì)外暴露自己的服務(wù)边灭,需要在service包下新建自己的包异希,一般以模塊名作為報(bào)名威始,在該包下提供服務(wù)的接口和相關(guān)常量

interface IShareService : IProvider {
   
    fun showShareDialog(fragmentManager: FragmentManager, shareType: Int, shareParams: HashMap<String, Any>, miniProgramShareParams: HashMap<String, Any>)

    fun getShareImageUrl(shareParams: HashMap<String, Any>, consumer: Consumer<String>, error: Consumer<Throwable>): Disposable
}

6.各業(yè)務(wù)模塊通過(guò)RouterManager來(lái)跳轉(zhuǎn)

RouterManager.goGoodsDetail(model.pitemId, model.exhibitionParkType)

7.各業(yè)務(wù)模塊通過(guò)ServiceManager調(diào)用服務(wù)

private val appUserInfo by lazy {
    ServiceManager.getUserInfo()
}

4棒搜、單獨(dú)調(diào)試

當(dāng)工程被拆分為組件化的時(shí)候,那么Module的單獨(dú)調(diào)試就顯得尤為重要,無(wú)論是對(duì)問(wèn)題的跟蹤還是業(yè)務(wù)線的并行開(kāi)發(fā)八千,都需要工程具備單獨(dú)運(yùn)行調(diào)試的能力。這里單獨(dú)調(diào)試同樣是對(duì)gradle的操作与殃,通過(guò)對(duì)編譯腳本的編寫來(lái)達(dá)到組件單獨(dú)運(yùn)行的目的母赵。

1.新建appConfig.gradle對(duì)需要單獨(dú)運(yùn)行的Module抽取變量進(jìn)行記錄:

ext {

    app = [
            versionName: "1.5.0"
    ]

    runAlone = [
            login          : false,
            mine           : false,
            main           : false
    ]

}

并在工程的build.gradle中apply

apply from: "appConfig.gradle"

2.在對(duì)應(yīng)module的編譯腳本文件中添加判斷module是以lib形式依賴還是以app方式進(jìn)行依賴,如下代碼:

def runAlone = rootProject.ext.runAlone.ssbb.toBoolean()

if (runAlone) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

添加Application中一些必要的元素授药,清單文件Manifest.xml文件频轿,但是這個(gè)xml文件是組件在單獨(dú)運(yùn)行的過(guò)程中所需要的,所以這里要放到一個(gè)runalone的目錄下烁焙,在java目錄下建一個(gè)runalone包航邢,存放該組件單獨(dú)編譯時(shí)需要的代碼:

image.png

同時(shí)在此基礎(chǔ)上通過(guò)編譯腳本配置單獨(dú)運(yùn)行時(shí)獲取的Android相關(guān)文件以及在作為library時(shí)剔除相關(guān)文件:

android {
    sourceSets {
        main {
            if (runAlone) {
                manifest.srcFile 'src/main/runAlone/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java{
                    exclude 'src/main/runAlone/*'
                }
            }
        }
    }
}

單獨(dú)調(diào)試時(shí)增加applicationId,集成調(diào)試時(shí)移除

android {
    defaultConfig {
        if (runAlone) {
            applicationId "com.jokerwan.baby"
        }
    }
}

根據(jù)以上配置骄蝇,就可以使得業(yè)務(wù)模塊單獨(dú)運(yùn)行起來(lái)膳殷,調(diào)試起來(lái)非常方便,但我們調(diào)用接口都是需要cookie的九火,也就是單獨(dú)編譯調(diào)試時(shí)是需要先登錄的赚窃,繼續(xù)往下看

3.修改業(yè)務(wù)組件依賴關(guān)系:

業(yè)務(wù)組件依賴基礎(chǔ)業(yè)務(wù)組件common和基礎(chǔ)服務(wù)組件common_service,再根據(jù)單獨(dú)編譯時(shí)是否需要依賴其他組件對(duì)相應(yīng)的組件進(jìn)行依賴:

dependencies {
    kapt rootProject.ext.dependencies["arouter-compiler"]

    implementation project(path: ':common')
    implementation project(path: ':common_service')

    if (runAlone) {
        implementation project(path: ':login')
        implementation project(path: ':exhibition')
        implementation project(path: ':share')
    }
}

4.模塊單獨(dú)編譯打包時(shí)登錄處理:

image.png

在base模塊新建一個(gè)delayaction岔激,用來(lái)處理需要驗(yàn)證某些條件后才能出發(fā)的動(dòng)作勒极,比如登錄后才能跳轉(zhuǎn)到某個(gè)Activity,沒(méi)有登錄時(shí)先跳轉(zhuǎn)到登錄頁(yè)面虑鼎,SingleCall具體實(shí)現(xiàn)邏輯如下辱匿,通過(guò)單例存儲(chǔ)一個(gè)Action,在Action執(zhí)行前先判斷執(zhí)行條件炫彩,執(zhí)行條件沒(méi)通過(guò)就調(diào)用doMakeValid()去使執(zhí)行條件有效匾七,執(zhí)行條件通過(guò)后再手動(dòng)觸發(fā)call()來(lái)執(zhí)行Action

/**
 * Created by JokerWan on 2019-10-31.
 * Function: 延遲任務(wù)處理的類需要實(shí)現(xiàn)的接口
 */
public interface Action {

    /**
     * 前置條件通過(guò)后執(zhí)行的回調(diào)
     */
    void call();
}
/**
 * Created by JokerWan on 2019-10-31.
 * Function: 一個(gè)執(zhí)行單元
 */
public class CallUnit {

    //目標(biāo)行為
    private Action action;
    //驗(yàn)證模型隊(duì)列
    private Queue<Condition> conditionQueue = new ArrayDeque<>();
    //上一個(gè)執(zhí)行的Condition
    private Condition lastCondition;

    public CallUnit() {
    }

    public CallUnit(Action action) {
        this.action = action;
    }

    public Action getAction() {
        return action;
    }

    public void setAction(Action action) {
        this.action = action;
    }

    public Condition getLastCondition() {
        return lastCondition;
    }

    public void setLastCondition(Condition lastCondition) {
        this.lastCondition = lastCondition;
    }

    public CallUnit addCondition(Condition condition) {
        conditionQueue.add(condition);
        return this;
    }

    public Queue<Condition> getConditionQueue() {
        return conditionQueue;
    }

}
/**
 * Created by JokerWan on 2019-10-31.
 * Function: 條件驗(yàn)證模型
 */
public interface Condition {

    /**
     * 是否滿足檢驗(yàn)器的要求,如果不滿足的話江兢,則執(zhí)行doMakeValid方法昨忆。如果滿足,則執(zhí)行目標(biāo)action
     */
    boolean check();

    /**
     * 不滿足檢驗(yàn)器時(shí)執(zhí)行
     */
    void doMakeValid();
}
/**
 * Created by JokerWan on 2019-10-31.
 * Function: Condition與Action管理
 */
public class SingleCall {

    private CallUnit callUnit = new CallUnit();

    /**
     * 獲取單例
     */
    public static SingleCall getInstance() {
        return SingletonHolder.mInstance;
    }

    private static class SingletonHolder {
        private static SingleCall mInstance = new SingleCall();
    }

    public SingleCall addAction(Action action) {
        clear();
        callUnit.setAction(action);
        return this;
    }

    /**
     * 添加需要驗(yàn)證的條件
     *
     * @param condition 需驗(yàn)證的條件
     * @return this
     */
    public SingleCall addCondition(Condition condition) {
        // 只添驗(yàn)證不通過(guò)的
        if (condition.check()) {
            return this;
        }
        callUnit.addCondition(condition);
        return this;
    }

    public void call() {

        // 上一個(gè)condition沒(méi)有驗(yàn)證通過(guò)的話杉允,是不允許再發(fā)起call的
        if (callUnit.getLastCondition() != null && !callUnit.getLastCondition().check()) {
            return;
        }

        // 執(zhí)行action
        if (callUnit.getConditionQueue().size() == 0 && callUnit.getAction() != null) {
            callUnit.getAction().call();
            // 清空
            clear();
        } else {
            // 執(zhí)行驗(yàn)證邑贴。
            Condition condition = callUnit.getConditionQueue().poll();
            callUnit.setLastCondition(condition);
            if (condition != null) {
                condition.doMakeValid();
            }
        }

    }

    /**
     * 清空回調(diào),建議在Activity/Fragment生命周期的onDestroy
     * 回調(diào)中調(diào)用該方法避免內(nèi)存泄漏
     */
    public void clear() {
        callUnit.getConditionQueue().clear();
        callUnit.setAction(null);
        callUnit.setLastCondition(null);
    }
}

下面我們以baby模塊的代碼來(lái)具體看下在模塊單獨(dú)打包時(shí)是如何獲取登錄狀態(tài)

首先在login模塊中加入如下代碼

// 登錄成功之后調(diào)用
    private fun finishLogin() {
        var goMain = false
        // 'goMain' set value
        if (goMain) {
            RouterManager.goMain(0, "Login")
        } else {
            // 在這里觸發(fā)call()來(lái)執(zhí)行登錄成功之后的Action
            SingleCall.getInstance().call()
        }
        finish()
    }

xxx模塊runalone.xxx包下

image.png

BabyApp繼承WApp叔磷,WApp是定義在common模塊下的通用Application拢驾,包括一些通用的第三方組件的初始化

class BabyApp : WApp()
open class WApp : Application() {

    override fun onCreate() {
        super.onCreate()
        app = this

        initARouter()
        initUpgradeManager()
        initThirdServiceInBackground()
        initRefreshLayout()
        initUmengPush()
        initLeakCanary()
    }
}

LoginCondition是登錄驗(yàn)證條件

class LoginCondition : Condition {

    /**
     * 驗(yàn)證條件
     */
    override fun check(): Boolean {
        val userInfo =
            ARouter.getInstance().build(ServiceName.SERVICE_LOGIN).navigation() as IAppUserInfo
        return userInfo.getId() != 0L
    }

    /**
     * 執(zhí)行驗(yàn)證條件
     */
    override fun doMakeValid() {
        RouterManager.goLoginWillBack("sst")
    }
}

BabyRouterActivity是登錄跳轉(zhuǎn)中間類,主要作用是路由轉(zhuǎn)發(fā)或者提供裝載模塊Fragment容器(該模塊只有Fragment世澜,沒(méi)有Activity)

class BabyRouterActivity : BaseActivity(),Action{

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        StatusBarUtil.setStatusBarColorWhite(this)

        // 執(zhí)行登錄條件驗(yàn)證
        SingleCall.getInstance()
            .addAction(this)
            .addCondition(LoginCondition())
            .call()
    }

   /**
     * onDestroy()時(shí)清空SingleCall
     */
    override fun onDestroy() {
        super.onDestroy()
        SingleCall.getInstance().clear()
    }

    /**
     * 登錄條件驗(yàn)證通過(guò)的回調(diào)
     */
    override fun call() {
        startActivity(Intent(this, BabyActivity::class.java))
        finish()
        
        // addFragment(android.R.id.content, MineFragment())
    }
}

當(dāng)驗(yàn)證條件通過(guò)后會(huì)回調(diào)call()方法独旷,在call()方法里可以跳轉(zhuǎn)到模塊的Activity或者直接用此Activity裝載Fragment

5、整體調(diào)試

將appConfig.gradle中的模塊單獨(dú)調(diào)試變量全部改為false

根據(jù)上述方案,app殼其實(shí)只需要依賴業(yè)務(wù)組件即可:

dependencies {
    
    implementation project(path: ':common')
    implementation project(path: ':common_service')

    if (!rootProject.ext.runAlone.login.toBoolean()) {
        implementation project(path: ':login')
    }

    if (!rootProject.ext.runAlone.mine.toBoolean()) {
        implementation project(path: ':mine')
    }
}

6嵌洼、資源名沖突

color案疲,shape,drawable麻养,圖片資源褐啡,布局資源,或者anim資源等等鳖昌,都有可能造成資源名稱沖突备畦。有時(shí)候大家負(fù)責(zé)不同的模塊,如果不是按照統(tǒng)一規(guī)范命名许昨,則會(huì)偶發(fā)出現(xiàn)該問(wèn)題

可以通過(guò)設(shè)置 resourcePrefix 來(lái)避免懂盐。設(shè)置了這個(gè)值后,你所有的資源名必須以指定的字符串做前綴糕档,否則會(huì)報(bào)錯(cuò)莉恼。但是 resourcePrefix 這個(gè)值只能限定 xml 里面的資源,并不能限定圖片資源速那,所有圖片資源仍然需要你手動(dòng)去修改資源名俐银。

android {
    // 所有xml資源命名以 "login_" 開(kāi)頭、否則編譯報(bào)紅
    resourcePrefix "login_"
}

7端仰、組件Application初始化

自定義 Application 需要聲明在 AndroidManifest.xml 中捶惜。其次,每個(gè) Module 都有該清單文件荔烧,但是最終的 APK 文件只能包含一個(gè)吱七。因此,在構(gòu)建應(yīng)用時(shí)茴晋,Gradle 構(gòu)建會(huì)將所有清單文件合并到一個(gè)封裝到 APK 的清單文件中陪捷。

合并的優(yōu)先級(jí)是:

App Module > Library Module

合并的規(guī)則:

image.png

結(jié)合我們的情況,是值 A 合并值 B诺擅,會(huì)產(chǎn)生沖突錯(cuò)誤:

Execution failed for task ':app:processDebugManifest'.
> Manifest merger failed : Attribute application@name value=(com.baseres.BaseApplication) from AndroidManifest.xml:8:9-51
    is also present at [:carcomponent] AndroidManifest.xml:14:9-55 value=(com.carcomponent.CarApplication).
    Suggestion: add 'tools:replace="android:name"' to <application> element at AndroidManifest.xml:7:5-24:19 to override.

錯(cuò)誤信息中給出了解決建議,在高優(yōu)先級(jí)的 App Module 中使用 tools:replace="android:name"啡直,但這樣做是直接用值 A 替換了值 B烁涌,并非我們想要的結(jié)果。另外再推薦給大家一個(gè)方法酒觅,打開(kāi) App Module 的 AndroidManifest.xml 文件撮执,選擇下方 Merged Manifest 選項(xiàng)卡,可以看到預(yù)合并結(jié)果舷丹。

解決方法一:每個(gè)業(yè)務(wù)模塊通過(guò)Arouter暴露出模塊初始化服務(wù)抒钱,app模塊可在適當(dāng)?shù)臅r(shí)機(jī)(為了增強(qiáng)秒開(kāi)體驗(yàn),盡量別在app模塊的Application中初始化,可以在MainActivity中進(jìn)行初始化或者在跳轉(zhuǎn)業(yè)務(wù)模塊前初始化)調(diào)用各模塊的服務(wù)進(jìn)行初始化

解決方法二:通過(guò)反射在app模塊的Application的onCreate()方法中調(diào)用組件Application的初始化方法

1.在base模塊中新增BaseAPP

public abstract class BaseApp extends Application {
    /**
     * Application 初始化
     */
    public abstract void initModuleApp(Application application);
    private static Context appContext;

    @Override
    public void onCreate() {
        super.onCreate();
        appContext = getApplicationContext();
    }

    public static Context getAppContext() {
        return appContext;
    }
}

2.在live模塊中的LiveApplication繼承BaseApp實(shí)現(xiàn)initModuleApp()方法谋币,并在此方法中做初始化操作仗扬,作為library時(shí)初始化操作在initModuleApp()方法中,作為獨(dú)立app時(shí)蕾额,初始化操作在onCreate()中早芭。

public class LiveApplication extends BaseApp {

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void initModuleApp(Application application) {
        // init
    }
}

3.在base模塊中增加AppConfig類,用來(lái)配置需要初始化的組件Application

public class AppConfig {
    private static final String LiveApp = "cn.com.live.LiveApplication";

    public static String[] moduleApps = {
            LiveApp
    };
}

4.在app的Application的onCreate()方法中通過(guò)反射初始化在AppConfig中聲明類全路徑的組件Application類

public class WApp extends BaseApp {

    @Override
    public void onCreate() {
        super.onCreate();

        initModuleApp(this);
    }
    
    @Override
    public void initModuleApp(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                SBBaseApp baseApp = (SBBaseApp) clazz.newInstance();
                baseApp.initModuleApp(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}

8诅蝶、組件代碼混淆

方法一:直接在app模塊的proguard-rules.pro中編寫所有組件的混淆規(guī)則

優(yōu)點(diǎn):簡(jiǎn)單無(wú)腦

缺點(diǎn):若app模塊依賴的組件很多退个,則proguard-rules.pro中混淆規(guī)則龐大不利于維護(hù),使用app模塊編寫所有混淆命令是基于業(yè)務(wù)模塊當(dāng)中不再編寫混淆命令為前提调炬,所以在打包將業(yè)務(wù)模塊上傳到私有倉(cāng)庫(kù)時(shí)语盈,業(yè)務(wù)模塊都是不開(kāi)啟混淆功能的!

但是

上述結(jié)論都是建立在以implementation或者api形式依賴的前提下缰泡,開(kāi)發(fā)階段我們是以

implementation project(':live')

這種形式進(jìn)行依賴的刀荒,你會(huì)發(fā)現(xiàn)當(dāng)以這種形式進(jìn)行依賴時(shí),不管業(yè)務(wù)模塊minifyEnabled是true還是false匀谣,只要app模塊寫上了正確的混淆規(guī)則那么程序都能正常運(yùn)行照棋!

方法二:各個(gè)業(yè)務(wù)組件單獨(dú)編寫混淆規(guī)則(推薦)

優(yōu)點(diǎn):各組件自己配置混淆文件,易于維護(hù)

在模塊中的build.gradle中配置

android {
    buildTypes {
        release {
            consumerProguardFiles 'proguard-rules.pro'
        }
    }
}

使用這種配置最大的一個(gè)好處就是業(yè)務(wù)模塊的是否混淆完全由app模塊來(lái)決定武翎,這種配置有一個(gè)非常重要的關(guān)鍵點(diǎn)就是不能設(shè)置minifyEnabled true烈炭,因?yàn)樵O(shè)置為true之后業(yè)務(wù)模塊是否混淆的控制權(quán)將只能由該模塊自身決定,app模塊將無(wú)法控制業(yè)務(wù)模塊的混淆與否而且設(shè)置為true之后還必須額外配置

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

9宝恶、組件庫(kù)的獨(dú)立發(fā)布和維護(hù)

原有拆分的本地組件徹底分離出去符隙,采取獨(dú)立發(fā)布和維護(hù)的方式迭代更新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末垫毙,一起剝皮案震驚了整個(gè)濱河市霹疫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌综芥,老刑警劉巖丽蝎,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異膀藐,居然都是意外死亡屠阻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門额各,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)国觉,“玉大人,你說(shuō)我怎么就攤上這事虾啦÷榫鳎” “怎么了痕寓?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蝇闭。 經(jīng)常有香客問(wèn)我呻率,道長(zhǎng),這世上最難降的妖魔是什么丁眼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任筷凤,我火速辦了婚禮,結(jié)果婚禮上苞七,老公的妹妹穿的比我還像新娘藐守。我一直安慰自己,他們只是感情好蹂风,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布卢厂。 她就那樣靜靜地躺著,像睡著了一般惠啄。 火紅的嫁衣襯著肌膚如雪慎恒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,071評(píng)論 1 285
  • 那天撵渡,我揣著相機(jī)與錄音融柬,去河邊找鬼。 笑死趋距,一個(gè)胖子當(dāng)著我的面吹牛粒氧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播节腐,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼外盯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了翼雀?” 一聲冷哼從身側(cè)響起饱苟,我...
    開(kāi)封第一講書(shū)人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎狼渊,沒(méi)想到半個(gè)月后箱熬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狈邑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年坦弟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片官地。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖烙懦,靈堂內(nèi)的尸體忽然破棺而出驱入,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布亏较,位于F島的核電站莺褒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏雪情。R本人自食惡果不足惜遵岩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巡通。 院中可真熱鬧尘执,春花似錦、人聲如沸宴凉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)弥锄。三九已至丧靡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間籽暇,已是汗流浹背温治。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戒悠,地道東北人熬荆。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像救崔,于是被迫代替她去往敵國(guó)和親惶看。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345