java 热补丁_Android热补丁之AndFix原理解析
背景
2015年下半年開源了很多Android熱更新的項目,其中大部分是以QQ空間技術團隊寫的那篇文章為依據寫出的基于multidex的熱更新框架,包括Nuwa、HotFix、DroidFix等;還有這篇文章的主角,阿里開源的AndFix。
在這之前,熱補丁框架并沒有那么火,原因無非就是要么用起來太重,要么不支持ART。比如攜程出品的DynamicAPK,這種框架是為了解決平臺級的產品相關業務開發之間的解耦,熱補丁只是其附屬功能,對于量級沒有那么大的項目,沒有必要采用這種很重的框架。另外就是基于阿里出品的基于Xposed的AOP框架dexposed,剝離掉Xposed的root部分功能,主要應該與AOP編程、插樁 (如測試、性能監控等)、在線熱補丁、SDK hooking等,用起來比較重并且不支持ART。
眾多的熱補丁框架為開發者帶來了福利,不用發版本就可以緊急修復線上版本的bug。
這篇文章主要是分析AndFix的實現原理。
AndFix
使用方法
引用
1
2
3dependencies?{
compile?'com.alipay.euler:andfix:0.3.1@aar'
}
初始化
1
2patchManager?=?new?PatchManager(context);
patchManager.init(appversion);//current?version
加載補丁,盡量在Application的onCreate方法中使用
1patchManager.loadPatch();
應用補丁
1patchManager.addPatch(path);//path?of?the?patch?file?that?was?downloaded
項目中提供了一個生成補丁(后綴為.apatch)的工具apkpatch
用法:
1usage:?apkpatch?-f??-t??-o??-k??-p??-a??-e?
-a,--alias??????keystore?entry?alias.
-e,--epassword????keystore?entry?password.
-f,--from?????????new?Apk?file?path.
-k,--keystore?????keystore?path.
-n,--name????????patch?name.
-o,--out?
-p,--kpassword????keystore?password.
-t,--to???????????old?Apk?file?path.
如下生成補丁文件
1./apkpatch.sh?-f?new.apk?-t?old.apk?-o?./?-k?../one.keystore?-p?***?-a?one?-e?***
apkPatch工具解析
apkpatch是一個jar包,并沒有開源出來,我們可以用JD-GUI或者procyon來看下它的源碼,版本1.0.3。
首先找到Main.class,位于com.euler.patch包下,找到Main()方法
1
2
3
4
5
6public?static?void?main(final?String[]?args)?{
.....
//根據上面命令輸入拿到參數
final?ApkPatch?apkPatch?=?new?ApkPatch(from,?to,?name,?out,?keystore,?password,?alias,?entry);
apkPatch.doPatch();
}
ApkPatch的doPatch方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public?void?doPatch()?{
try?{
//生成smali文件夾
final?File?smaliDir?=?new?File(this.out,?"smali");
if?(!smaliDir.exists())?{
smaliDir.mkdir();
}
//新建diff.dex文件
final?File?dexFile?=?new?File(this.out,?"diff.dex");
//新建diff.apatch文件
final?File?outFile?=?new?File(this.out,?"diff.apatch");
//第一步,拿到兩個apk文件對比,對比信息寫入DiffInfo
final?DiffInfo?info?=?new?DexDiffer().diff(this.from,?this.to);
//第二步,將對比結果info寫入.smali文件中,然后打包成dex文件
this.classes?=?buildCode(smaliDir,?dexFile,?info);
//第三步,將生成的dex文件寫入jar包,并根據輸入的簽名信息進行簽名,生成diff.apatch文件
this.build(outFile,?dexFile);
//第四步,將diff.apatch文件重命名,結束
this.release(this.out,?dexFile,?outFile);
}
catch?(Exception?e2)?{
e2.printStackTrace();
}
}
以上可以簡單描述為兩步對比apk文件,得到需要的信息
將結果打包為apatch文件
對比apk文件
DexDiffer().diff()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public?DiffInfo?diff(final?File?newFile,?final?File?oldFile)?throws?IOException?{
//提取新apk的dex文件
final?DexBackedDexFile?newDexFile?=?DexFileFactory.loadDexFile(newFile,?19,?true);
//提取舊apk的dex文件
final?DexBackedDexFile?oldDexFile?=?DexFileFactory.loadDexFile(oldFile,?19,?true);
final?DiffInfo?info?=?DiffInfo.getInstance();
boolean?contains?=?false;
for?(final?DexBackedClassDef?newClazz?:?newDexFile.getClasses())?{
final?Set?extends?DexBackedClassDef>?oldclasses?=?oldDexFile.getClasses();
for?(final?DexBackedClassDef?oldClazz?:?oldclasses)?{
//對比相同的方法,存儲為修改的方法
if?(newClazz.equals(oldClazz))?{
//對比class文件的變量
this.compareField(newClazz,?oldClazz,?info);
//對比class文件的方法,如果同一個類中沒有相同的方法
//則判定為新增方法
this.compareMethod(newClazz,?oldClazz,?info);
contains?=?true;
break;
}
}
if?(!contains)?{
//否則是新增的類
info.addAddedClasses(newClazz);
}
}
//返回包含diff信息的DiffInfo對象
return?info;
}
其原理就是根據?dex diff得到兩個apk文件的差別信息。對比方法過程中對比兩個dex文件中同時存在的方法,如果方法實現不同則存儲為修改過的方法;如果方法名不同,存儲為新增的方法,也就是說AndFix支持增加新的方法,這一點已經測試證明。另外,在比較Field的時候有如下代碼
1
2
3
4
5
6
7
8
9
10
11
12
13public?void?addAddedFields(DexBackedField?field)?{
addedFields.add(field);
throw?new?RuntimeException("can,t?add?new?Field:"?+
field.getName()?+?"("?+?field.getType()?+?"),?"?+?"in?class?:"?+
field.getDefiningClass());
}
public?void?addModifiedFields(DexBackedField?field)?{
modifiedFields.add(field);
throw?new?RuntimeException("can,t?modified?Field:"?+
field.getName()?+?"("?+?field.getType()?+?"),?"?+?"in?class?:"?+
field.getDefiningClass());
}
也就是說AndFix不支持增加成員變量,但是支持在新增方法中增加的局部變量。也不支持修改成員變量。已經測試證明這一點。
還有一個地方要注意,就是提取dex文件的地方,在DexFileFactory類中
1
2
3
4public?static?DexBackedDexFile?loadDexFile(File?dexFile,?int?api,?boolean?experimental)?throws?IOException
{
return?loadDexFile(dexFile,?"classes.dex",?new?Opcodes(api,?experimental));
}
可以看到,只提取出了classes.dex這個文件,所以源生工具并不支持multidex,如果使用了multidex方案,并且修復的類不在同一個dex文件中,那么補丁就不會生效。所以這里并不像作者在issue中提到的支持multidex那樣,不過我們可以通過JavaAssist工具修改apkpatch這個jar包,來達到支持multidex的目的,后續我們會講到。
將對比結果打包
這一步我們重點關注拿到DiffInfo后將其存入smali文件的過程
ApkPatch.buildCode()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14private?static?Set?buildCode(final?File?smaliDir,?final?File?dexFile,?final?DiffInfo?info)?throws?IOException,?RecognitionException,?FileNotFoundException?{
final?ClassFileNameHandler?outFileNameHandler?=?new?ClassFileNameHandler(smaliDir,?".smali");
final?ClassFileNameHandler?inFileNameHandler?=?new?ClassFileNameHandler(smaliDir,?".smali");
final?DexBuilder?dexBuilder?=?DexBuilder.makeDexBuilder();
for?(final?DexBackedClassDef?classDef?:?list)?{
final?String?className?=?classDef.getType();
baksmali.disassembleClass(classDef,?outFileNameHandler,?options);
final?File?smaliFile?=?inFileNameHandler.getUniqueFilenameForClass(TypeGenUtil.newType(className));
classes.add(TypeGenUtil.newType(className).substring(1,?TypeGenUtil.newType(className).length()?-?1).replace('/',?'.'));
SmaliMod.assembleSmaliFile(smaliFile,?dexBuilder,?true,?true);
}
dexBuilder.writeTo(new?FileDataStore(dexFile));
return?classes;
}
將上一步得到的diff信息寫入smali文件,并且生成diff.dex文件。smali文件的命名以_CF.smali結尾,并且在修改的地方用自定義的Annotation(MethodReplace)標注,用于在替換之前查找修復的變量或方法,如下。
1
2
3
4
5
6.method?private?getUserProfile()V
.locals?2
.annotation?runtime?Lcom/alipay/euler/andfix/annotation/MethodReplace;
clazz?=?"com.boohee.account.UserProfileActivity"
method?=?"getUserProfile"
.end?annotation
在打包生成的diff.dex文件中,反編譯出來可以看到這段代碼
1
2
3
4
5
6
7
8//生成的注解
@MethodReplace(clazz="com.boohee.account.UserProfileActivity",?method="onCreate")
public?void?onCreate(Bundle?paramBundle)
{
super.onCreate(paramBundle);
getUserProfile();
addPatch();
}
然后就是簽名,打包,加密的流程,就不具體分析了。注意,apkPatch在生成.apatch補丁文件的時候會加入簽名信息,并且會進行加密操作,在應用補丁的時候會驗證簽名信息是否正確。
打補丁原理
Java層
PatchManager.init()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public?void?init(String?appVersion)?{
SharedPreferences?sp?=?mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String?ver?=?sp.getString(SP_VERSION,?null);
//根據版本號加載補丁文件,版本號不同清空緩存目錄
if?(ver?==?null?||?!ver.equalsIgnoreCase(appVersion))?{
cleanPatch();
sp.edit().putString(SP_VERSION,?appVersion).commit();
}?else?{
initPatchs();
}
}
private?void?initPatchs()?{
//?緩存目錄data/data/package/file/apatch/會緩存補丁文件
//?即使原目錄被刪除也可以打補丁
File[]?files?=?mPatchDir.listFiles();
for?(File?file?:?files)?{
addPatch(file);
}
}
addPatch和loadPatch()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public?void?addPatch(String?path)?throws?IOException?{
...
FileUtil.copyFile(src,?dest);//?copy?to?patch's?directory
Patch?patch?=?addPatch(dest);
if?(patch?!=?null)?{
loadPatch(patch);
}
}
private?void?loadPatch(Patch?patch)?{
Set?patchNames?=?patch.getPatchNames();
ClassLoader?cl;
List?classes;
for?(String?patchName?:?patchNames)?{
if?(mLoaders.containsKey("*"))?{
cl?=?mContext.getClassLoader();
}?else?{
cl?=?mLoaders.get(patchName);
}
if?(cl?!=?null)?{
classes?=?patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(),?cl,?classes);
}
}
}
再看下AndFixManager的fix()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34...
//省略掉驗證簽名信息、安全檢查的代碼,安全方面做得很好
...
private?void?fixClass(Class>?clazz,?ClassLoader?classLoader)?{
...
for?(Method?method?:?methods)?{
//還記得對比過程中生成的Annotation注解嗎
//這里通過注解找到需要替換掉的方法
methodReplace?=?method.getAnnotation(MethodReplace.class);
if?(methodReplace?==?null)
continue;
//標記的類
clz?=?methodReplace.clazz();
//需要替換的方法
meth?=?methodReplace.method();
if?(!isEmpty(clz)?&&?!isEmpty(meth))?{
//所有找到的方法,循環替換
replaceMethod(classLoader,?clz,?meth,?method);
}
}
}
private?static?native?void?replaceMethod(Method?dest,?Method?src);
private?static?native?void?setFieldFlag(Field?field);
public?static?void?addReplaceMethod(Method?src,?Method?dest)?{
try?{
replaceMethod(src,?dest);
initFields(dest.getDeclaringClass());
}?catch?(Throwable?e)?{
Log.e(TAG,?"addReplaceMethod",?e);
}
}
后面就是調用native層的方法,寫在jni中,打包為.so文件供java層調用。
總結一下,java層的功能就是找到補丁文件,根據補丁中的注解找到將要替換的方法然后交給jni層去處理替換方法的操作。好了,繼續往下看。
Native層
在jni的代碼中支持Dalvik與ART,那么這是怎么區分的呢?在AndFixManager的構造方法中有這么一句
1mSupport?=?Compat.isSupport();1
2
3
4
5
6
7
8
9
10
11
12public?static?synchronized?boolean?isSupport()?{
if?(isChecked)
return?isSupport;
isChecked?=?true;
//?not?support?alibaba's?YunOs
//SDK?android?2.3?to?android?6.0
if?(!isYunOS()?&&?AndFix.setup()?&&?isSupportSDKVersion())?{
isSupport?=?true;
}
return?isSupport;
}
AndFix的`setUp()``方法
1
2
3
4
5
6
7
8
9
10
11
12
13public?static?boolean?setup()?{
try?{
final?String?vmVersion?=?System.getProperty("java.vm.version");
//判斷是否是ART
boolean?isArt?=?vmVersion?!=?null?&&?vmVersion.startsWith("2");
int?apilevel?=?Build.VERSION.SDK_INT;
//這里也是native方法
return?setup(isArt,?apilevel);
}?catch?(Exception?e)?{
Log.e(TAG,?"setup",?e);
return?false;
}
}
最后調用setup(isArt, apilevel);的native方法,在andfix.cpp中注冊jni方法
1
2
3
4
5
6
7static?JNINativeMethod?gMethods[]?=?{
/*?name,?signature,?funcPtr?*/
{?"setup",?"(ZI)Z",?(void*)?setup?},
{?"replaceMethod",
"(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V",(void*)?replaceMethod?},
{?"setFieldFlag",
"(Ljava/lang/reflect/Field;)V",?(void*)?setFieldFlag?},?};
native實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20static?jboolean?setup(JNIEnv*?env,?jclass?clazz,?jboolean?isart,
jint?apilevel)?{
isArt?=?isart;
LOGD("vm?is:?%s?,?apilevel?is:?%i",?(isArt???"art"?:?"dalvik"),
(int?)apilevel);
if?(isArt)?{
return?art_setup(env,?(int)?apilevel);
}?else?{
return?dalvik_setup(env,?(int)?apilevel);
}
}
static?void?replaceMethod(JNIEnv*?env,?jclass?clazz,?jobject?src,
jobject?dest)?{
if?(isArt)?{
art_replaceMethod(env,?src,?dest);
}?else?{
dalvik_replaceMethod(env,?src,?dest);
}
}
根據上層傳過來的isArt判斷調用Dalvik還是Art的方法。
以Dalvik為例,繼續往下分析,代碼在dalvik_method_replace.cpp中
dalvik_setup方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15extern?jboolean?__attribute__?((visibility?("hidden")))?dalvik_setup(
JNIEnv*?env,?int?apilevel)?{
jni_env?=?env;
void*?dvm_hand?=?dlopen("libdvm.so",?RTLD_NOW);
if?(dvm_hand)?{
...
//使用dlsym方法將dvmCallMethod_fnPtr函數指針指向libdvm.so中的//dvmCallMethod方法,也就是說可以通過調用該函數指針執行其指向的方法
//下面會用到dvmCallMethod_fnPtr
dvmCallMethod_fnPtr?=?dvm_dlsym(dvm_hand,
apilevel?>?10??
"_Z13dvmCallMethodP6ThreadPK6MethodP6ObjectP6JValuez"?:
"dvmCallMethod");
...
}
}
替換方法的關鍵在于native層怎么影響內存里的java代碼,我們知道java代碼里將一個方法聲明為native方法時,對此函數的調用就會到native世界里找,AndFix原理就是將一個不是native的方法修改成native方法,然后在native層進行替換,通過dvmCallMethod_fnPtr函數指針來調用libdvm.so中的dvmCallMethod()來加載替換后的新方法,達到替換方法的目的。Jni反射調用java方法時要用到一個jmethodID指針,這個指針在Dalvik里其實就是Method類,通過修改這個類的一些屬性就可以實現在運行時將一個方法修改成native方法。
看下dalvik_replaceMethod(env, src, dest);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56extern?void?__attribute__?((visibility?("hidden")))?dalvik_replaceMethod(
JNIEnv*?env,?jobject?src,?jobject?dest)?{
jobject?clazz?=?env->CallObjectMethod(dest,?jClassMethod);
ClassObject*?clz?=?(ClassObject*)?dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(),?clazz);
//設置為初始化完畢
clz->status?=?CLASS_INITIALIZED;
//meth是將要被替換的方法
Method*?meth?=?(Method*)?env->FromReflectedMethod(src);
//target是新的方法
Method*?target?=?(Method*)?env->FromReflectedMethod(dest);
LOGD("dalvikMethod:?%s",?meth->name);
meth->jniArgInfo?=?0x80000000;
//修改method的屬性,將meth設置為native方法
meth->accessFlags?|=?ACC_NATIVE;
int?argsSize?=?dvmComputeMethodArgsSize_fnPtr(meth);
if?(!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize?=?meth->insSize?=?argsSize;
//將新的方法信息保存到insns
meth->insns?=?(void*)?target;
//綁定橋接函數,java方法的跳轉函數
meth->nativeFunc?=?dalvik_dispatcher;
}
static?void?dalvik_dispatcher(const?u4*?args,?jvalue*?pResult,
const?Method*?method,?void*?self)?{
Method*?meth?=?(Method*)?method->insns;
meth->accessFlags?=?meth->accessFlags?|?ACC_PUBLIC;
if?(!dvmIsStaticMethod(meth))?{
Object*?thisObj?=?(Object*)?args[0];
ClassObject*?tmp?=?thisObj->clazz;
thisObj->clazz?=?meth->clazz;
argArray?=?boxMethodArgs(meth,?args?+?1);
if?(dvmCheckException_fnPtr(self))
goto?bail;
dvmCallMethod_fnPtr(self,?(Method*)?jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth),?&result,?thisObj,
argArray);
thisObj->clazz?=?tmp;
}?else?{
argArray?=?boxMethodArgs(meth,?args);
if?(dvmCheckException_fnPtr(self))
goto?bail;
dvmCallMethod_fnPtr(self,?(Method*)?jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth),?&result,?NULL,
argArray);
}
bail:?dvmReleaseTrackedAlloc_fnPtr((Object*)?argArray,?self);
}
通過dalvik_dispatcher這個跳轉函數完成最后的替換工作,到這里就完成了兩個方法的替換,有問題的方法就可以被修復后的方法取代。ART的替換方法就不講了,原理上差別不大。
總結
AndFix熱補丁原理就是在native動態替換方法java層的代碼,通過native層hook?java層的代碼。
優點因為是動態的,所以不需要重啟應用就可以生效
支持ART與Dalvik
與multidex方案相比,性能會有所提升(Multi Dex需要修改所有class的class_ispreverified標志位,導致運行時性能有所損失)
支持新增加方法
支持在新增方法中新增局部變量
足夠輕量,生成補丁文件簡單
安全性夠高,驗證簽名
缺點因為是動態的,跳過了類的初始化,設置為初始化完畢,所以對于靜態方法、靜態成員變量、構造方法或者class.forname()的處理可能會有問題
不支持新增成員變量和修改成員變量
官方apkPatch工具不支持multidex,但是可以通過修改工具來達到支持multidex的目的
由于是在native層替換方法,某些缺心眼廠商可能會修改源生關鍵部分的native層實現,導致可能在某些特定ROM支持不夠好
總結
以上是生活随笔為你收集整理的java 热补丁_Android热补丁之AndFix原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新发地卖海鲜的叫什么门
- 下一篇: 腊肠手撕包菜?