0 Android进阶解密-目录

2020/01/14 posted in  Android进阶解密

01 Android系统架构

1.1 Android系统架构

1.2 Android系统源码目录

1.2.1 整体结构

1.2.2 应用层部分

1.2.3 应用框架层部分

1.2.4 C/C++程序库部分

1.3 源码阅读

1.3.1 在线阅读

1.3.2 使用Source Insight

1.4 本章小结

2019/03/27 posted in  Android进阶解密

02 Android系统启动

2.1 init进程启动过程

2.1.1 引入init进程

Android系统启动流程的前几步:

  1. 启动电源以及系统启动
  2. 引导程序Bootloader
  3. linux内核启动
  4. init进程启动

2.1.2 init进程的入口函数

2.1.3 解析init.rc

2.1.4 解析Service类型语句

2.1.5 init启动Zygote

2.1.6 属性服务

2.1.7 init进程启动总结

init进程主要做了三件事:

  1. 创建一些文件夹并挂载设备
  2. 初始化和启动属性服务
  3. 解析init.rc配置文件并启动zygote进程

2.2 Zygote进程启动过程

2.2.1 Zygote概述

在Android系统中,DVM(Dalvik虚拟机)、应用程序进程以及运行系统的关键服务的SystemServer进程都是由Zygote进程来创建的,我们也将它称为孵化器。它通过fock(复制进程)的形式来创建应用程序进程和SystemServer进程,由于Zygote进程在启动时会创建DVM,因此通过fock而创建的应用程序进程和SystemServer进程可以在内部获取一个DVM的实例拷贝。

2.2.2 Zygote启动脚本

2.2.3 Zygote进程启动过程介绍

2.2.4 Zygote进程启动总结

Zygote进程共做了如下几件事:

  1. 创建AppRuntime并调用其start方法,启动Zygote进程。
  2. 创建JVM并为JVM注册JNI方法。
  3. 通过JNI调用ZygoteInit的main函数进入Zygote的Java框架层。
  4. 通过registerZygoteSocket函数创建服务端Socket,并通过runSelectLoop函数等待ActivityManagerService的请求来创建新的应用程序进程。
  5. 启动SystemServer进程。

2.3 SystemServer处理过程

2.3.1 Zygote处理SystemServer进程

2.3.2 解析SystemServer进程

2.3.3 SystemServer进程总结

SystemServer进程被创建后,主要做了如下工作:

  1. 启动Binder线程池,这样就可以与其他进程进行通信。
  2. 创建SystemServiceManager,其用于对系统的服务进行创建、启动和生命周期管理。
  3. 启动各种系统服务。

2.4 Launcher启动过程

2.4.1 Launcher概述

系统启动的最后一步是启动一个应用程序用来显示系统中已经安装的应用程序,这个应用程序就叫作Launcher。Launcher在启动过程中会请求PackageManagerService返回系统中已经安装的应用程序的信息,并将这些信息封装成一一个快捷图标列表显示在系统屏幕上,这样用户可以通过点击这些快捷图标来启动相应的应用程序。
通俗来讲Launcher就是Android系统的桌面,它的作用主要有以下两点:

  1. 作为Android系统的启动器,用于启动应用程序。
  2. 作为Android系统的桌面,用于显示和管理应用程序的快捷图标或者其他桌面组件。

2.4.2 Launcher启动过程介绍

2.4.3 Launcher中应用图标显示过程

2.5 Android系统启动流程

  1. 启动电源以及系统启动
    当电源按下时引导芯片代码开始从预定义的地方(固化在ROM)开始执行。加载引导程序Bootloader到RAM,然后执行。
  2. 引导程序BootLoader
    引导程序BootLoader是在Android操作系统开始运行前的一个小程序,它的主要作用是把系统OS拉起来并运行。
  3. Linux内核启动
    内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当内核完成系统设置,它首先在系统文件中寻找init.rc文件,并启动init进程。
  4. init进程启动
    初始化和启动属性服务,并且启动Zygote进程。
  5. Zygote进程启动
    创建JavaVM并为JavaVM注册JNI,创建服务端Socket,启动SystemServer进程。
  6. SystemServer进程启动
    启动Binder线程池和SystemServiceManager,并且启动各种系统服务。
  7. Launcher启动

2.6 本章小结

2019/03/27 posted in  Android进阶解密

03 应用程序进程启动过程

3.1 应用程序进程简介

要想启动一个应用程序,首先要保证这个应用程序所需要的应用程序进程已经启动。AMS在启动应用程序时会检查这个应用程序需要的应用程序进程是否存在,不存在就会请求Zygote进程启动需要的应用程序进程。在2.2节中,我们知道在Zygote的Java框架层中会创建一个Server端的Socket,这个Socket用来等待AMS请求Zygote来创建新的应用程序进程。Zygote进程通过fock自身创建应用程序进程,这样应用程序进程就会获得Zygote进程在启动时创建的虚拟机实例。当然,在应用程序进程创建过程中除了获取虚拟机实例外,还创建了Binder 线程池和消息循环,这样运行在应用进程中的应用程序就可以方便地使用Binder进行进程间通信以及处理消息了。

3.2 应用程序进程启动过程介绍

3.2.1 AMS发送启动应用程序进程请求

3.2.2 Zygote接收请求并创建应用程序进程

3.3 Binder线程池启动过程

3.4 消息循环创建过程

3.5 本章小结

2019/03/27 posted in  Android进阶解密

04 四大组件的工作过程

4.1 根Activity的启动过程

  1. Launcher请求AMS过程
  2. AMS到ApplicationThread的调用过程
  3. ActivityThread启动Activity的过程

4.1.1 Launcher请求AMS过程

4.1.2 AMS到ApplicationThread的调用过程

4.1.3 ActivityThread启动Activity的过程

4.1.4 根Activity启动过程中涉及的进程

4.2 Service的启动过程

  1. ContextImpl到AMS的调用过程
  2. ActivityThread启动Service

4.2.1 ContextImpl到AMS的调用过程

4.2.2 ActivityThread启动Service

4.3 Service的绑定过程

  1. ContextImpl到AMS的调用过程
  2. Service的绑定过程

4.3.1 ContextImpl到AMS的调用过程

4.3.2 Service的绑定过程

4.4 广播的注册、发送和接收过程

4.4.1 广播的注册过程

4.4.2 广播的发送和接收过程

  1. ContextImpl到AMS的调用过程
  2. AMS到BroadcastReceiver的调用过程。

4.5 Content Provider的启动过程

  1. query方法到AMS的调用过程
  2. AMS启动Content Provider的过程

4.5.1 query方法到AMS的调用过程

4.5.2 AMS启动Content Provider的过程

4.6 本章小结

2019/03/27 posted in  Android进阶解密

05 理解上下文Context

5.1 Context的关联类

从图中我们可以看出,ContextImpl和ContextWrapper继承自Context,ContextWrapper内部包含有Context类型的mBase对象,mBase具体指向的是ContextImpl。ContextImpl提供了很多功能,但是外界需要使用并拓展ContextImpl的功能,因此设计上使用了装饰模式,ContextWrapper是装饰类,它对ContextImpl进行包装,ContextWrapper主要是起了方法传递作用,ContextWrapper中几乎所有的方法实现都是调用ContextImpl的相应方法来实现的。ContextThemeWrapper、Service和Application都继承自ContextWrapper,这样他们都可以通过mBase来使用Context的方法,同时它们也是装饰类,在ContextWrapper的基础上又添加了不同的功能。ContextThemeWrapper中包含和主题相关的方法(比如: getTheme方法),因此,需要主题的Activity继承ContextThemeWrapper,而不需要主题的Service则继承ContextWrapper。

Context的关联类采用了装饰模式,主要有以下的优点:

  • 使用者(比如Service) 能够更方便地使用Context。
  • 如果ContextImpl发生了变化,它的装饰类ContextWrapper不需要做任何修改。
  • ContextImpl 的实现不会暴露给使用者,使用者也不必关心ContextImpl的实现。●通过组合而非继承的方式,拓展ContextImpl的功能,在运行时选择不同的装饰类,实现不同的功能。

5.2 Application Context的创建过程

5.3 Application Context的获取过程

5.4 Activity的Context创建过程

5.5 Service的Context创建过程

5.6 本章小结

2019/03/27 posted in  Android进阶解密

