Android熱修復(fù)及插件化原理示例詳解
1.前言
熱修復(fù)一直是這幾年來(lái)很熱門(mén)的話題,主流方案大致有兩種,一種是微信Tinker的dex文件替換,另一種是阿里的Native層的方法替換。這里重點(diǎn)介紹Tinker的大致原理。
2.類(lèi)加載機(jī)制
介紹Tinker原理之前,我們先來(lái)回顧一下類(lèi)加載機(jī)制。
我們編譯好的class文件,需要先加載到虛擬機(jī)然后才會(huì)執(zhí)行,這個(gè)過(guò)程是通過(guò)ClassLoader來(lái)完成的。

雙親委派模型:
- 1.加載某個(gè)類(lèi)的時(shí)候,這個(gè)類(lèi)加載器不會(huì)自己立刻去加載,它會(huì)委托給父類(lèi)去加載
- 2.如果這個(gè)父類(lèi)還存在父類(lèi)加載器,則進(jìn)一步委托,直到最頂層的類(lèi)加載器
- 3.如果父類(lèi)加載器可以完成加載任務(wù),就成功返回,否則就再委派給子類(lèi)加載器
- 4.如果都未加載成功就拋出ClassNotFoundException
作用:
1.避免類(lèi)的重復(fù)加載。
比如有兩個(gè)類(lèi)加載器,他們都要加載同一個(gè)類(lèi),這時(shí)候如果不是委托而是自己加載自己的,則會(huì)將類(lèi)重復(fù)加載到方法區(qū)。
2.避免核心類(lèi)被修改。
比如我們?cè)谧远x一個(gè) java.lang.String 類(lèi),執(zhí)行的時(shí)候會(huì)報(bào)錯(cuò),因?yàn)?String 是 java.lang 包下的類(lèi),應(yīng)該由啟動(dòng)類(lèi)加載器加載。
JVM并不會(huì)一開(kāi)始就加載所有的類(lèi),它是當(dāng)你使用到的時(shí)候才會(huì)去通知類(lèi)加載器去加載。
3.Android類(lèi)加載
當(dāng)我們new一個(gè)類(lèi)時(shí),首先是Android的虛擬機(jī)(Dalvik/ART虛擬機(jī))通過(guò)ClassLoader去加載dex文件到內(nèi)存。
Android中的ClassLoader主要是PathClassLoader和DexClassLoader,這兩者都繼承自BaseDexClassLoader。它們都可以理解成應(yīng)用類(lèi)加載器。
PathClassLoader和DexClassLoader的區(qū)別:
- PathClassLoader只能指定加載apk包路徑,不能指定dex文件解壓路徑。該路徑是寫(xiě)死的在/data/dalvik-cache/路徑下。所以只能用于加載已安裝的apk。
- DexClassLoader可以指定apk包路徑和dex文件解壓路徑(加載jar、apk、dex文件)
當(dāng)ClassLoader加載類(lèi)時(shí),會(huì)調(diào)用它的findclass方法去查找該類(lèi)。
下方是BaseDexClassLoader的findClass方法實(shí)現(xiàn):
public class BaseDexClassLoader extends ClassLoader {
...
@UnsupportedAppUsage
private final DexPathList pathList;
...
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 首先檢查該類(lèi)是否存在shared libraries中.
if (sharedLibraryLoaders != null) {
for (ClassLoader loader : sharedLibraryLoaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException ignored) {
}
}
}
//再調(diào)用pathList.findClass去查找該類(lèi),結(jié)果為null則拋出錯(cuò)誤。
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;
}
}
接下來(lái)我們?cè)賮?lái)看看DexPathList的findClass實(shí)現(xiàn):
public DexPathList(ClassLoader definingContext, String librarySearchPath) {
...
/**
* List of dex/resource (class path) elements.
* 存放dex文件的一個(gè)數(shù)組
*/
@UnsupportedAppUsage
private Element[] dexElements;
...
public Class<?> findClass(String name, List<Throwable> suppressed) {
//遍歷Element數(shù)組,去查尋對(duì)應(yīng)的類(lèi),找到后就立刻返回了
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;
}
...
}
4.Tinker原理
- 1.使用DexClassLoader加載補(bǔ)丁包的dex文件
- 2.通過(guò)反射獲取DexClassLoader類(lèi)的pathList,再次通過(guò)反射獲得dexElements數(shù)組。
- 3.獲取加載應(yīng)用類(lèi)的PathClassLoader,同樣通過(guò)反射獲取它的dexElements數(shù)組。
- 4.合并兩個(gè)dexElements數(shù)組,且將補(bǔ)丁包的dex文件放在前面。
根據(jù)類(lèi)加載機(jī)制,一個(gè)類(lèi)只會(huì)被加載一次,DexPathList.findClass方法中是順序遍歷數(shù)組,所以將補(bǔ)丁的dex文件放在前面,這樣bug修復(fù)類(lèi)會(huì)被優(yōu)先加載,而原來(lái)的bug類(lèi)不會(huì)被加載,達(dá)到了替換bug類(lèi)的功能(補(bǔ)丁包中的修復(fù)類(lèi)名、包名要和bug類(lèi)相同) - 5.再次通過(guò)反射將合并后的dexElements數(shù)組賦值給PathClassLoader.dexElements屬性。
加載類(lèi)時(shí),Dalvik/ART虛擬機(jī)會(huì)通過(guò)PathClassLoader去查找已安裝的apk文件中的類(lèi)。
Ok,這樣就替換成功了,重啟App,再調(diào)用原來(lái)的bug類(lèi),將會(huì)優(yōu)先使用補(bǔ)丁包中的修復(fù)類(lèi)。
為什么要重啟:因?yàn)殡p親委派模型,一個(gè)類(lèi)只會(huì)被ClassLoader加載一次,且加載過(guò)后的類(lèi)不能卸載。
代碼實(shí)現(xiàn)
接下來(lái)我們動(dòng)手?jǐn)]一個(gè)乞丐版的Tinker。
首先我們寫(xiě)一個(gè)bug類(lèi)。
package com.baima.plugin;
class BugClass {
public String getTitle(){
return "這是個(gè)Bug";
}
}
接著我們新建一個(gè)module來(lái)生成補(bǔ)丁包apk。

