Android 面试之必问Java基础
Android 面试之必问Android基础知识
在Android早期的版本中,应用程序的运行环境是需要依赖Dalvik虚拟机的。不过,在后来的版本(大概是4.x版本),Android的运行环境却换到了 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。
不过,Dalvik模式下,开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行效率更高、启动也更快。
下面是AOT编译方式的一些优点:
ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。该实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件,该工具能够顺利编译所有有效的 DEX 文件。
垃圾回收 (GC) 可能有损于应用性能,从而导致显示不稳定、界面响应速度缓慢以及其他问题。ART模式从以下几个方面优化了垃圾回收的策略:
支持采样分析器
一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。支持的版本从KitKat (4.4)版本开始,为 Dalvik 的 Traceview 添加了采样支持。
支持更多调试功能
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 将执行堆压缩。
除了新的垃圾回收器之外,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大体的流程可以分为如下几步:
1、根据需求确定需要 hook 的对象
2、寻找要hook的对象的持有者,拿到需要 hook 的对象
3、定义“要 hook 的对象”的代理类,并且创建该类的对象
4、使用上一步创建出来的对象,替换掉要 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提供的默认混淆配置文件,我们需要的混淆的规则都放在这个文件中。
混淆命令
混淆通配符
:匹配类中的所有字段
:匹配类中所有的方法
:匹配类中所有的构造函数*
: 匹配任意长度字符,不包含包名分隔符(.)**
: 匹配任意长度字符,包含包名分隔符(.)***
: 匹配任意参数类型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 ;
}
# 保留在 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 (android.content.Context);
public (android.content.Context, android.util.AttributeSet);
public (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 ;
!private ;
!private ;
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 ;
}
# @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
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, "", "(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
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})
···
参考: Android NDK开发基础
动态加载技术在Web中很常见,对于Android项目来说,动态加载的目的是让用户不用重新安装APK就能升级应用的功能,主要的应用场景是插件化和热修复。
首先需要明确的一点,插件化和热修复不是同一个概念,虽然站在技术实现的角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;但是二者的出发点是不同的。
插件化,本质上是把需要实现的模块或功能当做一个独立的功能提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。而热修复则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。
为了方便说明,我们先理清几个概念:
关于插件化技术,最早可以追溯到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碎片化的问题,对热修复方案的适配也是一大考验。
参考: Android 热修复的简析
深入探索Android热修复技术原理