06 理解ActivityManagerService

6.1 AMS家族

6.1.1 Android 7.0的AMS家族

6.1.2 Android 8.0的AMS家族

6.2 AMS的启动过程

6.3 AMS与应用程序进程

AMS与应用程序进程的关系主要有以下两点:

  • 启动应用程序时AMS会检查这个应用程序需要的应用程序进程是否存在。
  • 如果需要的应用程序进程不存在,AMS就会请求Zygote进程创建需要的应用程序进程。

6.4 AMS重要的数据结构

6.4.1 解析ActivityRecord

ActivityRecord内部记录了Activity 的所有信息,因此它用来描述一个Activity, 它是在启动Activity 时被创建的,具体是在ActivityStarter 的startActivity方法中被创建的,具体可以查看4.1.2节。ActivityRecord 的部分重要成员变量如表6一1所示。

从表中可以看出ActivityRecord的作用,其内部存储了Activity 的所有信息,包括AMS的引用、AndroidManifest 节点信息、Activity 状态、Activity 资源信息和Activity 进程相关信息等,需要注意的是其中含有该ActivityRecord 所在的TaskRecord, 这就将ActivityRecord和TaskRecord关联在一起,它们是Activity 任务栈模型的重要成员,我们接着来查看TaskRecord。

6.4.2 解析TaskRecord

从表中可以发现TaskRecord的作用,其内部存储了任务栈的所有信息,包括任务栈的唯一标识符、任务栈的倾向性、任务栈中的Activity记录和AMS的引用等,需要注意的是其中含有ActivityStack,也就是当前Activity 任务栈所归属的ActivityStack,我们接着来查看ActivityStack。


6.4.3 解析ActivityStack

ActivityStack是一个管理类,用来管理系统所有Activity的各种状态,其内部维护了TaskRecord的列表,因此从Activity任务栈这一角度来说,ActivityStack也可以理解为Activity堆栈。它由ActivityStackSupervisor来进行管理的,而ActivityStackSupervisor在AMS中的构造方法中被创建。

ActivityStack的实例类型

```
public final class ActivityStackSupervisor implements DisplayListener {
   ...
    //用来存储Launcher App的所有Activity
    ActivityStack mHomeStack;

    //表示当前正在接收输入或启动下一个Activity的所有Activity
    ActivityStack mFocusedStack; 

    //表示此前接收输入的所有Activity。
    private ActivityStack mLastFocusedStack;
    ...
}
```

ActivityState

  • ActivityStack中通过枚举存储了Activity的所有的状态:

    enum ActivityState {
           INITIALIZING,
    RESUMED,
    PAUSING,
    PAUSED,
    STOPPING,
    STOPPED,
    FINISHING,
    DESTROYING,
    DESTROYED
    }

特殊状态的Activity

  • 这些特殊的状态都是ActivityRecord类型的,ActivityRecord用来记录一个Activity的所有信息。

    ActivityRecord mPausingActivity = null;//正在暂停的Activity
    ActivityRecord mLastPausedActivity = null;//上一个已经暂停的Activity
    ActivityRecord mLastNoHistoryActivity = null;//最近一次没有历史记录的Activity
    ActivityRecord mResumedActivity = null;//已经Resume的Activity
    ActivityRecord mLastStartedActivity = null;//最近一次启动的Activity
    ActivityRecord mTranslucentActivityWaiting = null;//传递给convertToTranslucent方法的最上层的Activity

维护的ArrayList

  • ActivityStack中维护了很多ArrayList,这些ArrayList中的元素类型主要有ActivityRecord和TaskRecord,其中TaskRecord用来记录Activity的Task。

6.5 Activity栈管理

Activity栈:存储和管理Activity。

6.5.1 Activity任务栈模型

  • ActivityRecord用来记录一个Activity 的所有信息
  • TaskRecord 中包含了一个或多个ActivityRecord, TaskRecord 用来表示Activity 的任务栈,用来管理栈中的ActivityRecord
  • ActivityStack又包含了一个或多个TaskRecord,它是TaskRecord的管理者。Activity 栈管理就是建立在Activity 任务栈模型之上的

有了栈管理,我们可以对应用程序进行操作,应用可以复用自身应用中以及其他应用的Activity, 节省了资源。

6.5.2 Launch Mode

  • standerd:默认模式,每次启动Activity都会创建一个新的Activity实例。
  • singleTop:如果要启动的Activity已经在栈顶,则不会重新创建Activity,同时该Activity的onNewIntent方法会被调用。如果要启动的Activity不在栈顶,则会重新创建该Activity的实例。
  • singleTask:如果要启动的Activity已经存在于它想要归属的栈中,那么不会创建该Activity实例,将栈中位于该Activity上的所有的Activity出栈,同时该Activity的onNewIntent方法会被调用。如果要启动的Activity不存在于它想要归属的栈中,并且该栈存在,则会重新创建该Activity的实例。如果要启动的Activity想要归属的栈不存在,则首先要创建一个新栈,然后创建该Activity实例并压入到新栈中。
  • singleInstance:和singleTask基本类似,不同的是启动Activity时,首先要创建在一个新栈,然后创建该Activity实例并压入新栈中,新栈中只会存在这一个Activity实例。

6.5.3 Intent的FLAG

  • FLAG_ACTIVITY_NO_HISTORY:Activity一旦退出,就不会存在于栈中。同样的,也可以在AndroidManifest.xml中设置“android:noHistory”。
  • FLAG_ACTIVITY_MULTIPLE_TASK:需要和FLAG_ACTIVITY_NEW_TASK一同使用才有效果,系统会启动一个新的栈来容纳新启动的Activity.
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:Activity不会被放入到“最近启动的Activity”列表中。
  • FLAG_ACTIVITY_BROUGHT_TO_FRONT:这个标志位通常不是由应用程序中的代码设置的,而是Launch Mode为singleTask时,由系统自动加上的。
  • FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY:这个标志位通常不是由应用程序中的代码设置的,而是从历史记录中启动的(长按Home键调出)。
  • FLAG_ACTIVITY_CLEAR_TASK:需要和FLAG_ACTIVITY_NEW_TASK一同使用才有效果,用于清除与启动的Activity相关栈的所有其他Activity。

6.5.4 taskAffinity

我们可以在AndroidManifest.xml设置android:taskAffinity,用来指定Activity希望归属的栈, 默认情况下,同一个应用程序的所有的Activity都有着相同的taskAffinity。taskAffinity在下面两种情况时会产生效果:

  • askAffinity与FLAG_ACTIVITY_NEW_TASK或者singleTask配合。如果新启动Activity的taskAffinity和栈的taskAffinity相同(栈的taskAffinity取决于根Activity的taskAffinity)则加入到该栈中。如果不同,就会创建新栈。
  • taskAffinity与allowTaskReparenting配合。如果allowTaskReparenting为true,说明Activity具有转移的能力。

6.6 本章小结

2019/03/27 posted in  Android进阶解密

07 理解WindowManager

7.1 Window、WindowManager和WMS

  • Window是一个抽象类,具体的实现类为PhoneWindow,它对View进行管理。
  • WindowManager 是一个接口类,继承自接口ViewManager,它是用来管理Window的,它的实现类为WindowManagerImpl。如果我们]想要对Window (View)进行添加、更新和删除操作就可以使用WindowManager,。
  • WindowManager 会将具体的工作交由WMS来处理,WindowManager 和WMS通过Binder来进行跨进程通信,WMS作为系统服务有很多API是不会暴露给WindowManager的,这一点与ActivityManager和AMS的关系有些类似。

Window包含了View并对View进行管理,Window用虚线来表示是因为Window是一个抽象概念,用来描述一个窗口,并不是真实存在的,Window 的实体其实也是View。WindowManager用来管理Window,而WindowManager所提供的功能最终会由WMS进行处理。

7.2 WindowManager的关联类

7.3 Window的属性

7.3.1 Window的类型和显示次序

Window的类型总体来说分为三大类:

  • Application Window(应用程序窗口)
  • Sub Window(子窗口)
  • System Window(系统窗口)

应用程序窗口

