6

Android interview must ask Java basics
Android interviews must ask Android basic knowledge

1. Compile mode

1.1 Concept

In the early version of Android, the running environment of the application depends on the Dalvik virtual machine. However, in later versions (probably version 4.x), Android's operating environment has been changed to Android Runtime, which handles application execution in a completely different way from Dalvik. Dalvik relies on a Just-In-Time (JIT) compilation Interpreter to interpret the bytecode.

However, in the Dalvik mode, the application code compiled by the developer needs to be run on the user's device through an interpreter. This mechanism is not efficient, but it makes it easier for the application to run on different hardware and architectures. ART completely changed this practice. The bytecode is pre-compiled into machine language when the application is installed. This mechanism is called Ahead-Of-Time (AOT) compilation. After the process of removing the interpreted code, the application executes more efficiently and starts faster.

1.2 AOT advantages

Here are some advantages of the AOT compilation method:

1.2.1 Pre-compilation

ART introduces a pre-compilation mechanism to improve application performance. ART also has stricter installation time verification than Dalvik. During installation, ART uses the dex2oat tool that comes with the device to compile the application. The utility accepts DEX files as input and generates compiled application executable files for the target device. The tool can successfully compile all valid DEX files.

1.2.2 Garbage collection optimization

Garbage collection (GC) may detract from application performance, resulting in unstable display, slow interface response speed, and other problems. The ART model optimizes the garbage collection strategy from the following aspects:

  • Only once (not twice) GC pause
  • Parallel processing while the GC remains paused
  • In the special case of cleaning up recently allocated short-term objects, the total GC time of the collector is shorter
  • The ergonomics of garbage collection is optimized, and parallel garbage collection can be performed more timely, which makes GC_FOR_ALLOC event extremely rare in typical use cases
  • Compress GC to reduce background memory usage and fragmentation

1.2.3 Optimization in development and debugging

supports sampling analyzer
For a long time, developers have used the Traceview tool (used to track application execution) as an analyzer. Although Traceview can provide useful information, the overhead incurred by each method call will cause deviations in Dalvik analysis results, and the use of this tool will obviously affect runtime performance. ART adds support for a dedicated sampling analyzer that does not have these limitations. Can understand the application execution more accurately without slowing down significantly. The supported version starts from KitKat (4.4), which adds sampling support for Dalvik's Traceview.

supports more debugging functions
ART supports many new debugging options, especially functions related to monitoring and garbage collection. For example, check which locks are held in the stack trace, and then jump to the thread holding the lock; ask for the number of currently active instances of a specified class, request to view instances, and view references to keep objects in a valid state; filter events for specific instances (Such as breakpoints) and so on.

Optimized the diagnostic details in exception and crash reports
When a runtime exception occurs, ART will provide you with as much context and detailed information as possible. ART will provide more exception details for java.lang.ClassCastException, java.lang.ClassNotFoundException and java.lang.NullPointerException (a higher version of Dalvik will provide more exception details for java.lang.ArrayIndexOutOfBoundsException and java.lang.ArrayStoreException Information, which now includes array size and out-of-bounds offset; ART also provides this type of information).

1.3 Garbage collection

ART provides a number of different GC schemes, which run different garbage collectors. The default GC scheme is CMS (Concurrent Mark Sweep), which mainly uses sticky CMS and some CMS. Sticky CMS is ART's non-moving generational garbage collector. It only scans the part of the heap that has been modified since the last GC, and can only reclaim objects allocated since the last GC. In addition to the CMS scheme, when the application changes the process state to a process state that is not noticeable (for example, background or cache), ART will perform heap compression.

In addition to the new garbage collector, ART also introduced a new bitmap-based memory allocation program called RosAlloc (slot running allocator). This new allocator has a shard lock and can add a thread's local buffer when the allocation scale is small, so the performance is better than DlMalloc (memory allocator).

Related knowledge of memory allocator can refer to: memory allocator

