Re-learning Android Basics Series (5): Android Virtual Machine Instructions

foreword

This series of articles mainly summarizes the technical articles of the big guys. Android基础部分As a qualified Android development engineer, we must be proficient in java and android. Let’s talk about these in this issue~

[非商业用途,如有侵权,请告知我,我会删除]

DD: Android进阶开发各类文档,也可关注公众号<Android苦做舟>获取。

1.Android高级开发工程师必备基础技能
2.Android性能优化核心知识笔记
3.Android+音视频进阶开发面试题冲刺合集
4.Android 音视频开发入门到实战学习手册
5.Android Framework精编内核解析
6.Flutter实战进阶技术手册
7.近百个Android录播视频+音视频视频dome
.......

Android virtual machine instructions

1. Interpretation of instruction set

1.1 JVM cross-language and bytecode

JVMIt is a cross-language platform. Many languages ​​can be compiled into bytecodes that comply with the specification, and these bytecodes can Javarun on virtual machines. The Java virtual machine doesn't care whether the bytecode comes from a Java program , it only needs each language to provide its own compiler, and the bytecode follows the bytecode specification, for example, the beginning of the bytecode is CAFEBABY.

Compilers that compile various languages ​​into bytecode files are called front-end compilers. In Javathe virtual machine, there are also compilers, such as just-in-time compilers, which are called back-end compilers here.

JavaThe virtual machine should be cross-language, and it should be the most powerful virtual machine at present. But it wasn't originally designed to be cross-language.

1.1.1 What are the benefits of a cross-language platform?

Thanks to the cross-language platform, multi-language mixed programming is more convenient, and domain-specific languages ​​are used to solve domain-specific problems.

For example, parallel processing is Clojurewritten in languages, the presentation layer is used JRuby/Rails, and the middle layer is Javawritten in. Each application layer can be written in a different language, and the interface is transparent to developers. Different languages ​​can call each other, just like calling the native API of their own language. They all run on the same virtual machine.

1.1.2 What is Bytecode?

In a narrow sense, bytecode is javacompiled by language, but because JVMit supports bytecode compiled in multiple languages, bytecode is a standard specification, because we should call it JVMbytecode.

Different compilers can compile the same bytecode file, and the bytecode file can also JVMrun in different environments on different operating systems.

Therefore, Javathe virtual machine Javais not necessarily associated with the language, and the virtual machine is only Classstrongly associated with the secondary file (file).

1.2 Interpretation of class bytecode

1.2.1 Class file structure

Class files are a set of binary streams based on 8 bytes. Each data item is arranged in the file in strict order and compactly without any separators in the middle, which makes almost all the contents stored in the entire class file It is data necessary for the program. When encountering a data item that needs to occupy more than 8 bytes of space, it will be divided into several 8 bytes for storage according to the high order first method.

The Class file format has only two data types: "unsigned number" and "table".

  • Unsigned number: It belongs to the basic data type. U1, u2, u4, and u8 represent unsigned numbers of 1 byte, 2 bytes, 4 bytes and 8 bytes respectively. Unsigned numbers can be used To describe numbers, index references, quantity values, or string values ​​formed according to UTF-8 encoding.
  • A table is a composite data type composed of multiple unsigned numbers or other tables as data items. For the convenience of distinction, the naming of all tables habitually ends with "_info". A table is used to describe data with a composite structure with a hierarchical relationship. The entire class file can also be a table in essence, arranged in strict order.

As shown in the figure below, it is the class structure:

2.1.1 Class file format:

  • Magic number and version of the class file: The first 4 bytes of each class file are called the magic number, and its only function is to determine whether the file is a class file that can be accepted by the virtual machine. The four bytes following the magic number store the version number of the class file: the 5th and 6th bytes are the minor version number, and the 7th and 8th bytes are the major version number. The Java version number starts from 45th.
  • The constant pool, following the major and minor version numbers is the entry of the constant pool. The constant pool can be compared to the source warehouse in the class file. It is the data most associated with other projects in the class file structure, and usually occupies the space of the class file. One of the largest data items, in addition, he is also the first table-type data item that appears in the class file. The entry of the constant pool needs to place an item of u2 type data, which represents the count value of the constant pool capacity. The counting of this capacity starts from 1 instead of 0. There are two main types of constants stored in the constant pool: literals and symbolic references. Literals are closer to the concept of constants at the Java level, such as text strings, constant values ​​declared as final, and so on. Symbolic references include the following types of constants:
    • Packages exported or exposed by modules
    • fully qualified names of classes and interfaces
    • Field names and descriptors
    • method name and descriptor
    • Method handle and method type
    • Dynamic call sites and dynamic constants