創(chuàng)建bug修復(fù)類(lèi),注意包名類(lèi)名要一樣。
package com.baima.plugin;
class BugClass {
public String getTitle(){
return "修復(fù)成功";
}
}
生成補(bǔ)丁apk,讓用戶下載這個(gè)補(bǔ)丁包。接下來(lái)就是加載這個(gè)apk文件并替換了。
public void loadDexAndInject(Context appContext, String dexPath, String dexOptPath) {
try {
// 加載應(yīng)用程序dex的Loader
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
//dexPath 補(bǔ)丁dex文件所在的路徑
//dexOptPath 補(bǔ)丁dex文件被寫(xiě)入后存放的路徑
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);
//合并兩個(gè)數(shù)組,且補(bǔ)丁包的dex文件在數(shù)組的前面
Object dexElements = combineArray(leftDexElements, rightDexElements);
//反射將合并后的數(shù)組賦值給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);// 創(chuàng)建一個(gè)類(lèi)型為clazz,長(zhǎng)度為k的新數(shù)組
System.arraycopy(arrayLeft, 0, result, 0, i);
System.arraycopy(arrayRight, 0, result, i, j);
return result;
}
ok,乞丐版Tinker完成了,使用時(shí)先在Splash界面檢查是否有插件補(bǔ)丁,有的話執(zhí)行替換,這時(shí)你再使用bug類(lèi)會(huì)發(fā)現(xiàn)它已經(jīng)被替換成補(bǔ)丁中的修復(fù)類(lèi)了。
5.插件化
插件化開(kāi)發(fā)模式,打包時(shí)是一個(gè)宿主apk+多個(gè)插件apk。
組件化開(kāi)發(fā)模式,打包時(shí)是一個(gè)apk,里面分多個(gè)module。
優(yōu)點(diǎn):
- 安裝的主apk包會(huì)小好多
- 給開(kāi)發(fā)者提供了業(yè)務(wù)功能擴(kuò)展,并且不需要用戶進(jìn)行更新
- 在非主apk包中的功能出現(xiàn)BUG時(shí),可以及時(shí)修復(fù)
- 用戶不需要的功能,完全就不會(huì)出現(xiàn)在系統(tǒng)里面,減輕設(shè)備的負(fù)擔(dān)
需要掌握的知識(shí):
- 1.類(lèi)加載機(jī)制
- 2.四大組件啟動(dòng)流程
- 3.AIDL、Binder機(jī)制
- 4.Hook、反射、代理
5.1 Activity啟動(dòng)流程簡(jiǎn)單介紹

