首页 > 使用教程 > gg游戏修改器守护进程退出_gg修改器 守护进程
gg游戏修改器守护进程退出_gg修改器 守护进程
  • gg游戏修改器免root版

  • 大小:18.66MB 版本:v7.17
  • 语言:简体中文系统:Android
无病毒 免积分 免充值

gg游戏修改器守护进程退出_gg修改器 守护进程

作者:佚名 来源:网友分享 发布日期:2024-3-29 00:09:27

各位游戏大佬大家好,今天小编为大家分享关于gg游戏修改器守护进程退出_gg修改器 守护进程的内容,轻松修改游戏数据,赶快来一起来看看吧。

前言

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

Android虚拟机指令

1.指令集解读

1.1 JVM 跨语言与字节码

JVM是跨语言的平台,很多语言都可以编译成为遵守规范的字节码,这些字节码都可以在Java虚拟机上运行。Java虚拟机不关心这个字节码是不是来自于Java程序,只需要各个语言提供自己的编译器,字节码遵循字节码规范,比如字节码的开头是CAFEBABY。

将各种语言编译成为字节码文件的编译器,称之为前端编译器。而Java虚拟机中,也有编译器,比如即时编译器,此处称为后端编译器。

Java虚拟机要做到跨语言,目前来看应该是当下最强大的虚拟机。但是并非一开始设计要跨语言。

1.1.1 跨语言的平台有利于什么?

由于有了跨语言平台,多语言混合编程就更加方便了,通过特定领域的语言去解决特定领域的问题。

比如并行处理使用Clojure语言编写,展示层使用JRuby/Rails,中间层用Java编写,每一应用层都可以使用不同的语言编写,接口对于开发者是透明的。不同语言可以相互调用,就像是调用自己语言原生的API一样。它们都运行在同一个虚拟机上。

1.1.2 何为字节码?

字节码狭义上是java语言编译而成,但是由于JVM是支持多种语言编译的字节码的,而字节码都是一个标准规范,因为我们应该称其为JVM字节码。

不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同操作系统上的不同JVM中运行。

因此,Java虚拟机实际上和Java语言并非强制关联的关系,虚拟机只和二级制文件(Class文件)强关联。

1.2 class字节码解读

1.2.1 Class类文件结构

Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的地排列在文件之中,中间没有添加任何分隔符,这使得整个class文件中存储的内容几乎全部都是程序的必要的数据。当遇到需要占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8字节进行存储。

Class文件格式只有俩种数据类型:“无符号数”和“表”。

如下图,为class类结构:

2.1.1 class文件格式:

常量池中每一项常量都是一个表,截至到jdk13,常量表中分别有17种不同类型的常量。