At the same time, compared with Dalvik, ART's CMS garbage collection also brings other improvements, as follows:

  • Compared with Dalvik, the number of pauses has been reduced from 2 to 1. Dalvik's first pause is mainly for root marking, that is, concurrent marking in ART, allowing threads to mark their own roots, and then immediately resume operation.
  • Similar to Dalvik, ART GC will pause once before the purge process starts. The main difference between the two in this regard is that during this pause, some Dalvik links are performed concurrently in ART. These links include java.lang.ref.Reference processing, system weak cleaning (for example, jni weak global, etc.), re-marking non-thread roots and card pre-cleaning. The phases that are still going on during the ART pause include scanning dirty cards and remarking the thread roots, which can help reduce the pause time.
  • Compared to Dalvik, the last aspect of ART GC improvement is that the sticky CMS collector increases GC throughput. Unlike ordinary generational GC, sticky CMS does not move. The system saves young objects in an allocation stack (basically a java.lang.Object array) instead of setting up a dedicated area for them. This can avoid moving the required objects to maintain a low number of pauses, but the disadvantage is that it is easy to add a large number of complex object images to the stack to make the stack longer.

Another major difference between ART GC and Dalvik is that ART GC introduces a mobile garbage collector. The purpose of using mobile GC is to reduce the memory used by background applications through heap compression. Currently, the event that triggers heap compression is a change in the state of the ActivityManager process. When the application goes to the background to run, it will notify ART that it has entered a process state where it no longer "perceives" stalls. At this time, ART will perform some operations (for example, compression and monitor compression), which will cause the application thread to pause for a long time.

Currently, the two mobile GCs currently being used by Android's ART are isomorphic space compression and half space compression. The differences between them are as follows:

  • half-space compression : Move the object between two closely spaced collision pointer spaces. This kind of mobile GC is suitable for small memory devices, because it can save a little more memory than isomorphic space compression. The additional space saved is mainly from tightly arranged objects, which can avoid the overhead of RosAlloc/DlMalloc allocator.
  • Isomorphic space compression is achieved by copying objects from one RosAlloc space to another RosAlloc space. This helps reduce memory usage by reducing heap fragmentation. This is currently the default compression mode for non-low memory devices. Compared with half-space compression, the main advantage of isomorphic space compression is that no heap conversion is required when the application switches from the background to the foreground.

2. Class loader

2.1 Classification of class loaders

At present, Android class loaders are mainly divided into BootstrapClassLoader (root class loader), ExtensionClassLoader (extended class loader) and AppClassLoader (application class loader) from bottom to top.

  • root class loader : This loader has no parent loader. It is responsible for loading the core class libraries of the virtual machine, such as java.lang.* etc. For example, java.lang.Object is loaded by the root class loader. The root class loader loads the class library from the directory specified by the system property sun.boot.class.path. The implementation of the root class loader depends on the underlying operating system and is part of the implementation of the virtual machine. It does not inherit the java.lang.ClassLoader class.
  • extended class loader : Its parent loader is the root class loader. It loads the class library from the directory specified by the java.ext.dirs system property, or loads the class library from the jre/lib/ext subdirectory (extension directory) of the JDK installation directory. If you put the JAR file created by the user in This directory will also be automatically loaded by the extension class loader. The extended class loader is a pure Java class, a subclass of the java.lang.ClassLoader class.
  • system class loader : also known as application class loader, its parent loader is an extended class loader. It loads classes from the directory specified by the environment variable classpath or the system property java.class.path. It is the default parent loader of the user-defined class loader. The system class loader is a pure Java class, a subclass of the java.lang.ClassLoader class.

The parent-child loader is not an inheritance relationship, which means that the child loader does not necessarily inherit the parent loader.

2.2 Parental delegation model

The so-called parent delegation mode refers to that when a specific class loader receives a request to load a class, it first delegates the loading task to the parent class loader, and then recursively. If the parent class loader can complete the class loading task, then Successfully returned; only when the parent class loader cannot complete the loading task, it loads by itself.

Because this can avoid repeated loading, when the father has already loaded the class, there is no need for the child ClassLoader to load it again. If this delegation model is not used, then we can use custom classes to dynamically replace some core classes at any time, which poses a very big security risk.

For example, in fact, the java.lang.String class is not loaded by our custom classloader, but is loaded by the bootstrap classloader. Why is this happening? In fact, this is the reason for the parent delegation mode, because before any custom ClassLoader loads a class, it will first entrust its parent ClassLoader to load it, and only when the parent ClassLoader fails to load successfully, will it be loaded by itself.

2.3 Android's class loader

The following is the model diagram of the Android class loader:
在这里插入图片描述
Let's take a look at DexClassLoader. DexClassLoader overloads the findClass method, and will call its internal DexPathList to load when the class is loaded. DexPathList is generated when DexClassLoader is constructed, and DexFile is included inside. The source code involved is as follows.

···
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;
}
···

For more content of class loader, please refer to: android class loader parent delegation mode