上圖是普通的Activity啟動(dòng)流程,和根Activity啟動(dòng)流程的區(qū)別是不用創(chuàng)建應(yīng)用程序進(jìn)程(Application Thread)。
啟動(dòng)過(guò)程:
- 應(yīng)用程序進(jìn)程中的Activity向AMS請(qǐng)求創(chuàng)建普通Activity
- AMS會(huì)對(duì)這個(gè)Activty的生命周期管和棧進(jìn)行管理,校驗(yàn)Activity等等
- 如果Activity滿足AMS的校驗(yàn),AMS就會(huì)請(qǐng)求應(yīng)用程序進(jìn)程中的ActivityThread去創(chuàng)建并啟動(dòng)普通Activity
他們之間的跨進(jìn)程通信是通過(guò)Binder實(shí)現(xiàn)的。
5.2 插件化原理
通過(guò)上面介紹的熱修復(fù),我們有辦法去加載插件apk里面的類(lèi),但是還沒(méi)有辦法去啟動(dòng)插件中的Activity,因?yàn)槿绻獑?dòng)一個(gè)Activity,那么這個(gè)Activity必須在AndroidManifest.xml中注冊(cè)。
這里介紹插件化的一種主流實(shí)現(xiàn)方式--Hook技術(shù)。
- 1.宿主App預(yù)留占坑Activity
- 2.使用classLoader加載dex文件到內(nèi)存
- 3.先使用占坑Activity繞過(guò)AMS驗(yàn)證,接著用插件Activity替換占坑的Activity。
步驟1、2這里就不在贅述了,2就是上面講到的熱修復(fù)技術(shù)。
5.2.1 繞開(kāi)驗(yàn)證
AMS是在SystemServer進(jìn)程中,我們無(wú)法直接進(jìn)行修改,只能在應(yīng)用程序進(jìn)程中做文章。
介紹一個(gè)類(lèi)--IActivityManager,IActivityManager它通過(guò)AIDL(內(nèi)部使用的是Binder機(jī)制)和SystemServer進(jìn)程的AMS通訊。所以IActivityManager很適合作為一個(gè)hook點(diǎn)。
Activity啟動(dòng)時(shí)會(huì)調(diào)用IActivityManager.startActivity方法向AMS發(fā)出啟動(dòng)請(qǐng)求,該方法參數(shù)包含一個(gè)Intent對(duì)象,它是原本要啟動(dòng)的Activity的Intent。
我們可以動(dòng)態(tài)代理IActivityManager的startActivity方法,將該Intent換為占坑Activity的Intent,并將原來(lái)的Intent作為參數(shù)傳遞過(guò)去,以此達(dá)到欺騙AMS繞開(kāi)驗(yàn)證。
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);
}
}
接下來(lái)就通過(guò)反射的方式,將ActivityManager中的IActivityManager替換成我們的代理對(duì)象。
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實(shí)例會(huì)因?yàn)锳ndroid版本不同而不同,具體獲取方法就需要去看源碼了解了。這里的代碼Android 8.0是可以運(yùn)行的。
5.2.2還原插件Activity
ActivityThread啟動(dòng)Activity的過(guò)程如下所示:

