Android熱修復(fù):Qfix方案的gradle實(shí)踐
一木蹬、Android熱修復(fù)方案的發(fā)展
Android熱修復(fù)技術(shù)現(xiàn)在主流分為NativeHook,ClassLoader以及新出現(xiàn)的Instant-Run方案儒旬,在工作中因?yàn)閳F(tuán)隊(duì)需要為團(tuán)隊(duì)引入了QZone的ClassLoader插樁方案,當(dāng)初開發(fā)的時候(15年底)市面上還只有AndFix和ClassLoader兩種方案康吵,而那時的AndFix兼容性也還比較差直砂,就選擇了ClassLoader方案幌陕,基于網(wǎng)上開源的Nuwa方案修改而來。
2016年的時候Android熱修復(fù)方案如雨后春筍存崖,ClassLoader方案新起之秀有Tinker,QFix等冻记。對比幾種新出的方案,調(diào)研之后認(rèn)為QFix與團(tuán)隊(duì)之前使用的ClassLoader方案最為接近来惧,同時無需插樁冗栗,避免了插樁性能損失問題,在16年下半年時候?qū)F(tuán)隊(duì)的熱修復(fù)方案升級到了QFix方案供搀。
由于團(tuán)隊(duì)的熱修復(fù)方案需要對原來的方案做不少兼容隅居,所以會有不少雍余的代碼。17年年初就想著自己寫個QFix的開源gradle實(shí)現(xiàn)葛虐,現(xiàn)項(xiàng)目已完成發(fā)布到Github,地址:https://github.com/alexclin0188/QFixPatch胎源。 這篇文章主要是對開發(fā)過程的一個記錄,發(fā)出來供有興趣的同學(xué)參考和自己以后復(fù)習(xí)使用屿脐。
二涕蚤、QFix方案原理
QFix方案原理介紹原文。有興趣的同學(xué)可以仔細(xì)研讀下這篇原理介紹的诵,這里就不再詳細(xì)講解万栅,補(bǔ)丁修復(fù)的基本原來和ClassLoader類同,反射在DexPahList類中dex數(shù)組前插入補(bǔ)丁dex西疤,區(qū)別在于QFix使用在native層調(diào)用dvmResolveClass方法來解決pre-verify的問題烦粒,不再需要插樁。
GitHub上有一個開源的簡單的QFix方案Eclipse工程demo:https://github.com/lizhangqu/QFix, QFixPatch項(xiàng)目即是基于這個demo擴(kuò)展而來的一個gradle實(shí)踐瘪阁。
三撒遣、Gradle插件開發(fā)
gradle插件生成補(bǔ)丁邏輯和Nuwa類似,只是增加了dexdump分析補(bǔ)丁類class-id的操作管跺,簡單的流程示意圖如下:
首先是buildBase义黎,構(gòu)建基礎(chǔ)apk和保存基礎(chǔ)信息,在dexTask之前增加hook豁跑,將原始apk中的類的sha值保存到hash.txt中廉涕,以便生成補(bǔ)丁時使用。然后在buildPatch執(zhí)行時比較找出修改的類(補(bǔ)丁類)艇拍,并通過dexdump分析這些補(bǔ)丁類在基礎(chǔ)apk的dex中的class-id狐蜕,將分析得到的class-id信息和補(bǔ)丁類通過dex工具打包成一個補(bǔ)丁apk
3.1 創(chuàng)建插件工程
在一個AS工程內(nèi),如果有buildSrc目錄卸夕,這個目錄就會被當(dāng)做gradle插件源碼目錄編譯层释,并可以直接在工程模塊中直接使用編寫的gradle插件,下面是QFixPatch工程gradle插件源碼目錄截圖
和Android模塊不同的是其中有resources目錄快集,下面有個META-INF.gradle-plugins文件夾贡羔,內(nèi)有propertites文件廉白,而這個文件是用來注冊插件入口類的,下面是alexclin.qfix.properties的內(nèi)容
//alexclin.qfix.properties文件乖寒,properties文件名稱即為gradle插件名
implementation-class=alexclin.qfix.QFixPlugin
我們gradle插件的使用方式是在模塊的build.gradle中增加apply猴蹂,如下
//build.gradle文件
......
apply plugin: 'alexclin.qfix'
......
而這里的apply plugin的作用最終就是讓項(xiàng)目在執(zhí)行g(shù)radle編譯時調(diào)用我們的實(shí)現(xiàn)類alexclin.qfix.QFixPlugin的apply函數(shù),下面讓我們看QFixPlugin類的代碼
//QFixPlugin類
public class QFixPlugin implements Plugin<Project> {
static final String CLASS_ID_TXT = "class-ids.txt"
static final String PATCH_NAME = "patch"
static final String DEBUG = "debug"
static final String PLUGIN_NAME = "qfix"
def debugOn
@Override
void apply(Project project) {
if (project.getPlugins().hasPlugin(AppPlugin)) {
project.extensions.create(PLUGIN_NAME, QFixExtension, project)
applyPlugin(project)
}
}
//真正的applyplugin邏輯代碼
private void applyPlugin(Project project) {
project.afterEvaluate {
def extension = project.extensions.findByName(PLUGIN_NAME) as QFixExtension
Creator creator = new Creator(project, extension);
debugOn = extension.debugOn
project.android.applicationVariants.each { variant ->
if (variant.name.contains(DEBUG) && !debugOn)
return;
configTasks(project, variant, creator);
}
//添加總的buildPatch Task
def releasePatch = project.tasks.findByName("assembleReleasePatch")
def debugPatch = project.tasks.findByName("assembleDebugPatch")
if (releasePatch != null || debugPatch != null) {
def buildPatchTask = project.task("buildPatch")
if (debugPatch) buildPatchTask.dependsOn debugPatch
if (releasePatch) buildPatchTask.dependsOn releasePatch
}
def assembleDebugBase = project.tasks.findByName("assembleDebugBase")
def assembleReleaseBase = project.tasks.findByName("assembleReleaseBase")
if (assembleDebugBase != null || assembleReleaseBase != null) {
def assembleBase = project.task("buildBase");
if (assembleDebugBase) assembleBase.dependsOn assembleDebugBase
if (assembleReleaseBase) assembleBase.dependsOn assembleReleaseBase
}
}
}
....
}
可以看到上面這段代碼里面楣嘁,主要是在真正的applyplugin函數(shù)對項(xiàng)目進(jìn)行了Task設(shè)置磅轻,并對buildBase,buildPatch,assembleDebugBase,assembleDebugPacth,assembleReleaseBase,assembleReleasePacth的依賴關(guān)系做了設(shè)置。
實(shí)際上只有assemble<Viriant>Base,assemble<Viriant>Patch的區(qū)別,前者是構(gòu)建基礎(chǔ)apk包和保存基礎(chǔ)信息逐虚,后者是構(gòu)建補(bǔ)丁
3.2 插件設(shè)置QFixExtension
gradle插件支持屬性設(shè)置聋溜,如我們的QFix插件在build.gradle中屬性設(shè)置,如下
//qfix插件在app模塊build.gradle中的屬性設(shè)置
qfix{
debugOn true //debug模式是否開啟補(bǔ)丁構(gòu)建task,可選
outputDir 'qfix_output'//補(bǔ)丁基礎(chǔ)信息默認(rèn)輸入目錄和構(gòu)建輸出目錄叭爱,必須
excludeClass = ["App.class"]//需要排除的類勤婚,會自動排除Application及其父類,此處只是示例涤伐,可選
//includePackage = []//需要包含的package匹舞,可選
strictMode true //嚴(yán)格模式,用于解決ART下dex激進(jìn)內(nèi)聯(lián)問題婿奔,詳情參看README,可選
}
而上面這個設(shè)置實(shí)際對應(yīng)的是一個Extension類睦尽,在qfix-gradle插件中對應(yīng)的就是QFixExtension類
package alexclin.qfix
import org.gradle.api.Project
class QFixExtension {
HashSet<String> includePackage = []
HashSet<String> excludeClass = []
//補(bǔ)丁構(gòu)建在debug版是否開啟器净,默認(rèn)開啟
boolean debugOn = false
//補(bǔ)丁基礎(chǔ)信息保存目錄和補(bǔ)丁輸出目錄
String outputDir
//嚴(yán)格模式,啟用則打包所有引用補(bǔ)丁類的class到補(bǔ)丁中,應(yīng)對ART激進(jìn)內(nèi)聯(lián)引起的問題
boolean strictMode = false;
QFixExtension(Project project) {
}
}
3.3 設(shè)置Task和hook dexTask
設(shè)置Task和hook dexTask的邏輯主要都在QFixPlugin的configTasks函數(shù)中当凡,以下是簡化的函數(shù)代碼山害。
static void configTasks(Project project, BaseVariant variant, Creator creator) {
Map hashMap
//獲取dexTask和proguardTask
def dexTask = ...
def proguardTask = ...
if (creator.patchTaskEnable()) {
//創(chuàng)建Task,尋找被修改的類沿量,只有assemble<Virant>Patch時才會被調(diào)用
def diffClassBeforeDex = "diffClassBeforeDex${variant.name.capitalize()}"
def diffClassBeforeDexTask = ...
diffClassBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)
//創(chuàng)建Task浪慌,將改變的類打成一個dex
def hotfixPatch = "assemble${variant.name.capitalize()}Patch"
def hotfixPatchTask = ...
hotfixPatchTask.dependsOn diffClassBeforeDexTask
}
//創(chuàng)建Hook Task, 在dexTask執(zhí)行之前 保存所有類的sha值到對應(yīng)目錄的hash.txt
def shaClassBeforeDex = "shaClassBeforeDex${variant.name.capitalize()}"
def shaClassBeforeDexTask = ...
//備份構(gòu)建過程中的mapping.txt,如果有
if (proguardTask) {
def mapFile = new File("${project.buildDir}/outputs/mapping/${variant.dirName}/mapping.txt")
def newMapFile = creator.getMappingOutFile(variant);
Utils.copyFile(mapFile, newMapFile)
}
}
shaClassBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)
//對assembleRelease或assembleDebug添加Hook朴则,保存apk
def assembleTaskName = "assemble${variant.name.capitalize()}Base";
def assembleTask = project.task(assembleTaskName);
Closure saveAssembleClosure = ...
//設(shè)置依賴關(guān)系权纤,保證assembleXXXBase在android自身的assembleDebug/assembleRelease之后執(zhí)行
assembleTask.doLast(saveAssembleClosure)
assembleTask.dependsOn shaClassBeforeDexTask
assembleTask.dependsOn project.tasks["assemble${variant.name.capitalize()}"]
}
上面創(chuàng)建的幾個task中間最重要的是shaClassBeforeDexTask和diffClassBeforeDexTask,前者是保存基礎(chǔ)apk中所有類的sha信息乌妒,后者則是對比基礎(chǔ)apk和修改后的代碼類的信息汹想,找出改變的類。先來看shaClassBeforeDexTask
//shaClassBeforeDexTask 保存基礎(chǔ)apk中所有類的sha信息
def shaClassBeforeDexTask = project.task(shaClassBeforeDex) << {
//準(zhǔn)備工作...
...
//保存所有類的sha值到hashFile中
Set<File> inputFiles = AndroidUtils.getDexTaskInputFiles(project, variant, dexTask)
if (proguardTask) {
inputFiles.each {
inputFile ->
if (inputFile.path.endsWith(".jar")) {
shaJarInfo(inputFile, creator.patchSetting, hashFile)
}
}
} else if (AndroidUtils.compareVersionName(Version.ANDROID_GRADLE_PLUGIN_VERSION, "2.2.3") > -1) {
//沒有混淆在2.2.3及以后插件上inputFiles不包含當(dāng)前模塊類
//合并所有jar包
Set<File> jarAndDir = new HashSet<>(inputFiles)
jarAndDir.add(new File(project.buildDir, "intermediates/classes/${variant.dirName}"))
File combinedJar = combineJarAndDir(project, jarAndDir)
shaJarInfo(combinedJar, creator.patchSetting, hashFile)
}
//備份構(gòu)建過程中的mapping.txt
......
}
diffClassBeforeDexTask的作用是找出改變的類并dump分析改變的類在基礎(chǔ)apk的dex中的class-id撤蚊,簡化的代碼如下:
//diffClassBeforeDexTask 找出改變的類并dump分析改變的類在基礎(chǔ)apk的dex中的class-id
def diffClassBeforeDexTask = project.task(diffClassBeforeDex) << {
//補(bǔ)丁準(zhǔn)備工作, 讀取原來apk的dex目錄古掏,proguard-mapping,hash.txt,以及初始化補(bǔ)丁輸出目錄
def baseDexDir = ...
File mappingFile = ...
hashMap = ...
File patchOutDir = ...
//獲取dexTask的輸入jar報(bào)參數(shù)
//比較所有類與之前保存的sha值是否有差異,有差異則保存到patchClassDir
Set<File> inputFiles = AndroidUtils.getDexTaskInputFiles(project, variant, dexTask)
if (proguardTask) {
inputFiles.each {
inputFile ->
if (inputFile.path.endsWith(".jar")) {
diffJar(inputFile, hashMap, creator, variant, finder)
}
}
} else if (AndroidUtils.compareVersionName(Version.ANDROID_GRADLE_PLUGIN_VERSION, "2.2.3") > -1) {
//沒有混淆在2.2.3及以后插件上inputFiles不包含當(dāng)前模塊類
//合并所有jar包
Set<File> jarAndDir = new HashSet<>(inputFiles)
jarAndDir.add(new File(project.buildDir, "intermediates/classes/${variant.dirName}"))
File combinedJar = combineJarAndDir(project, jarAndDir)
diffJar(combinedJar, hashMap, creator, variant, finder)
}
def allRefPatchClasses = ...
def appName = ...
//增加dexDump處理侦啸,分析補(bǔ)丁類在基礎(chǔ)apk的dex中class-id并保存
def dumpCmdPath = AndroidUtils.getDexDumpPath(project, creator.sdkDir);
File patchClassDir = creator.getClassOutDir(variant);
File classIdsFile = new File(patchClassDir, CLASS_ID_TXT);
DexClassIdResolve.dumpDexClassIds(dumpCmdPath, baseDexDir, patchClassDir, classIdsFile,appName,allRefPatchClasses)
}
可以看到重要的邏輯就是比較sha值和dexDump分析class-id, diffJar函數(shù)主要是讀取jar包中的類并保存sha信息槽唾,代碼如下
static void diffJar(File jarFile, HashMap hashMap, Creator builder, BaseVariant variant, SubClsFinder finder) {
File basePatchClassDir = builder.getClassOutDir(variant);
if (!basePatchClassDir.exists()) basePatchClassDir.mkdirs();
if (jarFile && jarFile.isFile()) {
def file = new JarFile(jarFile);
builder.patchSetting.addApplicationAndSuper(file, variant);
Enumeration enumeration = file.entries();
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement();
String entryName = jarEntry.getName();
InputStream inputStream = file.getInputStream(jarEntry);
if (!builder.patchSetting.isExcluded(entryName)) {
def bytes = Utils.readAllBytesAndClose(inputStream);
def hash = DigestUtils.shaHex(bytes)
if (Utils.notSame(hashMap, entryName, hash)) {
finder.addAbsPatchClass(bytes, entryName)
Utils.copyBytesToFile(bytes, Utils.touchFile(basePatchClassDir, entryName))
} else {
finder.addOutPatchClass(bytes, entryName)
}
}
}
Collection<String> collections = finder.getRefClasses();
for (String entryName : collections) {
ZipEntry zipEntry = file.getJarEntry(entryName);
InputStream inputStream = file.getInputStream(zipEntry);
def bytes = Utils.readAllBytesAndClose(inputStream);
Utils.copyBytesToFile(bytes, Utils.touchFile(basePatchClassDir, entryName))
inputStream.close();
}
file.close();
}
}
3.4 調(diào)用dexDump分析補(bǔ)丁類在原始apk的dex中class-id
dexDump的分析邏輯在alexclin.qfix.DexClassIdResolve類中丧枪,主要代碼是readClassIdMap函數(shù),該函數(shù)根據(jù)傳入的dex和補(bǔ)丁類信息夏漱,結(jié)合dexdump工具的輸出豪诲,將補(bǔ)丁類class-id保存下來
private static String readClassIdMap(InputStream mInputStream, int mDexIndex, HashSet<String> mPatchSet,Set<String> patchRefSet,ArrayList<ClassIdMap> mClassIdMapList) {
InputStreamReader isReader = null;
BufferedReader reader = null;
String entrance = null;
try {
isReader = new InputStreamReader(mInputStream);
reader = new BufferedReader(isReader);
boolean findHead = false;
boolean findClass = false;
int classIndex = -1;
long classIdx = -1;
def line
while ((line = reader.readLine()) != null) {
if (line.startsWith("Class #") && line.endsWith(" header:") && !findHead && classIndex < 0) {
findHead = true;
classIndex = Integer.parseInt(line.substring("Class #".length(), line.indexOf(" header:")));
} else if (line.startsWith("class_idx") && findHead && classIndex >= 0 && classIdx < 0) {
classIdx = Integer.parseInt(line.substring(line.indexOf(": ") + 2));
} else if (line.startsWith("Class #") && findHead && classIndex >= 0
&& line.contains(String.valueOf(classIndex)) && classIdx > 0) {
findClass = true;
} else if (line.startsWith(" Class descriptor") && findHead && findClass && classIndex >= 0 && classIdx > 0) {
String className = line.substring(line.indexOf("'L") + 2, line.indexOf(";'"));
if (mPatchSet.contains(className)) {
ClassIdMap item = new ClassIdMap(className, mDexIndex, classIdx);
mClassIdMapList.add(item);
}else if(!patchRefSet.contains(className)&&entrance==null){
entrance = className;
}
System.out.println("className:"+className)
findHead = false;
findClass = false;
classIndex = -1;
classIdx = -1;
}
}
} catch (Exception e) {
//異常處理和關(guān)閉流
......
}
return entrance;
}
因?yàn)樵贏PP注入補(bǔ)丁時調(diào)用dvmResolveClass需要傳入一個引用類,這里使用當(dāng)前dex中和補(bǔ)丁類無調(diào)用關(guān)系的一個類作為該dex中補(bǔ)丁類的入口類
3.5 生成補(bǔ)丁
經(jīng)過上面的流程挂绰,已經(jīng)有所有改變的類(補(bǔ)丁類)和補(bǔ)丁類在基礎(chǔ)apk的Dex中的class-ids信息(class-ids.txt)屎篱,生成補(bǔ)丁就直接調(diào)用dx工具類即可
def hotfixPatchTask = project.task(hotfixPatch) << {
//調(diào)用dx工具生成apk
......
AndroidUtils.dex(project, creator.getClassOutDir(variant), creator.sdkDir, pathFilePath)
//簽名補(bǔ)丁
SigningConfig signingConfig = variant.signingConfig;
if(signingConfig!=null){
......
if (AndroidUtils.signApk(patchFile, patchSignedFile, signingConfig,compatible))
patchFile.delete();
}
}
四、補(bǔ)丁客戶端應(yīng)用代碼開發(fā)
4.1 使用Application代理方式
因?yàn)锳pplication類被調(diào)起之后我們才有機(jī)會去加載我們自己的補(bǔ)丁葵蒂,而如果將Application調(diào)用了某些其他類交播,這些類的class也可能在我們加載補(bǔ)丁前就已經(jīng)被加載到ClassLoader中,這樣這些類就沒有機(jī)會被替換了。
為了解決如上這個問題践付,參考了Tinker的方式秦士,也才用Application代理來解決,將真正的Application邏輯寫在代理類中永高。demo中的Application類如下
public class App extends PatchApplication {
private static final String DELEGATE_NAME = "alexclin.qfix.qfixgradle.AppDelegate";
public App() {
super(DELEGATE_NAME);
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base);
File debugPatch = new File(Environment.getExternalStorageDirectory(),"debugPatch.apk");
PatchTool.installPatch(this,debugPatch);
}
}
下面是PatchApplication的代碼隧土,主要是反射調(diào)起Application代理類的對應(yīng)方法
public abstract class PatchApplication extends Application {
private String delegateClassName;
private ApplicationLifeCycle delegate;
public PatchApplication(String delegateClassName) {
this.delegateClassName = delegateClassName;
}
@Override
public final void onCreate() {
super.onCreate();
ensureDelegate();
delegate.onCreate();
}
@Override
public final void onTerminate() {
super.onTerminate();
if(delegate!=null) delegate.onTerminate();
}
@Override
public final void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if(delegate!=null) delegate.onConfigurationChanged(newConfig);
}
@Override
public final void onLowMemory() {
super.onLowMemory();
if(delegate!=null) delegate.onLowMemory();
}
@Override
public final void onTrimMemory(int level) {
super.onTrimMemory(level);
if(delegate!=null) delegate.onTrimMemory(level);
}
private void ensureDelegate(){
if(delegate==null){
try {
Class<?> delegateClass = Class.forName(delegateClassName,false,getClassLoader());
Constructor<?> constructor = delegateClass.getConstructor(Application.class);
delegate = (ApplicationLifeCycle) constructor.newInstance(this);
} catch (Exception e) {
throw new IllegalArgumentException("create app delegate failed",e);
}
}
}
}
4.1 反射注入補(bǔ)丁dex到ClassLoader中的數(shù)組前面
反射注入補(bǔ)丁dex到ClassLoader中的數(shù)組前面的方式和其它ClassLoader方案的注入方式差別不大,入口函數(shù)是PatchTool.installPatch(Application application,File patchFile)命爬,在此函數(shù)中先通過InjectUtil提供的函數(shù)注入補(bǔ)丁dex曹傀,再使用解析出來的class-id信息調(diào)用native函數(shù)
注入函數(shù)這里只簡單列出
//InjectUtil類代碼
//注入補(bǔ)丁Dex
static boolean injectDex(Application context, File patchFile) {
ArrayList<File> files = new ArrayList<File>();
files.add(patchFile);
try {
checkApkFiles(files);
if(isAliyunOs()){ //阿里云OS的注入
for(File file:files){
injectLexFile(context,file);
}
return true;
}else if(isAndroid()){ //普通android-os的注入
installDex(context,InjectUtil.class.getClassLoader(),context.getDir("dex", 0),files);
return true;
}
throw new IllegalStateException("Current system is not support");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
4.2 解析補(bǔ)丁apk中的補(bǔ)丁class-id信息
解析補(bǔ)丁apk中class-id信息的邏輯在PatchTool.readPatchClassIds函數(shù)中,代碼如下
//class-ids.txt中的格式如下 第一行是入口類信息饲宛,以:分割皆愉,格式:Dex1入口類:Dex2入口類
//之后是class-id信息,格式:classname-dexIndex-classId
private static List<Pair<String,Long>> readPatchClassIds(File patchFile, String defaultEntranceClass){
List<Pair<String,Long>> classIds = new ArrayList<Pair<String,Long>>();
InputStream inputStream = null;
BufferedReader reader = null;
SparseArray<String> dexEntrances = new SparseArray<String>();
try {
JarFile jarFile = new JarFile(patchFile);
ZipEntry entry = jarFile.getEntry(CLASS_ID_TXT);
inputStream = jarFile.getInputStream(entry);
reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
boolean isFirst = true;
while ((line=reader.readLine())!=null){
if(isFirst){
isFirst = false;
if(line.contains(":")){ //解析入口
String[] entrances = line.split(":");
for(int i=0;i<entrances.length;i++){
String entrance = entrances[i];
if(TextUtils.isEmpty(entrance))
continue;
dexEntrances.put(i+1,entrance);
}
boolean loadEntrance = loadEntranceClasses(dexEntrances);
if(!loadEntrance){
return null;
}
continue;
}
}
if (!TextUtils.isEmpty(line)) {
String[] infos = line.split("-");
if (infos.length == 3) {
long classId = Long.valueOf(infos[2]);
int dexIndex = Integer.valueOf(infos[1]);
String entrance = getEntranceClass(dexEntrances,dexIndex,defaultEntranceClass);
classIds.add(Pair.create(entrance,classId));
}
}
}
} catch (Exception e) {
//.....異常處理艇抠,關(guān)閉流
}
return classIds;
}
然后使用解析出來的class-id信息調(diào)用PatchTool.resolvePatchClass函數(shù)
private static boolean resolvePatchClass(Application app, String[] referrerClassList, long[] classIdxList, int size) {
if (!sIsLibLoaded) {
sIsLibLoaded = loadPatchToolLib();
}
if (!sIsLibLoaded) {
boolean unloadResult = InjectUtil.unloadPatchElement(app, 0);
Log.e(TAG, "load lib failed, unload patch result=" + unloadResult);
return false;
} else {
int resolveResult = nativeResolvePatchClass(referrerClassList, classIdxList, size);
if (resolveResult != CODE_RESOLVE_PATCH_ALL_SUCCESS) {
//resolve不成功從dex數(shù)組中卸載對應(yīng)dex
boolean unloadResult = InjectUtil.unloadPatchElement(app, 0);
Log.e(TAG, String.format(Locale.ENGLISH,"resolve patch class failed, unload patch result= %b,refClass1:%s",
unloadResult,referrerClassList[0]));
return false;
} else {
Log.d(TAG, "resolve patch class success");
return true;
}
}
}
4.3 調(diào)用nativeResolveClass函數(shù)
上一節(jié)中最終會調(diào)用到nativeResolvePatchClass方法幕庐,這個方法是native方法,實(shí)現(xiàn)在qfixlib/src/main/jni/ResolvePatch.c中,這個C文件也是直接使用https://github.com/lizhangqu/QFix的中的C文件
jint Java_alexclin_patch_qfix_tool_PatchTool_nativeResolvePatchClass(JNIEnv* env,
jobject thiz, jobjectArray referrerClassList, jlongArray classIdxList, jint size) {
LOGI("enter nativeResolvePatchClass");
int referrerClassSize = (*env)->GetArrayLength(env, referrerClassList);
int classIdxSize = (*env)->GetArrayLength(env, classIdxList);
if (size <= 0 || referrerClassSize != size || classIdxSize != size) {
LOGE("CODE_NATIVE_INIT_PARAMETER_ERROR");
return CODE_NATIVE_INIT_PARAMETER_ERROR;
}
jlong* jClassIdxArray = (*env)->GetLongArrayElements(env, classIdxList, 0);
if (jClassIdxArray == 0) {
LOGE("CODE_NATIVE_INIT_PARAMETER_ERROR");
return CODE_NATIVE_INIT_PARAMETER_ERROR;
}
void* handle = 0;
handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
if (handle) {
void* findFunc = 0;
int i = 0;
while(i < ARRAY_SIZE_FIND_CLASS) {
findFunc = dlsym(handle, ARRAY_SYMBOL_FIND_LOADED_CLASS[i]);
if (findFunc) {
break;
}
i++;
}
if (findFunc) {
g_pDvmFindLoadedClass_Addr = findFunc;
void* resolveFunc = 0;
i = 0;
while(i < ARRAY_SIZE_RESOLVE_CLASS) {
resolveFunc = dlsym(handle, ARRAY_SYMBOL_RESOLVE_CLASS[i]);
if (resolveFunc) {
break;
}
i++;
}
if (resolveFunc) {
g_pDvmResolveClass_Addr = resolveFunc;
i = 0;
while(i < size) {
jstring jClassItem = (jstring)((*env)->GetObjectArrayElement(env, referrerClassList, i));
const char* classItem = (*env)->GetStringUTFChars(env, jClassItem, 0);
if (classItem == 0) {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
LOGE("CODE_NATIVE_ITEM_PARAMETER_ERROR=%d", i);
return NUM_FACTOR_PATCH * i + CODE_NATIVE_ITEM_PARAMETER_ERROR;
}
if (strlen(classItem) < 5 || jClassIdxArray[i] < 0) {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
(*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
LOGE("CODE_NATIVE_ITEM_PARAMETER_ERROR=%d", i);
return NUM_FACTOR_PATCH * i + CODE_NATIVE_ITEM_PARAMETER_ERROR;
}
void* referrerClassObj = g_pDvmFindLoadedClass_Addr(classItem);
if (referrerClassObj) {
void* resClassObj = g_pDvmResolveClass_Addr(referrerClassObj, (unsigned int)jClassIdxArray[i], 1);
if (!resClassObj) {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
(*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
LOGE("CODE_PATCH_CLASS_OBJECT_ERROR=%d", i);
return NUM_FACTOR_PATCH * i + CODE_PATCH_CLASS_OBJECT_ERROR;
}
} else {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
(*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
LOGE("CODE_REFERRER_CLASS_OBJECT_ERROR=%d", i);
return NUM_FACTOR_PATCH * i + CODE_REFERRER_CLASS_OBJECT_ERROR;
}
(*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
i++;
}
} else {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
LOGE("CODE_RESOLVE_CLASS_ERROR");
return CODE_RESOLVE_CLASS_ERROR;
}
} else {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
LOGE("CODE_FIND_LOADED_CLASS_ERROR");
return CODE_FIND_LOADED_CLASS_ERROR;
}
} else {
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
LOGE("CODE_LOAD_DALVIK_SO_ERROR");
return CODE_LOAD_DALVIK_SO_ERROR;
}
(*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
LOGI("CODE_RESOLVE_PATCH_ALL_SUCCESS");
return CODE_RESOLVE_PATCH_ALL_SUCCESS;
}
到這里加載流程就結(jié)束了家淤。
五异剥、總結(jié)
QFix方案原理和實(shí)現(xiàn)相對Tinker方案來說都簡單不少,比較輕量級媒鼓,不過Tinker實(shí)現(xiàn)比較重的同時可以實(shí)現(xiàn)功能級更新届吁,而QFix方案更多是應(yīng)對bug的修復(fù)。
QFixPatch這個項(xiàng)目算是QFix方案的一個gradle實(shí)踐绿鸣,實(shí)際開發(fā)過程中原創(chuàng)東西不多疚沐,主要工作還是在gradle插件。對于Android熱修復(fù)我也是因?yàn)楣ぷ髦杏行枨笏粤私舛嘁恍┏蹦!m?xiàng)目中有不完善的地方也歡迎各位同學(xué)來github提issue.
熱修復(fù)目前主流是NativeHook,ClassLoader,InstantRun三種方案亮蛔,NativeHook,ClassLoader在android7.0版本上都會有一些兼容性問題,相對來說InstantRun方案兼容性會更好擎厢,目前我也在研究學(xué)習(xí)Robust和Aceso的路上究流,歡迎有興趣的同學(xué)一起來探討辣吃。
本人Github:https://github.com/alexclin0188 歡迎關(guān)注
Stashed changes