3,Android Hook

The so-called Hook is to intercept a certain piece of information in the process of program execution. The schematic diagram is as follows.
说到

The general process of Android Hook can be divided into the following steps:
1. Determine the objects that need to be hooked according to the needs
2. Find the holder of the object to be hooked, and get the object that needs to be hooked
3. Define the proxy class of the "object to hook" and create an object of this class
4. Use the object created in the previous step to replace the object to be hooked

The following is a simple Hook sample code, using Java's reflection mechanism.

@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);
        }
    }
}

In Android development, it is certainly not so simple to implement Hook. We need to use some Hook frameworks, such as Xposed, Cydia Substrate, Legend, etc.

Reference materials: Android Hook mechanism

4. Code obfuscation

4.1 Proguard

As we all know, Java code is very easy to decompile. In order to better protect the Java source code, we tend to obfuscate the compiled Class files. ProGuard is an open source project that obfuscates the code. Its main function is to obfuscate. Of course, it can also reduce the size and optimize the bytecode, but those are regarded as secondary functions for us.

Specifically, ProGuard has the following functions:

  • Shrink: Detect and delete unused classes, fields, methods and features.
  • Optimize: Analyze and optimize Java bytecode.
  • Obfuscate: Use short, meaningless names to rename classes, fields, and methods.

In Android development, to enable obfuscation, you need to set the minifyEnabled property under the app/build.gradle file to true, as shown below.

minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

proguard-android.txt is the default obfuscation configuration file provided by Android, and all the obfuscation rules we need are placed in this file.

4.2 Confusion rules

obfuscation command

  • keep: keep the class and the members in the class to prevent being confused or removed
  • keepnames: keep the class and the members in the class to prevent being confused, the members will be removed if they are not referenced
  • keepclassmembers: only keep members in the class to prevent confusion or removal
  • keepclassmembernames: only keep members in the class to prevent confusion, members will be removed without references
  • keepclasseswithmembers: keep the class and members in the class, prevent being confused or removed, keep the specified members
  • keepclasseswithmembernames: keep the class and the members in the class to prevent confusion, keep the specified members, and the members will be removed if they are not referenced

Confusion wildcard

  • <field> : match all fields in the class
  • <method> : Match all methods in the class
  • <init> : Match all constructors in the class
  • * : Match characters of any length, excluding the package name separator (.)
  • ** : Match characters of any length, including the package name separator (.)
  • *** : match any parameter type

The format of the keep rule is as follows:

[keep命令] [类] {
        [成员]
}

4.3 Confusion template

Some public templates in ProGuard can be reused, such as compression ratio, mixed case, and some activities and services provided by the system cannot be confused.

# 代码混淆压缩比,在 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 *;
}

If it is a plug-in such as aar, you can add the following obfuscation configuration in aar's build.gralde.

android {
    ···
    defaultConfig {
        ···
        consumerProguardFile 'proguard-rules.pro'
    }
    ···
}

5,NDK

If you want to ask about the advanced development knowledge of Android, then the NDK is definitely a must. So what kind of NDK, the full name of NDK is Native Development Kit, which is a set of tools that allows developers to use C/C++ in Android applications. Generally, NDK can be used in the following scenarios:

  • Get better performance from the device for computationally intensive applications, such as games or physics simulations.
  • Reuse your own or other developers' C/C++ libraries to facilitate cross-platform.
  • NDK integrates specific implementations of API specifications such as OpenSL and Vulkan to achieve functions that cannot be achieved in the Java layer, such as audio and video development and rendering.
  • Increase the difficulty of decompilation.

5.1, JNI basics

JNI stands for java native interface, which is the interface for interaction between Java and Native code.

5.1.1 JNI access to Java object methods

If there is a Java class as follows, the code is as follows.

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;
    }
}

Then, in the cpp directory, create a new native_lib.cpp and add the corresponding native implementation.

#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 development

5.2.1 Basic process

First, declare the Native method in the Java code as shown below.

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();
}

Then, create a new cpp directory and create a new cpp file named native-lib.cpp to implement related methods.

#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());
}

The cpp file follows the following rules:

  • The format of the function name follows the following rules: Java_package name_class name_method name.
  • extern "C" specifies the naming style of C language to compile, otherwise the specific function cannot be found during linking due to the different styles of C and C++
  • JNIEnv*: Represents a pointer to the JNI environment, through which you can access the interface methods provided by JNI
  • jobject: Represents this in the java object
  • JNIEXPORT and JNICALL: The macros defined by JNI can be found in the jni.h header file