Each constant in the constant pool is a table. As of jdk13, there are 17 different types of constants in the constant table.

  • Access flag (access_flag): After the end of the constant pool, the next 2 bytes represent the access flag, which is used to access information at the level of some classes or interfaces, including: whether this class is a class or an interface; whether it is defined as public ; Whether it is defined as an abstract type, etc. access_flag has a total of 16 kinds of flag bits that can be used, currently only 9 are defined, and the flag bits that are not used are all 0.
    insert image description here

  • Class index (this_class), parent class index (super_class) and interface index collection (interfaces); the class index and parent class index are both a u2 type data collection, and the interface index collection is a set of u2 type data collections, in the class file The inheritance relationship of this type is determined by these three items of data. The class index is used to determine the fully qualified name of this class, and the parent class index is used to determine the fully qualified name of the parent class of this class. The interface index collection is used to describe which interfaces this class implements, and these implemented interfaces will be arranged in the interface index collection from left to right according to the order of the interfaces after the implements keyword.

  • The field table (field_class) is used to describe the variables declared in the interface or class. Including class-level variables and instance-level variables, but excluding local variables declared inside methods. The modifiers that a field can include include the scope of the field (public, protect), instance variable or class variable (static), variability (final), concurrent visibility (volatile, read and write from main memory), whether it can be serialized (transient), field data type (basic type, object, array). Each of the above modifiers either has or does not exist, and it is suitable to use flags to represent them. Fields and field types can only be described by referring to constants in the constant pool. Following the access_flag are two index values: name_index and description_index. They are both references to the constant pool and represent simple names for fields and descriptors for fields and methods, respectively. Fully qualified name: similar to: org/test/testclass; simple name refers to the method or field name without type and parameter modification: similar to inc() inc, field mm; method and field descriptors are more complicated.



    The basic type and the void type representing no return value are represented by an uppercase character, while the object is represented by the field L plus the fully qualified name of the object. For arrays, each dimension will be described with a leading [ character, for example: java.lang.String-> [[Ljava.lang.String; When used to describe a method, it is described in the order of the parameter list first and then the return value, for example: int indexof(char[] source, int first) ->([CI)I. The field table collection will not list the fields inherited from the parent class or parent interface, but there may be fields that are not stored in the Java code.

  • Method table description; the method description in the class file storage format is almost the same as the description of the field. The structure of the method table is the same as the field table, including access flags, name indexes, descriptor indexes, and attribute table collections. . If the parent class method is not overridden in the subclass, the method information from the parent class will not appear in the method table collection. It is possible for the compiler's own method to appear.

  • Attribute table: class files, field tables, and method tables can all carry their own set of attribute tables to describe specific information for certain scenarios. The following is part of the attribute table information.

1.2.2 Bytecode and data type

The instruction of the Java virtual machine consists of a byte-length data representing the meaning of a specific operation (called an opcode), followed by zero or more parameters (called operands) that represent the required operations. Since the Java virtual machine adopts an operand-oriented stack rather than a register-oriented architecture, most instructions do not include operands, only one opcode, and the instruction parameters are placed in the operand stack. The opcode of the Java virtual machine is one byte (0-255), which means that the total number of opcodes in the instruction set cannot exceed 256. The class file format abandons the operand alignment of the compiled code, which means that when the virtual machine processes data that exceeds one byte, it has to rebuild the specific data structure from the byte at runtime.

The data types supported by the Java virtual machine instruction set are as follows.

  • Load and store instructions: Used to transfer data back and forth between local variables in the stack frame and the operand stack. For example: iload (load a local variable to the operand stack), istore (store a value from the operand stack to the local variable table), bipush (load a constant to the operand stack)

  • Operation instruction: It is used to perform a specific operation on the values ​​on the two operand stacks, and store the result back on the top of the operand stack. For example: iadd, isub, imul, idiv, irem, ineg.

  • Type conversion instruction: Two different numeric types can be converted to each other.

  • Object creation and access instructions: Although both class instances and arrays are objects, the Java virtual machine uses different bytecode instructions for the creation and operation of class instances and arrays. After the object is created, you can use the object access instruction to get the fields or array elements of the object instance

    • Create a class instruction: new; create an array instruction: newarray, anewarray, multianewarray
    • Access to class fields and instance fields: getfield, putfield, getstatic, putstatic
    • Instructions that load an array element onto the operand stack: baload, calod, etc.
    • Instructions that store elements of an operand stack into elements of an array: bastore, castore, etc.
    • Take the length of the array: arraylength; check the instruction of the class instance type: instanceof, checkcast;
  • Operand stack instructions: stack (pop), mutual (swap)

  • Control transfer instructions: ifeq, iflt, etc.

  • Method call and return instructions; invokevirtual (call object instance method, allocate according to the actual type of object), invokeinterface (call interface method, find an object that implements this interface method at runtime), invokespecoal (specially handled instance method, Similar to private method, parent class method, initialization method), invokestatic (class static method), invokedynamic (dynamically resolve the method referenced by the call site qualifier at runtime). Its allocation logic is set by the boot method set by the user. Return command: ireturn

  • Exception handling instructions: Handling exceptions in the Java virtual machine is done using exception tables.

  • Synchronization instruction: The Java virtual machine supports the synchronization of a method level and a sequence of instructions inside the method. Both of these are implemented using monitro. Synchronization of a sequence of instructions is usually represented by a synchronized statement block in the Java language. In the Java virtual machine The instructions have monitorenter and monitorexit to support the semantics of synchronized.

1.3 Hotspot Dalvik ART relationship comparison

1.3.1 Introduction to Dalvik

1. A virtual machine designed by Google for the Android platform;

2. Support the operation of java applications that have been converted into dex format; dex is a compression format specially designed for Dalvik

3. Allow multiple virtual machine instances to run simultaneously in limited memory, and run each Dalvik application as a single and independent Linux process;

4. After 5.0, Google directly deleted Dalvik and replaced it with ART.

1.3.2 Difference between Dalvik and JVM

1. Dalvik is based on registers, and JVM is based on stacks;

2. Dalvik runs the dex file, and the JVM runs the java bytecode;

3. Since Android2.2, Dalvik supports JIT (just-in-time compilation technology).

1.3.3 ART(Android Runtime)

1. Under Dalvik, every time the application runs, the bytecode needs to be converted into machine code by the just-in-time compiler, which will slow down the running efficiency of the application;

2. Under ART, when the application is installed for the first time, the bytecode will be mutated into machine code in advance, making it truly a local application. This process is called Ahead of Time (AOT), which makes it faster every time it starts and executes.

The biggest difference between Dalvik and ART is: Dalvik is just-in-time compilation, which is compiled before each run; while ART uses pre-compilation.

Advantages and disadvantages of ART

advantage:

1. Significantly improved system performance;

2. The application starts faster, runs faster, and the experience is smoother;

3. Longer battery life;

4. Support lower hardware.

shortcoming:

1. The machine code takes up more storage space;

2. The application installation time becomes longer.

1.3.4 Dex

The Dex file is the executable file of Dalvik, and Dalvik is a java virtual machine designed for embedded devices, so there is a big difference in the structure of the Dex file and the Class file. In order to make better use of the resources embedded in your device, Dalvik needs to use the dx tool to integrate several Class files generated by compilation into a Dex file after compiling the java program. In this way, the various classes can share data, reduce redundancy, and make the file structure more compact.

Before a device executes a Dex file, it needs to optimize the Dex file and generate a corresponding Odex file, and then the Odex file is executed by Dalvik. The essence of an Odex file is a Dex file, but it is optimized for the target platform, including a series of processing on the internal bytecode, mainly for bytecode verification, replacement optimization and empty method elimination.

1.3.5 Difference between Dalvik and Art

Android can run multiple apps, corresponding to running multiple dalvik instances, each application has an independent linux process, and the independent process can prevent the virtual machine from crashing and causing all programs to close. Just like the lights on a light bulb are all connected in parallel, if one light bulb is broken, the other light bulbs will not be affected, and if a program crashes, the other programs will not be affected.

  1. Compile Art once, use it for life, improve app loading speed, running speed, and save power; but the installation time is slightly longer, and the Rom volume is slightly larger
  2. Dalvik occupies a small Rom size, and the installation is slightly faster, but it takes a long time to load the app, runs slowly, and consumes more power.

1.4 The storage structure and operating principle of the stack

1.4.1 What is stored in the stack?

1.每个线程都有自己的栈,栈中存储的是栈帧。 2.在这个线程上正在执行的每个方法都各自对应一个栈帧。方法与栈帧是一对一的关系。 3.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

1.4.2 栈的运行原理

1.JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈。 2.在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。 3.执行引擎运行的字节码只对当前栈帧进行操作。 4.如果该方法调用的其他的方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

栈的运行原理图: 如下图所示,有四个方法,方法1调用方法2,2调用3,3调用4。 这时栈中会有4个栈帧。当前栈帧是方法4对应的栈帧,位于栈顶。 方法执行完成,将依次出栈。出栈顺序为4,3,2,1。
insert image description here
5.栈帧是线程私有的,其它的线程不能引用另外一个线程的栈帧。
6.当前方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
7.Java函数返回方式有两种,使用return或者抛出异常。不管哪种方式,都会导致栈帧被弹出。
insert image description here

1.5 栈帧的内部结构

1.每个栈帧中存储着局部变量表

2.操作数栈

3.动态链接(指向运行时常量池的方法引用)

4.方法返回地址(或方法正常退出或者异常推出的意义)

5.一些附加信息

在JAVA虚拟机中以方法作为最基本的执行单元,“栈帧”则是用于支持虚拟机方法调用和执行的数据结构。它也是虚拟机运行时数据区中的栈中的栈元素。

From the perspective of a JAVA program, at the same time, in the same thread, all methods on the call stack are in the execution state at the same time. But for the execution engine, in the active thread, only the method at the top of the stack is running, that is, only the method at the top of the stack is valid, which is called the "current stack frame", and the stack frame associated with it The method is called the "current method", and all bytecode instructions run by the execution engine operate only on the current stack frame.

The stack frame stores the method's local variable table, operand stack, dynamic link and method return address. These parts are introduced one by one below.

1.5.1 Local variable table

A local variable represents a storage space for a set of variable values, and is used to store method parameters and local variables defined inside the method. The capacity of the local variable table takes the variable slot as the minimum unit, and a variable slot occupies a memory space of 32 bits in length, that is, among the 8 types of data in the stack, except double and long which need to occupy two variable slots, the rest all occupy one variable slot .

It should be noted that the local variable table is built in the stack of the thread, that is, the data private to the thread, that is, the reading and writing of the variable slot is thread-safe.

In addition, the variable slot 0 in the local variable table usually stores the this object reference, and other data is stored from the variable slot 1, which is stored in the local variable table through the bytecode instruction store, and can be retrieved through the load instruction when it needs to be called. At the same time, in order to save the memory space occupied by the stack frame, the variable slot of the local variable table can be reused, and its scope does not necessarily cover the entire method body. If the PC counter of the current bytecode has exceeded the scope of a variable, then This variable slot can then be handed over to other variables for reuse.

You can refer to the following code:


public  void method1(){
        int a = 0;
        int b = 2;
        int c = a+b;
    }
    public  void method2(){
        int d = 0;
        int e = 2;
        int f = d+e;
    }
public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 2
        line 11: 4
        line 12: 8
 
  public void method2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 2
        line 16: 4
        line 17: 8

It can be seen that in two different methods, the d, e, and f variables of method2 reuse the variable slots corresponding to a, b, and c in method1.

Although this can save overhead, it will also bring certain problems. Refer to the following code:

public static void main(String[] args) {
        {
            byte[] b = new byte[64*1024*1024];
        }
        System.gc();
    }
[GC (System.gc())  68813K->66384K(123904K), 0.0017888 secs]
[Full GC (System.gc())  66384K->66225K(123904K), 0.0074844 secs]

It can be seen that the array b that should have been recycled has not been recycled. This is mainly because the reference to b is still stored in the variable slot of the local variable table (although it has gone out of scope, the variable slot has no is reused, so the reference relationship remains), making it impossible to be garbage collected. You can reuse the corresponding variable slot by inserting int a =0 below the code block, break the reference relationship, or set b to null, both of which can realize the recovery of b.

In addition, the objects in the local variable table must be assigned values, and cannot be assigned default values ​​by the system like class variables

public class A{
    int a;//系统赋值a = 0
    public void method(){
        int b;//错误,必须要赋值
    }
}
1.5.2 Operand stack

The operand account is mainly used for calculations between variables in the method. Its main principle is to pop the two elements closest to the top of the stack to perform calculations when encountering calculation-related bytecode instructions (such as iadd). The specific workflow of the operand stack can refer to the following code:

public  void method1(){
        int a = 0;
        int b = 2;
        int c = a+b;
    }


In addition, in the virtual machine stack, two stack frames will overlap a part, that is, let some operands of the lower stack frame overlap with a part of the local variable table of the upper stack frame. , to directly share a part of the data without copying and passing additional parameters.

1.5.3 Dynamic link

Each stack frame contains a reference to the method to which the stack frame belongs in the runtime constant pool. This reference is held to support dynamic linking during the method call, that is, the constant pool must be dynamically linked during each run. Symbolic references to methods are converted to direct references.

1.5.4 Method return address

After the method is executed, there are two ways to exit the method. One is that the execution engine encounters a bytecode instruction (return) returned by any method. The second is that an exception occurs during the execution of the method, and the corresponding exception handler is not found in the exception table of the method. After the method exits, the program must return to the location where the original method was called before the program can continue to execute. The value of the PC counter of the calling method can be used as the return address, and the value of the counter will be saved in the stack frame.

1.6 Application analysis of Jclasslib and HSDB tools

1.6.1 jclasslib application analysis

The following is a grand introduction of a visual bytecode viewing plug-in: jclasslib.

You can install it directly in the IDEA plug-in management (installation steps omitted).

How to use :

  1. Open the class you want to study in IDEA.
  2. Compile this class or directly compile the whole project (this step can be skipped if the class you want to study is in the jar package).
  3. Open the "view" menu and select the "Show Bytecode With jclasslib" option.
  4. After selecting the above menu item, the jclasslib tool window will pop up in IDEA.
    insert image description here
    Then there is a powerful disassembly tool javap that comes with it, is it necessary to use this plug-in?

The power of this plugin is:

  1. There is no need to type commands, it is simple and direct, and it is convenient to compare and learn with the source code on the right.
  2. Bytecode commands support hyperlinks, and you can jump to jvms related chapters by clicking on the virtual machine instructions , which is super convenient.

This plug-in is very helpful for us to learn virtual machine instructions.

1.6.2 Use of HSDB

The full name of HSDB is HotSpotDebugger, a debugging tool for the HotSpot virtual machine. When using it, the program needs to be in a suspended state. You can directly use the debug tool of Idea. Using HSDB, you can see the relevant content in the stack.

Start HSDB

No matter which way to start, you need to know the process number of the current java program first, we use the jps command, as shown in the following figure:

Then we use the command jhsdb hsdb --pid=87854to start HSDB, as shown in the figure below:

Use HSDB to view JVM virtual machine stack information

We know that when creating a thread, there will be a jvm stack allocated for it. As shown in the figure above, we can see that there are 5 threads in java Threads. We select the main thread, and then click the icon to view the stack information above, as follows As shown in the figure:

1: On the original java Threads panel, click the second button to call the Stack Memory for main panel.

The main body of the Stack Memory for main panel has three parts, as shown in the figure above

2: The leftmost is the memory address of the stack

3: The middle column is the value stored at the address (mostly the address of other objects),

4: On the far right is the description of HotSpot

5: In the description on the right, we can have two stack frames (Frame) in the stack at this time

You see Young com/platform/tools/jvm/Main$TestObject, the object we defined, remember this address 0x00000001161d11e0means that this object is referenced in the stack

Use HSDB to view heap information

我们的对象大都是在堆里面,我们可以借助HSDB看堆中有多少个实例对象,如下图所示
insert image description here
1:点击 Tools->Object Histogram ,打开右边的Object Histogram面板

2:在2处输入我们的类全名,然后点3望远镜搜索,在下面会显示 我们的类,有三个实例

4:可以双击选中我们的类, 也可以放大镜,可以打开Show Objects of Type 面板 看到三个实例的详情

其中第三个,就是我们在栈中看到的方法内的成员变量.

对于另外两个,需要通过反向指针查询 ,看哪个类引用了这个实例,来看是哪个变量

HSDB使用revptrs 看实例引用

对于上面还有两个地址, 我们不能确定是什么对象,所以我们可以通过指针反查来看他们被什么所引用,如下图所示:

如上图,我们可以看到,一个被Class对象所引用, 是类静态变量,一个被jvm/Main , 也就是我们Main类引用, 是类成员变量. 通过这个我们也可以总结, 静态变量,其实也是存在堆里面.

Class,static及Klass的关系

这个版本的hsdb 有些指令不支持,如mem , whatis等,所以要深入学习的小伙伴可以用jdk1.8的hsdb试下上述两个命令

多个Java对象(Java Object,在堆中)对应同一个Klass(在MetaSpace中)对应同一个Class对象(在堆中), 类的静态变量地址都存在Class对象的后面(所以也在堆中).

2.深入Android内存管理

Both the Android Runtime (ART) virtual machine and the Dalvik virtual machine use paging (Paging) and memory mapping (Memory-mapped file) to manage memory . This means that any memory modified by the application , whether by allocating new objects or tapping memory-mapped pages , will always be resident in RAM and cannot be swapped out . To free memory from an application , only object references held by the application can be released , making the memory available to the garbage collector . There is one exception to this situation : for any unmodified memory-mapped file (eg: code) , it can be swapped out of RAM if the system wants to use its memory elsewhere .

1.1 The underlying difference between the Android virtual machine and the JVM

Virtual machine: The role of the JVM is to translate the bytecode in the platform-independent .class into platform-related machine code to achieve cross-platform. Dalvik and Art (the virtual machines used after Android 5.0) are the virtual machines used in Android.

What is a virtual machine, the difference between Jvm, Dalvik (DVM) and Art

1.1.1 The difference between JVM and Android virtual machine

Difference 1: dvm executes .dex format files jvm executes .class files and produces .class files after the android program is compiled. Then, the dex tool will process the .class files into .dex files, and then combine resource files and .dex files etc. are packaged into .apk files. Apk means android package. The jvm executes the .class file. Difference 2: dvm is a register-based virtual machine and jvm execution is a virtual stack-based virtual machine. The register access speed is much faster than the stack, and dvm can achieve maximum optimization according to the hardware, which is more suitable for mobile devices. Difference There is a lot of redundant information in .class files, and the dex tool will remove redundant information and convert all .class The files are consolidated into a .dex file. Reduced I/O operations and improved class lookup speed

Summary: The JVM uses Class as the execution unit, and the Android virtual machine uses Dex as the execution unit. The compilation process of the JVM can be loaded directly through Javac. The Android virtual machine needs to be compiled into dex first, and then compiled into apk. Finally, when the Android Art virtual machine is executed, the dex caches the local machine code during installation. The installation is relatively slow and consumes storage space. The Android Dalvik virtual machine translates while the program is running. Save space and consume cpu time. A typical example of exchanging space for time

1.1.2 What is the structural difference between dex and class?

Dex divides the file into three areas, which store the information of all java files in the entire project, so the advantages of dex come out when there are more and more classes. He only needs one dex file, and many areas can be reused, reducing the size of the dex file.

In essence they are the same, dex is evolved from the class file, but there is a lot of redundant information in calss, dex removes redundant information and integrates

1.1.3 Have you had a deep understanding of the concepts of stacks and registers before?

Summary: The Java virtual machine is based on a stack structure, while the Dalvik virtual machine is based on registers. Stack-based instructions are very compact, and the instructions used by the Java virtual machine only occupy one byte, so they are called bytecodes. Register-based instructions require more instruction space because they need to specify source and destination addresses. Certain instructions of the Dalvik virtual machine require two bytes. Stack-based and register-based instruction sets have their own advantages and disadvantages. Generally speaking, to perform the same function, stack-based instruction sets require more instructions (mainly load and store instructions), while register-based instruction sets require more instruction space. More instructions for the stack means more CPU time, and more instruction space for the registers means that the data cache (d-cache) is more likely to fail.

1.2 Garbage collection

The managed memory environment of the Android Runtime (ART) virtual machine or the Dalvik virtual machine keeps track of each memory allocation . Once a program determines that a piece of memory is no longer in use, it releases that memory on the heap without any intervention from the programmer. This mechanism of reclaiming unused memory in a managed memory environment is called garbage collection . Garbage collection has two goals: to find data objects in the program that cannot be accessed in the future, and to reclaim the resources used by these objects .

Android 's heap is generational , which means it keeps track of different allocation buckets based on the expected lifetime and size of allocated objects , for example: most recently allocated objects belong to the young generation, when an object has been kept alive long enough , which can be promoted to the older generation, then the permanent generation .

Each generation of the heap has its own private upper bound on the amount of memory that corresponding objects can occupy . Whenever a generation starts to fill up , the system performs a garbage collection event to free memory . The duration of a garbage collection depends on which generation of objects it reclaims and how many live objects are in each generation .

Although garbage collection is very fast , it still affects the performance of the application . Usually, we cannot control when garbage collection events occur from the code . The system has a set of criteria for determining when to perform garbage collection . When the conditions are met, the system will stop the execution process and start garbage collection . If garbage collection occurs during an intensive processing loop such as animation or music playback , it may increase processing time , which may cause code execution in the application to exceed the recommended 16ms threshold, and efficient and smooth frame rendering cannot be achieved .

Additionally, there are various things our code flow does that can force garbage collection events to happen more often or cause their duration to be longer than normal , for example : we allocate If multiple objects are used, a large number of objects may be created in the heap , in which case the garbage collector will perform multiple garbage collection events and may degrade the performance of the application .

1.3 Memory issues

1.3.1 Shared memory

In order to fit everything it needs in RAM , Android tries to share RAM pages across processes , which can be achieved in the following ways:

  • Each application process is forked from an existing process called Zygote . When the system starts and loads the common framework (Framework) code and resources (for example: Activity theme background) , the Zygote process starts accordingly. In order to start a new application process , the system will fork (fork) the Zygote process , and then load and run the application code in the new process . This method allows most of the RAM pages allocated by the framework (Framework) code and resources to be shared by all application processes. shared between .
  • Most static data is memory-mapped into a process, this approach allows data not only to be shared between processes, but also swapped out when needed . Examples of static data include: Dalvik code (directly memory-mapped by placing it in a pre-linked .odex file) , application resources (by designing the resource table as a memory-mapable structure and by aligning the APK's zip entries), and traditional Project elements (eg: native code in .so files) .
  • In many places, Android uses explicitly allocated shared memory regions (via ashmem or gralloc) to share the same dynamic RAM between processes . For example: the window surface uses memory shared between the application and the screen compositor , while the cursor buffer uses memory shared between the content provider and the client .
1.3.2 Allocating and reclaiming application memory

The Dalvik heap is limited to a single virtual memory range per application process . This defines the logical heap size , which can grow as needed , up to the upper limit defined by the system for each application .

The logical size of the heap is not the same as the amount of physical memory used by the heap . When examining the application heap , Android calculates a Proportioned Memory Size (PSS) value that takes into account both dirty and clean pages shared with other processes , but whose amount is proportional to the number of applications sharing that RAM . This (PSS) total is what the system thinks is the physical memory footprint .

The Dalvik heap does not compress the logical size of the heap, which means that Android does not defragment the heap to reduce space . Android can shrink the logical heap size only if there is unused space at the end of the heap , but the system can still reduce the physical memory used by the heap . After garbage collection , Dalvik walks the heap and finds unused pages , then uses madvise to return these pages to the kernel , so paired allocation and deallocation of large data blocks should cause all (or almost all) used physical memory to be reclaimed , but Reclaiming memory from smaller allocations is much less efficient because pages used for smaller allocations may still be shared with other data blocks that have not been freed .

1.3.3 Limit application memory

In order to maintain the normal operation of the multi-tasking environment , Android will set a hard upper limit for the heap size of each application . The exact heap size upper limit for different devices depends on the overall RAM size of the device . If the application attempts to allocate more memory after reaching the heap limit , it may receive an OutOfMemory exception.

In some cases , for example: in order to determine how much data to save in the cache , we can query the system to determine the exact size of the heap space available on the current device by calling the getMemoryClass() method . This method returns an integer representing the application heap of available megabytes .

1.3.4 Switch application

Android keeps non-foreground apps in the cache when the user switches between apps . Non-foreground applications refer to applications with foreground services (such as music playback) that are not visible to the user or that are not running . For example: when the user launches an application for the first time , the system creates a process for it , but when the user leaves the application , the process does not exit , the system keeps the process in the cache , and if the user returns to the application later , the system reuses the process to speed up app switching .

If an application has cached processes and retains resources that are not currently needed , it can affect the overall performance of the system even if the user is not using the application , and it will kill the processes in the cache when system resources (eg: memory) are low , the system will also consider terminating the most memory-hogging process to free up RAM .

要注意的是,当应用处于缓存中时,所占用的内存越少,就越有可能免于被终止并得以快速恢复,但是系统也可能根据当下的需求不考虑缓存进程的资源使用情况而随时将其终止

1.3.5 进程间的内存分配

Android平台在运行时不会浪费可用的内存,它会一直尝试利用所有可用的内存。例如:系统会在应用关闭后将其保留在内存中,以便用户快速切回到这些应用,因此,通常情况下,Android设备在运行时几乎没有可用的内存,所以要在重要系统进程和许多用户应用之间正确分配内存内存管理至关重要。

下面会讲解Android是如何为系统和用户应用分配内存的基础知识操作系统如何应对低内存情况

1.3.6 内存类型

Android devices contain three different types of memory : RAM , zRAM , and storage , as shown in the diagram below:

Note that the CPU and GPU access the same RAM .

  • RAM is the fastest type of memory , but its size is usually limited . High-end devices usually have the largest amount of RAM .
  • zRAM is the RAM partition used for swap space . All data is compressed when brought into zRAM , and decompressed when copied out of zRAM . This portion of RAM grows and shrinks as pages are moved in and out of zRAM . Device manufacturers can set an upper zRAM size limit .
  • Memory contains all persistent data (eg: file system, etc.) and object code added for all applications, libraries, and platforms . The storage capacity is much larger than the other two types of memory . On Android , the memory is not used for swap space as it is on other Linux implementations , because frequent writes can cause this memory to become corrupted and shorten the life of the storage medium .
1.3.7 Memory pages

Random Access Memory (RAM) is divided into pages . Typically, each page is 4KB of memory .

The system considers the page available or used . Free pages are unused RAM and used pages are RAM currently in use by the system , which can be broken down into the following categories :

  • Cache page:

    Memory backed by files in storage (eg, code or memory-mapped files). There are two types of cache memory:

    • Private pages: Owned by a process and not shared.

      • Clean Pages : Unmodified copies of files in memory that can be removed by the kernel swap daemon (kswapd) to increase available memory .
      • Dirty pages : Modified copies of files in memory that can be moved to zRAM by the kernel swap daemon (kswapd) or compressed in zRAM to increase available memory .
    • Shared pages: used by multiple processes.

      • Clean pages : Unmodified copies of files in memory that can be removed by the kernel swap daemon (kswapd) to increase available memory .
      • Dirty Pages : A modified copy of a file in memory that allows for additional memory space by writing changes back to the file in memory either through the kernel swap daemon (kswapd) or by explicitly using msync() or munmap() .
  • Anonymous page: Memory not backed by a file in storage (for example: allocated by mmap() with the MAP_ANONYMOUS flag set).

    • 脏页:可由内核交换守护进程(kswapd)移动到 zRAM或者在zRAM中进行压缩以增加可用内存

要注意的是,干净页包含存在于存储器文件(或者文件一部分) 精确副本。如果干净页不再包含文件精确副本(例如:因应用操作所致),则会变成脏页干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页不可以删除,否则数据将会丢失

内存不足管理

Android两种处理内存不足情况的主要机制:内核交换守护进程低内存终止守护进程

内核交换守护进程(kswapd)

The kernel swap daemon (kswapd) is a part of the Linux kernel that converts used memory into usable memory . This daemon becomes active when the available memory on the device becomes low . The Linux kernel has upper and lower thresholds for available memory . When the available memory falls below the lower threshold, kswapd starts to reclaim memory ; when the available memory reaches the upper threshold, kswapd stops reclaiming memory .

kswapd can delete clean pages to reclaim them, since these pages are memory - backed and unmodified . If a process tries to process a clean page that has been deleted , the system copies the page from storage to RAM , an operation called demand paging .

The following figure shows memory-backed clean pages being removed :

kswapd can move cached private and anonymous dirty pages to zRAM for compaction , which frees free memory (free pages) in RAM . If a process tries to process a dirty page in zRAM , the page will be decompressed and moved back to RAM . If the process associated with a compressed page is killed , the page is removed from zRAM . If the amount of free memory falls below a certain threshold , the system starts terminating processes .

The image below shows dirty pages being moved to zRAM and compacted :
insert image description here

1.3.8 Low Memory Kill Daemon (LMK)

Many times, the kernel swap daemon (kswapd) cannot free enough memory for the system . In this case, the system uses the onTrimMemory() method to notify the app that memory is low and that its allocation should be reduced . If that wasn't enough, the Linux kernel starts killing processes to free up memory , and it does this using the low memory kill daemon (LMK) .

LMK uses an out-of-memory score called oom_adj_score to determine the priority of running processes to determine which processes to terminate . The process with the highest score is terminated first . Background apps are the first to be terminated, and system processes are the last to be terminated .

The figure below lists the LMK scoring categories from high to low , the highest scoring category, i.e. the items in the first row will be the first to be terminated :
insert image description here

  • Background apps : Apps that have run before and are not currently active . LMK will kill background processes first, starting with the app with the highest oom_adj_score .
  • Previous app : The most recently used background app . The previous app has a higher priority (lower score) than the background app because the user is more likely to switch to the previous app than to a background app .
  • Home app : This is the launcher app . Terminating the app will make the wallpaper disappear .
  • Services (Services) : Services are initiated by the application , for example: synchronization or upload to the cloud .
  • Perceptible apps : Non-foreground apps that the user can perceive in some way , such as running a search that displays a small interface or listening to music .
  • Foreground app : the app currently in use . Terminating the foreground app looks like the app has crashed and may indicate to the user that something is wrong with the device .
  • Persistent (Services) (Persisient) : These are the core services of the device , such as: telephony and WLAN .
  • System (System) : system process . After these processes are killed , the phone may appear to be about to reboot .
  • Native : A very low-level process used by the system , for example: the kernel interactive termination daemon thread (kswapd) .

Be aware that device manufacturers can change the behavior of LMK .

1.3.9 Calculating memory usage

The kernel keeps track of all memory pages in the system .

The figure below shows the pages used by different processes : When determining the amount of memory
insert image description here
used by an application , the system must consider shared pages . Apps that access the same service or library will share memory pages , for example: Google Play services and a game app may share location information services , making it difficult to determine the amount of memory belonging to the entire service and each app . The image below shows a page shared by two applications (middle) : If you need to determine the memory footprint of your application , you can use either of the following metrics :
insert image description here

  • Resident Memory Size (RSS) : The number of shared and unshared pages used by the application .
  • 按比例分摊的内存大小(PSS)应用使用的非共享页面的数量加上共享页面的均匀分摊数量(例如:如果三个进程共享3MB,则每个进程的PSS为1MB)
  • 独占内存大小(USS)应用使用的非共享页面数量(不包括共享页面)

如果操作系统想要知道所有进程使用了多少内存,那么按比例分摊的内存大小(PSS)非常有用,因为 页面只统计一次,不过计算需要花很长时间,因为系统需要确定共享的页面以及共享页面的进程数量常驻内存大小(RSS)不区分 共享和非共享页面,因此计算起来更快更适合跟踪内存分配量的变化

1.3.10 管理应用内存

Random Access Memory (RAM) is a valuable resource in any software development environment , but especially in mobile operating systems where RAM is even more precious since physical memory is often limited . Although both the Android Runtime (ART) virtual machine and the Dalvik virtual machine perform routine garbage collection tasks, this does not mean that we can ignore where and when the application allocates and frees memory . We still need to avoid introducing memory leaks ( usually caused by keeping object references in static member variables) , and release all Reference objects at appropriate times (eg: lifecycle callbacks) .

1.3.11 Monitoring Available Memory and Memory Usage

We need to find memory usage issues in the app before we can fix them . You can use the Memory Profiler in Android Studio to help us find and diagnose memory problems :

  1. See how our app allocates memory over time . Memory Profiler can display real-time graphs , including: the application's memory usage , the number of allocated Java objects and the timing of garbage collection events .
  2. Initiate garbage collection events and take snapshots of the Java heap while the application is running .
  3. Log your app's memory allocations , then examine allocated objects , view the stack trace for each allocation , and jump to the corresponding code in the Android Studio editor .
1.3.12 Release memory in response to events

如上面所述,Android可以通过多种方式从应用中回收内存或者在必要完全终止应用,从而释放内存以执行关键任务。为了进一步帮助平衡系统内存避免系统需要终止我们的应用进程,我们可以在Activity类中实现ComponentCallback2接口并且重写onTrimMemory()方法,就可以在处于 前台或者后台监听与内存相关的事件,然后释放对象以响应指示系统需要回收内存的应用生命周期事件或者系统事件,示例代码如下所示:

/**
 * Created by TanJiaJun on 2020/7/7.
 */
class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
    /**
     * 当UI隐藏或者系统资源不足时释放内存。
     * @param level 引发的与内存相关的事件
     */
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /**
                 * 释放当前持有内存的所有UI对象。
                 *
                 * 用户界面已经移动到后台。
                 */
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /**
                 * 释放应用程序不需要运行的内存。
                 *
                 * 应用程序运行时,设备内存不足。
                 * 引发的事件表示与内存相关的事件的严重程度。
                 * 如果事件是TRIM_MEMORY_RUNNING_CRITICAL,那么系统将开始杀死后台进程。
                 */
            }
            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /**
                 * 释放进程能释放的尽可能多的内存。
                 *
                 * 该应用程序在LRU列表中,同时系统内存不足。
                 * 引发的事件表明该应用程序在LRU列表中的位置。
                 * 如果事件是TRIM_MEMORY_COMPLETE,则该进程将是第一个被终止的。
                 */
            }
            else -> {
                /**
                 * 发布任何非关键的数据结构。
                 *
                 * 应用程序从系统接收到一个无法识别的内存级别值,我们可以将此消息视为普通的低内存消息。
                 */
            }
        }
    }
}

