0%

概述

Java字节码以二进制的形式存储在.class文件中,每一个.class文件包含一个Java类或接口。Javaassist就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过完全手动的方式生成一个新的类对象。

阅读全文 »

概述

最近在做一个在 Android 工程编译期间动态插入一些随机代码的需求,我选择的是 Gradle Transform 技术,想起好久没有写过博客了,就记录一下这方面的一些基本使用。

一般来说,在 Android 工程的编译期间可以通过一些技术来动态插入一些代码逻辑甚至生成一些新的 Class 类,具体技术有:

  • APT(Annotation Processing Tool): 编译期注解处理技术,通过自定义注解和注解处理器来实现编译期生成代码的功能,并且将生成的代码和源代码一起编译成 class 文件。
  • AspectJ: 是一种编译器,在编译期间,将开发者编写的 Aspect 程序织入到目标程序中,扩展目标程序的功能。
  • Transform&Javassist: Transform 是 Android Gradle 提供的操作字节码的一种方式。它在 class 编译成 dex 之前通过一系列 Transform 处理来实现代码注入。Javassist 可以方便地修改 .class 文件,关于 Javassist 的用法可以参考 Javassist用法

这里还可以看看 AOP 和 IOC 的一些概念,参考 AOP-IOC概述

Android Gradle 工具从 1.5.0-beta1 版本开始提供了 Transform API 工具,它可以在将 .class 文件转换为 dex 文件之前对其进行操作。可以通过自定义 Gradle 插件来注册自定义的 Transform,注册后 Transform 会包装成一个 Gradle Task 任务,这个 Task 在 compile task 执行完毕后运行。

阅读全文 »

文件格式

apk

APK(Android package):android安装包,由aapt(Android Assert Packaging Tool)把AndroidManifest.xml、资源文件、dex(二进制字节码)文件组合而成。

dex

dex(Dalvik VM Excutors):Dalvik虚拟机执行程序,执行前需要优化。

阅读全文 »

概述

RePlugin是360在2017年推出的插件化框架,其目的是让尽可能多的模块变成插件。RePlugin与其他插件化的特色在于它只Hook住了ClassLoader,One Hook这个坚持,最大程度保证了稳定性、兼容性和可维护性。

RePlugin项目地址:RePlugin,不过RePlugin上次更新已经是2019年7月了,且其没有支持androidx,网上有一个androidx版本的RePlugin,地址:replugin-androidx。虽然RePlugin已经停更,但是还是挺值得研究一下的。

阅读全文 »

启动原理

注:本文基于Android 9.0源码,为了文章的简洁性,引用源码的地方可能有所删减。Activity启动中涉及到的核心相关类有:

  • ActivityManagerService:由SystemServer进程创建,实体运行在Binder服务端,远程操作Activity的生命周期。
  • ActivityThread:ActivityThread通过和AMS进行IPC通信来共同管理Activity的生命周期。
  • ApplicationThread:ActivityThread的内部类,本质上ActivityThread是通过它来进行和AMS的IPC通信的。其实体运行在客户端,AMS通过代理类与ApplicationThread通信。
  • Instrumentation:这个类有点像整个操作链的最外层,是ActivityThread操作的具体操作类,Activity中持有它的引用,在这个类中调用Activity相关的生命周期回调。
  • ActivityStack(AMS中):Activity栈,由AMS管理,AMS通过这个数据结构来得知activity的状态。
  • ActivityStackSuperisor(AMS中):Activity栈的管理者,这个类的作用就是管理栈,并且通过ActivityStack来获得要启动的activity的信息。
  • ActivityRecord:服务端的Actiivty信息载体类,在ActivityStack里面存储的并不是Activity实例,而是ActivityRecord的实例。
  • TaskRecord:ActivityTask的信息记录类。

接下来从Activity的启动流程来分析Activity的相关原理。

阅读全文 »

Java类加载器

每个类有自己的类加载器,用户也可以自定义类加载器,同一个类文件被不同加载器加载后他们也不是同一个类,使用 isInstance() 等方法将会得到 false。Java类加载机制相关见:Java类加载机制

Android类加载器

概述

Android_ClassLoader_UML

简单介绍一下部分类加载器:

  • ClassLoader: 抽象类,其中定义了 ClassLoader 的主要功能,子类重写 findClass 方法即可。
  • BootClassLoader: 在 ClassLoader.java 中,使用默认修饰符,因此我们无法直接访问到它。它加载的是系统类,如 HashMap, Intent, Activity 等。
  • BaseDexClassLoader: 是 PathClassLoader 和 DexClassLoader 的父类,其内有一个 DexPathList 属性,实现了 findClass 方法逻辑,PathClassLoader 和 DexClassLoader 都只是在构造函数上对其做了简单封装而已。
  • PathClassLoader: 它是 App 加载自身 Dex 文件所用到的类加载器,如 MainActivity, AppCompatActivity 等,其 parent 是 BootClassLoader。
  • DexClassLoader: 可以加载 dex 或 apk 等文件,可用于执行动态加载,因此很多插件化方案都是采用 DexClassLoader。