The code of System.loadLibrary() is located in the java/lang/System.java file. The source code is as follows:

@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

5.3 CMake build NDK

CMake is an open source cross-platform tool series designed to build, test, and package software. Starting from Android Studio 2.2, Android Sudio uses CMake and Gradle to build native libraries by default. Specifically, we can use Gradle to compile the C\C++ code into the native library, and then package these codes into our application. The Java code can then call the functions in our native library through the Java Native Interface (JNI).

To use CMake to develop NDK projects, you need to download the following packages:

  • Android Native Development Kit (NDK): This tool set allows us to develop Android code using C and C++, and provides numerous platform libraries that allow us to manage native Activity and access physical device components, such as sensors and touch input.
  • CMake: An external build tool that can be used with Gradle to build native libraries. If you only plan to use ndk-build, you don't need this component.
  • LLDB: A debugging program that Android Studio uses to debug native code.

We can open Android Studio and select [Tools]> [Android]> [SDK Manager]> [SDK Tools] and select LLDB, CMake and NDK.

To enable CMake, you also need to add the following code in app/build.gradle.

android {
    ···
    defaultConfig {
        ···
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }

        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a'
        }
    }
    ···
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

Then, create a CMakeLists.txt file in the corresponding directory and add the code.

# 定义了所需 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})
···

Reference: Android NDK development basis

6, dynamic loading

6.1 Basic concepts

Dynamic loading technology is very common in the Web. For Android projects, the purpose of dynamic loading is to allow users to upgrade application functions without reinstalling APK. The main application scenarios are plug-in and hot repair.

First of all, it needs to be clear that plug-in and hot repair are not the same concept, although from the perspective of technical implementation, they are all from the perspective of the system loader, whether it is hook, proxy or Other low-level implementations all use the way to "cheat" the Android system to allow the host to load and run the content in the plug-in (patch) normally; but the starting points of the two are different.

Plug-inization is essentially extracting the module or function that needs to be implemented as an independent function, reducing the size of the host, and loading the corresponding module when the corresponding function needs to be used. Hot fixes usually start from the perspective of bug fixes, emphasizing that they fix known bugs without the need for a second installation of the application.

For the convenience of explanation, let's clarify a few concepts:

  • Host: The currently running APP.
  • Plug-in: Compared with the plug-in technology, it is to load and run the apk file.
  • Patch: Compared with the hot repair technology, it is to load and run a series of files containing dex repair content such as .patch, .dex, *.apk and so on.

The following figure shows the overall architecture of the Android dynamic development framework.
在这里插入图片描述

6.2 Pluginization

AndroidDynamicLoader , which can be traced back to 2012, is based on the principle of dynamically loading different fragments to implement UI replacement. However, with 15 or 16 years of better solutions, this solution has gradually been eliminated. Later, there was Ren Yugang's dynamic-load-apk program, and a plug-in standard program began. The latter schemes are mostly based on the two directions of Hook and dynamic agent.

At present, there is no official plug-in solution for plug-in development. It is a technical implementation proposed in China. It is a technical means implemented by using the class loading mechanism of the virtual machine. It often requires hooking some system apis. Android 9.0 began to restrict the use of system private apis, which also caused plug-in compatibility issues. Now several popular plug-in technology frameworks are open sourced by major manufacturers based on their own needs, such as Didi’s VirtualAPK, 360's RePlugin, etc., you can understand the implementation principles of the technology yourself according to your needs.

6.3 Hot repair

6.3.1 Principle of hot repair

When it comes to the principle of hot fix, you have to mention the loading mechanism of classes. Similar to the regular JVM, the loading of classes in Android is also done through ClassLoader, specifically PathClassLoader and DexClassLoader, two Android-specific class loading The difference between these two classes is as follows.

  • PathClassLoader: Only apk files (/data/app directory) that have been installed in the Android system can be loaded. It is the class loader used by Android by default.
  • DexClassLoader: You can load dex/jar/apk/zip files in any directory, which is the patch we mentioned at the beginning.

These two classes are inherited from BaseDexClassLoader, the constructor of BaseDexClassLoader is as follows.

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

This constructor only does one thing, which is to initialize a DexPathList object with the relevant parameters passed in. The constructor of DexPathList is to encapsulate the program file (that is, the patch file) passed in the parameters into Element objects, and add these objects to an array collection of Element dexElements.