It should be noted that the onTrimMemory() method was added only in Android 4.0 . For earlier versions , we can use the onLowMemory() method. This callback method is roughly equivalent to the TRIM_MEMORY_COMPLETE event.

1.3.13 Check how much memory should be used

To allow multiple processes to run concurrently, Android places a hard limit on the heap size allocated to each application , which varies based on the overall available RAM on the device . If our application has reached the heap limit and tries to allocate more memory , the system will throw an OutOfMemory exception.

In order to avoid running out of memory , we can query the system to determine the heap space available on the current device . This value can be queried from the system by calling the getMemoryInfo() method. This method will return the ActivityManager.MemoryInfo object, which will provide the current memory of the device. Information about the state of memory , such as: available memory , total memory, and a memory threshold (if this memory level is reached, the system will start terminating processes) . The ActivityManager.MemoryInfo object also provides a Boolean value lowMemory , from which we can determine whether the device is out of memory . The sample code is as follows:

fun doSomethingMemoryIntensive() {
    // 在执行需要大量内存的逻辑之前,检查设备是否处于低内存状态
    if (!getAvailableMemory().lowMemory) {
        // 执行需要大量内存的逻辑
    }
}
// 获取设备当前内存状态的MemoryInfo对象
private fun getAvailableMemory(): ActivityManager.MemoryInfo =
        ActivityManager.MemoryInfo().also {
            (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(it)
        }
1.3.14 Use more memory-efficient code structures

We can choose a more efficient solution in the code to keep the memory usage of the application as low as possible .

1.3.15 Carefully use services (Service)

If our application needs a service (Service) to perform work in the background , please don't keep it running unless it really needs to run the job. After the service completes the task, it should stop running , otherwise it may cause a memory leak .

After we start a service , the system is more inclined to keep the process of this service running all the time . This behavior will cause the service process to be very expensive , because once the service uses a certain part of RAM , this part of RAM is no longer available . This will reduce the number of cache processes that the system can keep in the LRU cache , thereby reducing the efficiency of application switching . Memory thrashing can occur when memory is tight and the system cannot maintain enough processes to host currently running services .

Persistent services should generally be avoided because they make persistent demands on available memory , and we can use the JobScheduler to schedule background processes .

If we must use a service , the best way to limit the lifecycle of this service is to use IntentService , which will end itself as soon as the intent that started it is processed .

1.3.16 Using optimized data containers

Some classes provided by the programming language are not optimized for mobile devices . For example, the memory efficiency of the conventional HashMap implementation may be very low , because each map needs to correspond to a separate entry object .

The Android framework includes several optimized data containers , such as: SparseArray , SparseBooleanArray , and LongSparseArray . Taking SparseArray as an example, it is more efficient because it avoids the need for the system to autobox the key (and sometimes the value) (this 1~2 objects will be created for each entry) .

According to business needs, use as compact data structures as possible , such as arrays .

1.3.17 Be careful with code abstraction

开发者往往会将抽象简单地当做一种良好的编程做法,因为抽象可以提高代码灵活性和维护性,不过抽象代价很高通常它们需要更多的代码才能执行需要更多的时间和更多的RAM才能将代码映射到内存中,因此,如果抽象没有带来显著的好处时,我们就应该避免使用抽象

1.3.18 针对序列化数据使用精简版Protobuf

Protocol Buffers (Protocol Buffers) is a language- and platform-independent and extensible mechanism designed by Google for serializing structured data . It is similar to XML , but smaller , faster and simpler . Use a lite version of Protobuf in the mobile side , because regular Protobuf generates extremely verbose code , which can cause various problems in the app: for example: increased RAM usage , significantly increased APK size , and slower execution .

1.4 Avoid memory thrashing

As mentioned earlier, garbage collection events usually do not affect the performance of the application , but if many garbage collection events occur in a short period of time , it may quickly exhaust the frame time . The more time the system spends on garbage collection , the more time it can spend Less time is spent on other tasks such as rendering the interface or streaming audio .

In general, memory thrashing can lead to a large number of garbage collection events . In fact, memory thrashing can explain the number of allocated temporary objects at a given time . For example: we allocate multiple temporary objects in a for loop or in a View Create Paint objects or Bitmap objects in the onDraw() method of the application. In both cases, the application will quickly create a large number of objects . These operations can quickly consume all available memory in the young generation (young generation) area , thereby forcing garbage collection events. happens .

We can use the Memory Profiler in Android Studio to find the location of high memory jitter . After identifying the problem area in the code, try to reduce the number of allocations in the area that is critical to performance . You can consider placing some code logic Move out of the inner loop or use the factory method pattern .

Remove memory-intensive resources and libraries

Some resources and libraries in the code may consume memory without our knowledge . The overall size of the APK (including third-party libraries or embedded resources) may affect the memory consumption of the application . We can remove it from the code. Reduce the memory consumption of the application by removing any redundant , unnecessary or bloated components , resources or libraries .

Reduce overall APK size

We can significantly reduce the memory usage of the application by reducing the overall size of the application . Bitmap size , resources , animation frames , and third-party libraries all affect APK size. Android Studio and Android SDK provide a variety of tools to help us reduce the size of resources and external dependencies , these tools can reduce the code , for example: R8 compilation .

When we build projects using Android Gradle plugin version 3.4.0 and higher , this plugin no longer uses ProGuard to perform compile-time code optimization , but works with the R8 compiler to handle the following compile-time tasks:

  • Code shrinking (aka Tree Shaking) : Detect and safely remove unused classes, fields, methods, and properties from your app and its library dependencies (making it a very good tool for circumventing the 64K reference limit useful tool) . For example: If we only use a few APIs of a library dependency , minification can identify library code that is not used by the application and remove this part of the code from the application .
  • Resource shrinking : Remove unused resources from packaged applications , including unused resources in application library dependencies . This feature can be used in conjunction with code shrinking . Promptly remove all resources that are no longer referenced .
  • Obfuscation : Shorten class and member names , thereby reducing DEX file size .
  • 优化检查并重写代码以进一步减少应用的DEX文件的大小。例如:如果R8检测到从未使用过某段if/else语句的else分支的代码,则会移除else分支的代码

使用Android App Bundle上传应用(仅限于Google Play)

要在发布到Google Play时立即缩减应用大小,最简单的方法就是将应用发布为Android App Bundle,这是一种全新的上传格式,包含应用的所有编译好的代码资源Google Play负责处理APK生成和签名工作

Dynamic Delivery, Google Play 's new app service model, generates and delivers optimized APKs for each user's device configuration using our App Bundle , so they only need to download the code and resources needed to run our app , we don't Compile , sign and manage multiple APKs to support different devices , and users can also get smaller and more optimized download file packages .

It should be noted that Google Play stipulates that the compressed download size of the signed APK uploaded by us is limited to no more than 100MB , and the compressed download size of the application published using the App Bundle is limited to 150MB .

Using Android Size Analyzer

Android Size Analyzer工具可让我们轻松地发现和实施多种缩减应用大小的策略,它可以作为Android Studio插件或者独立JAR使用。

在Android Studio中使用Android Size Analyzer

我们可以使用Android Studio中的插件市场下载Android Size Analyzer插件,可以按着以下步骤操作:

  1. 依次选择Android Studio>Preferences,如果是Windows的话,依次选择File>Settings
  2. 选择左侧面板中的Plugins部分。
  3. 点击Marketplace标签。
  4. 搜索Android Size Analyzer插件。
  5. 点击分析器插件Install按钮。

As shown in the figure below: After
insert image description here
installing the plug-in , select Analyze>Analyze App Size from the menu bar to run the application size analysis on the current project . After analyzing the project, the system will display a tool window with suggestions on how to reduce the application size . As shown in the following figure: using the analyzer through the command line

We can download the latest version of Android Size Analyzer from GitHub as a TAR or ZIP file . After unzipping the file, use one of the following commands to run the size -analyzer script (on Linux or MacOS) or the size- analyzer.bat script (on Windows) :

./size-analyzer check-bundle <path-to-aab>
./size-analyzer check-project <path-to-project-directory>
1.4.1 Understand the APK structure

Before discussing how to reduce the size of the application , it is necessary to understand the structure of the APK . An APK file consists of a Zip archive that contains all the files that make up an application , including Java class files , resource files , and files containing compiled resources .

The APK contains the following folders :

  • META-INF/ : Contains the CERT.SF and CERT.RSA signature files, and the MANIFEST.MF manifest file.
  • assets/ : Contains the app's assets , which can be retrieved using the AssetManager object .
  • res/ : Contains resources not compiled into resources.arsc .
  • lib/ : Contains compiled code specific to the processor software layer . This directory contains subdirectories for each platform type , for example: armeabi , armeabi-v7a , arm64-v8a , x86 , x86_64 and mips .

The APK also contains the following files , of which only the AndroidManifest.xml is required :

  • resources.arsc : Contains compiled resources , this file contains the XML content of all configurations in the res/values/ folder . The packaging tool extracts this XML content, compiles it into a binary form , and compresses the content , including language strings and styles , as well as content not directly included in the resources.arsc file (such as layout files and images). path .
  • classes.dex : Contains classes compiled in a DEX file format understandable by the Android Runtime (ART) virtual machine and the Dalvik virtual machine .
  • AndroidManifest.xml : Contains the Android manifest file , which lists the application's name , version , access rights , and referenced library files . It uses Android's binary XML format .
1.4.2 Reduce the number and size of resources

The size of your APK affects how fast your app loads , how much memory it uses , and how much battery it consumes . An easy way to reduce the size of an APK is to reduce the number and size of resources it contains . Specifically, we can remove resources that are no longer used by the application , and we can replace image files with scalable Drawable objects .

1.4.3 Remove unused resources

The lint tool is a static code analyzer attached to Android Studio . It can detect resources that are not referenced by the code in the res/ folder . When the lint tool finds that there may be unused resources in the project, a message will be displayed . The message is as follows Show:

res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
        to be unused [UnusedResources]

Note that the lint tool does not scan the assets/ folder , assets referenced via reflection , and libraries linked into the application , and moreover, it does not remove assets , it only alerts us of their existence .

If we enable shrinkResource in the application's build.gradle file , Gradle can automatically remove unused resources for us . The sample code is as follows:

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

To use shrinkResource , we must enable code shrinking . During compilation , R8 first removes unused code , and then the Android Gradle plugin removes unused resources .

In Android Gradle Plugin version 0.7 and above , we can declare the configurations supported by the application . Gradle passes this information to the build system using the resConfig and resConfigs variants and the defaultConfig option, and the build system then prevents resources from other unsupported configurations from appearing in the APK , reducing the size of the APK .

Note that code minification can clean up some unnecessary code in a library , but may not remove large internal dependencies .

1.4.4 Minimize resource usage in libraries

When developing Android applications , we usually need to use external libraries to improve the usability and versatility of the application , for example: we can use Glide to realize the image loading function.

If the library is designed for server or desktop devices , it may contain many objects and methods that are not needed by the application . If the library license allows us to modify the library , we can edit the library 's files to remove unnecessary parts . We can also use Libraries suitable for mobile devices .

1.4.5 Only certain densities are supported

Android supports a variety of devices, covering a variety of screen densities . In Android 4.4 (API level 19) and higher , the framework supports various densities : ldpi , mdpi , tvdpi , hdpi , xhdpi , xxhdpi , and xxxhdpi . Although Android supports all of these densities , we don't need to export rasterized assets to each density .

If we don't add resources for a specific screen density , Android will automatically scale resources designed for other screen densities . It is recommended that each application include at least one xxhdpi image variant .

1.4.6 Using drawables

Some images do not require static image resources , and the framework can dynamically draw images at runtime . We can use the Drawable object (shape element in XML) to dynamically draw pictures , which only takes up a small amount of space in the APK . In addition, the Drawable object in XML can generate monochrome pictures that comply with Material Design guidelines .

1.4.7 Reusing resources

We can add separate assets for variations of an image , for example: toned , shaded , or rotated versions of the same image . It is recommended to reuse the same set of resources and customize them as needed at runtime .

On Android 5.0 (API level 21) and higher , use the android:tint and android:tintMode attributes to change the color of a resource , and for lower versions of the platform, use the ColorFilter class.

We can omit resources that are just the rotation equivalent of another resource . The following example shows turning thumbs up into thumbs down by rotating 180 degrees around the center of the image . The sample code is as follows:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_thumb_up"
    android:fromDegrees="180"
    android:pivotX="50%"
    android:pivotY="50%" />
1.4.8 Rendering from code

We can reduce the size of the APK by rendering pictures according to a certain program , which can free up a lot of space , because there is no need to store picture files in the APK .

1.4.9 Compressing PNG files

The aapt tool can optimize the image resources placed in res/drawable/ through lossless compression during compilation . For example: the aapt tool can convert a true-color PNG that does not require more than 256 colors into an 8-bit PNG through a palette , so that Doing produces images of the same quality but with a smaller memory footprint .

Be aware that the aapt tool has the following limitations:

  • The aapt tool will not minify PNG files contained in the assets/ folder .

  • Image files need to use 256 or fewer colors to be optimized by the aapt tool .

  • The aapt tool may expand the compressed PNG file . To prevent this, we can use the cruncherEnabled flag in Gradle to disable this process for the PNG file . The sample code is as follows:

    aaptOptions {
        cruncherEnabled = false
    }
    

Compress PNG and JPEG files

我们可以使用pngcrushpngquant或者zopflipng等工具缩减PNG文件的大小,同时不损失画质。所有这些工具都可以缩减PNG文件的大小,同时保持肉眼感知的画质不变

pngcrush工具是最有效的:该工具会迭代PNG过滤器zlib(Deflate)参数,使用过滤器参数每个组合来压缩图片,然后它会选择可产生最小压缩输出的配置

压缩JPEG文件,我们可以使用packJPGguetzli等工具。

使用WebP文件格式

If we target Android 3.2 (API level 13) and higher , we can use images in WebP file format instead of PNG or JPEG files . WebP format provides lossy compression (eg: JPEG) and transparency (eg: PNG) , but compared to PNG or JPEG , this format can provide better compression results .

We can use Android Studio to convert existing BMP , JPG , PNG or static GIF images into WebP format.

Note that Google Play only accepts launcher icons in PNG format .

use vector graphics

We can use vector graphics to create resolution-independent icons and other scalable media , which can drastically reduce the space an APK takes up . Vector pictures are expressed in the form of VectorDrawable objects in Android , and a 100-byte file can generate a clear picture with the same size as the screen .

It should be noted that it takes a lot of time for the system to render each VectorDrawable object , and it takes longer to display a large image using a VectorDrawable object . Therefore, it is recommended to use a VectorDrawable object when displaying a small image .

Use vector graphics for animated pictures

请勿使用AnimationDrawable创建逐帧动画,因为这样做需要为动画每个帧添加单独的位图(bitmap)文件,而这样做就会大大增加APK的大小,应该改为使用AnimatedVectorDrawableCompat创建动画矢量可绘制资源

1.5 减少原生(Native)和Java代码

我们可以使用多种方法缩减应用中的原生(Native) Java代码库的大小

1.5.1 移除不必要的生成代码

确保了解自动生成任何代码所占用的空间,例如:许多协议缓冲区工具会生成过多的类和方法,这可能会使应用的大小增加一倍或者两倍

1.5.2 避免使用枚举

A single enumeration will increase the size of the application's classes.dex file by about 1.0 to 1.4KB . These increased sizes can quickly accumulate , resulting in complex systems or shared libraries . If possible, please consider using @IntDef annotations and code reduction to remove Enums and convert them to integers , this type conversion preserves various safety benefits of enums .

1.5.3 Reduce the size of native binaries

If our application uses native code and Android NDK , we can also reduce the size of the release version of the application by optimizing the code . Removing debug symbols and avoiding unpacking native libraries are two very practical techniques.

remove debug symbols

如果应用正在开发中且仍需要调试,则使用调试符号非常合适,我们可以使用Android NDK中提供的arm-eabi-strip工具从原生库移除不必要的调试符号,之后,我们就可以编译发布版本

避免解压缩原生库

构建应用的发布版本时,我们可以通过在应用清单application元素中设置android:extractNativeLibs=“false” ,将未压缩的.so文件打包在APK中。停用此标记可防止PackageManager在安装过程中将 .so文件从APK复制到文件系统,并具有减少应用更新的额外好处。使用Android Gradle插件3.6.0版本及更高版本构建应用时,插件会默认将此属性设为false

1.6 维护多个精简APK

An APK may contain content that the user downloads but never uses , such as assets in other languages ​​or specific screen densities . To ensure the smallest download file for the user , we should use the Android App Bundle to upload the app to Google Play . By uploading the App Bundle , Google Play is able to generate and serve an optimized APK for each user's device configuration , so users only need to download the code and resources needed to run our app , and we no longer need to compile , sign and manage multiple APKs To support different devices , users can also get smaller , more optimized download files .

If we do not intend to publish the application to Google Play , we can subdivide the application into multiple APKs and differentiate them by factors such as screen size or GPU texture support .

When a user downloads our app, our device receives the correct APK based on the device's capabilities and settings , so that the device doesn't receive capabilities and resources that the device doesn't have , for example: if the user has a hdpi device, it doesn't need xxxhdpi resources provided for higher density displays .

1.7 Using Dagger2 to implement dependency injection

Dependency injection frameworks can simplify the code we write and provide an adaptive environment in which we can make testing and other configuration changes .

If we plan to use a dependency injection framework in our application , please consider using Dagger2 . Dagger2 does not use reflection to scan application code , and its static compile-time implementation means it can be used in Android applications without unnecessary runtime costs or memory consumption .

Other dependency injection frameworks that use reflection tend to scan the code for annotations to initialize the process , which can require more CPU cycles and RAM , and can cause noticeable delays when the application starts .

1.8 Use external libraries with caution

External library code is usually not written for the mobile environment and may run inefficiently on mobile clients . If we decide to use an external library , we may need to optimize the library for mobile devices , before deciding to use the library , please plan ahead and analyze the library in terms of code size and RAM consumption .

Even some libraries that are optimized for mobile devices may cause problems due to different implementations , for example: one library may use a compact version of Protobuf , while another library uses Micro Protobuf , resulting in two kinds of problems in our application Different Protobuf implementations . Different implementations of logging , analytics , image loading frameworks , and many other features that we didn't expect can all cause this.

While ProGuard can remove APIs and resources with appropriate tags , it cannot remove large internal dependencies of libraries . The functionality in these libraries that we need may require lower- level dependencies . This is especially problematic if: we use an Activity subclass from a library (which tends to have a lot of dependencies) , the library uses reflection (which is common and means we spend a lot of time manually tweak ProGuard to make it work) , etc.

Also, please avoid using a shared library for one or two functions out of dozens of functions , which will generate a lot of code and overhead that we don't even use at all . When considering whether to use this library , please find that it is a good fit for our needs. implementation , otherwise, we can decide to create the implementation ourselves .

3. Class loading mechanism

3.1 The life cycle of a class

3.1.1 Loading phase

The loading phase can be broken down as follows

  • Binary stream to load class
  • Data structure conversion, converting the static storage structure represented by the binary stream into the runtime data structure of the method area
  • Generate a java.lang.Class object as the access entry for various data of this class in the method area

method to load the binary stream of the class

  • Read from zip package. Our common JAR, AAR dependencies
  • Generated dynamically at runtime. Our common dynamic proxy technology uses ProxyGenerateProxyClass in java.reflect.Proxy to generate proxy binary streams for specific interfaces.
3.1.2 Verification

Verification is the first step in the connection phase. The purpose of this phase is to ensure that the information contained in the byte stream of the Class file meets the requirements of the current virtual machine and will not endanger the security of the virtual machine itself.

  1. File format verification: such as whether it starts with the magic number 0xCAFEBABE, whether the major and minor version numbers are within the processing range of the current virtual machine, constant rationality verification, etc. This stage ensures that the input byte stream can be correctly parsed and stored in the method area, and the format meets the requirements for describing a Java type information.
  2. Metadata verification: whether there is a parent class, whether the inheritance chain of the parent class is correct, whether the abstract class implements all the methods required to be implemented in its parent class or interface, whether the fields and methods conflict with the parent class, etc. In the second stage, it is guaranteed that there is no metadata information that does not conform to the Java language specification.
  3. Bytecode verification: Through data flow and control flow analysis, it is determined that the program semantics are legal and logical. For example, to ensure that jump instructions do not jump to bytecode instructions outside the method body.
  4. Symbolic reference verification: Occurs during the parsing phase to ensure that symbolic references can be converted into direct references.

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3.1.3 准备

类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

3.1.4 解析

虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行

3.1.5 初始化

到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 <clinit>() 方法的过程。

3.1.6 类加载的时机

虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)
  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类), 虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