相关参数:

  • dexPath: 包含类或者资源的 .jar/.apk/.dex 等路径,如果是多个路径,则用 : 分隔。
  • optimizedDirectory: 在 API 26(Android 8.0)的版本中,它表示 odex(optimized dex) 读写存放目录,如果传 null 则表示使用系统默认的目录来存储。自 Android 8.0 起,这个参数已经被弃用,不再生效,使用系统默认的目录。
  • librarySearchPath: native 库文件存放目录,多个库文件则用 : 分隔。
  • parent: 父类加载器。
  • isTrusted: 当前加载的 dex 是否受信任,如果受信任则可以访问平台隐藏的API,默认为 false。

ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public abstract class ClassLoader {
// --------系统默认使用的类加载器是PathClassLoader,parent为BootClassLoader
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}

static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}

private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

private final ClassLoader parent;

private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}

protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 判断当前类加载器是否已经加载过指定类,若已加载则直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果没有加载过,则调用parent的类加载递归加载该类
c = parent.loadClass(name, false);
} else {
// findBootstrapClassOrNull方法return null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 还没加载,则调用当前类加载器来加载
if (c == null) {
c = findClass(name);
}
}
return c;
}

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

BaseDexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}

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

// BaseDexClassLoader 实现了 findClass 方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实际上是通过 DexPathList 来加载类
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
// throw Exception;
}
return c;
}
}

BaseDexClassLoader 中有一个 DexPathList 类的 pathList 成员变量,它表示 dexPath 下的 .dex 列表。

PathClassLoader

1
2
3
4
5
6
7
8
9
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

PathClassLoader 构造函数很简单,直接调用父类 BaseDexClassLoader 的构造函数。第二个构造参数始终是 null,表示 optimizedDirectory 始终为 null。

DexClassLoader

1
2
3
4
5
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

DexClassLoader 跟 PathClassLoader 相似。和前面一样,第二个参数 optimizedDirectory 也从 Android 8.0 开始弃用,不再有效。

实例

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("LLL", "HashMap: " + HashMap.class.getClassLoader());
Log.d("LLL", "Intent: " + Intent.class.getClassLoader());
Log.d("LLL", "Activity: " + Activity.class.getClassLoader());
Log.d("LLL", "AppCompatActivity: " + AppCompatActivity.class.getClassLoader());
Log.d("LLL", "this: " + this.getClassLoader());
}
}

输出:

1
2
3
4
5
HashMap: java.lang.BootClassLoader@e8c0fdc
Intent: java.lang.BootClassLoader@e8c0fdc
Activity: java.lang.BootClassLoader@e8c0fdc
AppCompatActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/base.apk"],nativeLibraryDirectories=[/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/lib/arm64, /system/lib64, /vendor/lib64]]]
this: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/base.apk"],nativeLibraryDirectories=[/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/lib/arm64, /system/lib64, /vendor/lib64]]]

DexPathList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@hide
public final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";

private Element[] dexElements;
NativeLibraryElement[] nativeLibraryPathElements;

public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}

DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted);

this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
}
}
  • Element 用来描述一个 dex 文件所代表的元素。字段 dexElements 是 dex 文件元素列表,通过 makeDexElements() 方法来初始化。
  • NativeLibraryElement 用来描述一个库文件所代表的元素,字段 nativeLibraryPathElements 则为库文件元素列表,通过 makePathElements 方法来初始化。

热修复/插件化原理

类加载过程:以 PathClassLoader 加载为例,它会调用父类的 ClassLoader.loadClass() 方法,这个方法在上面已经看了,它遵循双亲委派原则,当 parent 加载不到时则调用自身的 findClass 方法,其父类加载器也是这个逻辑。因此会调用到 BaseDexClassLoader.findClass() 方法,这个方法会借助 DexPathList.findClass() 方法来加载。最终会调用 native 方法来查找 .dex 文件中相应的类,并在 native 层创建目标类的对象并添加到虚拟机列表。

在 DexPathList.findClass() 过程,一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。也就是说当两个类不同的dex中出现,会优先处理排在前面的dex文件,这便是热修复的核心精髓,将需要修复的类所打包的dex文件插入到dexElements前面。

热修复示例