public static final int FIRST_APPLICATION_WINDOW = 1;//1
public static final int TYPE_BASE_APPLICATION   = 1;//窗口的基础值,其他的窗口值要大于这个值
public static final int TYPE_APPLICATION        = 2;//普通的应用程序窗口类型
public static final int TYPE_APPLICATION_STARTING = 3;//应用程序启动窗口类型,用于系统在应用程序窗口启动前显示的窗口。
public static final int TYPE_DRAWN_APPLICATION = 4;
public static final int LAST_APPLICATION_WINDOW = 99;//2

应用程序窗口的Type值范围为1到99。

子窗口

子窗口,不能独立的存在,需要附着在其他窗口才可以,PopupWindow就属于子窗口。子窗口的Type值范围为1000到1999。

系统窗口
Toast、输入法窗口、系统音量条窗口、系统错误窗口都属于系统窗口。系统窗口的类型定义如下所示:系统窗口的类型值有接近40个,这里只列出了一小部分, 系统窗口的Type值范围为2000到2999。

窗口显示次序

当一个进程向WMS申请一个窗口时,WMS会为窗口确定显示次序。为了方便窗口显示次序的管理,手机屏幕可以虚拟的用X、Y、Z轴来表示,其中Z轴垂直于屏幕,从屏幕内指向屏幕外,这样确定窗口显示次序也就是确定窗口在Z轴上的次序,这个次序称为Z-Oder。Type值是Z-Oder排序的依据,我们知道应用程序窗口的Type值范围为1到99,子窗口1000到1999 ,系统窗口 2000到2999,,一般情况下,Type值越大则Z-Oder排序越靠前,就越靠近用户。

7.3.2 Window的标志

Window的标志也就是Flag,用于控制Window的显示,同样被定义在WindowManager的内部类LayoutParams中。

设置Window的Flag有三种方法:

  • 第一种是通过Window的addFlags方法;
  • 第二种通过Window的setFlags方法;
  • 第三种则是给LayoutParams设置Flag,并通过WindowManager的addView方法进行添加。

7.3.3 软键盘相关模式

为了使得软键盘窗口能够按照期望来显示,WindowManager的静态内部类LayoutParams中定义了软键盘相关模式:

设置SoftInputMode:

  • AndroidManifest中Activity的属性android:windowSoftInputMode;
  • Java代码getWindow().setSoftInputMode

7.4 Window的操作

对于Window的操作,最终都是交由WMS来进行处理。窗口的操作分为两大部分,一部分是WindowManager处理部分,另一部分是WMS处理部分。

7.4.1 系统窗口的添加过程

7.4.2 Activity的添加过程

  1. Activity 在启动过程中,如果Activity所在的进程不存在则会创建新的进程,创建新的进程之后就会运行代表主线程的实例ActivityThread;
  2. 当界面要与用户进行交互时,会调用ActivityThread 的handleResumeActivity方法;
  3. performResumeActivity方法最终会调用Activity 的onResume方法;
  4. 得到ViewManager类型的对象后,调用了ViewManager 的addView方法,而addView方法则是在WindowManagerImpl中实现的,addView方法的第一个参数为DecorView。

7.4.3 Window的更新过程

Window的更新过程和Window的添加过程是类似的。

  1. 调用ViewManager的updateViewLayout方法,updateViewLayout 方法在WindowManagerImpl 中实现,WindowManagerImpl的updateViewLayout方法会调用WindowManagerGlobal的updateViewI ayout方法
  2. 更新的参数设置到View中,得到要更新的窗口在View列表中的索引在ViewRootImpl列表中根据索引得到窗口的ViewRootlmpl,更新布局参数列表,调用ViewRootImpl的setLayoutParams方法将更新的参数设置到ViewRootImpl 中,ViewRootImpl的setLayoutParams 方法在最后会调用ViewRootImpl的scheduleTraversals方法。
  3. 在TraversalRunnable的run方法中调用了doTraversal方法,在doTraversal方法中又调用了performTraversals 方法,performTraversals 方法使得ViewTree开始View的工作流程:
    1. relayoutWindow方法内部会调用IWindowSession 的relayout方法来更新Window视图,最终会调用WMS的relayoutWindow方法。
    2. performTraversals 方法分别调用performMeasure、performLayout和performDraw方法,它们的内部又会调用View的measure、layout和draw方法,这样就完成了View 的工作流程。
  4. 在performTraversals方法中更新了Window视图,又执行Window中的View的工作流程,这样就完成了Window 的更新。

7.5 本章小结

2019/03/27 posted in  Android进阶解密

08 理解WindowManagerService

8.1 WMS的职责

窗口管理
窗口动画
输入系统的中转站
Surface管理

8.2 WMS的创建过程

8.3 WMS的重要成员

8.4 Window的添加过程(WMS处理部分)

addWindow方法分了3个部分来进行讲解,主要就是做了下面4件事:

  1. 对所要添加的窗口进行检查,如果窗口不满足一些条件,就不会再执行下面的代码逻辑。
  2. WindowToken相关的处理,比如有的窗口类型需要提供WindowToken,没有提供的话就不会执行下面的代码逻辑,有的窗口类型则需要由WMS隐式创建WindowToken。
  3. WindowState的创建和相关处理,将WindowToken和WindowState相关联。
  4. 创建和配置DisplayContent,完成窗口添加到系统前的准备工作。

8.5 Window的删除过程

Window的删除过程:

  1. 检查删除线程的正确性,如果不正确就抛出异常。
  2. 从ViewRootImpl列表、布局参数列表和View列表中删除与V对应的元素。
  3. 判断是否可以直接执行删除操作,如果不能就推迟删除操作。
  4. 执行删除操作,清理和释放与V相关的一切资源。

8.6 本章小结

2019/03/27 posted in  Android进阶解密

09 JNI原理

2019/03/27 posted in  Android进阶解密

10 Java虚拟机

10.1 概述

10.1.1 Java虚拟机家族

  1. HotSpot VM
  2. J9 VM
  3. Zing VM

10.1.2 Java虚拟机执行流程


10.2 Java虚拟机结构

10.2.1 Class文件格式

10.2.2 类的生命周期

类的生命周期:
一个Java文件,从被加载到Java虚拟机内存中从内存中卸载的过程。

  1. 加载:查找并加载Class文件。
  2. 链接
    1. 验证:确保被导入类型的正确性。
    2. 准备:为类的静态字段分配字段,并用默认值初始化这些字段。
    3. 解析:虚拟机将常量池内的符号引用替换为直接引用。
  3. 初始化:将类变量初始化为正确的初始值。
  4. 使用
  5. 卸载

10.2.3 类加载子系统

类加载子系统通过多种类加载器来查找和加载Class文件到Java虚拟机中。Java虚拟机有两种你那个类加载器:系统加载器和自定义加载器。

系统加载器:

  1. Bootstrap ClassLoader(引导类加载器)
  2. Extensions ClassLoader(拓展类加载器)
  3. Application ClassLoader(应用程序类加载器)

10.2.4 运行时数据区域

  1. 程序计数器
  2. Java虚拟机栈
  3. 本地方法栈
  4. Java堆
  5. 方法区
  6. 运行时常量池
  7. 直接内存

10.3 对象的创建

  1. 判断对象对应的类是否加载、链接和初始化
  2. 为对象分配内存
    1. 指针碰撞
    2. 空闲列表
  3. 处理并发安全问题
    1. 分配内存进行同步处理
    2. 本地线程分配缓冲
  4. 初始化分配到的内存空间
  5. 设置对象的对象头
  6. 执行init方法进行初始化

10.4 对象的堆内存布局

以HotSpot虚拟机为例,对象在堆内存的布局分为三个区域,分别是对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

10.5 oop-klass模型

oop-klass模型模型是用来描述Java对象实例的一种模型,它分为两个部分,OOP(Ordinary Object Pointer)指普通对象指针,用来表示对象的实例信息。klass用来描述元数据。

10.6 垃圾标记算法

10.6.1 Java中的引用

  1. 强引用:新建对象时引用,不会回收。
  2. 软引用:SoftReference,内存不足时回收。
  3. 弱引用:WeakReference,GC时回收。
  4. 虚引用:PhantomReference,和对象生命周期没有关系;被回收时会收到一个系统通知。

10.6.2 引用计数算法

基本思想:背个对象都有一个引用计数器,当对象在某处类引用的时候,它的引用计数器加1,引用失效时减1。当引用计数器中的值为0,则该对象就不能被使用,变成了垃圾。