3.2 类加载器

把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。

将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。

3.3 类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。 这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况

3.4 双亲委托机制


如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
//先从缓存中加没加载这个类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
//从parent中加载
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
//加载不到,就自己加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

好处

  • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  • 安全性考虑,防止核心API库被随意篡改。

3.5 Android中ClassLoader

  • ClassLoader是一个抽象类,定义了ClassLoader的主要功能
  • BootClassLoader is a subclass of ClassLoader (note that it is not an internal class, some materials say it is an internal class, which is wrong), it is used to load some classes required by the system Framework level, and is the final parent of all ClassLoaders on the Android platform
  • SecureClassLoader extends the ClassLoader class and adds permission functions to enhance security
  • URLClassLoader inherits SecureClassLoader, which is used to load classes and resources from jar files and folders through URI path, which is basically unusable in Android
  • BaseDexClassLoader implements most of the functions of Android ClassLoader
  • PathClassLoader loads the class of the application, it will load the dex file in the /data/app directory and the apk file or java file containing dex (some materials say that it will also load the system class, I did not find it, here is doubtful)
  • DexClassLoader can load custom dex files and apk files or jar files containing dex, and supports loading from SD card. When we use plug-in technology, we will use it
  • InMemoryDexClassLoader is used to load dex files in memory