此处反射获取PathClassLoader的过程与Android启动Application原理相关,具体过程可见:Android-Application,在理解了这部分源码后,便可知道下面为何要通过反射LoadedApk来获取PathClassLoader了。

反射工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public final class ReflectUtils {

private static Field getField(Class<?> cls, String fieldName) {
for (Class<?> acls = cls; acls != null; acls = acls.getSuperclass()) {
try {
final Field field = acls.getDeclaredField(fieldName);
setAccessible(field, true);

return field;
} catch (final NoSuchFieldException ex) {
}
}
Field match = null;
for (final Class<?> class1 : cls.getInterfaces()) {
try {
match = class1.getField(fieldName);
} catch (final NoSuchFieldException ex) {
}
}
return match;
}

public static Object readField(Object target, String fieldName) throws Exception {
return readField(target.getClass(), target, fieldName);
}

private static Object readField(Class<?> c, Object target, String fieldName) throws Exception {
Field f = getField(c, fieldName);

return readField(f, target);
}

private static Object readField(final Field field, final Object target) throws Exception {
return field.get(target);
}

public static void writeField(Object target, String fName, Object value) throws Exception {
writeField(target.getClass(), target, fName, value);
}

private static void writeField(Class<?> c, Object object, String fName, Object value) throws Exception {
Field f = getField(c, fName);
writeField(f, object, value);
}

private static void writeField(final Field field, final Object target, final Object value) throws Exception {
field.set(target, value);
}

private static void setAccessible(AccessibleObject ao, boolean value) {
if (ao.isAccessible() != value) {
ao.setAccessible(value);
}
}

public static Object combineArray(Object arrayLhs, Object arrayRhs) throws Exception {
Class<?> clazz = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = Array.getLength(arrayRhs);
int k = i + j;
Object result = Array.newInstance(clazz, k);
System.arraycopy(arrayLhs, 0, result, 0, i);
System.arraycopy(arrayRhs, 0, result, i, j);
return result;
}
}

修复工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class FixDexUtil {

private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";

private static final String PATCH_DIR = "patch";

private static HashSet<File> sLoadedDex = new HashSet<>();

public static boolean needFix(@NonNull Context context) {
sLoadedDex.clear();

// /data/data/包名/files/patch
File fileDir = new File(context.getFilesDir(), PATCH_DIR);
if (!fileDir.exists()) {
fileDir.mkdirs();
return false;
}
if (!fileDir.isDirectory()) {
fileDir.delete();
fileDir.mkdirs();
return false;
}

boolean canFix = false;
File[] listFiles = fileDir.listFiles();
if (listFiles == null) {
return false;
}
for (File file : listFiles) {
if ((file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX))) {
sLoadedDex.add(file);
canFix = true;
}
}
return canFix;
}

public static void fix(Application application) {
doFix(application, sLoadedDex);
}

