共计 17858 个字符,预计需要花费 45 分钟才能阅读完成。
Android 面试之必问 Java 根底
Android 面试之必问 Android 基础知识
1,编译模式
1.1 概念
在 Android 晚期的版本中,应用程序的运行环境是须要依赖 Dalvik 虚拟机的。不过,在起初的版本(大略是 4.x 版本),Android 的运行环境却换到了 Android Runtime,其解决应用程序执行的形式齐全不同于 Dalvik,Dalvik 是依附一个 Just-In-Time (JIT) 编译器去解释字节码。
不过,Dalvik 模式下,开发者编译后的利用代码须要通过一个解释器在用户的设施上运行,这一机制并不高效,但让利用能更容易在不同硬件和架构上运 行。ART 则齐全扭转了这套做法,在利用装置时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行效率更高、启动也更快。
1.2 AOT 长处
上面是 AOT 编译形式的一些长处:
1.2.1 事后编译
ART 引入了事后编译机制,可进步利用的性能。ART 还具备比 Dalvik 更严格的装置时验证。在装置时,ART 应用设施自带的 dex2oat 工具来编译利用。该实用工具承受 DEX 文件作为输出,并为指标设施生成通过编译的利用可执行文件,该工具可能顺利编译所有无效的 DEX 文件。
1.2.2 垃圾回收优化
垃圾回收 (GC) 可能有损于利用性能,从而导致显示不稳固、界面响应速度迟缓以及其余问题。ART 模式从以下几个方面优化了垃圾回收的策略:
- 只有一次(而非两次)GC 暂停
- 在 GC 放弃暂停状态期间并行处理
- 在清理最近调配的短时对象这种非凡状况中,回收器的总 GC 工夫更短
- 优化了垃圾回收的工效,可能更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为常见
- 压缩 GC 以缩小后盾内存应用和碎片
1.2.3 开发和调试方面的优化
反对采样分析器
始终以来,开发者都应用 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 也提供这类信息)。
1.3 垃圾回收
ART 提供了多个不同的 GC 计划,这些计划运行着不同垃圾回收器,默认的 GC 计划是 CMS(并发标记革除),次要应用粘性 CMS 和局部 CMS。粘性 CMS 是 ART 的不挪动分代垃圾回收器。它仅扫描堆中自上次 GC 后批改的局部,并且只能回收自上次 GC 后调配的对象。除 CMS 计划外,当利用将过程状态更改为觉察不到卡顿的过程状态(例如,后盾或缓存)时,ART 将执行堆压缩。
除了新的垃圾回收器之外,ART 还引入了一种基于位图的新内存分配程序,称为 RosAlloc(插槽运行分配器)。此新分配器具备分片锁,当调配规模较小时可增加线程的本地缓冲区,因此性能优于 DlMalloc(内存分配器)。
内存分配器的相干常识能够参考:内存分配器
同时,与 Dalvik 相比,ART 的 CMS 垃圾回收也带来了其余方面的改善,如下:
- 与 Dalvik 相比,暂停次数从 2 次缩小到 1 次。Dalvik 的第一次暂停次要是为了进行根标记,即在 ART 中进行并发标记,让线程标记本人的根,而后马上复原运行。
- 与 Dalvik 相似,ART GC 在革除过程开始之前也会暂停 1 次。两者在这方面的次要差别在于:在此暂停期间,某些 Dalvik 环节在 ART 中并发进行。这些环节包含 java.lang.ref.Reference 解决、零碎弱革除(例如,jni 弱全局等)、从新标记非线程根和卡片预清理。在 ART 暂停期间仍进行的阶段包含扫描脏卡片以及从新标记线程根,这些操作有助于缩短暂停工夫。
- 绝对于 Dalvik,ART GC 改良的最初一个方面是粘性 CMS 回收器减少了 GC 吞吐量。不同于一般的分代 GC,粘性 CMS 不挪动。零碎会将年老对象保留在一个调配堆栈(基本上是 java.lang.Object 数组)中,而非为其设置一个专属区域。这样能够防止挪动所需的对象以维持低暂停次数,但毛病是容易在堆栈中退出大量简单对象图像而使堆栈变长。
ART GC 与 Dalvik 的另一个次要区别在于 ART GC 引入了挪动垃圾回收器。应用挪动 GC 的目标在于通过堆压缩来缩小后盾利用应用的内存。目前,触发堆压缩的事件是 ActivityManager 过程状态的扭转。当利用转到后盾运行时,它会告诉 ART 已进入不再“感知”卡顿的过程状态。此时 ART 会进行一些操作(例如,压缩和监视器压缩),从而导致利用线程长时间暂停。
目前,Android 的 ART 正在应用的两个挪动 GC 是同构空间压缩和半空间压缩,它们的区别如下:
- 半空间压缩:将对象在两个严密排列的碰撞指针空间之间进行挪动。这种挪动 GC 实用于小内存设施,因为它能够比同构空间压缩略微多节俭一点内存,额定节俭出的空间次要来自严密排列的对象,这样能够防止 RosAlloc/DlMalloc 分配器占用开销。
- 同构空间压缩通过将对象从一个 RosAlloc 空间复制到另一个 RosAlloc 空间来实现。这有助于通过缩小堆碎片来缩小内存使用量。这是目前非低内存设施的默认压缩模式。相比半空间压缩,同构空间压缩的次要劣势在于利用从后盾切换到前台时无需进行堆转换。
2,类加载器
2.1 类加载器分类
目前,Android 的类加载器从下到上次要分为 BootstrapClassLoader(根类加载器)、ExtensionClassLoader(扩大类加载器)和 AppClassLoader(利用类加载器)三种。
- 根类加载器:该加载器没有父加载器。它负责加载虚拟机的外围类库,如 java.lang.* 等。例如 java.lang.Object 就是由根类加载器加载的。根类加载器从零碎属性 sun.boot.class.path 所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承 java.lang.ClassLoader 类。
- 扩大类加载器:它的父加载器为根类加载器。它从 java.ext.dirs 零碎属性所指定的目录中加载类库,或者从 JDK 的装置目录的 jre/lib/ext 子目录(扩大目录)下加载类库,如果把用户创立的 JAR 文件放在这个目录下,也会主动由扩大类加载器加载。扩大类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。
- 零碎类加载器 :也称为利用类加载器,它的父加载器为扩大类加载器。它从环境变量 classpath 或者零碎属性 java.class.path 所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。零碎类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。
父子加载器并非继承关系,也就是说子加载器不肯定是继承了父加载器。
2.2 双亲委托模式
所谓双亲委托模式,指的是某个特定的类加载器在接到加载类的申请时,首先将加载工作委托给父类加载器,顺次递归,如果父类加载器能够实现类加载工作,就胜利返回;只有父类加载器无奈实现此加载工作时,才本人去加载。
因为这样能够防止反复加载,当父亲曾经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。如果不应用这种委托模式,那咱们就能够随时应用自定义的类来动静代替一些外围的类,存在十分大的安全隐患。
举个例子,事实上,java.lang.String 这个类并不会被咱们自定义的 classloader 加载,而是由 bootstrap classloader 进行加载,为什么会这样?实际上这就是双亲委托模式的起因,因为在任何一个自定义 ClassLoader 加载一个类之前,它都会先 委托它的父亲 ClassLoader 进行加载,只有当父亲 ClassLoader 无奈加载胜利后,才会由本人加载。
2.3 Android 的类加载器
上面是 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 类加载器双亲委托模式
3,Android Hook
所谓 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 机制
4,代码混同
4.1 Proguard
家喻户晓,Java 代码是非常容易反编译的,为了更好的爱护 Java 源代码,咱们往往会对编译好的 Class 类文件进行混同解决。而 ProGuard 就是一个混同代码的开源我的项目。它的次要作用就是混同,当然它还能对字节码进行缩减体积、优化等,但那些对于咱们来说都算是主要的性能。
具体来说,ProGuard 具备如下性能:
- 压缩(Shrink): 检测和删除没有应用的类,字段,办法和个性。
- 优化(Optimize): 剖析和优化 Java 字节码。
- 混同(Obfuscate): 应用简短的无意义的名称,对类,字段和办法进行重命名。
在 Android 开发中,开启混同须要将 app/build.gradle 文件下的 minifyEnabled 属性设置为 true,如下所示。
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguard-android.txt 是 Android 提供的默认混同配置文件,咱们须要的混同的规定都放在这个文件中。
4.2 混同规定
混同命令
- keep:保留类和类中的成员,避免被混同或移除
- keepnames:保留类和类中的成员,避免被混同,成员没有被援用会被移除
- keepclassmembers:只保留类中的成员,避免被混同或移除
- keepclassmembernames:只保留类中的成员,避免被混同,成员没有援用会被移除
- keepclasseswithmembers:保留类和类中的成员,避免被混同或移除,保留指明的成员
- keepclasseswithmembernames:保留类和类中的成员,避免被混同,保留指明的成员,成员没有援用会被移除
混同通配符
<field>
:匹配类中的所有字段<method>
:匹配类中所有的办法<init>
:匹配类中所有的构造函数*
:匹配任意长度字符,不蕴含包名分隔符(.)**
:匹配任意长度字符,蕴含包名分隔符(.)***
:匹配任意参数类型
keep 的规定的格局如下:
[keep 命令] [类] {[成员]
}
4.3 混同模版
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'
}
···
}
5,NDK
如果要问 Android 的高级开发常识,那么 NDK 必定是必问的。那么什么的 NDK,NDK 全称是 Native Development Kit,是一组能够让开发者在 Android 利用中应用 C /C++ 的工具。通常,NDK 能够用在如下的场景中:
- 从设施获取更好的性能以用于计算密集型利用,例如游戏或物理模仿。
- 重复使用本人或其余开发者的 C/C++ 库,便当于跨平台。
- NDK 集成了譬如 OpenSL、Vulkan 等 API 标准的特定实现,以实现在 Java 层无奈做到的性能,如音视频开发、渲染。
- 减少反编译难度。
5.1, JNI 根底
JNI 即 java native interface,是 Java 和 Native 代码进行交互的接口。
5.1.1 JNI 拜访 Java 对象办法
如果,有如下一个 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;
}
5.2 NDK 开发
5.2.1 根本流程
首先,在 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 文件遵循如下的规定:
- 函数名的格局遵循遵循如下规定:Java_包名_类名_办法名。
- extern “C” 指定采纳 C 语言的命名格调来编译,否则因为 C 与 C++ 格调不同,导致链接时无奈找到具体的函数
- JNIEnv*:示意一个指向 JNI 环境的指针,能够通过他来拜访 JNI 提供的接口办法
- jobject:示意 java 对象中的 this
- JNIEXPORT 和 JNICALL:JNI 所定义的宏,能够在 jni.h 头文件中查找到
System.loadLibrary()的代码位于 java/lang/System.java 文件中,源码如下:
@CallerSensitive
public static void load(String filename) {Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
5.3 CMake 构建 NDK
CMake 是一个开源的跨平台工具系列,旨在构建、测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地应用 CMake 与 Gradle 搭配应用来构建原生库。具体来说,咱们能够应用 Gradle 将 C \ C++ 代码 编译到原生库中,而后将这些代码打包到咱们的利用中,Java 代码随后能够通过 Java 原生接口 (JNI) 调用 咱们原生库中的函数。
应用 CMake 开发 NDK 我的项目须要下载如下一些套件:
- Android 原生开发工具包 (NDK):这套工具集容许咱们 开发 Android 应用 C 和 C++ 代码,并提供泛滥平台库,让咱们能够治理原生 Activity 和拜访物理设施组件,例如传感器和触摸输出。
- CMake:一款内部构建工具,可与 Gradle 搭配应用来构建原生库。如果你只打算应用 ndk-build,则不须要此组件。
- LLDB:一种调试程序,Android Studio 应用它来调试原生代码。
咱们能够关上 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 开发根底
6,动静加载
6.1 基本概念
动静加载技术在 Web 中很常见,对于 Android 我的项目来说,动静加载的目标是让用户不必重新安装 APK 就能降级利用的性能,次要的利用场景是插件化和热修复。
首先须要明确的一点,插件化和热修复不是同一个概念,尽管站在技术实现的角度来说,他们都是从零碎加载器的角度登程,无论是采纳 hook 形式,亦或是代理形式或者是其余底层实现,都是通过“坑骗”Android 零碎的形式来让宿主失常的加载和运行插件(补丁)中的内容;然而二者的出发点是不同的。
插件化,实质上是把须要实现的模块或性能当做一个独立的性能提取进去,缩小宿主的规模,当须要应用到相应的性能时再去加载相应的模块。而热修复则往往是从修复 bug 的角度登程,强调的是在不须要二次装置利用的前提下修复已知的 bug。
为了不便阐明,咱们先理清几个概念:
- 宿主:以后运行的 APP。
- 插件:绝对于插件化技术来说,就是要加载运行的 apk 类文件。
- 补丁:绝对于热修复技术来说,就是要加载运行的.patch,.dex,*.apk 等一系列蕴含 dex 修复内容的文件。
下图展现了 Android 动态化开发框架的整体的架构。
6.2 插件化
对于插件化技术,最早能够追溯到 2012 年的 AndroidDynamicLoader,其原理是动静加载不同的 Fragment 实现 UI 替换,不过随着 15,16 年更好的计划,这个计划慢慢的被淘汰了。再起初有了任玉刚的 dynamic-load-apk 计划,开始有了插件化的规范计划。而前面的计划大多基于 Hook 和动静代理两个方向进行。
目前,插件化的开发并没有一个官网的插件化计划,它是国内提出的一种技术实现,利用虚拟机的类的加载机制实现的一种技术手段,往往须要 hook 一些零碎 api,而 Google 从 Android9.0 开始限度对系统公有 api 的应用,也就造成了插件化的兼容性问题,当初几个风行的插件化技术框架,都是大厂依据本人的需要,开源进去的,如滴滴的 VirtualAPK,360 的 RePlugin 等,大家能够依据须要自行理解技术的实现原理。
6.3 热修复
6.3.1 热修复原理
说到热修复的原理,就不得不提到类的加载机制,和惯例的 JVM 相似,在 Android 中类的加载也是通过 ClassLoader 来实现,具体来说就是 PathClassLoader 和 DexClassLoader 这两个 Android 专用的类加载器,这两个类的区别如下。
- PathClassLoader:只能加载曾经装置到 Android 零碎中的 apk 文件(/data/app 目录),是 Android 默认应用的类加载器。
- DexClassLoader:能够加载任意目录下的 dex/jar/apk/zip 文件,也就是咱们一开始提到的补丁。
这两个类都是继承自 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 须要经验如下两步:
- 在 DexClassLoader 的 findClass 办法中通过一个 DexPathList 对象 findClass()办法来获取 class。
- 在 DexPathList 的 findClass 办法中,对之前结构好 dexElements 数组汇合进行遍历,一旦找到类名与 name 雷同的类时,就间接返回这个 class,找不到则返回 null。
因而,基于下面的实践,咱们能够想到一个最简略的热修复计划。假如当初代码中的某一个类呈现 Bug,那么咱们能够在修复 Bug 之后,将这些个类打包成一个补丁文件,而后通过这个补丁文件封装出一个 Element 对象,并且将这个 Element 对象插到原有 dexElements 数组的最前端。这样,当 DexClassLoader 去加载类时,因为双亲加载机制的特点,就会优先加载插入的这个 Element,而有缺点的 Element 则没有机会再被加载。事实上,QQ 晚期的热修复计划就是这样的。
6.3.2 QQ 空间超级补丁计划
QQ 空间补丁计划就是应用 javaassist 插桩的形式解决了 CLASS_ISPREVERIFIED 的难题。波及的步骤如下:
- 在 apk 装置的时候零碎会将 dex 文件优化成 odex 文件,在优化的过程中会波及一个预校验的过程。
-
如果一个类的 static 办法,private 办法,override 办法以及构造函数中援用了其余类,而且这些类都属于同一个 dex 文件,此时该类就会被打上 CLASS_ISPREVERIFIED。
- 如果在运行时被打上 CLASS_ISPREVERIFIED 的类援用了其余 dex 的类,就会报错。
- 失常的分包计划会保障相干类被打入同一个 dex 文件。
- 想要使得 patch 能够被失常加载,就必须保障类不会被打上 CLASS_ISPREVERIFIED 标记。而要实现这个目标就必须要在分完包后的 class 中植入对其余 dex 文件中类的援用。
6.3.3 Tinker
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
6.3.4 HotFix
以上提到的两种形式,尽管策略有所不同,但总的来说都是从下层 ClassLoader 的角度登程,因为 ClassLoader 的特点,如果想要新的补丁文件再次失效,无论你是插桩还是提前合并,都须要重新启动利用来加载新的 DexPathList,从而实现 Bug 的修复。
AndFix 提供了一种运行时在 Native 批改 Filed 指针的形式,实现办法的替换,达到即时失效无需重启,对利用无性能耗费的目标。不过,因为 Android 在国内变成了安卓,各大手机厂商定制了本人的 ROM,所以很多底层实现的差别,导致 AndFix 的兼容性并不是很好。
6.3.5 Sophix
Sophix 采纳的是相似类修复反射注入形式, 把补丁 so 库的门路插入到 nativeLibraryDirectories 数组的最后面, 这样加载 so 库的时候就是补丁 so 库而不是原来的 so 库。
在修复类代码的缺点时,Sophix 对旧包与补丁包中 classes.dex 的程序进行了突破与重组,使得零碎能够天然地辨认到这个程序, 以实现类笼罩的目标。
在修复资源的缺点时,Sophix 结构了一个 package id 为 0x66 的资源包,这个包里只蕴含扭转了的资源项,而后间接在原有 AssetManager 中 addAssetPath 这个包即可,无需变更 AssetManager 对象的援用。
除了这些计划外,热修复计划还有美团的 Robust、饿了吗的 Amigo 等。不过,对于 Android 的热修复来说,很难有一种非常完满的解决方案。比方,在 Android 开发中,四大组件应用前须要在 AndroidManifest 中提前申明,而如果须要应用热修复的形式,无论是提前占坑亦或是动静批改,都会带来很强的侵入性。同时,Android 碎片化的问题,对热修复计划的适配也是一大考验。
参考:Android 热修复的简析
深刻摸索 Android 热修复技术原理