最有深度的模塊化、組件化、插件化、熱修復(fù)原理總結(jié),你確定不來(lái)看看嗎?

前言

談到熱修復(fù)相信大家應(yīng)該比較熟悉,因?yàn)樗悄壳氨容^重要的技術(shù),平常面試中也是被問的比較多。插件化和熱修復(fù)同出一門,倆者都屬于動(dòng)態(tài)更新,而模塊化和組件化是基礎(chǔ)。相信看完本篇的內(nèi)容,對(duì)于這些模糊的概念應(yīng)該會(huì)有一個(gè)比較清晰的了解。

原文鏈接:https://blog.csdn.net/csdn_aiyang/article/details/103735995?

一、模塊化

1、概念

模塊(Module),Android Studio提出的概念,根據(jù)不同關(guān)注點(diǎn)將原項(xiàng)目中共享的部分或業(yè)務(wù)抽取出來(lái)形成獨(dú)立module,這就類似我們最集成的第三方庫(kù)的SDK。

2、思想

實(shí)際開發(fā)中,我們通常會(huì)抽取第三方庫(kù)、整個(gè)項(xiàng)目的初始化的代碼、自定義的Utils工具類、自定義View 、圖片、xml這些(value目錄下的各種xml文件)等到一個(gè)共有的Common模塊中,其他模塊在配置Gradle依賴后,就能夠調(diào)用這些API。

特別注意的是style.xml文件,對(duì)于全局共用的style,我們應(yīng)該把它也放在common模塊中。例如我們的項(xiàng)目theme主題,本來(lái)是放在main組件的style里面,我們可以把它移到common中,這樣其他組件調(diào)試時(shí),作為一個(gè)單獨(dú)的項(xiàng)目,也能和主項(xiàng)目有一樣的主題。

總之,你認(rèn)為需要共享的資源,都應(yīng)該放在common組件中。

3、使用

每一個(gè)Module都可以在自身的 build.gradle 中進(jìn)行設(shè)置兩種格式:application和library。

apply plugin: 'com.android.application'
//或
apply plugin: 'com.android.library'

引用時(shí),就像添加依賴GitHub庫(kù)一樣。

二、組件化

1、概念

組件化是基于模塊化的,可以在打包時(shí)是設(shè)置為library,開始調(diào)試運(yùn)行是設(shè)置成application。目的是解耦與加快開發(fā)。組件化適用于多人合作開發(fā)的場(chǎng)景,隔離不需要關(guān)注的模塊,大家各自分工、各守其職。簡(jiǎn)而言之,就是把一個(gè)項(xiàng)目分開成多個(gè)項(xiàng)目

(1)好處

  • 業(yè)務(wù)模塊分開,解耦的同時(shí)也降低了項(xiàng)目的復(fù)雜度。
  • 開發(fā)調(diào)試時(shí)不需要對(duì)整個(gè)項(xiàng)目進(jìn)行編譯。
  • 多人合作時(shí)可以只關(guān)注自己的業(yè)務(wù)模塊,把某一業(yè)務(wù)當(dāng)成單一項(xiàng)目來(lái)開發(fā)。
  • 可以靈活的對(duì)業(yè)務(wù)模塊進(jìn)行組裝和拆分。

(2)規(guī)則

  • 只有上層的組件才能依賴下層組件,不能反向依賴,否則可能會(huì)出現(xiàn)循環(huán)依賴的情況;
  • 同一層之間的組件不能相互依賴,這也是為了組件之間的徹底解耦;

2、使用

1、在整個(gè)項(xiàng)目 gradle.properties 文件中,添加代碼

#是否處于debug狀態(tài)
isDebug = flase

2、在其他Module的 build.gradle 文件中,添加代碼

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

3、在宿主Module的 build.gradle 文件中,添加代碼

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    //...
    if(!isDebug.toBoolean()){//不是debug,就添加依賴其他模塊
        compile project(':home')
        compile project(':personal')
        compile project(':video')
    }
    if(isDebug.toBoolean()){
        compile project(':common')
    }
}

3、版本管理

每個(gè)Module的build.gradle文件中很多地方需要些寫版本號(hào),例如 targetSdkVersion、appcompat-v7、第三方庫(kù)等。修改時(shí)都要同時(shí)修改多份build.gradle文件。如果把版本號(hào)可以統(tǒng)一管理起來(lái),就會(huì)省時(shí)省力,又避免不同的組件使用的版本不一樣,導(dǎo)致合并在一起時(shí)引起沖突。

整個(gè)項(xiàng)目根目錄下的 build.gradle 文件中,添加代碼

ext {
    compileSdkVersion = 25
    buildToolsVersion = "25.0.2"
    minSdkVersion = 14
    targetSdkVersion = 25
    versionCode = 1
    versionName = "1.0"
}

每個(gè)Mudule的 build.Gradle 文件中,改寫代碼

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
    }
//...
}