private static void doFix(Application application, HashSet<File> loadedDex) {
try {
Context base = application.getBaseContext();
if (base == null) {
Log.d("LLL", "base == null");
return;
}

Object packageInfo = ReflectUtils.readField(base, "mPackageInfo");
if (packageInfo == null) {
Log.d("LLL", "packageInfo == null");
return;
}

// 这里也可以由context.getClassLoader()得到PathClassLoader
// 而loadedDex.getClass().getClassLoader()返回的是BootCLassLoader
ClassLoader classLoader = (ClassLoader) ReflectUtils.readField(packageInfo, "mClassLoader");
if (!(classLoader instanceof PathClassLoader)) {
Log.d("LLL", "classLoader == null");
return;
}

PathClassLoader pathLoader = (PathClassLoader) classLoader;

for (File dex : loadedDex) {
DexClassLoader dexLoader = new DexClassLoader(dex.getAbsolutePath(), null, null, pathLoader);

// 3.开始合并: 合并的目标是Element[], 重新赋值它的值即可

//3.1 准备好pathList的引用
Object dexPathList = ReflectUtils.readField(dexLoader, "pathList");
Object pathPathList = ReflectUtils.readField(pathLoader, "pathList");
//3.2 从pathList中反射出element集合
Object leftDexElements = ReflectUtils.readField(dexPathList, "dexElements");
Object rightDexElements = ReflectUtils.readField(pathPathList, "dexElements");
//3.3 合并两个dex数组
Object dexElements = ReflectUtils.combineArray(leftDexElements, rightDexElements);

Object pathList = ReflectUtils.readField(pathLoader, "pathList");
ReflectUtils.writeField(pathList, "dexElements", dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
public class PatchApplication extends Application {

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
if (FixDexUtil.needFix(this)) {
Log.d("LLL", "begin to fix");
FixDexUtil.fix(this);
}
}
}

需要修复的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 修复前
public class Util {
private static final String TAG = "LLL";

public static void test() {
Log.d(TAG, "first publish.");
}
}

// 修复后
public class Util {
private static final String TAG = "LLL";

public static void test() {
Log.d(TAG, "first publish.");
Log.d(TAG, "patch publish.");
}
}
  • 首先在手机上安装修复前的版本,调用Util.test(),会输出first publish.
  • 然后修改Util类,打包成apk,也可以只将Util类单独打包成dex,push到/data/data/pkg/files/patch下,由于我的测试机已经root,所以直接使用这个目录,否则可以添加从sd卡拷贝到私有目录的逻辑;
  • 重启后会加载修复包,多输出一行patch publish.
  • 打包成dex的命令:dx --dex --output=util.dex com\hearing\gopatch\Util.class

RePlugin

RePlugin原理解析

概述

AccessibilityService用于提供辅助功能服务,其在后台运行,并在触发AccessibilityEvents时由系统接收回调。此类事件表示用户界面中的某些状态转换,例如,焦点更改,按钮被单击等。此类服务可以可选地请求查询活动窗口内容的功能。

AccessibilityServiceInfo描述一个AccessibilityService,系统根据封装在此类中的信息将AccessibilityEvent通知给AccessibilityService。

AccessibilityService的生命周期仅由系统管理,并遵循Service的生命周期,用户只能通过在设备设置中显式打开服务来触发启动无障碍服务。系统绑定到服务后,它将调用AccessibilityService#onServiceConnected()。当用户在设备设置中将其关闭或调用AccessibilityService#disableSelf()时,AccessibilityService即会停止。

每个AccessibilityService在都是由AccessibilityManagerService注册的,当用户开启辅助服务后,系统会发送广播到AccessibilityManagerService,AccessibilityManagerService会注册AccessibilityService。当受到监控的App某个View发生了改变时,其内部会调用AccessibilityManager来发送Event。

声明

在Manifest文件中配置:

1
2
3
4
5
6
<service android:name=".MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>

配置

可以将AccessibilityService配置为接收特定类型的事件,仅侦听特定的程序包,在给定的时间范围内仅从每种类型获取事件一次,检索Window内容,指定设置Activity等。有两种配置的方法:

  1. 在Manifest中配置:

    1
    2
    3
    4
    5
    6
    <service android:name=".MyAccessibilityService">
    <intent-filter>
    <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" />
    </service>

    在res目录下新建xml目录,配置accessibilityservice.xml如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?xml version="1.0" encoding="utf-8"?>
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagReportViewIds|flagRequestTouchExplorationMode"
    android:canRetrieveWindowContent="true"
    android:description="@string/description"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm,com.eg.android.AlipayGphone" />

    与配置相关的可以参考官网:AccessibilityService

  2. 调用AccessibilityService#setServiceInfo(AccessibilityServiceInfo)进行配置,该方法任意时候都可以调用,用来动态改变该Service的配置。此方法仅允许设置动态可配置属性:

    • AccessibilityServiceInfo#eventTypes
    • AccessibilityServiceInfo#feedbackType
    • AccessibilityServiceInfo#flags
    • AccessibilityServiceInfo#notificationTimeout
    • AccessibilityServiceInfo#packageNames

Service Meta Data

属性 描述
android:description Descriptive text for the associated data
android:summary The summary for the item
android:settingsActivity Activity的Component name,允许用户修改此Service的设置
android:accessibilityEventTypes 监视的动作,见AccessibilityEvent
android:packageNames 监控的软件包名,使用逗号隔开
android:accessibilityFeedbackType 提供反馈类型,语音震动等等,见AccessibilityServiceInfo
android:notificationTimeout 两次相同类型的事件之间的间隔(以毫秒为单位)
android:accessibilityFlags 监视的view的状态,见AccessibilityServiceInfo
android:canRetrieveWindowContent 是否要能够检索活动窗口的内容,此设置不能在运行时改变
android:canRequestTouchExplorationMode 请求触摸模式的属性,在这种模式下,可以通过手势浏览UI
android:canRequestEnhancedWebAccessibility 请求增强的Web可访问性增强功能的属性
android:canRequestFilterKeyEvents 请求过滤关键事件的属性
android:canControlMagnification 控制显示倍率的属性
android:canPerformGestures 能否执行手势的属性
android:canRequestFingerprintGestures 能否从指纹传感器捕获手势的属性
android:nonInteractiveUiTimeout AccessibilityManager.getRecommendedTimeoutMillis(int,int)中使用的建议超​​时(以毫秒为单位),为不包含交互式控件的UI返回合适的值
android:interactiveUiTimeout AccessibilityManager.getRecommendedTimeoutMillis(int,int)中使用的建议超​​时(以毫秒为单位),为交互式控件的UI返回合适的值

Event types

  • AccessibilityEvent#TYPES_ALL_MASK
  • AccessibilityEvent#TYPE_VIEW_CLICKED
  • AccessibilityEvent#TYPE_VIEW_LONG_CLICKED
  • AccessibilityEvent#TYPE_VIEW_FOCUSED
  • AccessibilityEvent#TYPE_VIEW_SELECTED
  • AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED
  • AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED
  • AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED
  • AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START
  • AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END
  • AccessibilityEvent#TYPE_VIEW_HOVER_ENTER
  • AccessibilityEvent#TYPE_VIEW_HOVER_EXIT
  • AccessibilityEvent#TYPE_VIEW_SCROLLED
  • AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED
  • AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED
  • AccessibilityEvent#TYPE_ANNOUNCEMENT
  • AccessibilityEvent#TYPE_GESTURE_DETECTION_START
  • AccessibilityEvent#TYPE_GESTURE_DETECTION_END
  • AccessibilityEvent#TYPE_TOUCH_INTERACTION_START
  • AccessibilityEvent#TYPE_TOUCH_INTERACTION_END
  • AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUSED
  • AccessibilityEvent#TYPE_WINDOWS_CHANGED
  • AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED

Feedback types

  • AccessibilityServiceInfo#FEEDBACK_ALL_MASK
  • AccessibilityServiceInfo#FEEDBACK_AUDIBLE:表示可听(非语音)反馈
  • AccessibilityServiceInfo#FEEDBACK_HAPTIC:表示触觉反馈
  • AccessibilityServiceInfo#FEEDBACK_SPOKEN:表示语音反馈
  • AccessibilityServiceInfo#FEEDBACK_VISUAL:表示视觉反馈
  • AccessibilityServiceInfo#FEEDBACK_GENERIC:表示一般反馈
  • AccessibilityServiceInfo#FEEDBACK_BRAILLE:表示盲文反馈

Flags

  • AccessibilityServiceInfo#FLAG_ENABLE_ACCESSIBILITY_VOLUME:此标志请求由AudioManager.STREAM_ACCESSIBILITY控制系统范围内所有具有AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY的音频轨道。
  • AccessibilityServiceInfo#FLAG_INCLUDE_NOT_IMPORTANT_VIEWS:如果设置了此标志,通过View#IMPORTANT_FOR_ACCESSIBILITY_NO或View#IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS标记为对accessibility不重要的View,以及通过View#IMPORTANT_FOR_ACCESSIBILITY_AUTO标记为对accessibility潜在重要的View,在查询窗口内容时被报告,并且AccessibilityService也将从中接收事件。对于Android 4.1(API级别16)或更高版本的AccessibilityService,必须显式设置相关标志。
  • AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS:该标志请求AccessibilityService获得的包含源视图ID的AccessibilityNodeInfos。ID格式为"package:id/name"的标准资源名称,默认情况下未设置此标志。
  • AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON:系统的导航区域中将显示一个辅助功能按钮。
  • AccessibilityServiceInfo#FLAG_REQUEST_FILTER_KEY_EVENTS:该标志要求系统过滤关键事件。
  • AccessibilityServiceInfo#FLAG_REQUEST_FINGERPRINT_GESTURES:将所有指纹手势发送到AccessibilityService。想要设置此标志的服务必须声明具有检索窗口内容的功能,在meta-data中配置R.attr.canRequestFingerprintGestures,见SERVICE_META_DATA
  • AccessibilityServiceInfo#FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK。
  • AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE:该标志请求系统进入触摸浏览模式,系统将检测在触摸屏上执行的某些手势并通知此Service。在Android 4.3以上的设备必须声明canRequestTouchExplorationMode,见SERVICE_META_DATA
  • AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS:访问所有交互式窗口的内容。如果未设置此标志,服务将不会收到AccessibilityEvent.TYPE_WINDOWS_CHANGED事件,调用AccessibilityServiceAccessibilityService#getWindows()将返回一个空列表,而AccessibilityNodeInfo#getWindow()将返回null。必须声明canRetrieveWindowContent,见SERVICE_META_DATA

AccessibilityService

disableSelf

  • public final void disableSelf()
  • 关闭AccessibilityService服务。

findFocus

  • public AccessibilityNodeInfo findFocus(int focus)
  • 查找具有指定焦点类型的视图。
  • focus取值:AccessibilityNodeInfo#FOCUS_INPUT/AccessibilityNodeInfo#FOCUS_ACCESSIBILITY。

getAccessibilityButtonController

  • public final AccessibilityButtonController getAccessibilityButtonController()
  • 返回系统导航区域中的辅助功能按钮的控制器。
  • AccessibilityButtonController

getFingerprintGestureController

  • public final FingerprintGestureController getFingerprintGestureController()
  • 获取指纹手势的控制器。
  • FingerprintGestureController

getServiceInfo

  • public final AccessibilityServiceInfo getServiceInfo()

getRootInActiveWindow

  • public AccessibilityNodeInfo getRootInActiveWindow()
  • 获取当前活动窗口中的根节点。

AccessibilityNodeInfo

概述

此类表示Window Content中的一个Node。

addAction

  • public void addAction(AccessibilityNodeInfo.AccessibilityAction action)
  • 添加可以在节点上执行的操作。
  • AccessibilityAction

performAction

  • public boolean performAction(int action)
  • public boolean performAction(int action, Bundle arguments)

findAccessibilityNodeInfosByText

  • public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text)
  • 客户有责任调用AccessibilityNodeInfo#recycle()来回收接收到的信息,以避免创建多个实例。
  • 这里的text不单单是TextView的Text,还包括一些组件的ContentDescription。

findAccessibilityNodeInfosByViewId

  • public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(String viewId)
  • 客户有责任调用AccessibilityNodeInfo#recycle()来回收接收到的信息,以避免创建多个实例。
  • 组件的id获取可以通过Android Studio内置的工具Layout Inspector查看。
  • viewId:pkg:id/name,例如com.miui.securitycenter:id/am_detail_perm

AccessibilityEvent

概述

继承自AccessibilityRecord,表示当用户界面中发生了关注事件时系统发送的事件。

obtain

  • public static AccessibilityEvent obtain()
  • public static AccessibilityEvent obtain(int eventType)
  • public static AccessibilityEvent obtain(AccessibilityEvent event)
  • 返回一个缓存的实例(如果有)或实例化一个新实例。

recycle

  • public void recycle()

AccessibilityRecord

概述

表示AccessibilityEvent中的一条记录,并包含有关其Source View的状态更改的信息。

原理解析

概述

AccessibilityService相关类图如下:

AccessibilityService类图

AccessibilityService

在AccessibilityService的onBind方法中返回了一个IAccessibilityServiceClientWrapper对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public final IBinder onBind(Intent intent) {
return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() {
@Override
public void onServiceConnected() {
AccessibilityService.this.dispatchServiceConnected();
}

@Override
public void onInterrupt() {
AccessibilityService.this.onInterrupt();
}

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityService.this.onAccessibilityEvent(event);
}

@Override
public void init(int connectionId, IBinder windowToken) {
mConnectionId = connectionId;
mWindowToken = windowToken;

// The client may have already obtained the window manager, so
// update the default token on whatever manager we gave them.
final WindowManagerImpl wm = (WindowManagerImpl) getSystemService(WINDOW_SERVICE);
wm.setDefaultToken(windowToken);
}

// ...
});
}

