安全合规检测,说App未经用户同意,存在获取软件安装列表信息的行为。
原因是firebase_performance库中,在初始化的时候会去获取软件安装列表,判断当前是否是主进程。第一篇文章已经介绍了解决方案,这里主要介绍如何定位问题、再去看如何解决问题。
解决方案支持原生、flutter库,RN库。
先把解决方案放在最上面,不想看详细的过程就直接复制粘贴使用吧。
在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"/>
...
</>
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
堆栈信息可以向检测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启动的时候就初始化了,并改为通过代码层,等到用户同意后再初始化。
比如一些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开发,那就按照上面的分析,找到对应位置延时调用初始化函数即可。发现只要使用以下代码就能初始化,那我只要把这段代码等到用户同意App获取软件安装列表之后。
FirebasePerformance _performance = FirebasePerformance.instance;
后续又经过一轮检查,FirebasePerformance引发的合规问题并没有解决。
Flutter层延迟初始化不行,此时陷入苦思,从堆栈信息太难定位问题,没有一个很明确的函数调用能知道是从哪里开始的。去google搜了一圈也没有找到firebase_performance初始化的操作。
然后想到用debug断点的方式,通过断点的位置查看调用栈,是不是就能定位到问题。于是有了以下2个技巧:

从检测机构给出的调用栈栈顶,再去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篇文章做介绍,看懂就知道为什么了。
到了这一步,那就可以尝试解决问题,
在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类,为什么这个类会被执行,
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()类。
这个其实就是一个FirebasePerformance的初始化中间类,其中实现的onAppColdStart()方法中,正式调起FirebasePerformance的初始化。
把FirebasePerformanceInitializer作为参数传递给appStateMonitor,交由appStateMonitor在合适的时机调用。
public final class FirebasePerformanceInitializer implements AppColdStartCallback {
public FirebasePerformanceInitializer() {
}
public void onAppColdStart() {
FirebasePerformance.getInstance();
}
}
注册冷启动要执行的代码,内部就是用set存储起来要执行初始化的代码,在这里来说,就是把FirebasePerformanceInitializer()存储起来。
private Set<AppStateMonitor.AppColdStartCallback> appColdStartSubscribers = new HashSet();
public void registerForAppColdStart(AppStateMonitor.AppColdStartCallback subscriber) {
synchronized(this.appStateSubscribers) {
this.appColdStartSubscribers.add(subscriber);
}
}
查看核心代码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(),取出存储在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();
}
}
}
}
注意这里使用了@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(Runtime runtime, Context appContext) {
...
this.currentProcessName = this.getCurrentProcessName();
}

这里就调用了getRunningAppProcesses()方法,造成安全合规问题。