需求
最近要做sdk的熱更新
因?yàn)樾枨蠓降膕dk其實(shí)是jar包,只有class文件,沒有資源文件淆九,所以此文只針對(duì)class文件更新
首先羅列下一個(gè)輕量級(jí)更新框架的功能最小邊界:
- 需要配置文件描述更新包
- 更新包需要在線下載劲蜻,并檢驗(yàn)包的完整性
- 只針對(duì)特定版本
- 針對(duì)特定渠道
- 補(bǔ)丁包的版本控制
調(diào)研
市面上比較流行的熱更新有Tinker、QZone尤辱、AndFix砂豌、Sophix、Robust光督、Dexposed
這些大家都很熟悉阳距,但絕大部分是針對(duì)app的
先回顧下class文件是如何實(shí)現(xiàn)熱修復(fù)的:
概念:
DexClassLoader和PathClassLoader
回顧下Android中Classloader的知識(shí)
DexClassLoader
和PathClassLoader
都是繼承自BaseDexClassLoader
看下Android10的代碼
public class DexClassLoader extends BaseDexClassLoader {
/**
* ...
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
可見這兩個(gè)類沒有本質(zhì)上區(qū)別,網(wǎng)上說的optimizedDirectory
在Api26之后已廢棄
只不過PathClassLoader多了構(gòu)造函數(shù)參數(shù)sharedLibraryLoaders结借,可以加載系統(tǒng)類
ClassLoader
在Java的ClassLoader中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
findLoadedClass是保證加載的類不會(huì)被加載
findBootstrapClassOrNull返回的是空
所以調(diào)用的findClass方法
BaseDexClassLoader的findClass方法:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// First, check whether the class is present in our shared libraries.
if (sharedLibraryLoaders != null) {
for (ClassLoader loader : sharedLibraryLoaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException ignored) {
}
}
}
// Check whether the class in question is present in the dexPath that
// this classloader operates on.
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
可以由傳進(jìn)來的sharedLibraryLoaders來加載筐摘,但DexClassLoader的sharedLibraryLoaders為空
是交由DexPathList類來處理實(shí)現(xiàn)findClass
DexPathList
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
//native相關(guān)
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
...
}
通過Element[]數(shù)組來存儲(chǔ)dex,makeDexElements這個(gè)方法:
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
DexPathList在構(gòu)造函數(shù)通過makeDexElements方法船老,生成Element[]數(shù)組
在BaseDexClassLoader中調(diào)用的是DexPathList的findClass方法:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
很清楚的看到element是按順序取出
也就是說兩個(gè)element如果有相同的class咖熟,第二個(gè)就不會(huì)加載,如果將目標(biāo)dex包插隊(duì)到列隊(duì)最前面柳畔,則可以實(shí)現(xiàn)目標(biāo)dex包中的class替換掉其他dex中的class
熱修復(fù)的實(shí)現(xiàn):
- DexClassLoader加載目標(biāo)dex
- 將目標(biāo)的和系統(tǒng)的 dexElements 進(jìn)行合并
- 賦值給系統(tǒng)的 pathList
public void loadCustomDex(Context mContext){
//遍歷所有修復(fù)的dex
File fileDir = yourfilepath;
File[] listFiles = fileDir.listFiles();
for (File f : listFiles){
if(isValid(f)){
dexList.add(f);
}
}
//合并與應(yīng)用
PathClassLoader pathClassLoader = (PathClassLoader) mContext.getClassLoader();
for (File f : dexList){
//加載指定的dex文件馍管。
DexClassLoader dexClassLoader = new DexClassLoader(f.getAbsolutePath(),null,null,pathClassLoader);
//合并
Object dexObj = getPathList(dexClassLoader);
Object pathObj = getPathList(pathClassLoader);
Object dexElementsList = getDexElements(dexObj);
Object pathElementsList = getDexElements(pathObj);
Object newElements = combineArray(dexElementsList,pathElementsList);
//給PathList里面的dexElements賦值
Object pathList = getPathList(pathClassLoader);
setField(pathList,pathList.getClass(),"dexElements",newElements);
}
}
private Object getPathList(Object obj) throws Exception {
return reflectField(obj,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
private Object getDexElements(Object obj) throws Exception {
return reflectField(obj,obj.getClass(),"dexElements");
}
private static Object combineArray(Object array1, Object array2) {
Class<?> localClass = array1.getClass().getComponentType();
int i = Array.getLength(array1);
int j = i + Array.getLength(array2);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(array1, k));
} else {
Array.set(result, k, Array.get(array2, k - i));
}
}
return result;
}
方案:
上述的熱更新實(shí)現(xiàn)方式有兩個(gè)問題需要解決:
- app中可以在Application中的attachBaseContext中去做插入dex,SDK的類是不知道何時(shí)被調(diào)用的
- ISPREVERIFIED問題
如果一個(gè)類的static方法薪韩,private方法确沸,override方法以及構(gòu)造函數(shù)中引用了其他類,而且這些類都屬于同一個(gè)dex文件俘陷,此時(shí)該類就會(huì)被打上CLASS_ISPREVERIFIED
如果在運(yùn)行時(shí)被打上CLASS_ISPREVERIFIED的類引用了其他dex的類罗捎,就會(huì)報(bào)錯(cuò)
解決ISPREVERIFIED問題,常用的就是“插裝”(往字節(jié)碼里插入自定義的字節(jié)碼)岭洲,我們只要讓所有類都引用其他dex中的某個(gè)類就可以了:
- 當(dāng)安裝apk的時(shí)候宛逗,classes.dex內(nèi)的類都會(huì)引用一個(gè)在不相同dex中的XX類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標(biāo)志了盾剩,只要沒被打上這個(gè)標(biāo)志的類都可以進(jìn)行打補(bǔ)丁操作
- 我們需要在源碼編譯成字節(jié)碼之后雷激,在字節(jié)碼中進(jìn)行插入操作替蔬。對(duì)字節(jié)碼進(jìn)行操作的框架有很多,但是比較常用的則是ASM和javaassist
那么有沒有簡(jiǎn)單點(diǎn)的方法:
- 將工程拆分屎暇,保證要進(jìn)行更換的工程與其他工程沒有直接引用(都用反射)
- 對(duì)要進(jìn)行更換的工程采用全量替換方法承桥,考慮到sdk體積比較小,此方法最簡(jiǎn)單
配置文件
對(duì)應(yīng)需求中的最小邊界根悼,定義了如下的配置文件:
{
"checksum": {
"TYPE": "md5",
"value": "20d81614ab8ac44b3185af0f5e8a96b2"
},
"channel": "abcdefgh",
"version": "1.0.0",
"subVersion": 1,
"package": "http://robinfjb.github.io/dex.jar",
"className": "robin.sdk.sdk_impl2.ServiceImpl"
}
-
checksum
是對(duì)包完整性的驗(yàn)證 -
channel
是渠道凶异,在sdk一般用appkey作為渠道標(biāo)識(shí) -
version
目標(biāo)版本,由于sdk的定制化比較多挤巡,不同版本的代碼功能都不一樣剩彬,所以只針對(duì)某個(gè)版本更新 -
subVersion
補(bǔ)丁版本,只有新的補(bǔ)丁才會(huì)應(yīng)用 -
package
包的下載地址 -
className
新包的入口類名矿卑,后面會(huì)講到
工程
我們選擇類似類加載方案
將整個(gè)sdk拆分成 業(yè)務(wù)實(shí)現(xiàn)喉恋,更新模塊,對(duì)外api 三個(gè)模塊
- 業(yè)務(wù)實(shí)現(xiàn):
sdk-impl
, 更新則只要更新業(yè)務(wù)實(shí)現(xiàn)模塊 - 更新模塊:
sdk-dynamic
, 實(shí)現(xiàn)熱更新下載母廷,應(yīng)用 - 對(duì)外api:
sdk
轻黑,sdk對(duì)外暴露的類 - 代理:
sdk-proxy
為了避免sdk-impl
對(duì)sdk
的依賴,用單獨(dú)工程維護(hù)兩個(gè)模塊之間的接口 - 公共:
sdk-common
放一些基礎(chǔ)類琴昆,比如log之類
代碼
命名
補(bǔ)丁類可以命名為packagename+version
比如業(yè)務(wù)包名:robin.sdk.sdk_impl
補(bǔ)丁包名則為:robin.sdk.sdk_impl1
關(guān)鍵代碼:
代理:
例子中使用了CS結(jié)構(gòu)氓鄙,我們將service代理:
public interface ServiceProxy {
void onCreate(Context var1);
int onStartCommand(Intent var1, int var2, int var3);
IBinder onBind(Intent var1);
boolean onUnBind(Intent var1);
void onDestroy();
}
對(duì)外的service中實(shí)現(xiàn):
public final class RobinService extends Service {
@Override
public void onCreate() {
try {
context = getApplicationContext();
proxy = serviceLoad();
proxy.onCreate(this);
checkUpdate();
} catch (Throwable e) {
}
}
@Override
public int onStartCommand(Intent var1, int var2, int var3) {
return proxy.onStartCommand(var1, var2, var3);
}
@Override
public IBinder onBind(Intent var1) {
return proxy.onBind(var1);
}
@Override
public boolean onUnbind(Intent var1) {
return proxy.onUnBind(var1);
}
@Override
public void onDestroy() {
proxy.onDestroy();
}
}
然后在sdk-impl中實(shí)現(xiàn)代理接口:
public class ServiceImpl implements ServiceProxy {
}
這個(gè)類就是我們需要更換的類
下載補(bǔ)丁
在程序入口(Service onCreate
)方法, 開啟下載
先下載配置文件,下載完成后:
DyInfo dyInfo = new DyInfo(new JSONObject(response));
if (checkDyInfo(context, dyInfo)) {//檢查配置文件有效性
LogUtil.e(UPDATE_TAG, "downloadDyJar");
downloadDyJar(dyInfo, context);
}
checkDyInfo檢查當(dāng)前包是否需要下載與應(yīng)用補(bǔ)丁
幾個(gè)關(guān)鍵檢測(cè):
- 檢測(cè)key是否等于當(dāng)前key业舍,說明是針對(duì)某個(gè)key發(fā)的補(bǔ)丁抖拦,如果為空說明是全部渠道都應(yīng)用補(bǔ)丁
- 檢測(cè)SDK版本是否和配置文件中一致,補(bǔ)丁只針對(duì)某個(gè)版本
- 檢測(cè)補(bǔ)丁版本勤讽,在補(bǔ)丁應(yīng)用成功后會(huì)記錄應(yīng)用的補(bǔ)丁版本蟋座,只有新的補(bǔ)丁版本大于已應(yīng)用的,補(bǔ)丁才會(huì)下載與生效
下載完成后:檢查文件完整性與保存配置信息
if (!checkJarMd5(context, dyInfo.checksumValue)) {
LogUtil.e(UPDATE_TAG, "checkJarMd5 fail");
deleteFailjar(context);
} else {
//保存jar信息
SpUtil.setDyInfo(context, dyInfo);
LogUtil.e(UPDATE_TAG, "checkJarMd5 success");
}
應(yīng)用補(bǔ)丁
try {
DyInfo dyInfo = SpUtil.getDyInfo(context);
if (UpdateManager.checkDyInfo(context, dyInfo) && checkJar(dyInfo, usingJar)) {
DexClassLoader dexClassLoader = new DexClassLoader(usingJar.getAbsolutePath(),
context.getCacheDir().getAbsolutePath(), null, context.getClassLoader());
Class libclass = dexClassLoader.loadClass(dyInfo.className);
lib = (ServiceProxy) libclass.newInstance();
SpUtil.setPatchVersion(context, dyInfo.subVersion);
LogUtil.e(DYNAMIC_TAG, "動(dòng)態(tài)包已加載成功 ");
}
} catch (Throwable throwable) {
LogUtil.e(DYNAMIC_TAG, "動(dòng)態(tài)包加載異常:" + throwable.getLocalizedMessage());
}
if(lib == null) {
try {
Class libclass = context.getClassLoader().loadClass("robin.sdk.sdk_impl.ServiceImpl");
lib = (ServiceProxy) libclass.newInstance();
} catch (Throwable throwable) {
LogUtil.e(DYNAMIC_TAG, "正常包加載異常:" + throwable.getLocalizedMessage());
}
}
- 讀出上一次下載的配置文件與補(bǔ)丁包脚牍,進(jìn)行校驗(yàn)
-
DexClassLoader
加載目標(biāo)usingJar(data/user/0/packagename/file/robin/dex.jar)
的包 -
DexClassLoader
加載包中的類ServiceImpl
(類名比如:robin.sdk.sdk_impl1.ServiceImpl
) - 如果未加載到向臀,則加載默認(rèn)包中的類
robin.sdk.sdk_impl.ServiceImpl
腳本
sdk
Android studio的assembleXXX
往往打的都是aar包,我們需要一個(gè)jar包的gradle task:
task makeSdkJar(type: Jar) {
//指定生成的jar名
baseName 'sdk'
//從哪里打包c(diǎn)lass文件
from('build/intermediates/javac/release/classes/')
from('../sdk-proxy/build/intermediates/javac/release/classes/')
from('../sdk-common/build/intermediates/javac/release/classes/')
from('../sdk-dynamic/build/intermediates/javac/release/classes/')
from('../sdk-impl/build/intermediates/javac/release/classes/')
}
makeSdkJar.dependsOn(clean, 'compileReleaseJavaWithJavac')
注意:由于gradle版本不同,intermediates目錄的路徑可能會(huì)有變化
打出sdk.jar包诸狭,然后進(jìn)行混淆
task _proguardJar(dependsOn: makeSdkJar, type: proguard.gradle.ProGuardTask) {
String inJar = makeSdkJar.archivePath.getAbsolutePath()
println("正在混淆jar...path= " + inJar)
injars inJar
outjars "build/libs/proguard.jar"
configuration "$rootDir/sdk/proguard-rules.pro"
}
proguard-rules.pro
中券膀,需要keep住所有需要反射的類
-keep public class robin.sdk.*.ServiceImpl {*;}
-keep class robin.sdk.*.ServiceImpl$* {*;}
對(duì)于sdk對(duì)外的類,也需要keep:
-keep public class robin.sdk.hotfix.RobinClient {*;}
-keep class robin.sdk.hotfix.RobinClient$* {*;}
補(bǔ)丁
這里采用全量打包方式驯遇,將sdk的jar包解壓芹彬,去除hotfix,proxy叉庐,sdk_common舒帮,service_dynamic
目錄:
//打patch任務(wù)
task renameJar(type: Copy) {
from 'build/libs/'
include 'proguard.jar'
destinationDir file('build/libs/')
rename 'proguard.jar', "classes.zip"
}
task upzip(dependsOn: renameJar, type: Copy) {
def zipFile = file('build/libs/classes.zip')
def outputDir = file("build/libs/unzip")
from zipTree(zipFile)
into outputDir
}
task _patchProguardJar(dependsOn: upzip, type: Jar) {
//指定生成的jar名
baseName 'patch'
from('build/libs/unzip/')
exclude('robin/sdk/hotfix')
exclude('robin/sdk/proxy')
exclude('robin/sdk/sdk_common')
exclude('robin/sdk/service_dynamic')
doLast {
delete('build/libs/unzip')
delete('build/libs/classes.zip')
}
}
將jar包轉(zhuǎn)dex方便DexClassLoader加載:
task _jarToDex(type: Exec) {
commandLine 'cmd'
doFirst {
//jar文件對(duì)象
def srcFile = file("/build/libs/hot.jar")
//需要生成的dex文件對(duì)象
def desFile = file(srcFile.parent + "/" + "dex.jar")
workingDir srcFile.parent
//拼接dx.bat執(zhí)行的參數(shù)
def list = []
list.add("/c")
list.add("dx")
list.add("--dex")
list.add("--output")
list.add(desFile)
list.add(srcFile)
args list
}
}
測(cè)試驗(yàn)證
使用_proguardJar task打包sdk.jar文件,放到測(cè)試app中:
app的gradle依賴:
implementation files("libs/sdk.jar")
首次啟動(dòng):
日志如下:
此日志為CLient和Service直接的交互,可見Service為
robin.sdk.sdk_impl.ServiceImpl
,為原版的Service此日志為ServiceImpl中類的引用玩郊,目前的類為
robin.sdk.sdk_impl.a
(已混淆)
Service啟動(dòng)后會(huì)滿足條件自動(dòng)下載補(bǔ)丁包肢执;
第二次啟動(dòng):
日志如下:
加載動(dòng)態(tài)包后的:
原來的
robin.sdk.sdk_impl.a
已替換為robin.sdk.sdk_impl2.a
Service也替換成功