基本类型以及代表无返回值的void类型都用一个大写的字符表示,而对象则使用字段L加对象的全限定名来表示。对于数组,每一个维度将使用一个前置的[字符来描述,例如:java.lang.String[](#) -> [[Ljava.lang.String; 用来描述方法时,按照先参数列表后返回值的顺序描述,例如:int indexof(char[] source, int first) ->([CI)I。字段表集合不会列出从父类或者父接口继承而来的字段,但有可能出现Java代码不存的字段。

1.2.2 字节码与数据类型

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数据(称为操作码)以及跟随其后的零至多个代表此操作所需的参数(称为操作数)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包括操作数,只有一个操作码,指令参数都放在操作数栈中。Java虚拟机的操作码为一个字节(0-255),这意味着指令集的操作码总数不能超过256条。class文件格式放弃了编译后代码的操作数对齐,这就意味着虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体的数据结构。

如下为Java虚拟机指令集支持的数据类型。

1.3 Hotspot Dalvik ART关系对比

1.3.1 Dalvik简介

1、Google自己设计的用于Android平台的虚拟机;

2、支持已转化为dex格式的java应用程序运行;dex是专为Dalvik设计的一种压缩格式

3、允许在有限的内存中同时运行多个虚拟机实例,并未每一个Dalvik应用作为一和独立的Linux进程运行;

4、5.0以后,Google直接删除Dalvik,取而代之的是ART。

1.3.2 Dalvik与JVM区别

1、Dalvik是基于寄存器,JVM基于栈;

2、Dalvik运行dex文件,JVM运行java字节码;

3、自Android2.2以后,Dalvik支持JIT(即时编译技术)。

1.3.3 ART(Android Runtime)

1、在Dalvik下,应用每次运行,字节码都需要通过即时编译器转化为机器码,这样会拖慢应用的运行效率;

2、在ART下,应用第一次安装时,字节码就会预先变异成机器码,使其真正成为本地应用。这个过程叫做预编译(AOT),这样,每次启动和执行的时候都会更快。

Dalvik与ART区别最大的不同就是:Dalvik是即时编译,每次运行前都先编译;而ART采用预编译。

ART优缺点

优点:

1、系统性能显著提升;

2、应用启动更快,运行更快,体验更流畅;

3、更长的电池续航能力;

4、支持更低的硬件。

缺点:

1、机器码占用存储空间更大;

2、应用安装时间变长。

1.3.4 Dex

Dex文件是Dalvik的可执行文件,Dalvik是针对嵌入式设备设计的java虚拟机,所以Dex文件和Class文件的结构上有很大区别。为了更好的利用嵌入式你设备的资源,Dalvik在java程序编译后,还需要用dx工具将编译产生的数个Class文件整合成一个Dex文件。这样其中的各个类就可以共享数据,减少冗余,使文件结构更加紧凑。

一个设备在执行Dex文件之前,需要优化该Dex文件并生成对应的Odex文件,然后该Odex文件被Dalvik执行。Odex文件本质是个Dex文件,只是针对目标平台做了相关优化,包括对内部字节码进行一系列处理,主要为字节码验证,替换优化及空方法消除。

1.3.5 Dalvik和Art区别

安卓可以运行多个app,对应运行了多个dalvik实例,每一个应用都有一个独立的linux进程,独立的进程可以防止虚拟机崩溃造成所有程序都关闭。就像一条电灯泡上的电灯都是并联关系的,一个灯泡坏了其他灯泡不受影响,一个程序崩溃了其他程序也不受影响。

  1. Art一次编译,终身受用,提高app加载速度,运行速度,省电;不过安装时间略长,占Rom体积略大
  2. Dalvik占用Rom体积小,安装略快,不过加载app时间长,运行慢,更加耗电。

1.4 栈的存储结构和运行原理

1.4.1 栈中存储的是什么?

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。

5.栈帧是线程私有的,其它的线程不能引用另外一个线程的栈帧。
6.当前方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
7.Java函数返回方式有两种,使用return或者抛出异常。不管哪种方式,都会导致栈帧被弹出。

1.5 栈帧的内部结构

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

2.操作数栈

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

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

5.一些附加信息

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

从JAVA程序的角度来看,同一时刻,同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。但对于执行引擎来讲,在活动线程中,只有栈顶的方法才是在运行的,即只有栈顶的方法是生效的,其被称为“当前栈帧”,与这个栈帧所关联的方法被称为”当前方法”,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

栈帧中存储着方法的局部变量表,操作数栈,动态连接和方法返回地址。下面对这几个部分进行一一介绍。

1.5.1 局部变量表

局部变量表示一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽为最小单位,一个变量槽占用32位长度的内存空间,即栈中8个类型数据中除double和long需要占用两个变量槽之外,其余均占用一个变量槽。

需要注意的是,局部变量表是建立在线程的堆栈中的,即线程私有的数据,即对于变量槽的读写是线程安全的。

另外局部变量表中变量槽0通常存着this对象引用,其他数据从变量槽1开始存储,通过字节码指令store存入局部变量表,需要调用时,可通过load指令取出。同时为了节省栈帧占用的内存空间,局部变量表的变量槽是可以重用的,其作用域不一定会覆盖整个方法体,如果当前字节码的PC计数器已经超出某个变量的作用域,那么这个变量槽就可以交给其他变量来重用。

可以参照下面这段代码:


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

可以看到在两个不同的方法中,method2的d,e,f变量复用了method1中的a,b,c对应的变量槽。

这样虽然可以节省开销,却也会带来一定的问题,参考下面的代码:

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]

可以看到,本来应该被回收的数组b却并没有被回收,这主要是由于局部变量表的变量槽中依然还保存着对b的引用(虽然已经出了作用域,但该变量槽并没有被复用,因此引用关系依然保持),使得其无法被垃圾回收。可通过在代码块下方插入int a =0来复用相应的变量槽,打破引用关系,或者将b置为null,这两种方法均可以实现对b的回收。

另外局部变量表中的对象必须要进行赋值,不可以像类变量那样由系统赋予默认值

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

1.5.2 操作数栈

操作数占主要用于方法中变量之间的运算,其主要原理是遇到运算相关的字节码指令(如iadd)时,将最接近栈顶的两个元素弹出进行运算。操作数栈的具体工作流程可参照下面以这段代码:

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

此外在虚拟机栈中,两个栈帧会重叠一部分,即让下面栈帧的部分操作数与上面栈帧的局部变量表的一部分重叠在一起,这样不仅可以节省空间,亦可以在调用方法时,直接共用一部分数据,无需进行额外参数的复制传递。

1.5.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属的方法的引用,持有这个引用是为了支持方法调用过程中的动态连接,即每一次运行期间都要动态地将常量池中方法的符号引用转换为直接引用。

1.5.4 方法返回地址

方法在执行完毕后,有两种方式退出这个方法。一是执行引擎遇到任意一个方法返回的字节码指令(return)。二是方法执行过程中出现了异常,并且在方法的异常表中没有找到对应的异常处理器,在方法退出后,必须返回最初方法被调用的位置,程序才能继续执行。而主调方法的PC计数器的值就可以作为返回地址,,栈帧中会保存着这个计数器的值。

1.6 Jclasslib与HSDB工具应用分析

1.6.1 jclasslib应用分析

下面要隆重介绍的是一款可视化的字节码查看插件:jclasslib。

大家可以直接在 IDEA 插件管理中安装(安装步骤略)。

使用方法

  1. 在 IDEA 打开想研究的类。
  2. 编译该类或者直接编译整个项目( 如果想研究的类在 jar 包中,此步可略过)。
  3. 打开“view” 菜单,选择“Show Bytecode With jclasslib” 选项。
  4. 选择上述菜单项后 IDEA 中会弹出 jclasslib 工具窗口。

那么有自带的强大的反汇编工具 javap 还有必要用这个插件吗?

这个插件的强大之处在于:

  1. 不需要敲命令,简单直接,在右侧方便和源代码进行对比学习。
  2. 字节码命令支持超链接,点击其中的虚拟机指令即可跳转到 jvms 相关章节,超级方便。

该插件对我们学习虚拟机指令有极大的帮助。

1.6.2HSDB的使用

HSDB全称是HotSpotDebugger, HotSpot虚拟机的调试工具,在使用的时候,需要程序处在暂停的状态,可以直接使用Idea的debug工具. 使用HSDB可以看到堆栈里面相关的内容,

启动HSDB

无论哪种方式启动,都需要先知道当前java程序的进程号,我们使用jps命令,如下图所示:

然后我们使用命令 jhsdb hsdb –pid=87854 来启动HSDB,如下图所示:

使用HSDB查看JVM虚拟机栈信息

我们知道,在创建一个线程时,都会有一个为之分配一个jvm栈,如上图我们可以看到在java Threads中有5个线程,我们选中main线程,然后点击上面的查看栈信息的图标,如下图所示:

1:在原java Threads面板上,点第二个按钮,可召唤出Stack Memory for main 这个面板.

Stack Memory for main 面板主体有三大部分,如上图所述

2:最左侧是栈的内存地址

3:中间一列是该地址上存的值(大多是别的对象的地址),

4:最右侧是HotSpot的说明

5:在右侧的说明中, 我们可以此时栈中有两个栈帧(Frame)

大家看到 /platform/tools/jvm/Main$TestObject 这个我们定义的对象,记住这个地址0x00000001161d11e0 代表这个对象是在栈中被引用

使用HSDB查看堆信息

我们的对象大都是在堆里面,我们可以借助HSDB看堆中有多少个实例对象,如下图所示

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内存管理

Android Runtime(ART)虚拟机Dalvik虚拟机都使用分页(Paging)和** 内存映射(Memory-mapped file) 管理内存。这意味着应用修改的任何内存,无论修改的方式是分配新对象还是轻触内存映射的页面,都会一直驻留在RAM中,并且无法换出。要从应用释放内存,只能释放应用保留的对象引用,使内存可供垃圾回收器回收。这种情况有一个例外:对于任何未被修改的内存映射文件(例如:代码)** ,如果系统想要在其他位置使用其内存,可将其从RAM中换出。

1.1 Android虚拟机与JVM底层区别

虚拟机: JVM的作用是把平台无关的.class里面的字节码翻译成平台相关的机器码,来实现跨平台。Dalvik和Art(安卓5.0之后使用的虚拟机)就是安卓中使用的虚拟机。

虚拟机是什么,Jvm,Dalvik(DVM)与Art三者之间的区别

1.1.1 JVM和Android虚拟机的区别

区别一: dvm执行的是.dex格式文件 jvm执行的是.class文件 android程序编译完之后生产.class文件,然后,dex工具会把 .class文件处理成 .dex文件,然后把资源文件和.dex文件等打包成.apk文件。apk就是android package的意思。 jvm执行的是.class文件。 区别二: dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备 区别三: .class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度

总结: JVM以Class为执行单元,Android虚拟机以Dex执行单元,编译流程JVM直接通过Javac即可加载。Android 虚拟机需要先编译成dex,然后编译成apk。最后执行 Android Art虚拟机在安装的时候讲dex缓存本地机器码,安装比较慢,耗存储空间 Android Dalvik虚拟机在程序运行过程中进行翻译。节省空间,耗cpu时间。以空间换时间的典型

1.1.2 dex和class到底在结构上有什么区别?

dex 将文件划分为了 三个区域,这三个区域存储了整个工程中所有的java 文件的信息,所以 dex 在类越来越多的时候优势就提现出来了。他只要一个dex文件,很多区域都是可以进行复用的,减少了dex 文件的大小。

本质上他们是一样的,dex 是从 class 文件演变而来的,但是 calss 中存在了许多沉余信息,dex 去掉了沉余信息,并进行了整合

1.1.3 栈和寄存器的概念,你之前有深入理解过吗?

总结: Java虚拟机都是基于栈的结构,而Dalvik虚拟机则是基于寄存器。基于栈的指令很紧凑, Java虚拟机使用的指令只占一个字节,因而称为字节码。 基于寄存器的指令由于需要指定源地址和目标地址,因此需要占用更多的指令空间。 Dalvik虚拟机的某些指令需要占用两个字节。 基于栈和基于寄存器的指令集各有优劣,一般而言,执行同样的功能, 基于栈的需要更多的指令(主要是load和store指令),而基于寄存器需要更多的指令空间。 栈需要更多指令意味着要多占用CPU时间,寄存器需要更多指令空间意味着数据缓冲(d-cache)更易失效。

1.2 垃圾回收

Android Runtime(ART)虚拟机或者Dalvik虚拟机受管内存环境会跟踪每次内存分配一旦确定程序不再使用某块内存,它就会将该内存重新释放在堆中,无需程序员进行任何干预,这种回收受管内存环境中的未使用内存的机制称为垃圾回收垃圾回收有两个目标:在程序中查找将来无法访问的数据对象,并且回收这些对象使用的资源

Android分代的,这意味着它会根据分配对象的预期寿命和大小跟踪不同的分配存储分区,例如:最近分配的对象属于新生代,当某个对象保持活动状态达足够长的时间,可将其提升为较老代,然后是永久代

的每一代对相应对象可占用的内存量都有其自身的专用上限。每当一代开始填满时,系统便会执行垃圾回收事件释放内存垃圾回收的持续时间取决于它回收的是哪一代对象以及每一代有多少个活动对象

尽管垃圾回收速度非常快,但是仍然会影响应用的性能。通常情况下,我们无法从代码中控制何时发生垃圾回收事件,系统有一套专门确定何时执行垃圾回收的标准,当满足条件时,系统会停止执行进程并开始垃圾回收。如果在动画或者音乐播放密集型处理循环过程中发生垃圾回收,则可能会增加处理时间,进而可能会导致应用中的代码执行超出建议的16ms阈值,无法实现高效流畅帧渲染

此外,我们的代码流执行的各种工作可能迫使垃圾回收事件发生得更频繁或者导致其持续时间超过正常范围,例如:我们在Alpha混合动画每一帧期间,在for循环最内层分配多个对象,则可能在创建大量的对象,在这种情况下,垃圾回收器会执行多个垃圾回收事件,并可能降低应用的性能

1.3 内存问题

1.3.1 共享内存

为了在RAM中容纳所需的一切,Android会尝试跨进程共享RAM页面,它可以通过以下方式实现:

1.3.2 分配与回收应用内存

Dalvik堆局限于每个应用进程的单个虚拟内存范围。这定义了逻辑堆大小,该大小可以根据需要增长,但不能超过系统为每个应用定义的上限

堆的逻辑大小堆使用的物理内存量不同。在检查应用堆时,Android会计算按比例分摊内存大小(PSS)值,该值同时考虑与其他进程共享的脏页干净页但其数量与共享该RAM的应用数量成正比此(PSS)总量是系统认为的物理内存占用量

Dalvik堆不压缩堆的逻辑大小,这意味着Android不会对堆进行碎片整理来缩减空间。只有当堆末尾存在未使用的空间时,Android才能缩减逻辑堆大小,但是系统仍然可以减少堆使用的物理内存垃圾回收之后,Dalvik遍历查找未使用的页面,然后使用madvise将这些页面返回给内核,因此大数据块配对分配解除分配应该使所有(或者几乎所有)使用的物理内存被回收,但是从较小分配量回收内存效率低很多,因为用于较小分配量页面可能仍在与其他尚未释放数据块共享

1.3.3 限制应用内存

为了维持多任务环境正常运行Android会为每个应用堆大小设置硬性上限不同设备确切堆大小上限取决于设备的总体RAM大小。如果应用在达到堆容量上限后尝试分配更多内存,则可能会收到OutOfMemory异常。

某些情况下,例如:为了确定在缓存中保存多少数据比较安全,我们可以通过调用getMemoryClass()方法** 查询系统以确定当前设备上确切可用的堆空间大小,这个方法返回一个整数表示应用堆的可用兆字节数**。

1.3.4 切换应用

当用户在应用之间切换时,Android会将非前台应用保留在缓存中。非前台应用就是指用户看不到或者未运行的前台服务(例如:音乐播放)的** 应用。例如:当用户首次启动某个应用时,系统会为其创建一个进程,但是当用户离开此应用时,该进程不会退出系统会将该进程保留在缓存中,如果用户稍后返回该应用,系统就会重复使用该进程从而加快应用切换速度**。

如果应用具有缓存的进程并且保留了目前不需要的资源,那么即使用户未使用应用,它也会影响系统的整体性能,当系统资源(例如:内存)不足时,它就会终止缓存中的进程,系统还会考虑终止占用最多内存的进程以释放RAM

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

1.3.5 进程间的内存分配

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

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

1.3.6 内存类型

Android设备包含三种不同类型的内存RAMzRAM存储器,如下图所示:

要注意的是,CPUGPU访问同一个RAM

1.3.7 内存页面

随机存取存储器(RAM)分为** 多个页面。通常,每个页面4KB内存**。

系统会将页面视为可用或者已使用可用的页面是未使用的RAM已使用的页面是系统目前正在使用的RAM,可以分为以下类别

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

内存不足管理

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

内核交换守护进程(kswapd)

内核交换守护进程(kswapd)是** Linux内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态Linux内核设有可用内存上下限阈值当可用内存降至下限阈值以下时,kswapd开始回收内存当可用内存达到上限阈值时,kswapd停止回收内存**。

kswapd可以删除干净页回收它们,因为这些页面受到存储器支持未经修改。如果某个进程尝试处理已删除干净页,则系统会将该页面存储器复制到RAM,这个操作成为请求分页

下图展示的是由存储器支持的干净页已删除

kswapd可以将缓存私有脏页匿名脏页移动到zRAM进行压缩,这样可以释放RAM中的可用内存(可用页面) 。如果某个进程尝试处理zRAM中的脏页,该页面将被解压缩移回到RAM。如果与压缩页面关联的进程终止,则该页面将从zRAM删除。如果可用内存量低于特定阈值,系统会开始终止进程

下图展示的是脏页被移至zRAM并进行压缩

1.3.8 低内存终止守护进程(LMK)

很多时候,内核交换守护进程(kswapd)不能为系统释放足够多的内存。在这种情况下,系统会使用onTrimMemory()方法** 通知应用内存不足应该减少其分配量。如果这还不够,Linux内核会开始终止进程释放内存,它会使用低内存终止守护进程(LMK)** 来执行此操作。

LMK使用一个名为oom_adj_score内存不足分值确定正在运行的进程的优先级,以此决定要终止的进程最高得分的进程最先被终止后台应用最先被终止,系统进程最后被终止

下图列出了从高到低的LMK评分类别评分最高的类别,即第一行中的项目将最先被终止

要注意的是,设备制造商可以更改LMK的行为

1.3.9 计算内存占用量

内核会跟踪系统中的所有内存页面

下图展示的是不同进程使用的页面

在确定应用使用的内存量时,系统必须考虑共享的页面。访问相同服务或者应用共享内存页面,例如:Google Play服务某个游戏应用可能会共享位置信息服务,这样便很难确定属于整个服务每个应用内存量分别是多少。下图展示的是由两个应用共享的页面(中间)

如果需要确定应用内存占用量,可以使用以下任一指标

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

1.3.10 管理应用内存

随机存取存储器(RAM)在任何软件开发环境中都是一项** 宝贵资源,尤其是在移动操作系统中,由于物理内存通常都有限,因此RAM更加宝贵了。虽然Android Runtime(ART)虚拟机Dalvik虚拟机都执行例行的垃圾回收任务,但这并不意味着我们可以忽略应用分配释放内存位置时间。我们仍然需要避免引入内存泄漏问题 (通常因为在静态成员变量中保留对象引用而引起) ,并且在适当时间(例如:生命周期回调)** 释放所有Reference对象。

1.3.11 监控可用内存和内存使用量

我们需要先找到应用中内存使用问题,然后才能修复问题。可以使用Android Studio中的内存性能剖析器(Memory Profiler)来帮助我们** 查找和诊断内存问题**:

  1. 了解我们的应用一段时间内如何分配内存Memory Profiler可以显示实时图表,包括:应用的内存使用量分配的Java对象数量垃圾回收事件发生的时间
  2. 发起垃圾回收事件,并在应用运行时拍摄Java堆的快照。
  3. 记录应用的内存分配情况,然后检查有分配的对象查看每个分配的堆栈轨迹,并在Android Studio编辑器中跳转到对应的代码。

1.3.12 释放内存以响应事件

如上面所述,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 -> {
/**
* 发布任何非关键的数据结构。
*
* 应用程序从系统接收到一个无法识别的内存级别值,我们可以将此消息视为普通的低内存消息。
*/
}
}
}

}