4、模塊間跳轉(zhuǎn)

我們知道,通常在Gradle中依賴的庫(kù)是可以直接引用的,即通過startActivity跳轉(zhuǎn)。根據(jù)組件化的規(guī)則,宿主可以依賴下層組件,而組件之間不可以依賴。因此,當(dāng)常規(guī)業(yè)務(wù)模塊之間遇到業(yè)務(wù)需求,進(jìn)行互相跳轉(zhuǎn)時(shí)該怎么處理?

這里簡(jiǎn)單介紹兩種方式,即路由和反射。路由的方式以用阿里的ARouter/美團(tuán)的WMRouter,但是我覺得人少、項(xiàng)目小的公司必要用到這么強(qiáng)大的工具,直接反射就好。

放在common組件中的EventUtile工具類

public class EventUtil{
    /**
     * 頁(yè)面跳轉(zhuǎn)
     * className  全路徑類名
     */
    public static void open(Context context,String className){
        try {
            Class clazz = Class.forName(className);
            Intent intent = new Intent(context,clazz);
           context.startActivity(intent);
        } catch (ClassNotFoundException e) {
            Log.e("zhuang","未集成,無(wú)法跳轉(zhuǎn)");
        }
    }
    /**
     * 頁(yè)面跳轉(zhuǎn),可以傳參,參數(shù)放在intent中,所以需要傳入一個(gè)intent
     */
    public static void open(Context context,String className,Intentintent){
        try {
            Class clazz = Class.forName(className);
           intent.setClass(context,clazz);
           context.startActivity(intent);
        } catch (ClassNotFoundException e) {
            Log.e("zhuang","未集成,無(wú)法跳轉(zhuǎn)");
        }
    }
}

5、資源命名問題

首先,多組件集成時(shí),特別容易出現(xiàn)資源命名重復(fù)的問題。可以讓各個(gè)組件中使用統(tǒng)一前綴,比如home組件中的資源,以home_開通、video組件中以video_開頭。當(dāng)然,如果是嫌麻煩,我們可以在build.gradle文件中,加入如下代碼:

resourcePrefix"home_"

但是這個(gè)功能其實(shí)很弱。比較xml文件報(bào)錯(cuò),依然可以運(yùn)行,圖片文件不已home_為前綴,也不會(huì)報(bào)錯(cuò)。

三、插件化

也是屬于模塊化的一種體現(xiàn)。將完整的項(xiàng)目按業(yè)務(wù)劃分不同的插件,分治法,越小的模塊越容易維護(hù)。單位是apk,一個(gè)完整的項(xiàng)目。插件化比熱修復(fù)簡(jiǎn)單,插件化只是增加新的功能或資源文件。靈活性在于加載apk,按需下載,動(dòng)態(tài)更新。

實(shí)現(xiàn)原理

  1. 通過dexclassloader加載。
  2. 代理模式添加生命周期。
  3. hook思想跳過清單驗(yàn)證。

Android 使用Java的反射機(jī)制總結(jié)

Android 動(dòng)態(tài)代理與Hook機(jī)制詳解

總結(jié)

  • 宿主和插件分開編譯
  • 動(dòng)態(tài)更新插件
  • 按需下載插件
  • 緩解65535方法數(shù)限制

四、熱修復(fù)

1、概述

熱修復(fù)與插件化都利用classloader實(shí)現(xiàn)加載新功能。熱修復(fù)比插件化復(fù)雜,插件化只是增加新的功能或資源文件,所以不涉及搶先加載舊類的使命。熱修復(fù)為了修復(fù)bug,要將新的同名類替舊的同名bug類,要搶在加載bug類之前加載新的類。

2、流派

熱修復(fù)作為當(dāng)下熱門的技術(shù),在業(yè)界內(nèi)比較著名的有阿里巴巴的AndFix、Dexposed,騰訊QQ空間的超級(jí)補(bǔ)丁和微信的Tinker,以及大眾點(diǎn)評(píng)nuwa和美團(tuán)Robust。阿里百川推出的HotFix熱修復(fù)服務(wù)就基于AndFix技術(shù),定位于線上緊急BUG的即時(shí)修復(fù)。雖然Tinker支持修復(fù)的功能強(qiáng)大兼容性很好,但是不能即時(shí)生效、集成負(fù)責(zé)、補(bǔ)丁包大。

3、原理

(1)native修復(fù)方案

AndFix

