前言
談到熱修復(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)原理
- 通過dexclassloader加載。
- 代理模式添加生命周期。
- 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é)
模塊化、組件化、插件化通訊方式不同之處
- 模塊化相互引入,抽取了公共的common模塊,其他模塊自然要引入這個(gè)module。
- 組件化主流是隱式和路由。隱式使解耦和靈活大大降低,因此路由是主流。
- 插件化本身是不同進(jìn)程,因此是binder機(jī)制進(jìn)程間通訊。