要注意的是,onTrimMemory()方法是** Android4.0才添加的,对于早期版本,我们可以使用onLowMemory()方法,这个 回调方法大致相当于TRIM_MEMORY_COMPLETE**事件。

1.3.13 查看应该使用多少内存

为了允许多个进程同时运行,Android针对为每个应用分配的大小设置了硬性限制,这个限制会因设备总体可用的RAM多少而异。如果我们的应用已达到堆容量上限并尝试分配更多内存,系统会抛出OutOfMemory异常。

为了避免用尽内存,我们可以查询系统以确定当前设备上可用的堆空间,可以通过调用getMemoryInfo()方法向系统查询此数值,这个方法会返回** ActivityManager.MemoryInfo对象,这个对象会提供与设备当前的内存状态有关的信息,例如:可用内存总内存内存阈值(如果达到此内存级别,系统就会开始终止进程)** 。ActivityManager.MemoryInfo对象还会提供一个布尔值lowMemory,我们可以根据这个值确定设备是否内存不足。示例代码如下所示:

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 使用内存效率更高的代码结构

我们可以在代码中选择效率更高的方案,以尽可能降低应用的内存使用量

1.3.15 谨慎使用服务(Service)

如果我们的应用需要某项服务(Service)在** 后台执行工作,请不要让其保持运行状态,除非它真的需要运行作业,在服务完成任务后应该使其停止运行,否则可能会导致内存泄漏**。