目前主流Java虚拟机并没有选择使用引用计数算法,因为它没有解决对象之间互相循环引用的问题。

10.6.3 根搜索算法

思想:选定一些对象作为GC Roots,并组成根对象集合,然后以这些GC Roots的对象作为起始点,向下搜索,如果目标对象到GC Roots是连接着的,我们则称为该对象是可达的,如果不可达则说说明目标对象是可以被回收的对象。如图:

在Java中,可以作为GC Roots的对象主要有以下几种:

  • Java栈中引用的对象。
  • 本地方法栈中JNI引用的对象。
  • 方法区中运行时常量池引用的对象。
  • 方法区中静态属性引用的对象。
  • 运行中的线程。
  • 由引导类加载器加载的对象。
  • GC控制的对象。

10.7 Java对象在虚拟机中的生命周期

  1. 创建阶段(Created)
    1. 为对象分配存储空间
    2. 构造对象
    3. 从超类到子类对static成员进行初始化
    4. 递归调用超类的构造方法
    5. 调用子类的构造方法
  2. 应用阶段(In Use)
  3. 不可见阶段(Invisible)
  4. 不可达阶段(Unreachable)
  5. 收集阶段(Collected)
  6. 终结阶段(Finalized)
  7. 对象空间重新分配阶段(Deallocated)

10.8 垃圾收集算法

10.8.1 标记—清除算法

  • 标记阶段:标记处可以回收的对象
  • 清除阶段:回收被标记的对象所占的空间。

缺点:

  • 标记和清除的效率不高。
  • 容易产生大量不连续的内存碎片,碎片太多可能导致没有足够的连续内存分配给较大对象,从而触发新的一次垃圾收集动作。

10.8.2 复制算法

为了解决标记—清除算法效率不高的问题。

  1. 它把内存空间划分为两个相等的区域,每次只使用其中一个区域;
  2. 在垃圾收集时,遍历当前使用的区域,把存活的对象复制到另一个区域中,最后将当前使用的区域的可回收对象进行回收。

  • 这种算法每次对整个半区进行内存回收,不需要考虑内存碎片问题,代价就是使用内存为原来的一般。
  • 复制算法的效率与存活对象数目有很大关系,如果存活对象很少,复制算法的效率就会很高。所以复制算法广泛应用于新生代中。

10.8.3 标记-压缩算法

老年代不适用复制算法,因为老年代对象存活率高,会有很多复制操作,导致效率变低。

标记-压缩算法在标记可回收的对象后,将所有存活的对象压缩到内存的另一端,使他们紧凑地排列在一起,然后对边界以外的内存进行回收。

10.8.4 分代收集算法

对不同生命周期的对象采取不同的收集算法,这就是分代的概念。

  • 新生代
    • Eden空间
    • From Survivor空间
    • To Survivor空间
  • 老年代

Eden空间中大多数对象生命周期很短,Eden空间和两个Survivor空间所占比例为8:1。

根据Java堆区的空间划分,垃圾收集的类型分为两种:

  • Minor Collection:新生代垃圾收集
  • Full Collection :老年代收集。又称Major Collection。

Full Collection通常情况下伴随至少一次的Minor Collection,收集频率较低,耗时较长。

10.9 本章小结

2019/03/27 posted in  Android进阶解密

11 Dalvik和ART

11.1 Dalvik虚拟机

Dalvik虚拟机(Dalvik Virtual Machine ),简称Dalvik VM或者DVM。DVM是Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。需要注意的是DVM并不是一个Java 虚拟机(以下简称JVM)。

11.1.1 DVM与JVM的区别

DVM之所以不是一个 JVM,主要原因是DVM并没有遵循JVM规范来实现, DVM与JVM主要有以下区别。

  1. 基于的架构不同
    JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度变慢,对于性能有限的移动设备,显然不是很适合的。DVM是基于寄存器的,它没有基于栈的虚拟机在复制数据时而使用的大量的出入栈指令,同时指令更紧凑、更简洁。但是由于显式指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。

  2. 执行的字节码不同
    在Java SE程序中,Java类被编译成一个或多个.class文件,并打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为.java文件→.class 文件→.jar文件。
    而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该dex文件读取指令和数据。执行顺序为.java文件→.class文件一→.dex文件。

    jar 文件里面包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等。当JVM加载该jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。而在.apk文件中只包含了一个.dex文件,这个.dex文件将所有的.class里面所包含的信息全部整合在一起了,这样再加载就加快了速度。.class 文件存在很多的冗余信息,dex 工具会去除冗余信息,并把所有的.class文件整合到.dex文件中,减少了IO操作,加快了类的查找速度。

  3. DVM允许在有限的内存中同时运行多个进程
    DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间中,独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

  4. DVM由Zygote创建和初始化
    我们在第2章学习过Zygote,它是一个DVM进程,同时也用来创建和初始化DVM实例。每当系统需要创建一个应用程序时 ,Zygote 就会fock 自身,快速地创建和初始化一个DVM实例,用于应用程序的运行。对于一些只读的系统库,所有的DVM实例都会和Zygote共享一块内存区域,节省了内存开销。

  5. DVM有共享机制.
    DVM拥有预加载一共 享的机制,不同应用之间在运行时可以共享相同的类,拥有更 高的效率。而JVM机制不存在这种共享机制,不同的程序,打包以后的程序都是彼此独立的,即便它们在包里使用了同样的类,运行时也都是单独加载和运行的,无法进行共享。

  6. DVM早期没有使用JIT编译器
    JVM使用了JIT编译器(Just In Time Compiler,即时编译器),而DVM早期没有使用JIT编译器。早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,然后交给系统处理,效率不是很高。为了解决这一问题,从Android 2.2版本开始DVM使用了JIT 编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码,而不是每次都需要编译。需要注意的是,应用程序每一次重新运行的时候,都要重做这个编译工作,因此每次重新打开应用程序,都需要JIT编译。

11.1.2 DVM架构

首先Java编译器编译的.class文件经过DX工具转换为.dex文件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行,最后交于Linux处理。

11.1.3 DVM的运行时堆

DVM的运行时堆使用标记一清除(Mark一Sweep) 算法进行GC,它由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space (Zygote Heap)和Allocation Space(Active Heap)。Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,在Zygote进程和应用程序进程之间会共享Zygote Space。在Zygote进程fork第一个子进程之前,会把Zygote Space分为两个部分,原来的已经被使用的那部分堆仍旧叫Zygote Space, 而未使用的那部分堆就叫Allocation Space, 以后的对象都会在Allocation Space上进行分配和释放。Allocation Space不是进程间共享的,在每个进程中都独立拥有一份。除了这两个Space,还包含以下数据结构。

  • Card Table: 用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。
  • Heap Bitmap: 有两个Heap Bitmap, 一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。
  • Mark Stack: DVM的运行时堆使用标记一清除(Mark一 Sweep)算法进行GC,MarkStack就是在GC的标记阶段使用的,它用来遍历存活的对象。

11.1.4 DVM的GC日志

在10.6.2节中提到了Java虚拟机的GC日志。DVM和ART的GC日志与Java虚拟机的日志有较大的区别。在DVM中每次垃圾收集都会将GC日志打印到logcat中,具体的格式为:

D/dalvikvm: <GC Reason> <Amount freed>, <Heap stats>,<External memory stats>,<Pause t ime>

可以看到DVM的日志共有5个信息,其中GCReason有很多种,这里将它单独拿出来进行介绍。

1.引起GC的原因
GC Reason就是引起GC的原因,有以下几种。

  • GC_CONCURRENT: 当堆开始填充时,并发GC可以释放内存。
  • GC_FOR_MALLOC:当堆内存已满时,App尝试分配内存而引起的GC,系统必须停止App并回收内存。
  • GC_HPROF_DUMP_HEAP:当你请求创建HPROF文件来分析堆内存时出现的GC。
  • GC_EXPLICIT: 显式的GC,例如调用System.gc() (应该避免调用显式的GC,信任GC会在需要时运行)。
  • GC_EXTERNAL_ALLOC: 仅适用于API级别小于等于10, 且用于外部分配内存的GC。

