背景
在移动应用开发过程中,隐私保护是一项至关重要的工作。以往我们采用了一种动态隐私检查工具,通过xposed
方式实现,然而,这种方案存在着诸多限制。需要特定型号的手机和复杂的安装操作,不适用于集成到自动化测试系统中。
一、两种方案的比较
1、xposed 方式的实现
以往我们采用了xposed
框架实现了动态隐私检查功能。该方案通过修改Android
系统的运行时环境,拦截应用程序的方法调用,实现隐私检查。但该方案在使用过程中发现存在一些局限性:
- 对手机型号要求高,仅适用于部分支持
xposed
框架的手机型号。 - 操作复杂,需要用户安装多个软件并进行繁琐配置。
- 不适用于集成到自动化测试系统中进行测试。
2、插桩方式的实现
插桩的方式通过自定义gradlePlugin
,在项目构建过程中扫描项目中的类文件,并使用ASM
库定位隐私方法,修改类文件,以达到检查的目的。该方案的优势在于:
- 灵活性高,适用于各种手机型号。
- 操作简单,无需安装额外软件,可集成到自动化测试系统中进行测试。
综上所述,我们选择通过插桩方式来优化动态隐私检查,以解决xposed
方式存在的限制和不足。
二、方案实现
1、方案概述
在本方案中,我们采用新的 TransformApi
扫描项目中的所有类文件,利用ASM
库在这些class
文件中定位隐私方法。随后,在隐私方法执行结束时,我们通过插入一段代码来收集堆栈数据。一旦用户同意隐私政策,收集到的数据将被输出到JSON
文件中,以供进一步分析和处理。
2、方案设计
从 AGP1.5
开始,Transform API
一直是一个常用的工具,但在 AGP7.0
中已经被标记为废弃,并且在AGP8.0
将被移除。官方的替代方案没有一个统一的替代 API
。根据官方建议,有两种主要的方式来处理class
文件:一种是通过自定义 Task
结合使用 ASM
或者Javassist
,另一种是使用 AsmClassVisitorFactory
。
AsmClassVisitorFactory
集成了 ASM
操作,并根据官方说法,能够提高大约 18%
的编译速度,并且能够减少约 5
倍的代码量。它已经处理了增量逻辑,因此无需手动处理,只需进行一次 IO
操作,从而有效减少了IO
操作的次数。然而,需要注意的是,它与 ASM
字节码工具紧密绑定,不支持其他字节码修改工具。在实践中,我们最初尝试了自定义Task
加ASM
的方式,但需要处理增量编译的问题。后来我们转向了使用 AsmClassVisitorFactory
来处理,因为它能够更简单地对class
文件进行修改。
如图所示,我们自定义了两个关键模块:一个是名为gradle_plugin
的 Gradle
插件,另一个是privacy_check
功能模块。gradle_plugin
主要负责对class
文件进行插桩操作,而 privacy_check
则定义了需要插入的代码。
具体而言,gradle_plugin
任务的工作流程如下:
首先,它会将
privacy_check
模块中的检测隐私方法的collect
方法插入到需要检查的类文件中。然后,经过插桩处理的类文件会生成
transformedClass
。接下来,修改后的
transformedClass
将用于生成或更新class/jar
文件。class/jar
文件随后将被转换为.dex
文件,以便最终打包成APK
文件。
在应用运行时,用户同意隐私政策后,会调用 privacy_check
模块中的 save
方法。该方法的目的是收集检测到的隐私方法调用,并将结果生成为 result.json
文件。
3、功能实现
gradle_pllugin
中的主要实现
在 gradle_plugin
模块中,我们实现了一个名为 PrivacyClassVisitorFactory
的class
文件处理类,主要用于实现插桩功能,以下是该插件的关键功能类:
- PrivacyPlugin
PrivacyPlugin
是自定义的一个插件,主要功能就是为编译中的每个变体应用class
文件处理功能类:PrivacyClassVisitorFactory
,主要代码如下:
val androidComponents =
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)//
// Registers a callback to be called, when a new variant is configured
val extension = project.extensions.create("privacyCheck", PrivacyExtension::class.java)
androidComponents.onVariants { variant ->
if (!extension.enable) {
println("privacyCheck disable")
return@onVariants
}
variant.instrumentation.transformClassesWith(
PrivacyClassVisitorFactory::class.java,
InstrumentationScope.ALL
) {
}
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
}
- PrivacyClassVisitorFactory
PrivacyClassVisitorFactory
继承自AsmClassVisitorFactory,是class文件的处理类,它不用处理输入和输出,并且适配了增量编译,只需要定义ClassVisitor和是否需要处理当前class文件
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return PrivacyClassVisotor(
instrumentationContext.apiVersion.get(),
nextClassVisitor
)
}
//isInstrumentable:io.reactivex.internal.operators.observable.ObservableDetach$DetachObserver
//isInstrumentable:io.reactivex.internal.operators.observable.ObservableLastMaybe$LastObserver
override fun isInstrumentable(classData: ClassData): Boolean {
return !PrivacyPluginUtil.ignoreClass(classData.className)
}
- PrivacyClassVisitor
PrivacyClassVisitor
继承了ClassVisitor
,访问项目中class
文件中的每个方法,并对每一个方法在PrivacyMethodVisitor
类中进行处理,PrivacyMethodVisitor
继承了AdviceAdapter
,通过visitMethodInsn
方法收集隐私方法的调用,并在相应的位置插入收集到的隐私方法调用堆栈信息。下面是PrivacyMethodVisitor
中的关键代码:
override fun visitMethodInsn(
opcodeAndSource: Int,
owner: String?,
nameInsn: String?,
descriptor: String?,
isInterface: Boolean
) {
privacyMethodList.forEach { classMethod ->
if (owner == classMethod.className && nameInsn == classMethod.methodName) {
mv.visitLdcInsn("${className}#$name")
mv.visitLdcInsn("${owner.replace("/", ".")}#$nameInsn")
mv.visitMethodInsn(
INVOKESTATIC,
"com/xx/privacycheck/PrivacyCollectUtil",
"appendData",
"(Ljava/lang/String;Ljava/lang/String;)V",
false
)
}
}
super.visitMethodInsn(opcodeAndSource, owner, nameInsn, descriptor, isInterface)
}
}
其中privacyMethodList
是本地保存的待检测的隐私方法列表, 这里如果检测到了隐私方法的调用就把当前调用堆栈信息插入相应位置。
下面是 PrivacyMethodVisitor
类中的 visitFieldInsn
方法。该方法的主要作用是在类中查找每个方法的属性调用。当发现隐私方法调用时,会将其替换为代理类和方法。在代理方法中会插入代码,用于收集隐私方法信息,从而实现检测隐私属性调用的功能。其中ProxyBuild
类代理了系统Build
类。
override fun visitFieldInsn(
opcode: Int,
owner: String?,
nameField: String?,
descriptor: String?
) {
var filedOwner = owner
var filedName = nameField
privacyFieldList.forEach { field ->
if (owner == field.className && nameField == field.methodName) {
filedOwner = "com/xx/privacycheck/proxy/ProxyBuild"
filedName = field.methodName
}
}
super.visitFieldInsn(opcode, filedOwner, filedName, descriptor)
}
隐私方法的收集与保存
在 privacy_check
模块中,我们定义了隐私方法的收集和保存功能。正如前文所述,TransformClassWork
负责将检测隐私的方法插入相应的 class
文件中。当用户同意隐私政策后,调用 save
方法将检测结果输出为.json
文件,以供后续展示和保存使用。
3、插件性能
使用插件后,通过BuildAnalyzer
可以查看一次全量编译的时间如下所示:transformClassWithAsm
任务的执行时间为5s
。
在修改了一个类并且修改了一个布局视图的ID
后,增量编译的时间如下所示:
transformClassWithAsm
任务的执行时间为2.2s
。
由于我们的自动化测试只有在需要测试时才会启用该插件,因此在平时开发过程中,不会启用该插件,因此这些时间在实际开发中并不算长。
三、插件的使用
1. 引入插件
在使用过程中,需在 app/build.gradle.kts
中引用插件并开启检测开关。由于隐私检测不是频繁需要的功能,建议仅在需要测试时打开,以避免对实际用户的干扰和影响。同时隐私检测插件和插桩操作可能会对应用的性能和稳定性产生一定影响。因此,通过在测试环境中进行详细的检测和调整,可以确保在上线前解决潜在的隐私权限审核问题。
plugins {
id("com.xx.privacycheck")
}
android {
privacyCheck{
enable = true
}
}
在代码中,用户点击同意后,只有debug
环境才会输出检测结果
if(BuildConfig.DEBUG){
PrivacyCollectUtil.stopCollect(this@PreProcessingActivity)
}
2. 手动测试
运行应用程序,进入应用后点击“同意”。如果检测到隐私方法的调用,会在 Logcat
中输出相关信息,并将结果保存为 JSON
文件存储在存储卡中。
3. 测试结果
若仅作为上线前的测试,通过Logcat
输出即可判断是否存在隐私方法调用的问题。下图就是Logcat
中输出的检测到的隐私方法的调用
若需具体查看隐私方法的调用位置,需要检查存储卡中的详细数据。下图中的
monitorStackTrace
显示了具体的调用堆栈,可以看到该测试 APK
在点击“同意”前调用了 Build.MODEL
等属性。
如果集成到 APM
中进行自动测试,则可以通过 APM
后台获取存储卡中的数据,并在 APM
后台展示页面输出结果。APM
后台结果显示如下图所示,检测出来的每个结果,点击后是堆栈信息。
4. 结果处理
通过查看堆栈信息,找到调用隐私方法的位置,把隐私方法的调用放到点击同意隐私政策后初始化。
四、自动化测试的实现
我们采用了 Python
结合 uiautomator2
实现自动化测试。Python
程序负责实现待检测应用的安装和用户同意隐私政策的操作。我们的 APM
后台通过调用 Python
程序来执行隐私方法的检测和数据的保存和展示。
##需要开启开发者模式中的可模拟点击功能
if __name__ == '__main__':
print(sys.argv)
global adbPath
global apkPath
if len(sys.argv) == 3:
adbPath = sys.argv[1]
apkPath = sys.argv[2]
else :
print("python params error")
installApk()
startCheck()
loadJson()
上面是Python
程序的主要代码,执行这段python
程序需要在手机开发者模式中打开可模拟点击的功能,以便uiautomator2
可以执行自动化操作。installApk
方法执行了卸载和安装apk的操作,startCheck
是uiautomator2
对手机的操作,loadJosn
方法会把结果传递给APM
后台。这部分 Python
功能与 uiautomator2
的操作并不复杂,有兴趣的同学可以参考相关文档。
五、总结与展望
以上方案实现了动态隐私方法的检测,并且通过代理替换属性调用的方式,实现了系统隐私属性的监测,比如 android.os.Build.SERIAL
。未来,我们的插件还可以进一步优化,例如增加白名单功能,以排除某些功能模块不插入检测代码,从而提升插桩速度。尽管与之前的 xposed
方式相比,新方案具有更广泛的适用性和更好的扩展性,但仍有改进的空间。我们希望本文介绍的方案能够对大家的移动应用开发工作有所帮助。