文章目录
- 系列文章
- 背景
- 强烈推荐优先阅读,后续会有相关知识
- 依赖环境
- 解决方案
- 注意事项
- 堆栈信息
- getRunningAppProcesses的作用
- 解决问题思路
- 从Flutter代码层面,延迟初始化
- 从原生层面排查
- 如何解决
- 精益求精
- FirebasePerfProvider
- FirebasePerformanceInitializer
- AppStateMonitor
- registerForAppColdStart
- onActivityResumed
- sendAppColdStartUpdate
- FirebasePerformance
- GaugeMetadataManager
- 拓展
- 参考文章
- 20220901更新,补充内容
系列文章
- android使用ContentProvider初始化sdk,初始化时机
- Android ContentProvider初始化流程简化分析
- Android-Firebase快速解决合规问题第1篇,汇总篇,一步解决问题
- Android-Firebase快速解决合规问题第2篇,解决FirebasePerformance库获取软件安装列表的行为
- Android-Firebase快速解决合规问题第3篇,解决FirebaseCrashlytics库违规网络请求、获取AndroidId问题
- Android-Firebase快速解决合规问题第4篇,解决FirebaseAnalytics库违规获取应用列表问题
背景
安全合规检测,说App未经用户同意,存在获取软件安装列表信息的行为。 原因是firebase_performance库中,在初始化的时候会去获取软件安装列表,判断当前是否是主进程。第一篇文章已经介绍了解决方案,这里主要介绍如何定位问题、再去看如何解决问题。
解决方案支持原生、flutter库,RN库。
强烈推荐优先阅读,后续会有相关知识
- android使用ContentProvider初始化sdk,初始化时机
依赖环境
demo的环境如下,只是为了演示firebase出现的问题,本篇文章基于Flutter作为开发语言,实现了demo演示问题,原生库、RN库同理可以解决问题。
android版本:
build.gradle
compileSdkVersion 31
minSdkVersion 21
targetSdkVersion 31
futter版本:Flutter 2.10.5
pubspec.yaml
firebase_core: 1.10.0
firebase_messaging: 10.0.0
firebase_crashlytics: 2.2.0
firebase_analytics: 9.1.0
firebase_performance: 0.7.0+3
dio_firebase_performance: ^0.3.0
解决方案
先把解决方案放在最上面,不想看详细的过程就直接复制粘贴使用吧。
在AndroidManifest.xml中接入以下代码,重点在tools:node=“remove”,将这个provider移除掉。
<application>
...
<provider
android:authorities="${applicationId}.firebaseperfprovider"
android:exported="false"
android:initOrder="101"
android:name="com.google.firebase.perf.provider.FirebasePerfProvider"
tools:node="remove"/>
...
</>
注意事项
不仅是项目中使用了firebase_performance,还要注意第三方库中依赖了该项目。
比如:dio_firebase_performance中就依赖了firebase_performance。
为了方便排查第三方库依赖的库,可以使用以下命令
flutter pub deps
堆栈信息
堆栈信息可以向检测app的机构获取,比如小米、华为应用商店都有检测app是否存在合规问题,可以索要堆栈信息。以下是一段由firebase_performance引起问题的堆栈信息。
[
"android.app.ActivityManager.getRunningAppProcesses()",
"com.google.firebase.perf.session.gauges.GaugeMetadataManager.getCurrentProcessName(GaugeMetadataManager.java:3)",
...
"com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"
]
问题分析:firebase_performance使用了android系统的getRunningAppProcesses()方法,来判断当前运行的是否是主线程。getRunningAppProcesses方法能获取当前正在运行的进程信息,并且能从中知道正在运行的app有哪些,从而间接知道手机上安装了哪些应用,所以被工信部认定为不安全因素,需要告知用户使用的目的,等用户同意后才能使用。
堆栈分析:可以看到在android启动后,main()函数开始执行,最终由getRunningAppProcesses()引发问题。也就是说这个函数的触发时机在app启动的时候就发生了,1.可能是flutter层调用了该库相关代码(flutter代码是在主入口MainActivity中)。2.可能是原生层面触发了相关代码。
解决方案:既然是因为firebase_performance在android启动时就触发了getRunningAppProcesses(),那就找到firebase_performance是如何在app启动的时候就初始化了,并改为通过代码层,等到用户同意后再初始化。
getRunningAppProcesses的作用
比如一些sdk,为了防止应用多次执行初始化的代码我们只在主进程去做初始化的操作,那么如何判断一个进程是否为主进程就成了需要解决的问题。
// 获取正在运行的进程信息,找到自己进程的名字
public static String getProcessName(Context cxt) {
int pid = android.os.Process.myPid();
ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
if (runningApps == null) {
return null;
}
for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
if (procInfo.pid == pid) {
return procInfo.processName;
}
}
return null;
}
// 获取应用进程名Application的getProcessName方法
public static String getProcessName() {
return ActivityThread.currentProcessName();
}
//判断是否为主线程
public static boolean isMainProcess(Context context) {
try {
if (null != context) {
return context.getPackageName().equals(getProcessName(context));
}
} catch (Exception e) {
return false;
}
return true;
}
解决问题思路
经过上面分析,firebase_performance在android启动时就触发了getRunningAppProcesses()。那就要找到firebase_performance初始化的地方。
从Flutter代码层面,延迟初始化
既然使用了Flutter开发,那就按照上面的分析,找到对应位置延时调用初始化函数即可。发现只要使用以下代码就能初始化,那我只要把这段代码等到用户同意App获取软件安装列表之后。
FirebasePerformance _performance = FirebasePerformance.instance;
后续又经过一轮检查,FirebasePerformance引发的合规问题并没有解决。
Flutter层延迟初始化不行,此时陷入苦思,从堆栈信息太难定位问题,没有一个很明确的函数调用能知道是从哪里开始的。去google搜了一圈也没有找到firebase_performance初始化的操作。
从原生层面排查
然后想到用debug断点的方式,通过断点的位置查看调用栈,是不是就能定位到问题。于是有了以下2个技巧:
- debug断点方式
- 利用github自带的在线vscode查看源码
- firebase-android-sdk源码
- flutter_firebase_performance源码
从检测机构给出的调用栈栈顶,再去firebase_performance源码中找到相应位置,打个断点,通过debug模式运行,发现app启动的时候就走到这个断点了,结合Flutter层的代码无效,那由此可以推断出是由原生层面在app启动时就执行firebase_performance相关代码。
而且我还发现debug断点的调用栈,跟检测机构给出的调用栈基本是一样的。
如何解决
通过上面的思路分析,找到了突破口,那就结合debug断点继续定位,找到栈底慢慢往上找跟firebase_performance相关的函数。
我看到一个FirebasePerformanceInitializer类,里面执行了FirebasePerformance.getInstance();初始化的操作,这时借助github查看源码。
重大发现,在FirebasePerfProvider类中,重写了attachInfo()方法,并在其中new FirebasePerformanceInitializer()创建了初始化类。
这个FirebasePerfProvider继承自ContentProvider,在apk包中的AndroidManifest.xml中找到了相应的配置,利用了ContentProvider来做无侵入初始化,怪不得会在app启动的时候就自动加载了。
ContentProvider初始化有2篇文章做介绍,看懂就知道为什么了。
- android使用ContentProvider初始化sdk,初始化时机
- Android ContentProvider初始化流程简化分析
到了这一步,那就可以尝试解决问题,
在AndroidManifest.xml中接入以下代码,重点在tools:node=“remove”,将这个provider移除掉。
<application>
...
<provider
android:authorities="${applicationId}.firebaseperfprovider"
android:exported="false"
android:initOrder="101"
android:name="com.google.firebase.perf.provider.FirebasePerfProvider"
tools:node="remove"/>
...
</>
再用debug断点模式重启app,这时可以发现app不会触发断点,也就说明不会调用getRunningAppProcesses()方法了,至此firebase_performance库获取软件安装列表的行为的问题得以解决。
// 并不会执行
android.app.ActivityManager.getRunningAppProcesses()
不影响功能使用,最后还要在Flutter层,等到用户同意app收集软件安装列表的行为后,再执行FirebasePerformance的初始化。
FirebasePerformance _performance = FirebasePerformance.instance;
精益求精
已经知道如何解决问题了,那就顺着firebase_performance的思路,来看看最终是怎么在app启动时完成初始化,并调用到getRunningAppProcesses()方法。
FirebasePerfProvider
重点当然是FirebasePerfProvider类,为什么这个类会被执行,
public class FirebasePerfProvider extends ContentProvider {
public void attachInfo(Context context, ProviderInfo info) {
checkContentProviderAuthority(info);
super.attachInfo(context, info);
...
AppStateMonitor appStateMonitor = AppStateMonitor.getInstance();
appStateMonitor.registerActivityLifecycleCallbacks(this.getContext());
appStateMonitor.registerForAppColdStart(new FirebasePerformanceInitializer());
...
}
}
可以看到attchInfo执行后,初始化了AppStateMonitor,这是一个app状态管理类,从registerActivityLifecycleCallbacks可以知道,AppStateMonitor注册Activity生命周期回调。并且又在registerForAppColdStart中传递了一个FirebasePerformanceInitializer()类。
FirebasePerformanceInitializer
这个其实就是一个FirebasePerformance的初始化中间类,其中实现的onAppColdStart()方法中,正式调起FirebasePerformance的初始化。
把FirebasePerformanceInitializer作为参数传递给appStateMonitor,交由appStateMonitor在合适的时机调用。
public final class FirebasePerformanceInitializer implements AppColdStartCallback {
public FirebasePerformanceInitializer() {
}
public void onAppColdStart() {
FirebasePerformance.getInstance();
}
}
AppStateMonitor
registerForAppColdStart
注册冷启动要执行的代码,内部就是用set存储起来要执行初始化的代码,在这里来说,就是把FirebasePerformanceInitializer()存储起来。
private Set<AppStateMonitor.AppColdStartCallback> appColdStartSubscribers = new HashSet();
public void registerForAppColdStart(AppStateMonitor.AppColdStartCallback subscriber) {
synchronized(this.appStateSubscribers) {
this.appColdStartSubscribers.add(subscriber);
}
}
onActivityResumed
查看核心代码onActivityResumed(),前面已经调用了registerActivityLifecycleCallbacks注册Activity生命周期回调。那就是说等到app启动Activity时,AppStateMonitor就会收到onActivityResumed的回调。
public synchronized void onActivityResumed(Activity activity) {
if (this.activityToResumedMap.isEmpty()) {
...
if (this.isColdStart) {
this.sendAppColdStartUpdate();
this.isColdStart = false;
}
}
...
}
sendAppColdStartUpdate
执行到sendAppColdStartUpdate(),取出存储在appColdStartSubscribers中的类,依次执行,所以从这里开始相当于执行了FirebasePerformanceInitializer中的FirebasePerformance.getInstance(),自此开始了FirebasePerformance初始化。
private void sendAppColdStartUpdate() {
synchronized(this.appStateSubscribers) {
Iterator i = this.appColdStartSubscribers.iterator();
while(i.hasNext()) {
AppStateMonitor.AppColdStartCallback callback = (AppStateMonitor.AppColdStartCallback)i.next();
if (callback != null) {
callback.onAppColdStart();
}
}
}
}
FirebasePerformance
注意这里使用了@Inject依赖注入
@NonNull
// 初始化
public static FirebasePerformance getInstance() {
return (FirebasePerformance)FirebaseApp.getInstance().get(FirebasePerformance.class);
}
// 依赖注入
@Inject
@VisibleForTesting
FirebasePerformance(FirebaseApp firebaseApp, Provider<RemoteConfigComponent> firebaseRemoteConfigProvider, FirebaseInstallationsApi firebaseInstallationsApi, Provider<TransportFactory> transportFactoryProvider, RemoteConfigManager remoteConfigManager, ConfigResolver configResolver, GaugeManager gaugeManager) {
...
if (firebaseApp == null) {
...
} else {
...
gaugeManager.setApplicationContext(appContext);
...
}
}
}
GaugeMetadataManager
该类作用就是提供一些基础功能,其中有一个就是获取当前进程名,可以用来判断当前是否在主线程中。
GaugeMetadataManager(Runtime runtime, Context appContext) {
...
this.currentProcessName = this.getCurrentProcessName();
}
这里就调用了getRunningAppProcesses()方法,造成安全合规问题。
拓展
上面介绍了<provider>是初始化的关键,那<service>这个模块是干嘛的?
<service>这里面的<meta-data>使用类FirebasePerfRegistrar,标记了FirebasePerformance模块信息,也就表示FirebaseApp支持这个模块功能,如果把<service>下的内容去掉,表示FirebasePerformance模块被移除,在项目中使用代码初始化FirebasePerformance.getInstance()也会返回null。
所以正常使用的时候,<service>标签要保留,表示注册该模块。但要去掉<provider>标签内容,通过代码进行初始化。
一个完整的调用链
FirebasePerfProvider中进行注册registerForAppColdStart监听activity的生命周期
-> 调用firebase初始化FirebaseApp initializeApp()注册各个模块
-> FirebasePerfRegistrar$getComponents()在FireabaseApp进行注册,提供该模块信息
-> FirebasePerformanceInitializer$onAppColdStart()
-> FirebasePerformance.getInstance()
-> FirebaseApp.getInstance().get(FirebasePerformance.class)
-> 通过firebase种的依赖注入获取到走回到FirebasePerfRegistrar$providesFirebasePerformance()
-> 开始初始化FirebasePerformance(FirebaseApp firebaseApp, ...)
-> 最后走到getRunningAppProcesses()方法,造成安全合规问题。
所以可以知道
- 标签<provider>提供了FirebasePerformance库的初始化时机。
- 标签<service>将FirebasePerformance模块注册到FirebaseApp。
参考文章
- 在Android上控制firebase init
- android使用ContentProvider初始化sdk,初始化时机
- Android ContentProvider初始化流程简化分析
20220901更新,补充内容
FirebasePerfProvider类中,禁用了Provider初始化,还会有一些负面影响。先看一下完整的源码:
public class FirebasePerfProvider extends ContentProvider {
public void attachInfo(Context context, ProviderInfo info) {
checkContentProviderAuthority(info);
super.attachInfo(context, info);
// 配置解析器,提前初始化 ConfigResolver 以访问设备缓存层。
// Initialize ConfigResolver early for accessing device caching layer.
ConfigResolver configResolver = ConfigResolver.getInstance();
configResolver.setContentProviderContext(this.getContext());
// 这里是初始化
AppStateMonitor appStateMonitor = AppStateMonitor.getInstance();
appStateMonitor.registerActivityLifecycleCallbacks(this.getContext());
appStateMonitor.registerForAppColdStart(new FirebasePerformanceInitializer());
// 以下是监听app启动时间
AppStartTrace appStartTrace = AppStartTrace.getInstance();
appStartTrace.registerActivityLifecycleCallbacks(this.getContext());
this.mainHandler.post(new StartFromBackgroundRunnable(appStartTrace));
SessionManager.getInstance().updatePerfSession(ApplicationProcessState.FOREGROUND);
}
}
- 无法监听app启动时间。
// 以下是监听app启动时间
AppStartTrace appStartTrace = AppStartTrace.getInstance();
// 内部收集完App启动时间后,会自动取消监听周期回调
appStartTrace.registerActivityLifecycleCallbacks(this.getContext());
this.mainHandler.post(new StartFromBackgroundRunnable(appStartTrace));
监听app启动时间,这个功能是绑定在FirebasePerfProvider中,禁用后这个功能基本就没有用了。
如果能清楚知道FirebasePerformance收集数据时机、上报时机,再改动源码等到用户同意隐私政策后再上报,那还是可以用,但改动较大。
如果不在意这个功能,可以忽略
- 无法提前初始化 ConfigResolver 以访问设备缓存层。
// 配置解析器,提前初始化 ConfigResolver 以访问设备缓存层。
// Initialize ConfigResolver early for accessing device caching layer.
ConfigResolver configResolver = ConfigResolver.getInstance();
configResolver.setContentProviderContext(this.getContext());
ConfigResolver configResolver配置解析器,这里有个保存Context的操作,注释说是提前访问设备缓存层(不清楚这里包括什么数据)。估计是在统计app启动时间后,上报数据的时候会使用。
现在禁用了Provider初始化也就无法较早获取设备缓存层数据,等用户同意app隐私政策后,再执行FirebasePerformance的初始化(下面代码)。初始化中也有保存Context的操作,也就是在初始化后,该功能不受影响。
@Inject
FirebasePerformance(
FirebaseApp firebaseApp,
...
ConfigResolver configResolver,
SessionManager sessionManager){
this.configResolver.setApplicationContext(appContext);
}