Callbacks

该接口定义的方法与IAccessibilityServiceClient中是对应的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Callbacks {
void onAccessibilityEvent(AccessibilityEvent event);
void onInterrupt();
void onServiceConnected();
void init(int connectionId, IBinder windowToken);
boolean onGesture(int gestureId);
boolean onKeyEvent(KeyEvent event);
void onMagnificationChanged(@NonNull Region region,
float scale, float centerX, float centerY);
void onSoftKeyboardShowModeChanged(int showMode);
void onPerformGestureResult(int sequence, boolean completedSuccessfully);
void onFingerprintCapturingGesturesChanged(boolean active);
void onFingerprintGesture(int gesture);
void onAccessibilityButtonClicked();
void onAccessibilityButtonAvailabilityChanged(boolean available);
}

IAccessibilityServiceClient

IAccessibilityServiceClient是一个aidl接口,其方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
oneway interface IAccessibilityServiceClient {

void init(in IAccessibilityServiceConnection connection, int connectionId, IBinder windowToken);
void onAccessibilityEvent(in AccessibilityEvent event, in boolean serviceWantsEvent);
void onInterrupt();
void onGesture(int gesture);
void clearAccessibilityCache();
void onKeyEvent(in KeyEvent event, int sequence);
void onMagnificationChanged(int displayId, in Region region, float scale, float centerX, float centerY);
void onSoftKeyboardShowModeChanged(int showMode);
void onPerformGestureResult(int sequence, boolean completedSuccessfully);
void onFingerprintCapturingGesturesChanged(boolean capturing);
void onFingerprintGesture(int gesture);
void onAccessibilityButtonClicked();
void onAccessibilityButtonAvailabilityChanged(boolean available);
}

