標籤:
AndFix介紹AndFix是一個Android App的線上熱補丁架構。使用此架構,我們能夠在不重複發版的情況下,線上修改App中的Bug。AndFix就是 “Android Hot-Fix”的縮寫。AndFix支援Android 2.3到6.0版本,並且支援arm 與 X86系統架構的裝置。完美支援Dalvik與ART的Runtime。AndFix 的補丁檔案是以 .apatch 結尾的檔案。AndFix是阿里的開源項目。https://github.com/alibaba/AndFix小例子:下載demo APK http://120.55.185.35:8080/old.apkdemo ,或者掃描下面二維碼,安裝好apk後運行AndFixDemo,點擊“提示資訊”按鈕,跳出“未修複的toast”,點擊“開始修複”,app會到遠程服務端下載補丁封裝更新,大概會持續幾秒鐘,等待幾秒後再次點擊“提示資訊”按鈕,會彈出修複好的內容
實際運行中則不需要點擊“開始修複”按鈕,在demo中為了對比效果,所以加了按鈕去控制實際運行中檢測需要打補丁的方案:
- 類似檢測升級,在開啟app或者某個頁面去檢測
- 服務端推送(需要和服務端定義更多實現細節)
AndFix的優點很明顯
- 補丁包很小(上面的例子,補丁包才幾K),打補丁的速度很快
- 打好補丁後,後續都不用再打了
使用方法:1. 添加依賴
2. 在自訂Application中初始化,並在AndroidManifest.xml中註冊該Application
3.在activity 中打補丁,(這裡省略了檢測步驟,預設就是直接下載補丁並更新,實際開發中需要對補丁的版本進行檢測,更新好後刪除補丁包,動態擷取補丁路徑等等)
4.到這裡已經完成了配置工作,接下來用正式的key的打包,就產生了old.apk, 把old.apk放到了伺服器上,項目就上線了
5.項目上線後,會有各種突發情況,(比如文案修改,緊急bug,造成app crash等麻煩,正常情況只有進行版本升級),這裡看下AndFix熱修補的步驟,以修改showToast方法為例子
6.現在把showToast方法修改,並打包,命名為fix.apk
7.下載apkpatch工具
8.製作補丁包,cd 到目錄下,運行apkpatch.bat -f fix.apk -t old.apk -o output1 -k demo.jks -p 123456 -a demo -e 123456
(這裡-t 為老的apk -f為修複過的apk,-o 為輸出目錄 -k 為打包的key -p -e 為密碼 -a 為別名)
9.螢幕輸出了增加了修改了toast的方法 ,同時目錄下新增了output1 目錄,點進去查看,其中.apatch為真正的補丁包
10. 用dex2Jar工具把diff檔案轉成jar檔案,在用jd.gui查看
其實這個工具就是比對兩個dex檔案,分析出修改過的地方,然後產生補丁包
11. 把.apatch命名為app.apatch上傳至伺服器,坐等用戶端打補丁
源碼分析:1首先來看下在application中的初始化
@Override public void onCreate() { super.onCreate(); // 初始化patch管理類 mPatchManager = new PatchManager(this); // 初始化patch版本 mPatchManager.init("2.0"); // 載入已經添加到PatchManager中的patch mPatchManager.loadPatch(); }
2 構造了PatchManager對象,來看下代碼
public PatchManager(Context context) { mContext = context; mAndFixManager = new AndFixManager(mContext); //初始化AndFixManager,等會再介紹 //getFileDir 擷取的是/data/data/<application package>/files //在這裡是 /data/data/<application package>/files/apatch mPatchDir = new File(mContext.getFilesDir(), DIR); // 支援並發訪問的有序的補丁集合 mPatchs = new ConcurrentSkipListSet<Patch>(); // ClassLoader的集合,同樣也是基於安全執行緒的 mLoaders = new ConcurrentHashMap<String, ClassLoader>(); }
3 初始化patch版本,代碼
public void init(String appVersion) { //再次檢測patch存放路徑 if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail Log.e(TAG, "patch dir create error."); return; } else if (!mPatchDir.isDirectory()) {// not directory mPatchDir.delete(); return; } //用SharedPreferences 擷取path的版本 SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); String ver = sp.getString(SP_VERSION, null); if (ver == null || !ver.equalsIgnoreCase(appVersion)) { //如果是第一次使用或者版本不一致,則刪除所有Patch ,這裡equalsIgnoreCase是忽略大小寫equals cleanPatch(); // 存放版本 sp.edit().putString(SP_VERSION, appVersion).commit(); } else { // 載入有所的Patch initPatchs(); } } private void initPatchs() { File[] files = mPatchDir.listFiles(); for (File file : files) { addPatch(file); } }
4 載入已經添加到PatchManager中的patch,代碼
/** * load patch,call when application start * 這裡寫的也很清楚了,在程式啟動的時候調用 */ public void loadPatch() { //首先載入了通用的類載入器 mLoaders.put("*", mContext.getClassLoader());// wildcard Set<String> patchNames; List<String> classes; //遍曆每個補丁包 Patch 的結構HashMap<String, List<String>>() //實際中,只會有一個key,就是你修改後apk的名字,list中存放修改的className //fix----[cv.cocoa.com.andfixdemo.MainActivity_CF] for (Patch patch : mPatchs) { patchNames = patch.getPatchNames(); for (String patchName : patchNames) { classes = patch.getClasses(patchName); //更新補丁 mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(), classes); } } }
5.在Application中的初始化代碼就完成了,其中還有一個AndFixManager沒講,AndFixManager在構造前,會做一個相容性的檢測,放在了Compat 類中,代碼
public class Compat { public static boolean isChecked = false; public static boolean isSupport = false; /** * whether support on the device * 需要對相容性進行檢測,檢測的判斷是不能是YunOs的手機,sdk的版本必須是在2.3-6.0之間 * @return true if the device support AndFix */ public static synchronized boolean isSupport() { if (isChecked) return isSupport; isChecked = true; // AndFix.setup()判斷是Dalvik還是Art虛擬機器,來註冊Native方法 if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) { isSupport = true; } if (inBlackList()) { isSupport = false; } return isSupport; }}
/** * initialize * * @return true if initialize success * * 判斷是Dalvik還是Art虛擬機器,並初始化native的方法, * * 官方文檔中說到https://developer.android.com/guide/practices/verifying-apps-art.html * You can verify which runtime is in use by calling System.getProperty("java.vm.version"). If ART is in use, the property‘s value is "2.0.0" or higher. * */ public static boolean setup() { try { final String vmVersion = System.getProperty("java.vm.version"); boolean isArt = vmVersion != null && vmVersion.startsWith("2"); int apilevel = Build.VERSION.SDK_INT; return setup(isArt, apilevel); } catch (Exception e) { Log.e(TAG, "setup", e); return false; } }
6.然後再來看下AndFixManager 的代碼
//最關鍵的就是這裡,擷取到Class對象後,用反射擷取修改的方法private void fixClass(Class<?> clazz, ClassLoader classLoader) { //java反射擷取Class的方法 Method[] methods = clazz.getDeclaredMethods(); MethodReplace methodReplace; String clz; String meth; for (Method method : methods) { // 找到MethodReplace的註解 methodReplace = method.getAnnotation(MethodReplace.class); if (methodReplace == null) continue; //對照上面的demo,clz就是cv.cocoa.com.andfixdemo.MainActivity clz = methodReplace.clazz(); //對照上面的demo,meth 就是showToast meth = methodReplace.method(); if (!isEmpty(clz) && !isEmpty(meth)) { //替換方法 replaceMethod(classLoader, clz, meth, method); } } } /** * replace method * * @param classLoader classloader * @param clz class * @param meth name of target method * @param method source method */ private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) { try { String key = clz + "@" + classLoader.toString(); Class<?> clazz = mFixedClass.get(key); if (clazz == null) {// class not load Class<?> clzz = classLoader.loadClass(clz); // initialize target class clazz = AndFix.initTargetClass(clzz); } if (clazz != null) {// initialize class OK mFixedClass.put(key, clazz); Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes()); //調用jni的方法 AndFix.addReplaceMethod(src, method); } } catch (Exception e) { Log.e(TAG, "replaceMethod", e); } }
Android 熱修補方案(AndFix)