3.6 Source Code Analysis of ClassLoader's Loading Process

-> ClassLoader.java class

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized(this.getClassLoadingLock(name)) {
        //先查找class是否已经加载过,如果加载过直接返回
        Class<?> c = this.findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (this.parent != null) {
                   //委托给parent加载器进行加载 ClassLoader parent;
                    c = this.parent.loadClass(name, false);
                } else {
                   //当执行到顶层的类加载器时,parent = null
                    c = this.findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException var10) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                c = this.findClass(name);
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                //如果parent加载器中没有找到,
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            this.resolveClass(c);
        }
        return c;
    }
}

Implemented by subclasses

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

findClass method in BaseDexClassLoader class

protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // pathList是DexPathList,是具体存放代码的地方。
    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;
}
public Class<?> findClass(String name, List<Throwable> suppressed) {
    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;
}
public Class<?> findClass(String name, ClassLoader definingContext,
        List<Throwable> suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
}
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}
// 调用 Native 层代码
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

3.7 Hot Repair Technology

3.7.1 Introduction of Hot Repair Technology
  • Re-releases are costly, costly, untimely, and poor user experience. There are several solutions for this:
  1. Hybrid: Native + H5 hybrid development, the disadvantage is that the labor cost is high, and the user experience is not as good as the pure native solution;
  2. Plug-in: the cost of transplantation is high, the transformation of old code is time-consuming and laborious, and it cannot be modified dynamically;
  3. Hot repair technology, upload the patch to the cloud, and the app can directly download the patch from the cloud and apply it directly;
  • Hot repair technology is a more practical function for domestic developers, which can solve the following problems:
  1. The cost of releasing a new version is high, and the cost of downloading and installing for users is high;
  2. The efficiency of the version update requires a long time to complete the version coverage;
  3. There is a problem with the upgrade rate of the version update. Users who do not upgrade the version cannot be repaired, and the upgrade is more violent.
  4. For small but important functions, version coverage needs to be completed in a short period of time, such as festival activities.
  • Advantages of hot repair: no version release, no user perception, high repair success rate, and short time;