在我们启动某项服务后,系统更倾向于让此服务的进程始终保持运行状态,这种行为会导致服务进程代价十分高昂,因为一旦服务使用了某部分RAM,那么这部分RAM不再供其他进程使用,这样会减少系统可以在LRU缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的服务时,就可能导致内存抖动

通常应该避免使用持久性服务,因为它们会对可用内存提出持续性的要求,我们可以使用JobScheduler调度后台进程

如果我们必须使用某项服务,则限制此服务的生命周期的最佳方式是使用IntentService它会在处理完启动它的intent后立即自行结束

1.3.16 使用经过优化的数据容器

编程语言所提供的部分类并未针对移动设备做出优化,例如:常规HashMap实现的内存效率可能十分低下,因为每个映射都需要分别对应一个单独的条目对象

Android框架包含几个经过优化的数据容器,例如:SparseArraySparseBooleanArrayLongSparseArray,以SparseArray为例,它的效率更高,因为它可以避免系统需要对键(有时还对值)进行自动装箱(这会为每个条目分别创建1~2个对象)

根据业务需要,尽可能使用精简的数据结构,例如:数组

1.3.17 谨慎对待代码抽象

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

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

协议缓冲区(Protocol Buffers)是** Google设计的一种无关语言和平台并且可扩展的机制,用于对结构化数据进行序列化,它与XML类似,但是更小更快更简单。在移动端中使用精简版的Protobuf,因为常规Protobuf生成极其冗长的代码,这会导致应用出现各种问题:例如:RAM使用量增多APK大小显著增加执行速度变慢**。

