开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情
背景
去年11月,我们团队就已经宣布解散,但是由于隐私监管的问题,我们还得维护最后一个版本,满足隐私监管的要求。 我们团队的隐私问题主要是由我来负责,回想起这一年,真是被各种隐私问题折腾得死去活来, 所幸最后一个版本终于上线了,现针对隐私问题总结一下各种应对的方法。
隐私检测的流程
流程 1.0
在刚开始被要求隐私检测的时候,我们的流程是这样的:
在定包前提交隐私检测,然后根据检测结果进行相应的更改,但是在实际操作的过程中发现有以下问题:
- 提交隐私检测之后有新的代码提交,可能会造成新隐私问题;
- 检测出问题之后再改,测试时间短,风险大,可能会造成定包延期;
- 在定包前进行修改,测试没有足够的人力来支持;
因此在和 PM 讨论之后,我们将流程进行了一些变更。
流程 2.0
我们在每个版本定包之后系统测试的时候提交隐私检测,然后将检测出来的问题转换成下个版本的需求。
然而在实行一段时间之后,发现仍然有问题。
我们团队分为北京和深圳两个团队,一般情况下我们是每两周发一个版本,深圳和北京交替发布,比如深圳发布了 4.7.0,接下来北京发布 4.7.5。
通常情况下,我们在等隐私检测结果出来后,下一个版本的需求早就排好。如下图所示,隐私检测大概需要三天左右,当我们针对 4.7.0 版本提交隐私检测,拿到检测结果之后,4.7.5 版本的需求早就已经排好,此时想要跟 4.7.5 版本就比较困难,只能将需求排进 4.8.0 版本。
所以第二个流程仍然有以下问题:
- 发版较为频繁,470 检测出来的问题,475 无法解决,需延迟到 480 才能解决;
- 470 检测出来的问题,475 检测出来仍然有同样的问题,浪费检测资源;
- 在系统测试期间,需要手动提交隐私检测,增加测试工作量;
在和隐私检测的同事讨论之后,我们对流程又进行了一些变更。
流程 3.0
既然隐私检测无法避免,与其被动的接受检测,不如主动的拥抱变化;
具体包含以下几项:
- 针对存量的隐私问题,我们仍然使用流程 2.0 的方式解决;
- 针对新的需求,我们和 PM 讨论之后,在产品内审和需求评审阶段要求增加隐私自查,尽可能将隐私问题提前暴露,包括以下几项:
| 检查项 | |
|---|---|
| 1 | 该需求是否需要调用权限,是已有权限还是之前没有标注写明过的权限,同时需要在需求文档陈述若用户拒绝授权后的流程 |
| 2 | 该需求会不会接入新的 sdk,需要进行 sdk 明示 |
| 3 | 该需求是否有其他的数据分享/共享情况,需要文字补充 |
- 流水线引入隐私检测
我们在release版的流水线增加提交隐私检测的步骤,
这一步会在首次自动化测试通过之后执行,会首先判断该版本是否已经提交检测,若已经提交,则不再提交,若未提交,则会将对应的 apk 上传至检测平台。
除了流程上的问题,还有各种技术层面的问题。
应对隐私检测的各种姿势
1. 提前获取数据和获取频率
针对提前获取数据的这种情况,我创建了一个类 PrivacyInit, 在用户未授权时,先将需要初始化的代码放到 PrivacyCallBack 里面,等用户授权之后再初始化; 若用户已经授权,则直接执行。
//用户授权之后调用
public static void privacyAllInit() {
for (PrivacyCallBack callBack : callBackList) {
try {
callBack.call();
} catch (Exception e) {
e.printStackTrace();
ABCLog.e(TAG, e.getMessage());
}
}
}
//判断是否授权,若未授权则先不初始化
public static void privacyInit(PrivacyCallBack callBack) {
if (isAuthorized()) {
try {
if (callBack != null) {
callBack.call();
}
} catch (Exception e) {
e.printStackTrace();
ABCLog.e(TAG, e.getMessage());
}
} else {
callBackList.add(callBack);
}
}
//隐私回调接口
public interface PrivacyCallBack {
void call();
}
针对获取频率的问题就比较简单,直接使用一个全局的静态变量或者单例保存相关的隐私数据即可,避免多次获取。
2. 权限问题
新的隐私规范要求我们每次使用某个权限的时候,均需要给出弹窗提示,然后才能真正的进行权限申请。 如果针对每个场景均进行覆盖和修改,显然成本比较大,不过好在之前针对权限申请做过收拢操作。 Android 原生申请权限的方式如下: 通常我们在某个地方申请了权限,然后在对应的 onRequestPermissionsResult 里面拿到权限申请的结果
public class xxxActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//申请权限
this.requestPermissions(permissions, 100);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
//获取权限申请的结果
}
}
这样做的坏处就是申请权限和获取结果分散在不同的地方,另外不同的 Activity 申请权限,不方便统一管理。 因此我们创建了一个透明的 Activity 专门用于申请权限,然后申请到的结果通过回调的方式返回。
<activity
android:name="***.***.***.PermissionActivity"
android:theme="@style/Transparent" >
</activity>
最后实现的结果如下:
//申请权限
PermissionHelper.with(context).permission(Manifest.permission.CAMERA)
.callBack((int requestCode, String[] permissions, int[] grantResults) -> {
//得到权限申请的结果
}).request();
同时,由于所有的权限申请被收拢在一个地方,因此可以很方便的应对权限监管的各种花里胡哨的需求。
3. sdk问题
如果隐私检测出 sdk 有隐私问题,我们都会直接找到对应的团队,让他们帮忙解决,更新 sdk 的版本。 但是也有一些特殊的情况,比如 sdk 已经不维护了。
反编译
遇到这种情况,内心特别崩溃,若是去掉 sdk,可能会引发现有功能的异常,若是引入新的 sdk,针对最后一个版本的场景,改动成本会比较大, 如何以最小的成本来解决隐私问题呢? 我想到了反编译。 可以将jar包反编译成 smali,修改 smali,然后将其编译成新的 jar 包,替换掉原来的 jar 包就可以了。 不过这不是一个常规的手段,慎用!
终极大招
反编译毕竟不是一个常规的手段,而且需要修改 smali 代码,对应的语法也不熟悉,但是没有关系,我们还可以使用 Transfrom,直接修改 sdk 里面的代码。 通常情况,我们遇到 sdk 的隐私问题可以分为两类,访问了隐私相关的变量或者访问了隐私相关的方法。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {
println("getdevice:"+Build.DEVICE)//访问了隐私相关的变量
println("getImei:"+getImei())//访问了隐私相关的方法
}
}
1. 访问隐私相关的变量 针对这种情况,我们可以在 AdviceAdapter 当中复写 visitFieldInsn 方法,当访问到对应的字段时可以直接返回我们自己设定的变量
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
if ("android.os.Build".equals(owner) || "DEVICE".equals(name) ) {
//替换成我们自己的变量
super.visitFieldInsn(opcode, "***/***/***/Privacy", "device", "Ljava/lang/String;");
} else {
super.visitFieldInsn(opcode, owner, name, descriptor);
}
}
经过替换,我们可以发现,访问 android.os.Build.Device 字段已经被替换成 Privacy.device 了
2. 访问隐私相关方法 假定 MainActivity.getImei() 方法是一个隐私相关的方法,我们想把它替换成 Privacy.newGetImei() 方法, 我们可以在 AdviceAdapter 当中复写 visitMethodInsn 方法,如下所示,可以替换成我们想要的方法。
@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
if ("getImei".equals(methodName) && "()Ljava/lang/String;".equals(descriptor)&&""***/"***/"***/MainActivity".equals(owner)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "***/"***/"***/Privacy", "newGetImei", "()Ljava/lang/String;", false);
} else {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
}
}
经过替换,我们发现访问的方法已经变成 Privacy.newGetImei() 了
总结
隐私合规已经告一段落,这中间让我感到很崩溃的是,隐私的政策一直在变,比如刚开始并未把 AndroidID 纳入隐私字段,后面加上了,再后来访问频次也加入了检测。 还有就是隐私检测每次检测出来的问题都不一样,比如第一次检测出 ABC 问题,我们改了之后,又检测出 DE 问题,导致问题很难得到收敛。 不过,最黑暗的时刻已经过去了。 另外感谢一直并肩作战的产品同学,虽然都已经活水到了其他部门,但是我们坚持到了最后,这个需求我感觉我们已经做出了革命的友谊😄 。