Android 面试之必问Java基础
Android 面试之必问Android基础知识javascript
在Android早期的版本中,应用程序的运行环境是须要依赖Dalvik虚拟机的。不过,在后来的版本(大概是4.x版本),Android的运行环境却换到了 Android Runtime,其处理应用程序执行的方式彻底不一样于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。前端
不过,Dalvik模式下,开发者编译后的应用代码须要经过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不一样硬件和架构上运 行。ART 则彻底改变了这套作法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行效率更高、启动也更快。java
下面是AOT编译方式的一些优势:android
ART 引入了预先编译机制,可提升应用的性能。ART 还具备比 Dalvik 更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。该实用工具接受 DEX 文件做为输入,并为目标设备生成通过编译的应用可执行文件,该工具可以顺利编译全部有效的 DEX 文件。git
垃圾回收 (GC) 可能有损于应用性能,从而致使显示不稳定、界面响应速度缓慢以及其余问题。ART模式从如下几个方面优化了垃圾回收的策略:github
支持采样分析器
一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行状况)做为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会致使 Dalvik 分析结果出现误差,并且使用该工具明显会影响运行时性能ART 添加了对没有这些限制的专用采样分析器的支持,于是可更准确地了解应用执行状况,而不会明显减慢速度。支持的版本从KitKat (4.4)版本开始,为 Dalvik 的 Traceview 添加了采样支持。web
支持更多调试功能
ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,查看堆栈跟踪中保留了哪些锁,而后跳转到持有锁的线程;询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考;过滤特定实例的事件(如断点)等。面试
优化了异常和崩溃报告中的诊断详细信息
当发生运行时异常时,ART 会为您提供尽量多的上下文和详细信息。ART 会提供 java.lang.ClassCastException、java.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多异常详细信息(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多异常详细信息,这些信息如今包括数组大小和越界偏移量;ART 也提供这类信息)。算法
ART 提供了多个不一样的 GC 方案,这些方案运行着不一样垃圾回收器,默认的GC方案是 CMS(并发标记清除),主要使用粘性 CMS 和部分 CMS。粘性 CMS 是 ART 的不移动分代垃圾回收器。它仅扫描堆中自上次 GC 后修改的部分,而且只能回收自上次 GC 后分配的对象。除 CMS 方案外,当应用将进程状态更改成察觉不到卡顿的进程状态(例如,后台或缓存)时,ART 将执行堆压缩。bootstrap
除了新的垃圾回收器以外,ART 还引入了一种基于位图的新内存分配程序,称为 RosAlloc(插槽运行分配器)。此新分配器具备分片锁,当分配规模较小时可添加线程的本地缓冲区,于是性能优于 DlMalloc(内存分配器)。
内存分配器的相关知识能够参考:内存分配器
同时,与 Dalvik 相比,ART的 CMS垃圾回收也带来了其余方面的改善,以下:
ART GC 与 Dalvik 的另外一个主要区别在于 ART GC 引入了移动垃圾回收器。使用移动 GC 的目的在于经过堆压缩来减小后台应用使用的内存。目前,触发堆压缩的事件是 ActivityManager 进程状态的改变。当应用转到后台运行时,它会通知 ART 已进入再也不“感知”卡顿的进程状态。此时 ART 会进行一些操做(例如,压缩和监视器压缩),从而致使应用线程长时间暂停。
目前,Android的ART正在使用的两个移动 GC 是同构空间压缩和半空间压缩,它们的区别以下:
目前,Android的类加载器从下到上主要分为BootstrapClassLoader(根类加载器)、 ExtensionClassLoader (扩展类加载器)和 AppClassLoader(应用类加载器)三种。
所谓双亲委托模式,指的是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成此加载任务时,才本身去加载。
由于这样能够避免重复加载,当父亲已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。若是不使用这种委托模式,那咱们就能够随时使用自定义的类来动态替代一些核心的类,存在很是大的安全隐患。
举个例子,事实上,java.lang.String这个类并不会被咱们自定义的classloader加载,而是由bootstrap classloader进行加载,为何会这样?实际上这就是双亲委托模式的缘由,由于在任何一个自定义ClassLoader加载一个类以前,它都会先 委托它的父亲ClassLoader进行加载,只有当父亲ClassLoader没法加载成功后,才会由本身加载。
下面是Android类加载器的模型图:
下面看一下DexClassLoader,DexClassLoader 重载了 findClass 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造 DexClassLoader 时生成的,其内部包含了 DexFile,涉及的源码以下。
··· public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; } ···
类加载器更多的内容,能够参考:android 类加载器双亲委托模式
所谓Hook,就是在程序执行的过程当中去截取其中的某段信息,示意图以下。
Android的Hook大致的流程能够分为以下几步:
一、根据需求肯定须要 hook 的对象
二、寻找要hook的对象的持有者,拿到须要 hook 的对象
三、定义“要 hook 的对象”的代理类,而且建立该类的对象
四、使用上一步建立出来的对象,替换掉要 hook 的对象
下面是一段简单的Hook的示例代码,用到了Java的反射机制。
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"}) public static void hook(Context context, final View view) {// try { // 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者 Method method = View.class.getDeclaredMethod("getListenerInfo"); method.setAccessible(true);//因为getListenerInfo()方法并非public的,因此要加这个代码来保证访问权限 Object mListenerInfo = method.invoke(view);//这里拿到的就是mListenerInfo对象,也就是点击事件的持有者 // 要从这里面拿到当前的点击事件对象 Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 这是内部类的表示方法 Field field = listenerInfoClz.getDeclaredField("mOnClickListener"); final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真实的mOnClickListener对象 // 2. 建立咱们本身的点击事件代理类 // 方式1:本身建立代理类 // ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance); // 方式2:因为View.OnClickListener是一个接口,因此能够直接用动态代理模式 // Proxy.newProxyInstance的3个参数依次分别是: // 本地的类加载器; // 代理类的对象所继承的接口(用Class数组表示,支持多个接口) // 代理类的实际逻辑,封装在new出来的InvocationHandler内 Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入本身的逻辑 return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑 } }); // 3. 用咱们本身的点击事件代理类,设置到"持有者"中 field.set(mListenerInfo, proxyOnClickListener); } catch (Exception e) { e.printStackTrace(); } } // 自定义代理类 static class ProxyOnClickListener implements View.OnClickListener { View.OnClickListener oriLis; public ProxyOnClickListener(View.OnClickListener oriLis) { this.oriLis = oriLis; } @Override public void onClick(View v) { Log.d("HookSetOnClickListener", "点击事件被hook到了"); if (oriLis != null) { oriLis.onClick(v); } } }
而在Android开发中,想要实现Hook,确定是没有这么简单的,咱们须要借助一些Hook框架,好比Xposed、Cydia Substrate、Legend等。
参考资料:Android Hook机制
众所周知,Java代码是很是容易反编译的,为了更好的保护Java源代码,咱们每每会对编译好的Class类文件进行混淆处理。而ProGuard就是一个混淆代码的开源项目。它的主要做用就是混淆,固然它还能对字节码进行缩减体积、优化等,但那些对于咱们来讲都算是次要的功能。
具体来讲,ProGuard具备以下功能:
在Android开发中,开启混淆须要将app/build.gradle文件下的minifyEnabled属性设置为true,以下所示。
minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguard-android.txt是Android提供的默认混淆配置文件,咱们须要的混淆的规则都放在这个文件中。
混淆命令
混淆通配符
<field>
:匹配类中的全部字段<method>
:匹配类中全部的方法<init>
:匹配类中全部的构造函数*
: 匹配任意长度字符,不包含包名分隔符(.)**
: 匹配任意长度字符,包含包名分隔符(.)***
: 匹配任意参数类型keep的规则的格式以下:
[keep命令] [类] { [成员] }
ProGuard中有些公共的模版是能够复用的,好比压缩比、大小写混合和一些系统提供的Activity、Service不能混淆等。
# 代码混淆压缩比,在 0~7 之间,默认为 5,通常不作修改 -optimizationpasses 5 # 混合时不使用大小写混合,混合后的类名为小写 -dontusemixedcaseclassnames # 指定不去忽略非公共库的类 -dontskipnonpubliclibraryclasses # 这句话可以使咱们的项目混淆后产生映射文件 # 包含有类名->混淆后类名的映射关系 -verbose # 指定不去忽略非公共库的类成员 -dontskipnonpubliclibraryclassmembers # 不作预校验,preverify 是 proguard 的四个步骤之一,Android 不须要 preverify,去掉这一步可以加快混淆速度。 -dontpreverify # 保留 Annotation 不混淆 -keepattributes *Annotation*,InnerClasses # 避免混淆泛型 -keepattributes Signature # 抛出异常时保留代码行号 -keepattributes SourceFile,LineNumberTable # 指定混淆是采用的算法,后面的参数是一个过滤器 # 这个过滤器是谷歌推荐的算法,通常不作更改 -optimizations !code/simplification/cast,!field/*,!class/merging/* ############################################# # # Android开发中一些须要保留的公共部分 # ############################################# # 保留咱们使用的四大组件,自定义的 Application 等等这些类不被混淆 # 由于这些子类都有可能被外部调用 -keep public class * extends android.app.Activity -keep public class * extends android.app.Appliction -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference -keep public class * extends android.view.View -keep public class com.android.vending.licensing.ILicensingService # 保留 support 下的全部类及其内部类 -keep class android.support.** { *; } # 保留继承的 -keep public class * extends android.support.v4.** -keep public class * extends android.support.v7.** -keep public class * extends android.support.annotation.** # 保留 R 下面的资源 -keep class **.R$* { *; } # 保留本地 native 方法不被混淆 -keepclasseswithmembernames class * { native <methods>; } # 保留在 Activity 中的方法参数是view的方法, # 这样以来咱们在 layout 中写的 onClick 就不会被影响 -keepclassmembers class * extends android.app.Activity { public void *(android.view.View); } # 保留枚举类不被混淆 -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } # 保留咱们自定义控件(继承自 View)不被混淆 -keep public class * extends android.view.View { *** get*(); void set*(***); public <init>(android.content.Context); public <init>(android.content.Context, android.util.AttributeSet); public <init>(android.content.Context, android.util.AttributeSet, int); } # 保留 Parcelable 序列化类不被混淆 -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; } # 保留 Serializable 序列化的类不被混淆 -keepnames class * implements java.io.Serializable -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; !static !transient <fields>; !private <fields>; !private <methods>; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } # 对于带有回调函数的 onXXEvent、**On*Listener 的,不能被混淆 -keepclassmembers class * { void *(**On*Event); void *(**On*Listener); } # webView 处理,项目中没有使用到 webView 忽略便可 -keepclassmembers class fqcn.of.javascript.interface.for.webview { public *; } -keepclassmembers class * extends android.webkit.webViewClient { public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); public boolean *(android.webkit.WebView, java.lang.String); } -keepclassmembers class * extends android.webkit.webViewClient { public void *(android.webkit.webView, java.lang.String); } # js -keepattributes JavascriptInterface -keep class android.webkit.JavascriptInterface { *; } -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } # @Keep -keep,allowobfuscation @interface android.support.annotation.Keep -keep @android.support.annotation.Keep class * -keepclassmembers class * { @android.support.annotation.Keep *; }
若是是aar这种插件,能够在aar的build.gralde中添加以下混淆配置。
android { ··· defaultConfig { ··· consumerProguardFile 'proguard-rules.pro' } ··· }
若是要问Android的高级开发知识,那么NDK确定是必问的。那么什么的NDK,NDK 全称是 Native Development Kit,是一组可让开发者在 Android 应用中使用C/C++ 的工具。一般,NDK能够用在以下的场景中:
JNI即java native interface,是Java和Native代码进行交互的接口。
假如,有以下一个Java类,代码以下。
package com.xzh.jni; public class MyJob { public static String JOB_STRING = "my_job"; private int jobId; public MyJob(int jobId) { this.jobId = jobId; } public int getJobId() { return jobId; } }
而后,在cpp目录下,新建native_lib.cpp,添加对应的native实现。
#include <jni.h> extern "C" JNIEXPORT jint JNICALL Java_com_xzh_jni_MainActivity_getJobId(JNIEnv *env, jobject thiz, jobject job) { // 根据实例获取 class 对象 jclass jobClz = env->GetObjectClass(job); // 根据类名获取 class 对象 jclass jobClz = env->FindClass("com/xzh/jni/MyJob"); // 获取属性 id jfieldID fieldId = env->GetFieldID(jobClz, "jobId", "I"); // 获取静态属性 id jfieldID sFieldId = env->GetStaticFieldID(jobClz, "JOB_STRING", "Ljava/lang/String;"); // 获取方法 id jmethodID methodId = env->GetMethodID(jobClz, "getJobId", "()I"); // 获取构造方法 id jmethodID initMethodId = env->GetMethodID(jobClz, "<init>", "(I)V"); // 根据对象属性 id 获取该属性值 jint id = env->GetIntField(job, fieldId); // 根据对象方法 id 调用该方法 jint id = env->CallIntMethod(job, methodId); // 建立新的对象 jobject newJob = env->NewObject(jobClz, initMethodId, 10); return id; }
首先,在 Java代码中声明 Native 方法,以下所示。
public class MainActivity extends AppCompatActivity { // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("MainActivity", stringFromJNI()); } private native String stringFromJNI(); }
而后,新建一个 cpp 目录,而且新建一个名为native-lib.cpp的cpp 文件,实现相关方法。
#include <jni.h> extern "C" JNIEXPORT jstring JNICALL Java_com_xzh_jni_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
cpp文件遵循以下的规则:
System.loadLibrary()的代码位于java/lang/System.java文件中,源码以下:
@CallerSensitive public static void load(String filename) { Runtime.getRuntime().load0(Reflection.getCallerClass(), filename); }
CMake 是一个开源的跨平台工具系列,旨在构建、测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地使用 CMake 与 Gradle 搭配使用来构建原生库。具体来讲,咱们可使用 Gradle 将 C \ C++ 代码 编译到原生库中,而后将这些代码打包到咱们的应用中, Java 代码随后能够经过 Java 原生接口 ( JNI ) 调用 咱们原生库中的函数。
使用CMake开发NDK项目须要下载以下一些套件:
咱们能够打开Android Studio,依次选择 【Tools】 > 【Android】> 【SDK Manager】> 【SDK Tools】选中LLDB、CMake 和 NDK便可。
启用CMake还须要在 app/build.gradle 中添加以下代码。
android { ··· defaultConfig { ··· externalNativeBuild { cmake { cppFlags "" } } ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } } ··· externalNativeBuild { cmake { path "CMakeLists.txt" } } }
而后,在对应目录新建一个 CMakeLists.txt 文件,添加代码。
# 定义了所需 CMake 的最低版本 cmake_minimum_required(VERSION 3.4.1) # add_library() 命令用来添加库 # native-lib 对应着生成的库的名字 # SHARED 表明为分享库 # src/main/cpp/native-lib.cpp 则是指明了源文件的路径。 add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/native-lib.cpp) # find_library 命令添加到 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。 # 可使用此变量在构建脚本的其余部分引用 NDK 库 find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log) # 预构建的 NDK 库已经存在于 Android 平台上,所以,无需再构建或将其打包到 APK 中。 # 因为 NDK 库已是 CMake 搜索路径的一部分,只须要向 CMake 提供但愿使用的库的名称,并将其关联到本身的原生库中 # 要将预构建库关联到本身的原生库 target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib}) ···
动态加载技术在Web中很常见,对于Android项目来讲,动态加载的目的是让用户不用从新安装APK就能升级应用的功能,主要的应用场景是插件化和热修复。
首先须要明确的一点,插件化和热修复不是同一个概念,虽然站在技术实现的角度来讲,他们都是从系统加载器的角度出发,不管是采用hook方式,亦或是代理方式或者是其余底层实现,都是经过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;可是两者的出发点是不一样的。
插件化,本质上是把须要实现的模块或功能当作一个独立的功能提取出来,减小宿主的规模,当须要使用到相应的功能时再去加载相应的模块。而热修复则每每是从修复bug的角度出发,强调的是在不须要二次安装应用的前提下修复已知的bug。
为了方便说明,咱们先理清几个概念:
下图展现了Android动态化开发框架的总体的架构。
关于插件化技术,最先能够追溯到2012年的 AndroidDynamicLoader ,其原理是动态加载不一样的Fragment实现UI替换,不过随着15,16年更好的方案,这个方案渐渐的被淘汰了。再后来有了任玉刚的dynamic-load-apk方案,开始有了插件化的标准方案。然后面的方案大多基于Hook和动态代理两个方向进行。
目前,插件化的开发并无一个官方的插件化方案,它是国内提出的一种技术实现,利用虚拟机的类的加载机制实现的一种技术手段,每每须要hook一些系统api,而Google从Android9.0开始限制对系统私有api的使用,也就形成了插件化的兼容性问题,如今几个流行的插件化技术框架,都是大厂根据本身的需求,开源出来的,如滴滴的VirtualAPK,360的RePlugin等,你们能够根据须要自行了解技术的实现原理。
说到热修复的原理,就不得不提到类的加载机制,和常规的JVM相似,在Android中类的加载也是经过ClassLoader来完成,具体来讲就是PathClassLoader 和 DexClassLoader 这两个Android专用的类加载器,这两个类的区别以下。
这两个类都是继承自BaseDexClassLoader,BaseDexClassLoader的构造函数以下。
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
这个构造函数只作了一件事,就是经过传递进来的相关参数,初始化了一个DexPathList对象。DexPathList的构造函数,就是将参数中传递进来的程序文件(就是补丁文件)封装成Element对象,并将这些对象添加到一个Element的数组集合dexElements中去。
前面说过类加载器的做用,就是将一个具体的类(class)加载到内存中,而这些操做是由虚拟机完成的,对于开发者来讲,只须要关注如何去找到这个须要加载的类便可,这也是热修复须要干的事情。
在Android中,查找一个名为name的class须要经历以下两步:
所以,基于上面的理论,咱们能够想到一个最简单的热修复方案。假设如今代码中的某一个类出现Bug,那么咱们能够在修复Bug以后,将这些个类打包成一个补丁文件,而后经过这个补丁文件封装出一个Element对象,而且将这个Element对象插到原有dexElements数组的最前端。这样,当DexClassLoader去加载类时,因为双亲加载机制的特色,就会优先加载插入的这个Element,而有缺陷的Element则没有机会再被加载。事实上,QQ早期的热修复方案就是这样的。
QQ 空间补丁方案就是使用javaassist 插桩的方式解决了CLASS_ISPREVERIFIED的难题。涉及的步骤以下:
若是一个类的static方法,private方法,override方法以及构造函数中引用了其余类,并且这些类都属于同一个dex文件,此时该类就会被打上CLASS_ISPREVERIFIED。
QQ空间超级补丁方案在遇到补丁文件很大的时候耗时是很是严重的,由于一个大文件夹加载到内存中构建一个Element对象时,插入到数组最前端是须要耗费时间的,而这很是影响应用的启动速度。基于这些问题,微信提出了Tinker 方案。
Tinker的思路是,经过修复好的class.dex 和原有的class.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 总体替换原有的dexPathList的中的内容,进而从根本上修复Bug,下图是演示图。
相比QQ空间超级补丁方案,Tinker 提供的思路能够说效率更高。对Tinker热修复方案感兴趣的同窗能够去看看Tinker 源码分析之DexDiff / DexPatch
以上提到的两种方式,虽然策略有所不一样,但总的来讲都是从上层ClassLoader的角度出发,因为ClassLoader的特色,若是想要新的补丁文件再次生效,不管你是插桩仍是提早合并,都须要从新启动应用来加载新的DexPathList,从而实现Bug的修复。
AndFix 提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。不过,因为Android在国内变成了安卓,各大手机厂商定制了本身的ROM,因此不少底层实现的差别,致使AndFix的兼容性并非很好。
Sophix采用的是相似类修复反射注入方式,把补丁so库的路径插入到nativeLibraryDirectories数组的最前面, 这样加载so库的时候就是补丁so库而不是原来的so库。
在修复类代码的缺陷时,Sophix对旧包与补丁包中classes.dex的顺序进行了打破与重组,使得系统能够天然地识别到这个顺序,以实现类覆盖的目的。
在修复资源的缺陷时,Sophix构造了一个package id 为 0x66 的资源包,这个包里只包含改变了的资源项,而后直接在原有AssetManager中addAssetPath这个包便可,无需变动AssetManager对象的引用。
除了这些方案外,热修复方案还有美团的Robust、饿了吗的Amigo等。不过,对于Android的热修复来讲,很难有一种十分完美的解决方案。好比,在Android开发中,四大组件使用前须要在AndroidManifest中提早声明,而若是须要使用热修复的方式,不管是提早占坑亦或是动态修改,都会带来很强的侵入性。同时,Android碎片化的问题,对热修复方案的适配也是一大考验。