ActivityThread會(huì)通過(guò)H在主線程中去啟動(dòng)Activity,H類(lèi)是ActivityThread的內(nèi)部類(lèi)并繼承自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中重寫(xiě)的handleMessage方法會(huì)對(duì)LAUNCH_ACTIVITY類(lèi)型的消息進(jìn)行處理,最終會(huì)調(diào)用Activity的onCreate方法。那么在哪進(jìn)行替換呢?接著來(lái)看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類(lèi)型的mCallback不為null,就會(huì)執(zhí)行mCallback的handleMessage方法。因此,mCallback可以作為Hook點(diǎn),我們可以用自定義的Callback來(lái)替換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(啟動(dòng)占坑Activity的Intent)
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
//得到此前保存起來(lái)的Intent(啟動(dòng)插件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設(shè)置給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();
}
}
其實(shí)要想啟動(dòng)一個(gè)Activity到這步還沒(méi)有完,一個(gè)完整的Activity應(yīng)該還需要布局文件,而我們的宿主APP并不會(huì)包含插件的資源。
5.3 加載插件資源
5.3.1 Resources&AssetManager
android中的資源大致分為兩類(lèi):一類(lèi)是res目錄下存在的可編譯的資源文件,比如anim,string之類(lèi)的,第二類(lèi)是assets目錄下存放的原始資源文件。因?yàn)锳pk編譯的時(shí)候不會(huì)編譯這些文件,所以不能通過(guò)id來(lái)訪問(wèn),當(dāng)然也不能通過(guò)絕對(duì)路徑來(lái)訪問(wèn)。于是Android系統(tǒng)讓我們通過(guò)Resources的getAssets方法來(lái)獲取AssetManager,利用AssetManager來(lái)訪問(wèn)這些文件。
其實(shí)Resource的getString, getText等各種方法都是通過(guò)調(diào)用AssetManager的私有方法來(lái)完成的。 過(guò)程就是Resource通過(guò)resource.arsc(AAPT工具打包過(guò)程中生成的文件)把ID轉(zhuǎn)換成資源文件的名稱,然后交由AssetManager來(lái)加載文件。
AssetManager里有個(gè)很重要的方法addAssetPath(String path)方法,App啟動(dòng)的時(shí)候會(huì)把當(dāng)前apk的路徑傳進(jìn)去,然后AssetManager就能訪問(wèn)這個(gè)路徑下的所有資源也就是宿主apk的資源了。我們可以通過(guò)hook這個(gè)方法將插件的path傳進(jìn)去,得到的AssetManager就能同時(shí)訪問(wèn)宿主和插件的所有資源了。
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());
//接下來(lái)我們要將宿主原有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();
}
}
5.3.2 id沖突
新的問(wèn)題又出現(xiàn)了,宿主apk和插件apk是兩個(gè)不同的apk,他們?cè)诰幾g時(shí)都會(huì)產(chǎn)生自己的resources.arsc。即他們是兩個(gè)獨(dú)立的編譯過(guò)程。那么它們的resources.arsc中的資源id必定是有相同的情況。這樣我們上面生成的新Resources中就出現(xiàn)了資源id重復(fù)的情況,這樣在運(yùn)行的時(shí)候使用資源id來(lái)獲取資源就會(huì)報(bào)錯(cuò)。
怎么解決資源Id沖突的問(wèn)題?這里介紹一下VirtualApk采用的方案。
修改aapt的產(chǎn)物。即編譯后期重新整理插件Apk的資源,編排ID,更新R文件
VirtualApkhook了ProcessAndroidResourcestask。這個(gè)task是用來(lái)編譯Android資源的。VirtualApk拿到這個(gè)task的輸出結(jié)果,做了以下處理:
- 1.根據(jù)編譯產(chǎn)生的R.txt文件收集插件中所有的資源
- 2.根據(jù)編譯產(chǎn)生的R.txt文件收集宿主apk中的所有資源
- 3.過(guò)濾插件資源:過(guò)濾掉在宿主中已經(jīng)存在的資源
- 4.重新設(shè)置插件資源的資源ID
- 5.刪除掉插件資源目錄下前面已經(jīng)被過(guò)濾掉的資源
- 6.重新編排插件resources.arsc文件中插件資源ID為新設(shè)置的資源ID
- 7.重新產(chǎn)生R.java文件
大致原理是這樣的,但如何保證新的Id不會(huì)重復(fù)了,這里在介紹一下資源Id的組成。

