之前寫過一片:與Android熱更新方案Amigo的初次接觸,主要是記敘了Amigo的接入和使用。
最近讀了一下Amigo的原始碼,而且看了看網上其他講Amigo原始碼的文章,它們所針對的程式碼都和最新的Amigo程式碼有所出路,所以針對最新的程式碼,淺淺的分析一下。
Amigo的最近更新已經是8個月之前了。我最近更新了Android Studio3.0,gradle版本3.1.1。但是Amigo使用的gradle版本2.3.1。如果專案還是要使用gradle3.x版本的話,會報錯。所以我更新了Amigo外掛使用的gradle版本(因為的gradle3.0比起2.0有一些修改,所以也修改了部分Amigo外掛程式碼),如果有使用Amigo的老鐵同時用的是gradle3.x版本的話,可以找我要程式碼。
Amigo主要有兩個部分,在Github上可以看到,amigo-lib和buildSrc。
amigo-lib對應:
dependencies {
...
compile 'me.ele:amigo-lib:0.6.7'
}
複製程式碼
buildSrc對應外掛:
dependencies {
......
classpath 'me.ele:amigo:0.6.8'
}
複製程式碼
apply plugin: 'me.ele.amigo'
複製程式碼
先說外掛,這個外掛的作用就是修改AndroidManifest.xml,將專案原本的Application替換稱Amigo.java,並且將原來的 application 的 name 儲存在了一個名為acd的類中。AndroidManifest.xml 中將原來的 application 做為一個 Activity。
再說amigo-lib,這個是熱更新的重點。我們從更新的入口開始說起:
button.setOnClickListener {
var file = File(Environment.getExternalStorageDirectory().path + File.separator + "test.apk")
if(file.exists()){
Amigo.workLater(this, file) {
if(it){
toast("更新成功!")
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
android.os.Process.killProcess(android.os.Process.myPid())
}
}
}
}
複製程式碼
本地已經有了一個新的APK,直接呼叫Amigo.workLater()開始更新。
private static void workLater(Context context, File patchFile, boolean checkSignature,
WorkLaterCallback callback) {
String patchChecksum = PatchChecker.checkPatchAndCopy(context, patchFile, checkSignature);
if (checkWithWorkingPatch(context, patchChecksum)) return;
if (patchChecksum == null) {
Log.e(TAG, "#workLater: empty checksum");
return;
}
if (callback != null) {
AmigoService.startReleaseDex(context, patchChecksum, callback);
} else {
AmigoService.startReleaseDex(context, patchChecksum);
}
}
複製程式碼
第一步
將新的安裝包拷貝到了/data/data/{package_name}/files/amigo/{checksum}/patch.apk。checksum是根據APK算出的,每個APK都不同,可以理解為APK的id。當然,在拷貝之前做了一些校驗,主要是以前拷貝過嗎?是不是現在正在執行的版本?第二步
釋放Dex。在 AmigoService.startReleaseDex()
中主要是啟動了AmigoService
。在AmigoService.java中會呼叫:
private synchronized void handleReleaseDex(Intent intent) {
String checksum = intent.getStringExtra(EXTRA_APK_CHECKSUM);
if (apkReleaser == null) {
apkReleaser = new ApkReleaser(getApplicationContext());
}
apkReleaser.release(checksum, msgHandler);
}
複製程式碼
具體的釋放Dex在ApkReleaser的release()中:
public void release(final String checksum, final Handler msgHandler) {
if (isReleasing) {
Log.w(TAG, "release : been busy now, skip release " + checksum);
return;
}
Log.d(TAG, "release: start release " + checksum);
try {
this.amigoDirs = AmigoDirs.getInstance(context);
this.patchApks = PatchApks.getInstance(context);
} catch (Exception e) {
Log.e(TAG,
"release: unable to create amigo dir and patch apk dir, abort release dex files",
e);
handleDexOptFailure(checksum, msgHandler);
return;
}
isReleasing = true;
service.submit(new Runnable() {
@Override
public void run() {
if (!new DexExtractor(context, checksum).extractDexFiles()) {
Log.e(TAG, "releasing dex failed");
handleDexOptFailure(checksum, msgHandler);
isReleasing = false;
FileUtils.removeFile(amigoDirs.dexDir(checksum), false);
return;
}
// todo
// just create a link point to /data/app/{package_name}/libs
// if none of the native libs are changed
int errorCode;
if ((errorCode =
NativeLibraryHelperCompat.copyNativeBinaries(patchApks.patchFile(checksum),
amigoDirs.libDir(checksum))) < 0) {
Log.e(TAG, "coping native binaries failed, errorCode = " + errorCode);
handleDexOptFailure(checksum, msgHandler);
FileUtils.removeFile(amigoDirs.dexDir(checksum), false);
FileUtils.removeFile(amigoDirs.libDir(checksum), false);
isReleasing = false;
return;
}
final boolean dexOptimized = Build.VERSION.SDK_INT >= 21 ? dexOptimizationOnArt(checksum)
: dexOptimizationOnDalvik(checksum);
if (dexOptimized) {
Log.e(TAG, "optimize dex succeed");
handleDexOptSuccess(checksum, msgHandler);
isReleasing = false;
return;
}
Log.e(TAG, "optimize dex failed");
FileUtils.removeFile(amigoDirs.dexDir(checksum), false);
FileUtils.removeFile(amigoDirs.libDir(checksum), false);
FileUtils.removeFile(amigoDirs.dexOptDir(checksum), false);
handleDexOptFailure(checksum, msgHandler);
isReleasing = false;
}
});
}
複製程式碼
這裡又分了3小步:1.釋放dex;2.釋放lib中的so;3.優化dex。
在DexExtractor(context, checksum).extractDexFiles()
中是具體的釋放dex過程:
public boolean extractDexFiles() {
if (Build.VERSION.SDK_INT >= 21) {
return true; // art supports multi-dex natively
}
return performExtractions(PatchApks.getInstance(context).patchFile(checksum),
AmigoDirs.getInstance(context).dexDir(checksum));
}
//把patchApk(新包)中的dex解壓到dexDir:/data/data/{package_name}/files/amigo/{checksum}/dexes
private boolean performExtractions(File patchApk, File dexDir) {
ZipFile apk = null;
try {
apk = new ZipFile(patchApk);
int dexNum = 0;
ZipEntry dexFile = apk.getEntry("classes.dex");
for (; dexFile != null; dexFile = apk.getEntry("classes" + dexNum + ".dex")) {
String fileName = dexFile.getName().replace("dex", "zip");
File extractedFile = new File(dexDir, fileName);
extract(apk, dexFile, extractedFile);
verifyZipFile(extractedFile);
if (dexNum == 0) ++dexNum;
++dexNum;
}
return dexNum > 0;
} catch (IOException ioe) {
ioe.printStackTrace();
return false;
} finally {
try {
apk.close();
} catch (IOException var16) {
Log.w("DexExtractor", "Failed to close resource", var16);
}
}
}
複製程式碼
這裡有個extract()
方法,進去看一下:
private void extract(ZipFile patchApk, ZipEntry dexFile, File extractTo) throws IOException {
boolean reused = reusePreExistedODex(patchApk, dexFile);
Log.d(TAG, "extracted: "
+ dexFile.getName() + " success ? "
+ reused
+ ", by reusing pre-existed secondary dex");
//可以如果複用舊dex
if (reused) {
return;
}
//不能複用,就執行拷貝
InputStream in = null;
File tmp = null;
ZipOutputStream out = null;
try {
in = patchApk.getInputStream(dexFile);
tmp = File.createTempFile(extractTo.getName(), ".tmp", extractTo.getParentFile());
try {
out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
ZipEntry classesDex = new ZipEntry("classes.dex");
classesDex.setTime(dexFile.getTime());
out.putNextEntry(classesDex);
if (buffer == null) {
buffer = new byte[16384];
}
for (int length = in.read(buffer); length != -1; length = in.read(buffer)) {
out.write(buffer, 0, length);
}
} finally {
if (out != null) {
out.closeEntry();
out.close();
}
}
if (!tmp.renameTo(extractTo)) {
throw new IOException("Failed to rename \""
+ tmp.getAbsolutePath()
+ "\" to \""
+ extractTo.getAbsolutePath()
+ "\"");
}
} finally {
closeSilently(in);
if (tmp != null) tmp.delete();
}
}
複製程式碼
在這裡我們看到了具體的拷貝過程,先是拷貝到了tmp檔案,然後改的名。但是在拷貝之前有一個操作reusePreExistedODex()
,在這個方法中判斷了當前執行的App的dex與更新包的dex是否一致,如果一致做了一個link,把當前APP的dex檔案link到了我們需要拷貝的目錄dexDir:/data/data/{package_name}/files/amigo/{checksum}/dexes
,如果不一致再做拷貝操作。
到此dex釋放完畢,下一步釋放so檔案。NativeLibraryHelperCompat.copyNativeBinaries()
這裡就是判斷,根據不同的系統版本(比如是64位還32位系統),呼叫了不同拷貝方法。具體的這篇文章有詳細說。
然後就是優化dex了。final boolean dexOptimized = Build.VERSION.SDK_INT >= 21 ? dexOptimizationOnArt(checksum) : dexOptimizationOnDalvik(checksum)
這裡針對系統版本呼叫了不同的方法。
第三步
儲存標誌,然後重啟:
private void handleDexOptSuccess(String checksum, Handler msgHandler) {
saveDexAndSoChecksum(checksum);
PatchInfoUtil.updateDexFileOptStatus(context, checksum, true);
PatchInfoUtil.setWorkingChecksum(context, checksum);
if (msgHandler != null) {
msgHandler.sendEmptyMessage(AmigoService.MSG_ID_DEX_OPT_SUCCESS);
}
}
複製程式碼
到此釋放階段結束。
下面是重啟後,讀取資源。程式碼在Amigo.java中,先看看attachApplication()
.
在attachApplication()
中先做了一系列判斷,主要是判斷是否需要讀取釋放後的檔案(比如是否有更新檔案,是否需要更新),我們直接進入讀取檔案的地方attachPatchApk(workingChecksum);
private void attachPatchApk(String checksum) throws LoadPatchApkException {
try {
if (isPatchApkFirstRun(checksum)
|| !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
PatchInfoUtil.updateDexFileOptStatus(this, checksum, false);
releasePatchApk(checksum);
} else {
PatchChecker.checkDexAndSo(this, checksum);
}
setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum));
setApkResource(checksum);
revertBitFlag |= getClassLoader() instanceof AmigoClassLoader ? 1 : 0;
attachPatchedApplication(checksum);
PatchCleaner.clearOldPatches(this, checksum);
shouldHookAmAndPm = true;
Log.i(TAG, "#attachPatchApk: success");
} catch (Exception e) {
throw new LoadPatchApkException(e);
}
}
複製程式碼
這裡有兩個地方setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum)); setApkResource(checksum);
分別是設定 ClassLoader
和載入資源。
先說ClassLoader,這裡Hook了一個ClassLoader,使用了自己的AmigoClassLoader。
public static AmigoClassLoader newInstance(Context context, String checksum) {
return new AmigoClassLoader(PatchApks.getInstance(context).patchPath(checksum),
getDexPath(context, checksum),
AmigoDirs.getInstance(context).dexOptDir(checksum).getAbsolutePath(),
getLibraryPath(context, checksum),
AmigoClassLoader.class.getClassLoader().getParent());
}
複製程式碼
通過上面的程式碼可以看出AmigoClassLoader使用了我們之前釋放的資源的目錄,也就是dex,libs等。
private void setApkResource(String checksum) throws Exception {
PatchResourceLoader.loadPatchResources(this, checksum);
Log.i(TAG, "hook Resources success");
}
static void loadPatchResources(Context context, String checksum) throws Exception {
AssetManager newAssetManager = AssetManager.class.newInstance();
invokeMethod(newAssetManager, "addAssetPath", PatchApks.getInstance(context).patchPath(checksum));
invokeMethod(newAssetManager, "ensureStringBlocks");
replaceAssetManager(context, newAssetManager);
}
複製程式碼
replaceAssetManager(context, newAssetManager)
中Hook了一些其他AssetManager用到的地方。
然後把shouldHookAmAndPm
設成了true。
到這裡attachApplication()
執行完成。然後走onCreate()
public void onCreate() {
super.onCreate();
try {
setAPKApplication(realApplication);
} catch (Exception e) {
// should not happen, if it does happen, we just let it die
throw new RuntimeException(e);
}
if(shouldHookAmAndPm) {
try {
installAndHook();
} catch (Exception e) {
try {
clear(this);
attachOriginalApplication();
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
}
realApplication.onCreate();
}
複製程式碼
這裡有個:
private void installAndHook() throws Exception {
boolean gotNewActivity = ActivityFinder.newActivityExistsInPatch(this);
if (gotNewActivity) {
setApkInstrumentation();
revertBitFlag |= 1 << 1;
setApkHandlerCallback();
revertBitFlag |= 1 << 2;
} else {
Log.d(TAG, "installAndHook: there is no any new activity, skip hooking " +
"instrumentation & mH's callback");
}
installHookFactory();
dynamicRegisterNewReceivers();
installPatchContentProviders();
}
複製程式碼
首先判斷是否有新的Activity,如果有,就需要HookmInstrumentation
。
installHookFactory()
中替換了ClassLoader,動態註冊了Receiver,安裝ContentProvider。
最後呼叫attachOriginalApplication()
,把之前的Application替換回來,然後走正常的流程。
【2018/10/11】
最近,適配了Android O。有需要的,拿去-> github。