2.其他的信息
除了引起GC的原因,其他的信息如下。

  • Amount freed: 本次GC释放内存的大小。
  • Heap_stats: 堆的空闲内存百分比(已用内存) / (堆的总内存)。
  • External_memory_stats: API小于等于级别10的内存分配(已分配的内存) / (引起GC的阈值)。
  • Pausetime:暂停时间,更大的堆会有更长的暂停时间。并发暂停时间会显示两个暂停时间,即一个出现在垃圾收集开始时,另一个出现在垃圾收集快要完成时。

3.实例分析

D/dalvikvm: GC CONCURRENT freed 2012K, 63号 free 3213K/9291K, external 4501K/5161K,paused 2ms+2ms

这个GC日志的含义为:引起GC的原因是GC_CONCURRENT;本次GC释放的内存为2012KB; 堆的空闲内存百分比为63%, 已用内存为3213KB,堆的总内存为9291KB;暂停的总时长为4ms。

11.2 ART虚拟机

ART (Android Runtime)虚拟机是Android 4.4发布的,用来替换Dalvik虚拟机,Android4.4默认采用的还是DVM,系统会提供一 个选项来开启ART。在Android 5.0版本中默认采用了ART, DVM从此退出历史舞台。

11.2.1 ART与DVM的区别

  1. 从11.1节我们知道,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART中,系统在安装应用程序时会进行一次AOT (ahead of time compilation, 预编译),将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电量也会降低。这就好比我们在线阅读漫画,DVM是我们阅读到哪就加载哪,ART则是直接加载一章的漫画,虽然一开始加载速度有些慢,但是后续的阅读体验会很流畅。采用AOT也会有缺点,主要有两个:第一个是AOT会使得应用程序的安装时间变长,尤其是一些复杂的应用;第二个是字节码预先编译成机器码,机器码需要的存储空间会多一些。为了解决上面的缺点,Android 7.0版本中的ART加入了即时编译器JIT,作为AOT的一个补充,在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机器码,从而缩短应用程序的安装时间并节省了存储空间。
  2. DVM是为32位CPU设计的,而ART支持64位并兼容32位CPU,这也是DVM被淘汰的主要原因之一。
  3. ART对垃圾回收机制进行了改进,比如更频繁地执行并行垃圾收集,将GC暂停由2次减少为1次等。
  4. ART的运行时堆空间划分和DVM不同。

11.2.2 ART的运行时堆

与DVM的GC不同的是,ART采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了CMS(Concurrent Mark一Sweep)方案,该方案主要使用了sticky一CMS和partial一CMS。根据不同的CMS方案,ART的运行时堆的空间也会有不同的划分,默认是由4个Space和多个辅助数据结构组成的,4个Space分别是Zygote Space、AllocationSpace、Image Space和Large Object Space。Zygote Space、Allocation Space和DVM中的作用是一样的,Image Space用来存放一些预加载类, Large Object Space用来分配一些大对象(默认大小为12KB),其中Zygote Space和Image Space是进程间共享的。采用标记一清除算法的运行时堆空间划分如图11一3所示。

除了这四个Space,ART的Java堆中还包括两个Mod Union Table, 一个Card Table,两个Heap Bitmap,两个Object Map,以及三个Object Stack。

11.2.3 ART的GC日志

ART的GC日志与DVM不同,ART会为那些主动请求的垃圾收集事件或者认为GC速度慢时才会打印GC日志。GC速度慢指的是GC暂停超过5ms或者GC持续时间超过100ms。如果App未处于可察觉的暂停进程状态,那么它的GC不会被认为是慢速的。
ART的GC日志具体的格式为:

I/art: <GC_Reason> <GC_Name> <0bjects_freed> (<Size_freed>) AllocSpace Objects,<Large_objects_freed> (<Large_object_size_freed>) <Heap_stats> LOS objects,
<Pause time(s)>

1.引起GC原因
ART的引起GC原因(GC_Reason) 要比DVM多一些,有以下几种。

  • Concurrent: 并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不会阻止内存分配。
  • Alloc: 当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在.分配内存的线程中。
  • Explicit: App 显示的请求垃圾收集,例如调用System.gc()。与DVM一样,最佳做法是应该信任GC并避免显式地请求GC,显式地请求GC会阻止分配线程并不必要地浪费CPU周期。如果显式地请求GC导致其他线程被抢占,那么有可能会导致jank(App同一帧画了多次)。
  • NativeAlloc: Native 内存分配时,比如为Bitmaps或者RenderScript分配对象,这会导致Native内存压力,从而触发GC。
  • CollectorTransition: 由堆转换引起的回收,这是运行时切换GC而引起的。收集器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。 当前,收集器转换仅在以下情况下出现:在内存较小的设备.上,App将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
  • HomogeneousSpaceCompact: 齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在当App已经移动到可察觉的暂停进程状态时。这样做的主要原因是减少了内存使用并对堆内存进行碎片整理。
  • DisableMovingGc: 不是真正触发GC的原因,发生并发堆压缩时,由于使用了GetPrimitiveArrayCritical,收集会被阻塞。在一般情况下,强烈建议不要使用GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
  • HeapTrim: 不是触发GC的原因,但是请注意,收集会一直被阻塞,直到堆内存整理完毕。

2.垃圾收集器名称
GC_Name指的是垃圾收集器名称,有以下几种。

  • Concurrent Mark Sweep (CMS):CMS 收集器是一种以获取最短收集暂停时间为目标的收集器,采用了标记一清除算法实现。它是完整的堆垃圾收集器,能释放除了Image Space外的所有的空间。
  • Concurrent Partial Mark Sweep:部分完整的堆垃圾收集器,能释放除了Image Space和Zygote Space外的所有空间。
  • Concurrent Sticky Mark Sweep:粘性收集器,基于分代的垃圾收集思想,它只能释放自.上次GC以来分配的对象。这个垃圾收集器比一个完整的或部分完整的垃圾收集器扫描得更频繁,因为它更快并且有更短的暂停时间。
  • Marksweep + Semispace:非并发的GC,复制GC用于堆转换以及齐性空间压缩(堆碎片整理)。

3.其他信息

  • Objects freed:本次GC从非Large Object Space中回收的对象的数量。
  • Size_ freed: 本次GC从非Large Object Space中回收的字节数。
  • Large objects freed:本次GC从Large Object Space中回收的对象的数量。
  • Large object size freed:本次GC从Large Object Space中回收的字节数。
  • Heap stats:堆的空闲内存百分比,即(已用内存) / (堆的总内存)。
    Pause times: 暂停时间, 暂停时间与在GC运行时修改的对象引用的数量成比例。目前,ART的CMS收集器仅有一次暂停,它出现在GC的结尾附近。移动的垃圾收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。

4.实例分析

I/art : Explicit concurrent mark sweep GC freed 104710 (7MB) AllocSpace objects,21 (416KB) LOS objects, 33号free, 25MB/ 38MB, paused 1.230ms total 67.21 6ms

这个GC日志的含义为引起GC原因是Explicit; 垃圾收集器为CMS收集器;释放对象的数量为104710个,释放字节数为7MB;释放大对象的数量为21个,释放大对象字节数为416KB;堆的空闲内存百分比为33%,已用内存为25MB,堆的总内存为38MB; GC暂停时长为1.230ms, GC总时长为67.216ms。

11.3 DVM和ART的诞生

11.4 本章小结

2019/03/27 posted in  Android进阶解密

12 理解ClassLoader

12.1 Java中的ClassLoader

类加载子系统,主要作用就是通过多种类加载器(ClassLoader)来查找和加载Class文件到Java虚拟机中。

12.1.1 ClassLoader的类型

  • 系统类加载器
    • Bootstrap ClassLoader
    • Extensions ClassLoader
    • Application ClassLoader
  • 自定义类加载器

Bootstrap ClassLoader 引导类加载器
C/C++代码实现的加载器,用于加载指定的JDK核心类库,比如java.lang.java.util.等系统类。它用来加载以下目录中的类库 :

  • $JAVA HOME/jre/lib 目录。
  • -Xbootclasspath 参数指定的目录 。
    Java 虚拟机的启动就是通过 Bootstrap ClassLoader 创建一个初始类来完成的。

Extensions ClassLoader
Java 中的实现类为 ExtClassLoader,因此可以简称为ExtClassLoader,它用于加载 Java的拓展类,提供除了系统类之外的额外功能。 ExtClassLoader用来加载以下目录中的类库:

  • 加载$JAVA_HOME/jre/lib/ext 目录。
  • 系统属性 java.ext.dir所指定的目录。