IAccessibilityServiceClientWrapper

IAccessibilityServiceClientWrapper继承自IAccessibilityServiceClient.Stub,且实现了HandlerCaller.Callback接口,很显然与Binder IPC相关。其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// IAccessibilityServiceClientWrapper
public static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback {
public IAccessibilityServiceClientWrapper(Context context, Looper looper,
Callbacks callback) {
mCallback = callback;
mCaller = new HandlerCaller(context, looper, this, true /*asyncHandler*/);
}

public void init(IAccessibilityServiceConnection connection, int connectionId,
IBinder windowToken) {
Message message = mCaller.obtainMessageIOO(DO_INIT, connectionId,
connection, windowToken);
mCaller.sendMessage(message);
}

public void onInterrupt() {
Message message = mCaller.obtainMessage(DO_ON_INTERRUPT);
mCaller.sendMessage(message);
}

public void onAccessibilityEvent(AccessibilityEvent event, boolean serviceWantsEvent) {
Message message = mCaller.obtainMessageBO(
DO_ON_ACCESSIBILITY_EVENT, serviceWantsEvent, event);
mCaller.sendMessage(message);
}

@Override
public void executeMessage(Message message) {
switch (message.what) {
case DO_ON_ACCESSIBILITY_EVENT: {
AccessibilityEvent event = (AccessibilityEvent) message.obj;
boolean serviceWantsEvent = message.arg1 != 0;
if (event != null) {
// Send the event to AccessibilityCache via AccessibilityInteractionClient
AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event);
if (serviceWantsEvent
&& (mConnectionId != AccessibilityInteractionClient.NO_ID)) {
// Send the event to AccessibilityService
mCallback.onAccessibilityEvent(event);
}
// Make sure the event is recycled.
try {
event.recycle();
} catch (IllegalStateException ise) {
/* ignore - best effort */
}
}
} return;

case DO_ON_INTERRUPT: {
if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
mCallback.onInterrupt();
}
} return;

case DO_INIT: {
mConnectionId = message.arg1;
SomeArgs args = (SomeArgs) message.obj;
IAccessibilityServiceConnection connection =
(IAccessibilityServiceConnection) args.arg1;
IBinder windowToken = (IBinder) args.arg2;
args.recycle();
if (connection != null) {
AccessibilityInteractionClient.getInstance().addConnection(mConnectionId,
connection);
mCallback.init(mConnectionId, windowToken);
mCallback.onServiceConnected();
} else {
AccessibilityInteractionClient.getInstance().removeConnection(
mConnectionId);
mConnectionId = AccessibilityInteractionClient.NO_ID;
AccessibilityInteractionClient.getInstance().clearCache();
mCallback.init(AccessibilityInteractionClient.NO_ID, null);
}
} return;

// ...
}
}
}