1.4 避免内存抖动

如前面所述,垃圾回收事件通常不会影响应用的性能,不过如果在短时间内发生许多垃圾回收事件,就可能会快速耗尽帧时间,系统花在垃圾回收上的时间越多,能够花在呈现界面或者流式传输音频等其他任务上的时间就越少

通常,内存抖动可能会导致出现大量的垃圾回收事件,实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量,例如:我们在for循环中分配多个临时对象或者在ViewonDraw()方法中创建** Paint对象或者Bitmap对象,在这两种情况下,应用都会快速创建大量对象,这些操作可以快速消耗新生代(young generation)区域中的所有可用内存,从而迫使垃圾回收事件发生**。

我们可以借助Android Studio内存性能剖析器(Memory Profiler)找到** 内存抖动较高的位置,确定代码中问题区域后,尝试减少对性能至关重要的区域中的分配数量,可以考虑将某些代码逻辑从内部循环中移出或者使用工厂方法模式**。

移除会占用大量内存的资源和库

代码中的某些资源可能会在我们不知情的情况下吞噬内存APK的总体大小(包括第三方库或者嵌入式资源)可能会影响应用的** 内存消耗量,我们可以通过从代码中移除任何冗余不必要或者臃肿组件资源或者降低应用的内存消耗量**。