提供了一種運(yùn)行時(shí)在Native修改Filed指針的方式,實(shí)現(xiàn)方法的替換,達(dá)到即時(shí)生效無(wú)需重啟,對(duì)應(yīng)用無(wú)性能消耗的目的。實(shí)現(xiàn)過程三步驟:

  • setup():對(duì)于Dalvik的即時(shí)編譯機(jī)制(JIT),在運(yùn)行時(shí)裝載libdvm.so動(dòng)態(tài)鏈接庫(kù),從而獲取native層內(nèi)部函數(shù):dvmThreadSelf( ):查詢當(dāng)前的線程;dvmDecodeIndirectRef( ):根據(jù)當(dāng)前線程獲得ClassObject對(duì)象。
  • setFieldFlag():把 private、protected的方法和字段都改為public,這樣才可被動(dòng)態(tài)庫(kù)看見并識(shí)別,因?yàn)閯?dòng)態(tài)庫(kù)會(huì)忽略非public屬性的字段和方法。
  • replaceMethod():該步驟是方法替換的核心。拿到新舊方法的指針,將指針指向新的替換方法來(lái)實(shí)現(xiàn)方法替換。

(2)Dex 分包方案

概述

DEX分包是為了解決65536方法限制,系統(tǒng)在應(yīng)用打包APK階段,會(huì)將有調(diào)用關(guān)系的類打包在同一個(gè)Dex文件中,并且同一個(gè)dex中的類會(huì)被打上CLASS_ISPREVERIFIED的標(biāo)志。因?yàn)榧虞d后的類不能卸載,必須通過重啟后虛擬機(jī)進(jìn)行加載才能實(shí)現(xiàn)修復(fù),所以此方案不支持即時(shí)生效。

QQ空間超級(jí)補(bǔ)丁

是把BUG方法修復(fù)以后放到一個(gè)patch.dex,拿到當(dāng)前應(yīng)用BaseDexClassloader后,通過反射獲取到DexPathList屬性對(duì)象pathList、再反射調(diào)用pathList的dexElements方法把patch.dex轉(zhuǎn)化為Element[],兩個(gè)Element[]進(jìn)行合并,最后把patch.dex插入到dexElements數(shù)組的最前面,讓虛擬機(jī)去加載修復(fù)完后的方法,就可以達(dá)到修復(fù)目的。

問題

而然,問題就是兩個(gè)有調(diào)用關(guān)系的類不再同一個(gè)Dex文件中,那么就會(huì)拋“unexpected DEX problem”異常報(bào)錯(cuò)。解決辦法,就是單獨(dú)放一個(gè)AnitLazyLoad類在另外DEX中,在每一個(gè)類的構(gòu)造方法中引用其他DEX中的唯一AnitLazyLoad類,避免類被打上CLASS_ISPREVERIFIED標(biāo)志。

不足

此方案通過增加dex來(lái)修復(fù),但是修復(fù)的類到了一定數(shù)量,就需要花不少的時(shí)間加載。對(duì)手淘這種航母級(jí)應(yīng)用來(lái)說(shuō),啟動(dòng)耗時(shí)增加2s以上是不能夠接受的事。在ART模式下,如果類修改了結(jié)構(gòu),就會(huì)出現(xiàn)內(nèi)存錯(cuò)亂的問題。為了解決這個(gè)問題,就必須把所有相關(guān)的調(diào)用類、父類子類等等全部加載到patch.dex中,導(dǎo)致補(bǔ)丁包異常的大,進(jìn)一步增加應(yīng)用啟動(dòng)加載的時(shí)候,耗時(shí)更加嚴(yán)重。

微信Tinker

微信Tinker采用的是DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級(jí)補(bǔ)丁技術(shù)基本相同,但不將patch.dex增加到elements數(shù)組中。差量的方式拿到patch.dex,開啟新進(jìn)程的服務(wù)TinkerPatchService,將patch.dex與應(yīng)用中的classes.dex合并,得到一個(gè)新的fix_classess.dex。通過反射操作得到PathClassLoader的DexPatchList,再反射調(diào)用patchlist的makeDexElements()方法,把fix_classess.dex直接替換到Element[]數(shù)組中去,達(dá)到修復(fù)的目的。從而提高了兼容性和穩(wěn)定性。

(3)Instand Run 方案

Instant Run,是android studio2.0新增的一個(gè)運(yùn)行機(jī)制,用來(lái)減少對(duì)當(dāng)前應(yīng)用的構(gòu)建和部署的時(shí)間。

構(gòu)建項(xiàng)目的流程:

構(gòu)建修改的部分 → 部署修改的dex或資源 → 熱部署,溫部署,冷部署。

熱拔插:方法實(shí)現(xiàn)的修改,或者變量值修改,不需要重啟應(yīng)用,不需要重建當(dāng)前activity。

溫拔插:代碼修改涉及到了資源文件,activity需要被重啟。

冷拔插:修改了繼承規(guī)則、修改了方法簽名,app需要被重啟,但是仍然不需要重新安裝 。

五、總結(jié)

模塊化、組件化、插件化通訊方式不同之處

  1. 模塊化相互引入,抽取了公共的common模塊,其他模塊自然要引入這個(gè)module。
  2. 組件化主流是隱式和路由。隱式使解耦和靈活大大降低,因此路由是主流。
  3. 插件化本身是不同進(jìn)程,因此是binder機(jī)制進(jìn)程間通訊。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容