// HandlerCaller.Callback
public interface Callback {
public void executeMessage(Message msg);
}

AccessibilityInteractionClient

AccessibilityService用来进程跨进程通信,最终执行findAccessibilityNodeInfosByXXX和performAction的是AccessibilityInteractionClient。

其执行流程总结如下:

  1. AccessibilityInteractionClient没做什么操作,直接通过Binder调用了AccessibilityManagerService对应的方法。
  2. AccessibilityManagerService最终还是通过Binder调用了ViewRootImpl对应的方法。
  3. ViewRootImpl仅作为Binder中的服务端接收调用,真正的操作交给AccessibilityInteractionController来做。
  4. AccessibilityInteractionController对应的方法被调用之后,并没有直接进行操作,而是通过Handler做了一次转发,以便从Binder线程转到UI线程。
  5. 以performAction(ACTION_CLICK)点击事件为例,最终调用的实际是View的mOnClickListener。
  6. 以findAccessibilityNodeInfosByText为例,最终调用的实际是View的findViewsWithText方法,其方法内部实际对比的值是mContentDescription。需要特别说明的是TextView重写了该方法,其内部实际对比的值是mText。

AccessibilityInteractionClient流程图

流程总结

当View发生改变时,会发出一个AccessibilityEvent出来,这个Event会通过Binder驱动发送给IAccessibilityServiceClientWrapper,调用他的onAccessibilityEvent(AccessibilityEvent)方法,这个方法通过Handler发送了一个Message给自己,目的是为了从Binder线程转回主线程。然后调用了mCallback.onAccessibilityEvent(event),间接的调用了AccessibilityService.this.onAccessibilityEvent(event),即我们自己实现的方法。

AccessibilityService流程图