Application ClassLoader
简称为APPClassLoader,又称为System ClassLoader(系统类加载器),它用来加载以下目录的类库:

  • 当前程序的Classpath目录。
  • 系统属性java.class.path指定的目录。

12.1.2 ClassLoader的继承关系

12.1.3 双亲委托模式

双亲委托模型:

  1. 首先判断该Class是否已经加载,如果没有则委托父加载器进行查找
  2. 依次进行递归,直到委托到最顶层的Bootstrap ClassLoader
  3. 如果Bootstrap ClassLoader找到了该Class,直接返回;如果没有找到,则继续依次向下查找,如果还没找到最后交由自身去查找。

双亲委托模型的好处:

  • 避免重复加载。
  • 更加安全。比如无法通过自定义String类替代系统的String类。

12.1.4 自定义ClassLoader

系统提供的类加载器只能够加载指定目录下的 jar 包和 Class 文件,如果想要加载网络上的或者 D 盘某一文件中的 jar 包和 Class 文件则需要自定义 ClassLoader。实现自定义ClassLoader 需要如下两个步骤:

  1. 定义一个自定义 ClassLoade 并继承抽象类 ClassLoader。
  2. 复写 findClass 方法,并在 findClass 方提中调用 defineClass 方法。

12.2 Android中的ClassLoader

12.2.1 ClassLoader的类型

12.2.2 ClassLoader的继承关系

12.2.3 ClassLoader的加载过程

12.2.4 BootClassLoader的创建

12.2.5 PathClassLoader的创建

12.3 本章小结 311

2019/03/27 posted in  Android进阶解密

13 热修复原理

13.1 热修复的产生

13.2 热修复框架的种类和对比

13.3 资源修复

13.3.1 Instant Run概述

Instant Run是Android Studio 2.0以后新增的一个运行机制, 能够显著减少开发人员第二次及以后的构建和部署时间。在没有使用Instant Run前,我们编译部署应用程序的流程如图所示。

从图13一1可以看出,传统的编译部署需要重新安装App和重启App,这显然会很耗时,Instant Run会避免这一情况, 如图所示。

从上图可以看出InstantRun的构建和部署都是基于更改的部分的。InstantRun部署有三种方式,Instant Run会根据代码的情况来决定采用哪种部署方式,无论哪种方式都不需要重新安装App,这一点就已经提高了不少的效率。

  • Hotswap:从名称也可以看出HotSwap是效率最高的部署方式,代码的增量改变不需要重启App,甚至不需要重启当前的Activity。 修改一个现有方法中的代码时会采用Hot Swap。
  • Warm Swap: App不需重启,但是Activity 需要重启。修改或删除一个现有的资源文件时会采用WarmSwap。
  • Cold Swap: App 需要重启,但是不需要重新安装。采用Cold Swap的情况很多,比如添加、删除或修改一个字段和方法、添加一个类等。

13.3.2 Instant Run的资源修复

Instant Run中的资源热修复可以简单地总结为两个步骤:

  1. 创建新的AssetManager, 通过反射调用addAssetPath方法加载外部的资源,这样新创建的AssetManager就含有了外部资源。
  2. 将AssetManager类型的mAssets字段的引用全部替换为新创建的AssetManager。

13.4 代码修复

代码修复主要有3个方案,分别是底层替换方案、类加载方案和Instant Run方案。

13.4.1 类加载方案

类加载方案基于Dex分包方案,什么是Dex分包方案呢?这个得先从65536限制和LinearAlloc限制说起。

1. 65536限制

随着应用功能越来越复杂,代码量不断地增大,引入的库也越来越多,可能会在编译时提示如下异常:

com. android. dex. DexIndexOverflowException: method ID not in [0, 0xffff] : 65536

这说明应用中引用的方法数超过了最大数65536个。产生这一问题的原因就是系统的65536限制,65536 限制的主要原因是DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits, 最多能引用65535个方法。

2. LinearAlloc 限制

在安装应用时可能会提示INSTALL_FAILED_DEXOPT,产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数超出了缓存区的大小时会报错。

为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。Dex 分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。

Dex分包方案主要有两种,分别是Google官方方案、Dex自动拆包和动态加载方案。

在12.2.3节中学习了ClassLoader的加载过程,其中一个环节就是调用DexPathList 的findClass的方法,如下所示:

 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//1
            Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。

多个Element组成了有序的Element数组dexElements。当要查找类时,会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组),注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。
根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案,如下图所示。

类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。
虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的:

  • QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载。
  • 微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。
  • 饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组。

采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的QFix、饿了么的Amigo和Nuwa等等。

13.4.2 底层替换方案

与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。

底层替换方案和反射的原理有些关联,就拿方法替换来说,

  1. 方法反射我们可以调用java.lang.Class.getDeclaredMethod.invoke()
  2. invoke方法是个native方法,对应Jni层的代码为:art/runtime/native/java_lang_reflect_Method.cc
  3. Method_invoke函数中又调用了InvokeMethod函数:art/runtime/reflection.cc
  4. 然后可以获取传入的javaMethod(Key的show方法)在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等
  5. ArtMethod结构中比较重要的字段是dex_cache_resolved_methods_和的entry_point_from_quick_compiled_code_,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。

AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。

底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。

13.4.3 Instant Run方案

除了资源修复,代码修复同样也可以借鉴Instant Run的原理, 可以说Instant Run的出现推动了热修复框架的发展。
Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码:

IncrementalChange localIncrementalChange = $change;//1
        if (localIncrementalChange != null) {//2
            localIncrementalChange.access$dispatch(
                    "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                            paramBundle });
            return;
        }
  1. 其中注释1处是一个成员变量localIncrementalChange ,它的值为\(change,\)change实现了IncrementalChange这个抽象接口。
  2. 当我们点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。
  3. 如果方法有变化,就生成替换类,
    1. 这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,
    2. 这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),
    3. 根据列表会将MainActivity的$change设置为MainActivity$override,因此满足了注释2的条件,会执行MainActivity$override的access$dispatch方法,
    4. accessdispatch方法中会根据参数'onCreate.(Landroid/os/Bundle;)V;',执行'MainActivityoverride'的onCreate方法,从而实现了onCreate方法的修改。

借鉴Instant Run的原理的热修复框架有Robust和Aceso。

13.5 动态链接库的修复

Android平台的动态链接库主要指的是so库,为了更好地理解,本章动态链接库简称为so。热修复框架的so的修复的主要是更新so,换句话说就是重新加载so,因此so的修复的基础原理就是加载。

13.5.1 System的load和loadLibarary方法

加载so主要用到了System类的load和loadLibarary方法。

  • System的load方法传入的参数是so在磁盘的完整路径,用于加载指定路径的so。
  • System的loadLibrary方法传入的参数是so的名称,用于加载App安装后自动从apk包中复制到/data/data/ packagename/lib下的so。

1. System的load方法

  1. Runtime.getRuntime()会得到当前Java应用程序的运行环境Runtime
  2. 调用了doLoad方法,并将加载该类的类加载器作为参数传入进去:
  3. doLoad方法会调用native方法nativeLoad

2. System的loadLibrary方法

  1. 先调用Runtime的loadLibrary0方法,loadLibrary0方法分为两个部分,一个是传入的ClassLoader不为null的部分,另一个是ClassLoader为null的部分
  2. 我们先来看ClassLoader 为null的部分。先遍历getLibPaths方法,这个方法会返回java.library.path选项配置的路径数组,然后拼接出so路径作为参数调用doLoad方法中。
  3. 当ClassLoader不为null时,先通过ClassLoader的findLibrary方法来得到fileName,然后调用doLoad 方法。
  4. findLibrary:ClassLoader的findLibrary方法在实现类BaseDexClassLoader中实现,然后会调用DexPathList的findLibrary方法,这和13.3.1 节讲到的DexPathList的findClass方法类似,
    1. 在NativeLibraryElement数组中的每一个NativeLibraryElement对应一个so库,
    2. 然后调用NativeLibraryElement的findNativeLibrary方法就可以返回so的路径。结合的类加载方案,就可以得到so的修复的一种方案,就是:
    3. 将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回,并调用Runtime的doLoad方法进行加载,
    4. 在doLoad方法中会调用native 方法nativeLoad。