缩减总体APK大小

我们可以通过缩减应用的总体大小显著降低应用的内存使用量位图(bitmap)大小资源动画帧数第三方库都会影响APK的大小。Android StudioAndroid SDK提供了帮助我们缩减资源和外部依赖项大小多种工具,这些工具可以缩减代码,例如:R8编译

当我们使用Android Gradle插件3.4.0版本及更高版本构建项目时,这个插件不再使用ProGuard来执行编译时代码优化,而是与R8编译器协同工作来处理以下编译时任务:

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

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

Google Play的新应用服务模式Dynamic Delivery会使用我们提供的App Bundle针对每位用户的设备配置生成并提供经过优化的APK,因此他们只需下载运行我们的应用所需的代码资源,我们不需要再编译签署管理多个APK支持不同的设备,而用户也可以获得更小更优化下载文件包

要注意的是,Google Play规定我们上传的签名APK压缩下载大小限制为不超过100MB,而对使用App Bundle发布的应用压缩下载大小限制为150MB

使用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按钮。

如下图所示:

安装插件后,从菜单栏依次选择Analyze>Analyze App Size,对当前项目运行应用大小分析,分析了项目后,系统会显示一个工具窗口,其中包含有关如何缩减应用大小的建议,如下图所示:

通过命令行使用分析器