packageId: 前兩位是packageId,相當(dāng)于一個(gè)命名空間,主要用來(lái)區(qū)分不同的包空間(不是不同的module)。目前來(lái)看,在編譯app的時(shí)候,至少會(huì)遇到兩個(gè)包空間:android系統(tǒng)資源包和咱們自己的App資源包。大家可以觀察R.java文件,可以看到部分是以0x01開(kāi)頭的,部分是以0x7f開(kāi)頭的。以0x01開(kāi)頭的就是系統(tǒng)已經(jīng)內(nèi)置的資源id,以0x7f開(kāi)頭的是咱們自己添加的app資源id。
typeId:typeId是指資源的類(lèi)型id,我們知道android資源有animator、anim、color、drawable、layout,string等等,typeId就是拿來(lái)區(qū)分不同的資源類(lèi)型。
entryId:entryId是指每一個(gè)資源在其所屬的資源類(lèi)型中所出現(xiàn)的次序。注意,不同類(lèi)型的資源的Entry ID有可能是相同的,但是由于它們的類(lèi)型不同,我們?nèi)匀豢梢酝ㄟ^(guò)其資源ID來(lái)區(qū)別開(kāi)來(lái)。
所以為了避免沖突,插件的資源id通常會(huì)采用0x02 - 0x7e之間的數(shù)值。
以上就是Android熱修復(fù)及插件化原理詳解的詳細(xì)內(nèi)容,更多關(guān)于Android熱修復(fù)插件化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android事件分發(fā)機(jī)制(下) View的事件處理
這篇文章主要介紹了Android事件分發(fā)機(jī)制下篇, View的事件處理的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
詳解Android開(kāi)發(fā)之MP4文件轉(zhuǎn)GIF文件
這篇文章介紹的是將錄下來(lái)的視頻選取一小段轉(zhuǎn)為 GIF 文件,不僅時(shí)間段可以手動(dòng)選取,而且還需要支持截取視頻的局部區(qū)域轉(zhuǎn)為 GIF,網(wǎng)上調(diào)研了一下技術(shù)方案,覺(jué)得還是有必要把實(shí)現(xiàn)過(guò)程拿出來(lái)分享下,有需要的可以直接拿過(guò)去用。下面來(lái)一起看看。2016-08-08
android app判斷是否有系統(tǒng)簽名步驟詳解
這篇文章主要為大家介紹了android app判斷是否有系統(tǒng)簽名步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
Android 自定義View結(jié)合自定義TabLayout實(shí)現(xiàn)頂部標(biāo)簽滑動(dòng)效果
小編最近在做app的項(xiàng)目,需要用到tablayout實(shí)現(xiàn)頂部的滑動(dòng)效果,文中代碼用到了自定義item,代碼也很簡(jiǎn)單,感興趣的朋友跟隨腳本之家小編一起看看吧2018-07-07
RecyclerView實(shí)現(xiàn)列表倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)列表倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
Android實(shí)現(xiàn)圖片浮動(dòng)隨意拖拽效果
這篇文章主要介紹了Android的圖片在界面隨意拖動(dòng)的功能,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧2018-04-04