13.5.2 nativeLoad方法分析

LoadNativeLibrary函数的行数很多,这里来做一个总结,LoadNativeLibrary 函数主要做了如下3方面工作:

  1. 判断so是否被加载过,两次ClassLoader是否是同一个,避免so重复加载。
  2. 打开so并得到so句柄,如果so句柄获取失败,就返回false。创建新的SharedLibrary,如果传入path对应的library 为空指针,就将新创建的SharedIibrary赋值给library, 并将library存储到libraries_中。
  3. 查找JNI_OnLoad 的函数指针,根据不同情况设置was_successful 的值,最终返回该was_successful。

讲到这里总结一下so修复主要有两个方案:

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

13.6 本章小结

2019/03/27 posted in  Android进阶解密

14 Hook技术

说到Hook技术得先提到逆向工程,逆向工程源于商业及军事领域中的硬件分析,其主要目的是在不能轻易获得必要的生产信息的情况下,直接从成品分析,推导出产品的设计原理。

逆向分析分为静态分析和动态分析,其中静态分析指的是一种在不执行程序的情况下对程序行为进行分析的技术;动态分析是指在程序运行时对程序进行调试的技术。

Hook技术就属于动态分析,它不仅在Android平台中被应用,早在Windows平台中就已经被应用了。

14.1 Hook技术概述

我们知道应用程序进程之间是彼此独立的,应用程序进程和系统进程之间也是如此,想要在应用程序进程更改系统进程的某些行为很难直接实现,有了Hook 技术,我们就可以在进程间进行行为更改,如图所示。

可以看到Hook可以将自己融入到它所要劫持的对象(对象B)所在的进程中,成为系统进程的一部分,这样我们就可以通过Hook来更改对象B的行为。被劫持的对象(对象B),称作Hook点,为了保证Hook的稳定性,Hook点一般选择容易找到并且不易变化的对象,静态变量和单例就符合这一条件。

14.2 Hook技术分类

Hook技术知识点比较多,因此Hook技术根据不同的角度会有很多种分类,这里介绍其中的三种分类。

  • 根据Hook的API语言划分,分为Hook Java和Hook Native。
    • Hook Java主要通过反射和代理来实现,应用于在SDK开发环境中修改Java代码。
    • HookNative则应用于在NDK开发环境和系统开发中修改Native代码。
  • 根据Hook的进程划分,分为应用程序进程Hook和全局Hook。
    • 应用程序进程Hook只能Hook当前所在的应用程序进程。
    • 应用程序进程是Zygote进程fock出来的,如果对Zygote进行Hook, 就可以实现Hook系统所有的应用程序进程,这就是全局Hook。
  • 根据Hook的实现方式划分,分为如下两种。
    • 通过反射和代理实现,只能Hook当前的应用程序进程。
    • 通过Hook框架来实现,比如Xposed, 可以实现全局Hook, 但是需要root。

Hook Native、全局Hook和通过Hook框架实现这些分类和插件化技术关联不大,本章主要需要学习的是Hook Java,想要更好地学习Hook Java,首先要了解代理模式。

14.3 代理模式

代理模式也叫委托模式,是结构型设计模式的一一种。在现实生活中我们用到类似代理模式的场景有很多,比如代购、代理上网、打官司等。

定义:为其他对象提供一种代理以控制对这个对象的访问称为代理模式。

在代理模式中有如下角色。

  • Subject: 抽象主题类,声明真实主题与代理的共同接口方法。
  • RealSubject: 真实主题类,定义了代理所表示的集体对象,客户端通过代理类间接调用真实主题类的方法。
  • Proxy: 代理类,持有对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行。
  • Client: 客户端类。

14.3.1 代理模式简单实现

14.3.2 动态代理的简单实现

从编码的角度来说,代理模式分为静态代理和动态代理。

  • 静态代理,在代码运行前就已经存在了代理类的class编译文件
  • 动态代理则是在代码运行时通过反射来动态地生成代理类的对象,并确定到底来代理谁。Java 提供了动态的代理接口InvocationHandler,实现该接口需要重写invoke方法。

  1. 创建代理类,实现InvocationHandler接口
  2. 客户端类代码:

    public class Client {
        public static void main(String[] args) {
    //创建LiuWangShu
    IShop liuwangshu = new LiuWangShu() ;
    //创建动态代理
    DynamicPurchasing mDynamicPurchasing = new DynamicPurchasing(liuwangshu) ;
    //创建LiuWangShu的ClassLoader
    ClassLoader loader = liuwangshu.getClass().getClassLoader() ;
    //动态创建代理类
    IShop purchasing = (IShop)Proxy.newProxyInstance(loader, new Class[] {IShop.class}, mDynamicPurchasing) ;
    purchasing.buy() ;
    }
    }

14.4 Hook startActivity方法

我们知道Hook可以用来劫持对象,被劫持的对象叫作Hook点,用代理对象来替代Hook点,这样我们就可以在代理上实现自己想做的操作。这里以Hook常用的startActivity方法来举例,startActivity 方法分为两个,如下所示:

  • startActivity (intent) ;
  • getApplicationContext () . startActivity (intent) ;

第一个是Actvity的startActivity方法,第二个是Context的startActivity 方法。

14.4.1 Hook Activity的startActivity方法

14.4.2 Hook Context的startActivity方法

14.4.3 Hook startActivity总结

14.5 本章小结

2019/03/27 posted in  Android进阶解密

15 插件化技术

插件化技术和热修复技术都属于动态加载技术。

15.1 动态加载技术

在Android传统开发中,一旦应用的代码被打包成APK并被上传到各个渠道市场,我们就不能修改应用的源码了,只能通过服务器来控制应用中预留的分支代码。但是很多时候我们无法提前预知需求和突然发生的情况,也就不能提前在应用代码中预留分支代码,这时就需要采用动态加载技术。

在应用程序运行时,动态加载一些程序中原本不存在的可执行文件并运行这些文件里的代码逻辑。可执行文件总的来说分为两种,一种是动态链接库so,另一种是dex相关文件(dex 以及包含dex的jar/apk文件)。

随着应用开发技术和业务的逐步发展,动态加载技术派生出两个技术,分别是热修复技术和插件化技术,其中热修复技术主要用来修复Bug, 插件化技术则主要用于解决应用越来越庞大以及功能模块的解耦。

15.2 插件化的产生

15.2.1 应用开发的痛点和瓶颈

  1. 业务复杂,模块耦合
  2. 应用间的接入
  3. 65536限制,内存占用大

15.2.2 插件化思想

15.2.3 插件化定义

插件化的客户端由宿主和插件两个部分组成。宿主就是指先被安装到手机中的APK,就是平常我们加载的普通APK。插件一般是指经过处理的APK.so和dex等文件,插件可以被宿主进行加载,有的插件也可以作为APK独立运行。

讲到这里就可以引出插件化的定义:将一个应用按照插件的方式进行改造的过程就叫作插件化。

在协作方面,插件可以由一个人或者一个小组来进行开发,这样各个插件之间,以及插件和宿主之间的耦合度会降低。应用间的接入和维护也变得便捷,每个应用团队只需要负责自己的那一部分就可以了。应用以及主dex的体积也会相应变小,间接地避免了65536限制。第一次加载到内存的只有淘宝主客户端,当使用到其他插件时才会加载相应插件到内存,这样就减少了内存的占用。

15.3 插件化框架对比

15.4 Activity插件化

四大组件的插件化是插件化技术的核心知识点,而Activity 插件化更是重中之重,Activity插件化主要有3种实现方式,分别是反射实现、接口实现和Hook技术实现。反射实现会对性能有所影响,主流的插件化框架没有采用此方式,关于接口实现可以阅读dynamic-load-apk的源码,目前Hook技术实现是主流,因此本章主要介绍Hook技术实现。

Hook技术实现主要有两种解决方案,一种是通过Hook IActivityManager来实现,另一种是Hook Instrumentation实现。在讲到这两个解决方案前,我们需要从整体上了解Activity的启动流程。

15.4.1 Activity的启动过程回顾

首先Launcher进程向AMS请求创建根Activity, AMS会判断根Activity所需的应用程序进程是否存在并启动,如果不存在就会请求Zygote进程创建应用程序进程。应用程序进程启动后, AMS会请求应用程序进程创建并启动根Activity。