Controversial Hotfix Framework

  • Dexposed in Mobile Taobao: open source , low-level replacement solution, based on Xposed, Java Method Hook technology for Dalvik runtime, but it is too dependent on the bottom layer of Dalvik, and cannot continue to be compatible with ART after Android 5.0, so give up;
  • Alipay's Andfix: open source , low-level replacement solution, with the help of Dexposed ideas, it is compatible with the full version of Dalvik and ART environments, but its low-level fixed structure replacement solution is not stable, and there are many restrictions on the scope of use, and it is difficult for resources and so the fix failed to materialize
  • Ali Baichuan's Hotfix: open source , low-level replacement solution, relying on Andfix and decoupling business logic, better security and ease of use, but there are still shortcomings of Andfix;
  • Qzone super patch: not open source, class loading scheme, will invade the packaging process
  • Meituan's Robust: open source , Instant Run solution,
  • Dianping's Nuwa: open source , class loading solution,
  • Ele.me's Amigo: open source, class loading solution
  • WeChat's Tinker: open source , class loading solution
  • Sophix on hand Amoy: not open source
3.7.2 Principles of Hot Repair Technology
  • There are three main types of core technologies in the hotfix framework, namely code repair, resource repair and dynamic link library repair

Code fixes:

  • There are three main schemes for code repair, namely, the underlying replacement scheme, the class loading scheme, and the Instant Run scheme.