我们可以从GitHubTAR或者ZIP文件形式下载最新版本Android Size Analyer,解压缩文件后,使用以下某个命令Android项目或者Android App Bundle运行size-analyzer脚本(在Linux或者MacOS上)或者** size-analyzer.bat脚本(在Windows上)** :

./size-analyzer check-bundle <path-to-aab>
./size-analyzer check-project <path-to-project-directory>

1.4.1 了解APK结构

在讨论如何缩减应用的大小之前,有必要了解下APK的结构APK文件由一个Zip压缩文件组成,其中包含构成应用的所有文件,这些文件包括Java类文件资源文件包含已编译资源的文件

APK包含以下文件夹

APK还包含以下文件,在这些文件中,只有AndroidManifest.xml必需的

1.4.2 缩减资源数量和大小

APK的大小会影响应用加载速度使用的内存量消耗的电量缩减APK大小的一种简单方法是缩减其包含的资源数量和大小,具体来说,我们可以移除应用不再使用的资源,并且可以用可伸缩的Drawable对象取代图片文件

1.4.3 移除未使用的资源

lint工具Android Studio中附带的静态代码分析器,可以检测到res/文件夹中** 未被代码引用的资源,当lint工具发现项目中有可能未使用的资源时,会显示一条消息**,消息如下所示:

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

要注意的是,lint工具不会扫描assets/文件夹通过反射引用的资源已链接至应用的库文件,此外,它不会移除资源,只会提醒我们它们的存在

如果我们在应用的build.gradle文件中启用了shrinkResource,那么Gradle可以帮我们自动移除未使用的资源,示例代码如下:

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

要使用shrinkResource,我们必须启用代码缩减功能,在编译过程中,R8首先会移除未使用的代码,然后Android Gradle插件移除未使用的资源

Android Gradle插件0.7版本及更高版本中,我们可以声明应用支持的配置Gradle会使用resConfigresConfigs变体以及defaultConfig选项将这些信息传递给编译系统,随后,编译系统阻止来自其他不受支持配置的资源出现在APK中从而缩减APK的大小

要注意的是,代码缩减可以清理库的一些不必要代码,但可能无法移除大型内部依赖项

1.4.4 尽量减少库中的资源使用量

在开发Android应用时,我们通常需要使用外部库提高应用的可用性和多功能性,例如:我们可以使用Glide来实现图片加载功能。

如果是为服务器或者桌面设备设计的,则它可能包含应用不需要的许多对象和方法,如果许可允许我们修改,我们可以编辑的文件来移除不需要的部分,我们还可以使用适合移动设备的库

1.4.5 仅支持特定密度

Android支持多种设备,涵盖了各种屏幕密度。在Android 4.4(API级别19)及更高版本中,框架支持各种密度ldpimdpitvdpihdpixhdpixxhdpixxxhdpi。尽管Android支持所有这些密度,但是我们无需将光栅化资源导出为每个密度