其外部调用的流程如下:

  1. 用户在设置页面启动了某个辅助模式服务;
  2. 系统发送了一条广播到AccessibilityManagerService,收到广播后,AccessibilityManagerService绑定了我们写的AccessibilityService,就这样调用了onBind方法。AIDL的Server端准备好了,AccessibilityManagerService是一个系统服务,由SystemService启动。
  3. 受到监控的App某个View发生了改变,其内部都会调用AccessibilityManager来发送event,其具体发送的对象是ViewRootImpl类来做的。
  4. 发出event后会通过Binder驱动调用到AccessibilityService,最终调用了我们复写的onAccessibilityEvent方法。
  5. 每一个View在AccessibilityService中都会被映射为一个AccessibilityNodeInfo对象,我们通过这个对象去查找具体View、触发事件,其本质是调用了AccessibilityInteractionClient类的对应方法。
  6. AccessibilityInteractionClient是一个可以执行accessibility交互的单例对象,它查询远程视图层次结构,查看视图的快照,以及来自这些层次结构的请求,以便在视图上执行某些操作。
  7. 如果利用AccessibilityInteractionClient操作正在被监控的App,比如点击按钮,那么View发生变化,又发送出一个Event,这样便形成一个循环。

AccessibilityService外部流程图

防御措施

检测辅助模式的开启

可以通过AccessibilityManagerService提供的方法获取所有的辅助模式应用:

1
2
3
4
5
6
public class AccessibilityManagerService extends IAccessibilityManager.Stub {
@Override
public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId) {
// ...
}
}

AccessibilityManagerService是com.android.server.accessibility包下的类,没有办法直接使用,可以通过AccessibilityManager来间接的操作AccessibilityManagerService,其内部利用Binder间接的调用了AccessibilityManagerService。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 取得正在监控目标包名的AccessibilityService
*/
private List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(String targetPackage) {
List<AccessibilityServiceInfo> result = new ArrayList<>();
AccessibilityManager accessibilityManager = (AccessibilityManager) getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager == null) {
return result;
}
List<AccessibilityServiceInfo> infoList = accessibilityManager.getInstalledAccessibilityServiceList();
if (infoList == null || infoList.size() == 0) {
return result;
}
for (AccessibilityServiceInfo info : infoList) {
if (info.packageNames == null) {
result.add(info);
} else {
for (String packageName : info.packageNames) {
if (targetPackage.equals(packageName)) {
result.add(info);
}
}
}
}
return result;
}

当info.packageNames为null时,表示监控所有包名。外挂有可能蒙混其中,但如果一刀切,也有可能误杀正常软件。

Event干扰

随机发送外挂感兴趣的Event出来,干扰其允许。代码如下:

1
textView.sendAccessibilityEvent(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);

屏蔽AccessibilityServices文案检查

在了解AccessibilityServices源码之后,我们知道其内部核心原理就是调用TextView的findViewsWithText方法,因此可以复写这个方法:

1
2
3
4
5
6
7
public class DefensiveTextView extends android.support.v7.widget.AppCompatTextView {

@Override
public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
outViews.remove(this);
}
}

屏蔽AccessibilityServices点击事件

像上面一样,通过源码了解原理之后,我们知道AccessibilityServices执行点击事件最终在调用View的mOnClickListener。因此可以利用onTouch代替onClick。

使用实例

开启辅助权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class AccessibilityHelper {

private static boolean hasAccessibility(Context context, Class serviceClass) {
if (context == null || serviceClass == null) {
return false;
}
int ok = 0;
try {
ok = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
}

TextUtils.SimpleStringSplitter ms = new TextUtils.SimpleStringSplitter(':');
if (ok == 1) {
String settingValue = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (settingValue != null) {
ms.setString(settingValue);
while (ms.hasNext()) {
String accessibilityService = ms.next();
if (accessibilityService.contains(serviceClass.getSimpleName())) {
return true;
}
}
}
}
return false;
}

public static void openAccessibility(Context context, Class service) {
if (context == null || service == null) {
return;
}

if (hasAccessibility(context, service)) {
return;
}
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

}

实现AccessibilityService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CallAccessibilityService extends AccessibilityService {

private static final String TAG = "CallAccessibility";

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.d(TAG, "onAccessibilityEvent: " + event.toString());
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> infos = nodeInfo.findAccessibilityNodeInfosByText("选择来电秀视频");
for (AccessibilityNodeInfo info : infos) {
info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}

}
}
}

@Override
public void onInterrupt() {
Log.d(TAG, "onInterrupt");
}
}

声明AccessibilityService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<service
android:name=".accessibility.CallAccessibilityService"
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>

<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility" />
</service>

配置

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeViewClicked"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagReportViewIds|flagRequestTouchExplorationMode"
android:canRetrieveWindowContent="true"
android:description="@string/description"
android:notificationTimeout="100"
android:packageNames="com.hearing.calltest" />