1. Class loading scheme
  • The class loading scheme needs to restart the App and let the ClassLoader reload the new class, because the class cannot be unloaded, and the App needs to be restarted to reload the new class, so the hot repair framework using the class loading scheme cannot take effect immediately.

advantage:

  • Does not require much adaptation;
  • Simple implementation without many restrictions;

shortcoming

  • Requires APP restart to take effect (cold start repair);
  • Dex instrumentation: The performance loss caused by instrumentation exists on the Dalvik platform, and the problem that the patch package may be too large due to the address offset problem on the Art platform;
  • Dex replacement: Dex merge memory consumption on the vm head, may OOM, resulting in merge failure
  • The virtual machine marks the CLASS_ISPREVERIFIED class for the class during installation to improve performance, and forcibly preventing the class from being marked will affect performance;

Dex subcontracting

  • The class loading scheme is based on the Dex subpackage scheme, and the Dex subpackage scheme is mainly to solve the 65536 limitation and the LinearAlloc limitation:
  1. 65536 limitation: the method call instruction invoke-kind index of the DVM instruction set is 16bits, and a maximum of 65535 methods can be referenced;
  2. LinearAlloc limitation: LinearAlloc in DVM is a fixed buffer area. When the number of methods exceeds the size of the buffer area, it will prompt INSTALL_FAILED_DEXOPT during installation;
  • Dex subpackaging scheme: When packaging, the application code is divided into multiple Dex, and the classes that must be used when the application starts and the classes directly referenced by these classes are placed in the main Dex, and other codes are placed in the secondary Dex. When the application is started, the main Dex is loaded first, and the secondary Dex is dynamically loaded after the application is started. There are two main schemes, Google official scheme, Dex automatic unpacking and dynamic loading scheme.