如果我们不添加用于特定屏幕密度的资源Android自动缩放为其他屏幕密度设计的资源,建议每个应用至少包含一个xxhdpi图片变体

1.4.6 使用可绘制对象

某些图片不需要静态图片资源,框架可以在运行时动态绘制图片。我们可以使用Drawable对象(XML中的shape元素)来** 动态绘制图片,它只会占用APK中的少量空间,此外,XMLDrawable对象可以生成符合Material Design准则的单色图片**。

1.4.7 重复使用资源

我们可以为图片的变体添加单独的资源,例如:同一图片经过色调调整阴影设置或者旋转的版本。建议重复使用同一组资源,并在运行时根据需要对其进行自定义

Android5.0(API级别21)及更高版本上,使用android:tintandroid:tintMode属性可以更改资源的颜色,对于较低版本的平台,则使用ColorFilter类。

我们可以省略仅是另一个资源的旋转等效项的资源,下面例子展示了通过绕图片中心位置旋转180度,将拇指向上变成拇指向下,示例代码如下所示:

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

1.4.8 从代码进行渲染

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

好处

3.5 Android中ClassLoader

3.6 ClassLoader的加载流程源码分析

-> ClassLoader.java 类

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

由子类实现

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

BaseDexClassLoader类中findClass方法

protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// pathList是DexPathList,是具体存放代码的地方。
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
fe = new ClassNotFoundException(
"Didn’t find class "" + name + "" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
fe;
}
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)

缺点

几种不同的实现:

  1. 采用替换ArtMethod结构体中的字段,这样会有兼容问题,因为手机厂商的修改 以及 android版本的迭代可能会导致底层ArtMethod结构的差异,导致方法替换失败;(AndFix)
  2. 同时使用类加载和底层替换方案,针对小修改,在底层替换方案限制范 围内,还会再判断所运行的机型是否支持底层替换方案,是就采用底层替换(替换整个ArtMethod结构体,这样不会存在兼容问题),否则使用类加载替换;(Sophix)

3. Instant Run方案

Instant Run新特性的原理就是当进行代码改动之后,会进行增量构建,也就是仅仅构建这部分改变的代码,并将这部分代码以补丁的形式增量地部署到设备上,然后进行代码的热替换,从而观察到代码替换所带来的效果。其实从某种意义上讲,Instant Run和热修复在本质上是一样的。

Instant Run打包逻辑

  1. manifest注入:InstantRun会生成一个自己的application,然后将这个application注册到manifest配置文件里面,这样就可以在其中做一系列准备工作,然后再运行业务代码;
  2. nstant Run代码放入主dex:manifest注入之后,会将Instant Run的代码放入到Android虚拟机第一个加载的dex文件中,包括classes.dex和classes2.dex,这两个dex文件存放的都是Instant Run本身框架的代码,而没有任何业务层的代码。
  3. 工程代码插桩——IncretmentalChange;这个插装里面会涉及到具体的IncretmentalChange类。
  4. 工程代码放入instantrun.zip;这里的逻辑是当整个App运行起来之后才回去解压这个包里面的具体工程代码,运行整个业务逻辑。

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

被废弃的Instant Run

Android Studio 3.5 中一个显著变化是引入了 Apply Changes,它取代了旧的 Instant Run。Instant Run 是为了更容易地对应用程序进行小的更改并测试它们,但它会产生一些问题。为了解决这一问题,谷歌已经彻底删除了 Instant Run,并从根本上构建了 Apply Changes ,不再在构建过程中修改 APK,而是使用运行时工具动态地重新定义类,它应该比立刻运行更可靠和更快。

优点

缺点

4. 资源修复

  1. 创建新的AssetManager,并通过反射调用addAssetPath加载完整的新资源包;
  2. 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处 替换为新AssetManager;

5. so库修复

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

@CallerSensitive
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

  1. 判断so文件是否已经加载,若已经加载判断与class_Loader是否一样,避免so重复加载;
  2. 如果so文件没有被加载,打开so并得到so句柄,如果so句柄获取失败,就返回false,常见新的SharedLibrary,如果传入path对应的library为空指针,就将创建的SharedLibrary赋值给library,并将library存储到libraries_中;
  3. 查找JNI_OnLoad的函数指针,根据不同情况设置was_successful的值,最终返回该was_successful;

两种方案:

  1. 将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载;
  2. 调用System.load方法来接管so的加载入口;

以上就是关于gg游戏修改器守护进程退出_gg修改器 守护进程的全部内容,游戏大佬们学会了吗?

技能推荐

热门下载

其他人还在搜