<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
熱修復一直是這幾年來很熱門的話題,主流方案大致有兩種,一種是微信Tinker的dex檔案替換,另一種是阿里的Native層的方法替換。這裡重點介紹Tinker的大致原理。
介紹Tinker原理之前,我們先來回顧一下類載入機制。
我們編譯好的class檔案,需要先載入到虛擬機器器然後才會執行,這個過程是通過ClassLoader來完成的。
雙親委派模型:
作用:
1.避免類的重複載入。
比如有兩個類載入器,他們都要載入同一個類,這時候如果不是委託而是自己載入自己的,則會將類重複載入到方法區。
2.避免核心類被修改。
比如我們在自定義一個 java.lang.String 類,執行的時候會報錯,因為 String 是 java.lang 包下的類,應該由啟動類載入器載入。
JVM並不會一開始就載入所有的類,它是當你使用到的時候才會去通知類載入器去載入。
當我們new一個類時,首先是Android的虛擬機器器(Dalvik/ART虛擬機器器)通過ClassLoader去載入dex檔案到記憶體。
Android中的ClassLoader主要是PathClassLoader和DexClassLoader,這兩者都繼承自BaseDexClassLoader。它們都可以理解成應用類載入器。
PathClassLoader和DexClassLoader的區別:
當ClassLoader載入類時,會呼叫它的findclass方法去查詢該類。
下方是BaseDexClassLoader的findClass方法實現:
public class BaseDexClassLoader extends ClassLoader { ... @UnsupportedAppUsage private final DexPathList pathList; ... @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 首先檢查該類是否存在shared libraries中. if (sharedLibraryLoaders != null) { for (ClassLoader loader : sharedLibraryLoaders) { try { return loader.loadClass(name); } catch (ClassNotFoundException ignored) { } } } //再呼叫pathList.findClass去查詢該類,結果為null則丟擲錯誤。 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; } }
接下來我們再來看看DexPathList的findClass實現:
public DexPathList(ClassLoader definingContext, String librarySearchPath) { ... /** * List of dex/resource (class path) elements. * 存放dex檔案的一個陣列 */ @UnsupportedAppUsage private Element[] dexElements; ... public Class<?> findClass(String name, List<Throwable> suppressed) { //遍歷Element陣列,去查尋對應的類,找到後就立刻返回了 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; } ... }
Ok,這樣就替換成功了,重啟App,再呼叫原來的bug類,將會優先使用修補程式包中的修復類。
為什麼要重啟:因為雙親委派模型,一個類只會被ClassLoader載入一次,且載入過後的類不能解除安裝。
接下來我們動手擼一個乞丐版的Tinker。
首先我們寫一個bug類。
package com.baima.plugin; class BugClass { public String getTitle(){ return "這是個Bug"; } }
接著我們新建一個module來生成修補程式包apk。
建立bug修復類,注意包名類名要一樣。
package com.baima.plugin; class BugClass { public String getTitle(){ return "修復成功"; } }
生成修補程式apk,讓使用者下載這個修補程式包。接下來就是載入這個apk檔案並替換了。
public void loadDexAndInject(Context appContext, String dexPath, String dexOptPath) { try { // 載入應用程式dex的Loader PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); //dexPath 修補程式dex檔案所在的路徑 //dexOptPath 修補程式dex檔案被寫入後存放的路徑 DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexOptPath, null, pathLoader); //利用反射獲取DexClassLoader和PathClassLoader的pathList屬性 Object dexPathList = getPathList(dexClassLoader); Object pathPathList = getPathList(pathLoader); //同樣用反射獲取DexClassLoader和PathClassLoader的dexElements屬性 Object leftDexElements = getDexElements(dexPathList); Object rightDexElements = getDexElements(pathPathList); //合併兩個陣列,且修補程式包的dex檔案在陣列的前面 Object dexElements = combineArray(leftDexElements, rightDexElements); //反射將合併後的陣列賦值給PathClassLoader的pathList.dexElements Object pathList = getPathList(pathLoader); Class<?> pathClazz = pathList.getClass(); Field declaredField = pathClazz.getDeclaredField("dexElements"); declaredField.set看,ccessible(true); declaredField.set(pathList, dexElements); } catch (Exception e) { e.printStackTrace(); } } private static Object getPathList(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader"); Field field = cl.getDeclaredField("pathList"); field.setAccessible(true); return field.get(classLoader); } private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException { Class<?> cl = pathList.getClass(); Field field = cl.getDeclaredField("dexElements"); field.setAccessible(true); return field.get(pathList); } private static Object combineArray(Object arrayLeft, Object arrayRight) { Class<?> clazz = arrayLeft.getClass().getComponentType(); int i = Array.getLength(arrayLeft); int j = Array.getLength(arrayRight); int k = i + j; Object result = Array.newInstance(clazz, k);// 建立一個型別為clazz,長度為k的新陣列 System.arraycopy(arrayLeft, 0, result, 0, i); System.arraycopy(arrayRight, 0, result, i, j); return result; }
ok,乞丐版Tinker完成了,使用時先在Splash介面檢查是否有外掛修補程式,有的話執行替換,這時你再使用bug類會發現它已經被替換成修補程式中的修復類了。
外掛化開發模式,打包時是一個宿主apk+多個外掛apk。
元件化開發模式,打包時是一個apk,裡面分多個module。
優點:
需要掌握的知識:
上圖是普通的Activity啟動流程,和根Activity啟動流程的區別是不用建立應用程式程序(Application Thread)。
啟動過程:
他們之間的跨程序通訊是通過Binder實現的。
通過上面介紹的熱修復,我們有辦法去載入外掛apk裡面的類,但是還沒有辦法去啟動外掛中的Activity,因為如果要啟動一個Activity,那麼這個Activity必須在AndroidManifest.xml中註冊。
這裡介紹外掛化的一種主流實現方式--Hook技術。
步驟1、2這裡就不在贅述了,2就是上面講到的熱修復技術。
AMS是在SystemServer程序中,我們無法直接進行修改,只能在應用程式程序中做文章。
介紹一個類--IActivityManager,IActivityManager它通過AIDL(內部使用的是Binder機制)和SystemServer程序的AMS通訊。所以IActivityManager很適合作為一個hook點。
Activity啟動時會呼叫IActivityManager.startActivity方法向AMS發出啟動請求,該方法引數包含一個Intent物件,它是原本要啟動的Activity的Intent。
我們可以動態代理IActivityManager的startActivity方法,將該Intent換為佔坑Activity的Intent,並將原來的Intent作為引數傳遞過去,以此達到欺騙AMS繞開驗證。
public class IActivityManagerProxy implements InvocationHandler { private Object mActivityManager; private static final String TAG = "IActivityManagerProxy"; public IActivityManagerProxy(Object activityManager) { this.mActivityManager = activityManager; } @Override public Object invoke(Object o, Method method, Object[] args) throws Throwable { if ("startActivity".equals(method.getName())) { Intent intent = null; int index = 0; for (int i = 0; i < args.length; i++) { if (args[i] instanceof Intent) { index = i; break; } } intent = (Intent) args[index]; Intent subIntent = new Intent(); String packageName = "com.example.pluginactivity"; subIntent.setClassName(packageName,packageName+".StubActivity"); subIntent.putExtra(HookHelper.TARGET_INTENT, intent); args[index] = subIntent; } return method.invoke(mActivityManager, args); } }
接下來就通過反射的方式,將ActivityManager中的IActivityManager替換成我們的代理物件。
public void hookAMS() { try { Object defaultSingleton = null; if (Build.VERSION.SDK_INT >= 26) { Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager"); defaultSingleton = FieldUtil.getObjectField(activityManagerClazz, null, "IActivityManagerSingleton"); } else { Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative"); defaultSingleton = FieldUtil.getObjectField(activityManagerNativeClazz, null, "gDefault"); } Class<?> singletonClazz = Class.forName("android.util.Singleton"); Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance"); Object iActivityManager = mInstanceField.get(defaultSingleton); Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager"); Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager)); mInstanceField.set(defaultSingleton, proxy); } catch (Exception e) { e.printStackTrace(); } }
Note: 這裡獲取IActivityManager範例會因為Android版本不同而不同,具體獲取方法就需要去看原始碼瞭解了。這裡的程式碼Android 8.0是可以執行的。
ActivityThread啟動Activity的過程如下所示:
ActivityThread會通過H在主執行緒中去啟動Activity,H類是ActivityThread的內部類並繼承自Handler。
private class H extends Handler { public static final int LAUNCH_ACTIVITY = 100; public static final int PAUSE_ACTIVITY = 101; ... public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; ... } ... }
H中重寫的handleMessage方法會對LAUNCH_ACTIVITY型別的訊息進行處理,最終會呼叫Activity的onCreate方法。那麼在哪進行替換呢?接著來看Handler的dispatchMessage方法:
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
Handler的dispatchMessage用於處理訊息,可以看到如果Handler的Callback型別的mCallback不為null,就會執行mCallback的handleMessage方法。因此,mCallback可以作為Hook點,我們可以用自定義的Callback來替換mCallback,自定義的Callback如下所示。
public class HCallback implements Handler.Callback{ public static final int LAUNCH_ACTIVITY = 100; Handler mHandler; public HCallback(Handler handler) { mHandler = handler; } @Override public boolean handleMessage(Message msg) { if (msg.what == LAUNCH_ACTIVITY) { Object r = msg.obj; try { //得到訊息中的Intent(啟動佔坑Activity的Intent) Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent"); //得到此前儲存起來的Intent(啟動外掛Activity的Intent) Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT); //將佔坑Activity的Intent替換為外掛Activity的Intent intent.setComponent(target.getComponent()); } catch (Exception e) { e.printStackTrace(); } } mHandler.handleMessage(msg); return true; } }
最後一步就是用反射將我們自定義的callBack設定給ActivityThread.sCurrentActivityThread.mH.mCallback
。
public void hookHandler() { try { Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Object currentActivityThread = FieldUtil.getObjectField(activityThreadClass, null, "sCurrentActivityThread"); Field mHField = FieldUtil.getField(activityThreadClass, "mH"); Handler mH = (Handler) mHField.get(currentActivityThread); FieldUtil.setObjectField(Handler.class, mH, "mCallback", new HCallback(mH)); } catch (Exception e) { e.printStackTrace(); } }
其實要想啟動一個Activity到這步還沒有完,一個完整的Activity應該還需要佈局檔案,而我們的宿主APP並不會包含外掛的資源。
android中的資源大致分為兩類:一類是res目錄下存在的可編譯的資原始檔,比如anim,string之類的,第二類是assets目錄下存放的原始資原始檔。因為Apk編譯的時候不會編譯這些檔案,所以不能通過id來存取,當然也不能通過絕對路徑來存取。於是Android系統讓我們通過Resources的getAssets方法來獲取AssetManager,利用AssetManager來存取這些檔案。
其實Resource的getString, getText等各種方法都是通過呼叫AssetManager的私有方法來完成的。 過程就是Resource通過resource.arsc(AAPT工具打包過程中生成的檔案)把ID轉換成資原始檔的名稱,然後交由AssetManager來載入檔案。
AssetManager裡有個很重要的方法addAssetPath(String path)方法,App啟動的時候會把當前apk的路徑傳進去,然後AssetManager就能存取這個路徑下的所有資源也就是宿主apk的資源了。我們可以通過hook這個方法將外掛的path傳進去,得到的AssetManager就能同時存取宿主和外掛的所有資源了。
public void hookAssets(Activity activity,String dexPath){ try { AssetManager assetManager = activity.getResources().getAssets(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class); addAssetPath.invoke(assetManager,dexPath); Resources mResources = new Resources(assetManager, activity.getResources().getDisplayMetrics(), activity.getResources().getConfiguration()); //接下來我們要將宿主原有Resources替換成我們上面生成的Resources。 FieldUtil.setObjectField(ContextWrapper.class,activity.getResources(),"mResources",mResources); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } }
新的問題又出現了,宿主apk和外掛apk是兩個不同的apk,他們在編譯時都會產生自己的resources.arsc。即他們是兩個獨立的編譯過程。那麼它們的resources.arsc中的資源id必定是有相同的情況。這樣我們上面生成的新Resources中就出現了資源id重複的情況,這樣在執行的時候使用資源id來獲取資源就會報錯。
怎麼解決資源Id衝突的問題?這裡介紹一下VirtualApk採用的方案。
修改aapt的產物。即編譯後期重新整理外掛Apk的資源,編排ID,更新R檔案
VirtualApkhook了ProcessAndroidResourcestask。這個task是用來編譯Android資源的。VirtualApk拿到這個task的輸出結果,做了以下處理:
大致原理是這樣的,但如何保證新的Id不會重複了,這裡在介紹一下資源Id的組成。
packageId: 前兩位是packageId,相當於一個名稱空間,主要用來區分不同的包空間(不是不同的module)。目前來看,在編譯app的時候,至少會遇到兩個包空間:android系統資源包和咱們自己的App資源包。大家可以觀察R.java檔案,可以看到部分是以0x01開頭的,部分是以0x7f開頭的。以0x01開頭的就是系統已經內建的資源id,以0x7f開頭的是咱們自己新增的app資源id。
typeId:typeId是指資源的型別id,我們知道android資源有animator、anim、color、drawable、layout,string等等,typeId就是拿來區分不同的資源型別。
entryId:entryId是指每一個資源在其所屬的資源型別中所出現的次序。注意,不同型別的資源的Entry ID有可能是相同的,但是由於它們的型別不同,我們仍然可以通過其資源ID來區別開來。
所以為了避免衝突,外掛的資源id通常會採用0x02 - 0x7e之間的數值。
以上就是Android熱修復及外掛化原理詳解的詳細內容,更多關於Android熱修復外掛化的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45