Several different implementations:

  1. Put the patch package in the first element of the Element array to be loaded first (Super patch and Nuwa in Qzone)
  2. Take out the Element corresponding to each dex in the patch package, and then form a new Element array, and replace the existing Element array with the new Element array through reflection at runtime (Ele.me’s Amigo);
  3. Diff the old and new apk to get patch.dex, then merge patch.dex with the classes.dex of the apk in the mobile phone to generate a new classes.dex, and then put the classes.dex in the Element array through reflection at runtime The first element (WeChat Tinker)
  4. Sophix: The comparison granularity of dex is in the class dimension, and the order of dex in the package is rearranged, classes.dex, classes2.dex..., can be regarded as a class insertion scheme at the dex file level, and the order of dex in the old package Break up and reorganize
2. Underlying alternatives
  • Its idea comes from the Xposed framework, which perfectly interprets AOP programming, and directly modifies the original class in the Native layer (no need to restart the APP). Since there are more restrictions on modifying the original class, the methods and methods of the original class cannot be added or deleted. Field, because it destroys the structure of the original class (causes index changes), although there are many restrictions, it is time-sensitive, easy to load, and immediately effective;

advantage

  • It takes effect in real time, does not need to be restarted, and loads lightly

shortcoming

  • Poor compatibility, because the implementation of each version of the Android system is different, so a lot of compatibility needs to be done.
  • Development needs to master jni related knowledge, and it is more difficult to troubleshoot native exceptions
  • Due to the inability to add new methods and fields, the function release level cannot be achieved

Several different implementations:

  1. Replace the fields in the ArtMethod structure, which will cause compatibility problems, because the modification of the mobile phone manufacturer and the iteration of the android version may cause differences in the underlying ArtMethod structure, resulting in failure of method replacement; (AndFix)
  2. Use class loading and bottom-level replacement schemes at the same time. For small modifications, within the limit of the bottom-level replacement scheme, it will be judged whether the running model supports the bottom-level replacement scheme. If it is, use the bottom-level replacement (replace the entire ArtMethod structure, so that There will be compatibility issues), otherwise use class loading instead; (Sophix)
3. Instant Run solution

The principle of the new feature of Instant Run is that when the code is changed, it will be incrementally built, that is, only this part of the changed code will be built, and this part of the code will be incrementally deployed to the device in the form of a patch, and then the code will be updated. Hot replacement, so as to observe the effect of code replacement. In fact, in a sense, Instant Run and hotfix are essentially the same.

Instant Run packaging logic

  • After accessing Instant Run, compared with the traditional method, there will be the following four differences when packaging
  1. Manifest injection: InstantRun will generate an application of its own, and then register this application in the manifest configuration file, so that a series of preparatory work can be done in it, and then the business code can be run;
  2. The nstant Run code is put into the main dex: After the manifest is injected, the Instant Run code will be put into the first loaded dex file of the Android virtual machine, including classes.dex and classes2.dex, both of which are stored in the dex file It is the code of Instant Run's own framework without any business layer code.
  3. Engineering code instrumentation - IncretmentalChange; this instrumentation will involve the specific IncretmentalChange class.
  4. Put the project code into instantrun.zip; the logic here is to go back and decompress the specific project code in this package after the entire App is running, and run the entire business logic.
  • When Instant Run builds the apk for the first time, it uses ASM to inject code similar to the following into each method (ASM is a Java bytecode manipulation framework. It can be used to dynamically generate classes or enhance the functionality of existing classes)
//$change实现了IncrementalChange这个抽象接口。
//当点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。
//如果方法有变化,就生成替换类,假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,
//这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法
//会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change设置为MainActivity$override
//因此满足了localIncrementalChange != null,会执行MainActivity$override的access$dispatch方法,
//access$dispatch方法中会根据参数”onCreate.(Landroid/os/Bundle;)V”执行MainActivity$override的onCreate方法,
//从而实现了onCreate方法的修改。
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {//2
    localIncrementalChange.access$dispatch(
            "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                    paramBundle });
    return;
}

Obsolete Instant Run

A notable change in Android Studio 3.5 is the introduction of Apply Changes, which replaces the old Instant Run. Instant Run is meant to make it easier to make small changes to the app and test them, but it creates some problems. To fix this, Google has completely removed Instant Run and built Apply Changes from the ground up, no longer modifying the APK during the build process, but instead using runtime tools to dynamically redefine classes, it should be faster than Instant Run Reliable and faster.

advantage

  • Take effect in real time, no restart required
  • Support for adding methods and classes
  • Supports method-level fixes, including static methods
  • For each function of each product code, a piece of code is automatically inserted during the compilation and packaging phase. The insertion process is completely transparent to business development

shortcoming

  • The code is intrusive, and relevant code will be added to the original class
  • Will increase the size of the apk
4. Resource repair
  • At present, most resource hot recovery solutions on the market basically refer to the implementation of Instant Run, which is mainly divided into two steps:
  1. Create a new AssetManager and call addAssetPath through reflection to load a complete new resource bundle;
  2. Find all the previous references to the original AssetManager, and replace the references with the new AssetManager through reflection;
  • For the specific principles here, please refer to the chapter Exploring the Android Open Source Framework - 10. The resource loading part of the plug-in principle;
  • Sophix: Construct a resource package with package id 0x66 (the original resource package is 0x7f), this package only contains changed resource items, and then addAssetPath package directly in the original AssetManager without modifying the reference of AssetManager place, the replacement is faster and safer
5. so library repair
  • It is mainly to update so, that is, to reload so, mainly using the load and loadLibrary methods of System
  • System.load(""): Pass in the full path of so on the disk to load the so of the specified path
@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
  • System.loadLibrary(""): Pass in the so name, which is used to load the so that is automatically copied from the apk package to /data/data/packagename/lib after the app is installed
@CallerSensitive
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
  • Eventually, LoadNativeLibrary() will be called, which mainly does the following work:
  1. Determine whether the so file has been loaded, and if it has been loaded, determine whether it is the same as class_Loader to avoid repeated loading of so;
  2. If the so file is not loaded, open the so and get the so handle, if the so handle fails to get, return false, common new SharedLibrary, if the library corresponding to the incoming path is a null pointer, assign the created SharedLibrary to the library, and Store library into libraries_;
  3. Find the function pointer of JNI_OnLoad, set the value of was_successful according to different situations, and finally return the was_successful;

Two options:

  1. Insert the so patch to the front of the NativeLibraryElement array, so that the path of the so patch is returned and loaded first;
  2. Call the System.load method to take over the loading entry of so;

Guess you like

Origin blog.csdn.net/m0_64420071/article/details/127677878