普通Activity和根Activity的启动过程大同小异,不涉及应用程序进程的创建,与Launcher也没关系:

在应用程序进程中的Activity 向AMS请求创建普通Activity (步骤1),AMS会对这个Activity的生命周期和栈进行管理,校验Activity等。如果Activity 满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity (步骤2)。

15.4.2 Hook IActivityManager方案实现

AMS存在于SystemServer 进程中,我们无法直接修改,只能在应用程序进程中做文章。可以采用预先占坑的方式来解决没有在AndroidManifest.xml 中显式声明的问题,具体做法就是先使用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验。接着用插件Activity替换占坑的Activity。

  1. 注册Activity进行占坑
  2. 使用占坑Activity通过AMS验证
  3. 还原插件Activity
  4. 插件Activity的生命周期

15.4.3 Hook Instrumentation方案实现

Hook Instrumentation 实现同样也需要用到占坑Activity,与Hook IActivityManager实现不同的是,用占坑Activity替换插件Activity以及还原插件Activity 的地方不同。

15.4.4 总结

15.5 Service插件化

15.5.1 插件化方面Service与Activity的不同

我们需要了解在插件化方面Activity和Service有何不同:

  • Activity是基于栈管理的,一个栈中的Activity的数量不会太多,因此插件化框架处理的插件Activity 数量是有限的,可以声明有限的占坑Activity 来实现。除去硬件和系统限制,插件化框架处理的插件Service的数量可以是近乎无限的,无法用有限的占坑Service来实现。
  • 在Standard模式下多次启动同一个占坑Activity 可以创建多个Activity 实例,但是多次启动占坑Service并不会创建多个Service实例。
  • 用户和界面的交互会影响到Activity 的生命周期,因此插件Activity 的生命周期需要交由系统来管理,Hook IActivityManager方案中还原插件Activity 就是为了这一点。Service的生命周期不受用户影响,可以由开发者管理生命周期,没有必要还原插件。

15.5.2 代理分发实现

Activity插件化的重点在于要保证它的生命周期,而Service插件化的重点是保证它的优先级,这就需要用一个真正的Service 来实现,而不是像占坑Activity那样起一个占坑的作用。当启动插件Service时,就会先启动代理Service,当这个代理Service 运行起来之后,在它的onStartCommand等方法里面进行分发,执行插件TargetService 的onCreate等方法,这一方案就叫作代理分发。

  1. 启动代理Service
  2. 代理分发

15.6 ContentProvider插件化

15.6.1 ContentProvider的启动过程回顾

15.6.2 VirtualApk的实现

  1. VirtualApk 初始化
  2. 启动代理ContentProvider
  3. 代理分发

15.7 BroadcastReceiver的插件化

15.7.1 广播插件化思路

IntentFilter的,讲到这里我们得到了一个新思路,那就是将静态注册的BroadcastReceiver全部转换为动态注册来处理,虽然静态和动态的BroadcastReceiver 的生命周期不同,但是为了实现插件化,这个缺点显然不是关键问题。

15.7.2 VirtualApk的实现

15.8 资源的插件化

15.8.1 系统资源加载

15.8.2 VirtualApk实现

资源的插件化方案主要有两种:一种是合并资源方案,将插件的资源全部添加到宿主的Resources中,这种方案插件可以访问宿主的资源。另一 种是构建插件资源方案,每个插件都构造出独立的Resources,这种方案插件不可以访问宿主资源。VirtualApk 采用了以上两种方案。

15.9 so的插件化

so热修复主要有两种方案:

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

so的插件化的方案和so热修复第一种方案类似,简单来说就是将so插件插入到NativeLibraryElement数组中,并且将存储so插件的文件添加到nativeLibraryDirectories 集合中就可以了。

15.10 本章小结

2019/03/27 posted in  Android进阶解密

16 绘制优化

16.1 绘制性能分析

Android应用需要将自己的界面展示给用户,用户会和界面进行交互,界面的流畅度至关重要,这一节我们就来学习绘制性能分析,首先讲解绘制原理,接着介绍绘制性能分析的工具: Profile GPU Rendering、Systrace 和Traceview。

16.1.1 绘制原理

View的绘制流程有3个步骤,分别是measure、layout 和draw,它们主要运行在系统的应用框架层,而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger 服务来完成的。

绘制过程主要由CPU来进行Measure、Layout.、Record、Execute的数据计算工作, GPU负责栅格化、渲染。CPU和GPU是通过图形驱动层来进行连接的,图形驱动层维护了一个队列,CPU将displaylist添加到该队列中,这样GPU就可以从这个队列中取出数据进行绘制。

说到绘制性能就需要提到帧数这个概念。帧数就是在1秒时间里传输的图片的量,也可以理解为图形处理器每秒钟能够刷新几次,通常用FPS (Frames Per Second)表示。每一帧其实就是静止的图像,通过快速连续地显示帧便形成了运动的假象。如果画面在60fps则不会感觉到卡顿,如果低于60fps,比如50fps则会感觉到卡顿。要想画面保持在60fps,需要屏幕在1秒内刷新60次,也就是每16.6667ms 刷新一次(绘制时长在16ms以内)。

Android系统每隔16ms 发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps, 那什么是VSYNC呢? VSYNC是VerticalSynchronization (垂直同步)的缩写,是一种定时中断,一旦收到VSYNC信号,CPU就开始处理各帧数据。如果某个操作要花费24ms,这样系统在得到VSYNC信号时无法进行正常的渲染,会发生丢帧,用户会在32ms中看到同一帧的画面。

产生卡顿原因有很多,主要有以下几点:

  • 布局Layout过于复杂,无法在16ms内完成渲染。
  • 同一时间动画执行的次数过多,导致CPU或GPU负载过重。
  • View过度绘制,导致某些像素在同一帧时间内被绘制多次。
  • 在UI线程中做了稍微耗时的操作。
  • GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间。

16.1.2 Profile GPU Rendering

16.1.3 Systrace

16.1.4 Traceview

16.2 布局优化

16.2.1 布局优化工具

16.2.2 布局优化方法

16.2.3 避免GPU过度绘制

16.3 本章小结

2019/03/27 posted in  Android进阶解密

17 内存优化

17.1 避免可控的内存泄漏

17.1.1 什么是内存泄漏

每个应用程序都需要内存来完成工作,为了确保Android系统的每个应用都有足够的内存,Android系统需要有效地管理内存分配。当内存不足时,Android运行时就会触发GC,GC采用的垃圾标记算法为根搜索算法。

内存泄漏就是指没有用的对象到GC Roots 是可达的(对象被引用),导致GC无法回收该对象。

17.1.2 内存泄漏的场景

  1. 非静态内部类的静态实例
    1. 非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接地长期维持着外部类的引用,阻止被系统回收。
  2. 多线程相关的匿名内部类/非静态内部类
  3. Handler内存泄漏
  4. 未正确使用Context
  5. 静态View
  6. WebView
  7. 资源对象未关闭
  8. 集合中对象没清理
  9. Bitmap对象
  10. 监听器未关闭

17.2 Memory Monitor

17.2.1 使用Memory Monitor

17.2.2 大内存申请与GC

17.2.3 内存抖动

内存抖动一般指在很短的时间内发生了多次内存分配和释放,严重的内存抖动还会导致应用程序卡顿。内存抖动出现的原因主要是短时间频繁地创建对象(可能在循环中创建对象),内存为了应对这种情况,也会频繁地进行GC。非并行GC在进行时,其他线程都会被挂起,等待GC操作完成后恢复工作。如果是频繁的GC就会产生大量的暂停时间,这会导致界面绘制时间减少,从而使得多次绘制一帧的时长超过了16ms, 产生的现象就是界面卡顿。综合起来就产生了内存抖动,产生了锯齿状的
抖动图。

17.3 Allocation Tracker

17.3.1 使用Allocation Tracker

17.3.2 alloc文件分析

17.4 Heap Dump

17.4.1 使用Heap Dump

17.4.2 检测内存泄漏

17.5 内存分析工具MAT

17.5.1 生成hprof文件

17.5.2 MAT分析hprof文件

17.6 LeakCanary

17.6.1 使用LeakCanary

17.6.2 LeakCanary应用举例

17.7 本章小结

2019/03/27 posted in  Android进阶解密