As mentioned earlier, the role of the class loader is to load a specific class (class) into memory, and these operations are completed by the virtual machine. For developers, they only need to pay attention to how to find the class that needs to be loaded. That's right, this is also what hot repair needs to do.

In Android, finding a class named name requires the following two steps:

  1. In the findClass method of DexClassLoader, the class is obtained through the findClass() method of a DexPathList object.
  2. In the findClass method of DexPathList, the previously constructed dexElements array collection is traversed. Once a class with the same class name and name is found, this class will be returned directly, and null will be returned if it is not found.

Therefore, based on the above theory, we can think of the simplest thermal repair program. Assuming that there is a bug in a certain class in the code, we can package these classes into a patch file after fixing the bug, and then encapsulate an Element object through the patch file, and insert the Element object into the original dexElements The front end of the array. In this way, when DexClassLoader loads a class, due to the characteristics of the parent loading mechanism, the inserted Element will be loaded first, and the defective Element has no chance to be loaded again. In fact, QQ's early hot repair program is like this.

6.3.2 QQ space super patch solution

The QQ space patch solution is to use javaassist instrumentation to solve the problem of CLASS_ISPREVERIFIED. The steps involved are as follows:

  • When the apk is installed, the system will optimize the dex file into an odex file, and a pre-verification process will be involved in the optimization process.
  • If the static method, private method, override method, and constructor of a class refer to other classes, and these classes belong to the same dex file, then the class will be marked with CLASS_ISPREVERIFIED.

    • If the class marked with CLASS_ISPREVERIFIED references other dex classes at runtime, an error will be reported.
  • The normal subcontracting scheme will ensure that related classes are entered into the same dex file.
  • To make the patch can be loaded normally, it is necessary to ensure that the class will not be marked with the CLASS_ISPREVERIFIED mark. To achieve this goal, it is necessary to implant references to classes in other dex files in the subpackaged class.
6.3.3 Tinker

The QQ space super patch solution is very time-consuming when it encounters a large patch file, because when a large folder is loaded into the memory to build an Element object, it takes time to insert into the front end of the array, and this is very Affect the startup speed of the application. Based on these issues, WeChat proposed the Tinker solution.

Tinker's idea is to use the repaired class.dex and the original class.dex to compare the difference between the patch.dex and the patch.dex. This patch.dex will be merged with the original class.dex to generate a new one. File fix_class.dex, replace the content of the original dexPathList with this new fix_class.dex as a whole, and then fix the bug fundamentally. The following picture is a demonstration picture.

在这里插入图片描述
Compared with the QQ space super patch solution, the ideas provided by Tinker can be said to be more efficient. Students who are interested in Tinker's hot repair program can go to see Tinker source code analysis of DexDiff / DexPatch

6.3.4 HotFix

Although the strategies of the two methods mentioned above are different, they are generally based on the perspective of the upper ClassLoader. Due to the characteristics of the ClassLoader, if you want the new patch file to take effect again, whether you are instrumenting or in advance To merge, you need to restart the application to load the new DexPathList, so as to fix the bug.

AndFix provides a way to modify the Filed pointer in Native at runtime, to implement method replacement, and achieve immediate effect without restarting, and no performance consumption for the application. However, since Android has become Android in China, and major mobile phone manufacturers have customized their own ROMs, many differences in the underlying implementation result in AndFix compatibility is not very good.

6.3.5 Sophix

Sophix uses a similar class repair reflection injection method, inserting the path of the patch so library into the front of the nativeLibraryDirectories array, so that when the so library is loaded, it is the patch so library instead of the original so library.

When repairing the defects of the class code, Sophix broke and reorganized the order of classes.dex in the old package and the patch package, so that the system can recognize this order naturally to achieve the purpose of class coverage.

When repairing resource defects, Sophix constructed a resource package with a package id of 0x66. This package only contains the changed resource items, and then directly addAssetPath this package in the original AssetManager without changing the reference of the AssetManager object.

In addition to these programs, there are also hot repair programs such as Robust from Meituan and Amigo from Are You Hungry. However, it is difficult to have a perfect solution for the hot fix of Android. For example, in Android development, the four major components need to be declared in the AndroidManifest before they are used. If hot fixes are required, whether it is preemptive pits or dynamic modifications, it will be very intrusive. At the same time, the problem of Android fragmentation is also a big test for the adaptation of the hot fix solution.

Reference: Android hot fix analysis
depth exploration of the principle of Android hot repair technology


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》