文章目录
系列文章
- android使用ContentProvider初始化sdk,初始化时机
- Android ContentProvider初始化流程简化分析
- Android-Firebase快速解决合规问题第1篇,汇总篇,一步解决问题
- Android-Firebase快速解决合规问题第2篇,解决FirebasePerformance库获取软件安装列表的行为
- Android-Firebase快速解决合规问题第3篇,解决FirebaseCrashlytics库违规网络请求、获取AndroidId问题
- Android-Firebase快速解决合规问题第4篇,解决FirebaseAnalytics库违规获取应用列表问题
背景
安全合规检测,app启动,未征得用户同意,发现以下问题:
- 发起了网络请求https://firebase-settings.crashlytics.com/
- 违规收集android id
FirebaseCrashlytics在国内依然可以使用,手机不开谷歌服务,不开vpn也能正常收集到数据。
强烈推荐优先阅读,后续会有相关知识
依赖环境
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
解决方案
解决方案支持原生、flutter库,RN库。
先把解决方案放在最上面,不想看详细的过程就直接复制粘贴使用吧。
在AndroidManifest.xml中接入以下代码,重点在tools:node=“remove”,将这个provider移除掉。
<!--禁用FirebaseApp初始化 -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:exported="false"
tools:node="remove"/>
再参考android原生、Flutter、RN通过代码在合适的时机进行初始化即可。
Flutter初始化文档
依照上篇文章Android-Firebase快速解决合规问题第2篇,解决firebase_performance库获取软件安装列表的行为,我们应该是找到FirebaseCrashlytics对应的provider,再禁用掉,为什么这里直接把整个Firebase的核心初始化都禁用了。
原因:FirebaseCrashlytics没有提供的对应的provider,FirebaseCrashlytics的初始化是依赖核心FirebaseApp初始化的时候去加载FirebaseCrashlytics,所以我们只能从根源上解决。
堆栈信息
app启动,未征得用户同意,安全合规检测堆栈信息。
问题1:网络请求
通过抓包可以看到以下结果
GET /spi/v2/platforms/android/
Host: firebase-settings.crashlytics.com
问题2:获取android id
-> android.provider.Settings$Secure.getStringForUser()
-> android.provider.Settings$Secure.getString(Settings.java:5275)
-> com.google.firebase.crashlytics.internal.common.CommonUtils.isEmulator(CommonUtils.java:1)
...
-> com.google.firebase.crashlytics.internal.common.BackgroundPriorityRunnable.run(BackgroundPriorityRunnable.java:2)
-> java.lang.Thread.run(Thread.java:919)
分析问题
问题1:可以知道在app启动的时候就会发生网络流量请求,是在FirebaseCrashlytics库中发生。
问题2:从栈顶可以知道,最终是触发了getString方法获取android id,但是找不到从哪里调起来的,那就根据上一篇文章学到的方法,运行debug模式尝试定位问题。
可以看到,通过debug模式,得出的堆栈信息比第三方检测机构给出的信息更全。那就一层一层找,最顶上就是调用获取android id的方法,往下(往父级调用)寻找,doOpenSession()这个方法,再往下就找不到了,就在当前文件中寻找调用的地方。
找到了openSession() -> doOpenSession()这条链路,这时需要借助github的在线vscode查询源码,谁调用了openSession() 。经过层层向上级调用者查询,得到以下调用链:
FirebaseCrashlytics.init()初始化 -> CrashlyticsCore类中onPreExecute -> CrashlyticsController类中enableExceptionHandling() -> openSession() -> CommonUtils.isEmulator() -> getString 获取android id。
解决方案:以上两个问题,可以结合在一起,可能都与FirebaseCrashlytics的初始化有关,尝试延迟FirebaseCrashlytics的初始化。
解决问题思路
从Flutter代码层面,延迟初始化
既然使用了Flutter开发,那就按照上面的分析,找到对应位置延时调用初始化函数即可。
FirebaseCrashlytics crashlytics = FirebaseCrashlytics.instance;
经过检查,FirebaseCrashlytics引发的两个问题并没有解决。
Flutter层延迟初始化不行,那就尝试在原生层解决问题。
从原生层面排查
想办法延迟FirebaseCrashlytics初始化,依然是以下2个技巧:
- debug断点方式
- 利用github自带的在线vscode查看源码
依葫芦画瓢
从上一篇文章可以知道,我们只要把<provider> 中的内容去掉再通过代码的方式初始化,但发现FirebaseCrashlytics库中没有<provider> ,只有一个<service>标签,<service>表示注册该模块到FirebaseApp中,这样该模块才能使用,所以是不能去掉。
去掉的结果:
<service
android:name="com.google.firebase.components.ComponentDiscoveryService">
<meta-data
android:name="com.google.firebase.components:com.google.firebase.crashlyshlyticsRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar"
tools:node="remove"/>
</service>
在使用FirebaseCrashlytics会报错,找不到该模块。
java.lang.NullPointerException: FirebaseCrashlytics component is not present., null, null)
可以推测,在FirebaseCrashlytics库中,不是通过<provider> 标签的方式来初始化。
解决问题
先找到FirebaseCrashlytics.init()被调用的地方
可以看到FirebaseCrashlytics.init()的初始化,是在CrashlyticsRegistrar类中,并且这个类,就是上面通过<service>标签注册到FirebaseApp中的类。
再结合断点的时机,可以看到FirebaseApp.initializeApp()初始化后会走到FirebaseCrashlytics.init(),那就是说FirebaseCrashlytics依赖FirebaseApp的初始化。得到这个结论,那就尝试解决问题,把FirebaseApp的初始化延后,查看FirebaseApp相关源码
看到这个就说明FirebaseApp的初始化,依赖<provider> ,老规则移除该provider
<application>
<!--禁用FirebaseApp初始化 -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:exported="false"
tools:node="remove"
/>
</>
// Flutter
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform,);
然后在Flutter层,在合适的时机通过代码初始化FirebaseApp,顺势就会初始化FirebaseCrashlytics库,2个问题解决。
参考Flutter官方初始化方法
精益求精
上面通过逆向推导的方式,找到获取Android id的调用链,最终确定延迟FirebaseApp初始化,进而延迟FirebaseCrashlytics的初始化,解决FirebaseCrashlytics合规的2个问题。
但在解决问题中确有3个疑问:
- 为什么FirebaseCrashlytics没有使用<provider> 引导初始化,依然会在FirebaseApp.initializeApp()中被初始化。
- 网络请求是在FirebaseCrashlytics初始化发生的,那具体是什么时候发生、怎么发生的?
- FirebaseCrashlytics是如何违规收集android id?
1.FirebaseApp执行FirebaseCrashlytics.init()的过程
从FirebaseApp.initializeApp()开始分析,这个是Firebase的核心。主要分析FirebaseApp与FirebaseCrashlytics的初始化关系。
FirebaseApp
public static final @NonNull String DEFAULT_APP_NAME = "[DEFAULT]";
public static FirebaseApp initializeApp(@NonNull Context context) {
synchronized (LOCK) {
if (INSTANCES.containsKey(DEFAULT_APP_NAME)) {
return getInstance();
}
FirebaseOptions firebaseOptions = FirebaseOptions.fromResource(context);
if (firebaseOptions == null) {
return initializeApp(context, firebaseOptions);
}
}
@NonNull
public static FirebaseApp initializeApp(
@NonNull Context context, @NonNull FirebaseOptions options, @NonNull String name) {
final FirebaseApp firebaseApp;
Context applicationContext;
if (context.getApplicationContext() == null) {
applicationContext = context;
} else {
applicationContext = context.getApplicationContext();
}
synchronized (LOCK) {
// 重点1
firebaseApp = new FirebaseApp(applicationContext, normalizedName, options);
INSTANCES.put(normalizedName, firebaseApp);
}
//重点2
firebaseApp.initializeAllApis();
return firebaseApp;
}
可以看到会先从缓存Map里面取初始化过的FirebaseApp,没有的话就会用默认的名字创建一个再缓存起来。
protected FirebaseApp(Context applicationContext, String name, FirebaseOptions options) {
// 解析所有<service>标签的模块
List<Provider<ComponentRegistrar>> registrars =
ComponentDiscovery.forContext(applicationContext, ComponentDiscoveryService.class)
.discoverLazy();
// 执行
componentRuntime =
ComponentRuntime.builder(UI_EXECUTOR)
.addLazyComponentRegistrars(registrars)
.addComponentRegistrar(new FirebaseCommonRegistrar())
.addComponent(Component.of(applicationContext, Context.class))
.addComponent(Component.of(this, FirebaseApp.class))
.addComponent(Component.of(options, FirebaseOptions.class))
.build();
}
registrars会把有<service> 标签的模块解析出来,存在registrars里面。
// AndroidManifest.xml
<service android:name="com.google.firebase.components.ComponentDiscoveryService" android:exported="false">
<meta-data android:name="com.google.firebase.components:com.google.firebase.crashlytics.CrashlyticsRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>
ComponentRuntime
componentRuntime执行build()有条件的初始化模块
private void discoverComponents(List<Component<?>> componentsToAdd) {
List<Runnable> runAfterDiscovery = new ArrayList<>();
synchronized (this) {
Iterator<Provider<ComponentRegistrar>> iterator = unprocessedRegistrarProviders.iterator();
while (iterator.hasNext()) {
Provider<ComponentRegistrar> provider = iterator.next();
ComponentRegistrar registrar = provider.get();
if (registrar != null) {
// 这里就是调用了getComponents(),将模块的初始化方法添加进去
componentsToAdd.addAll(registrar.getComponents());
iterator.remove();
}
}
for (Component<?> component : componentsToAdd) {
Lazy<?> lazy =
new Lazy<>(
() ->
component
.getFactory()
.create(new RestrictedComponentContainer(component, this)));
components.put(component, lazy);
}
}
...
maybeInitializeEagerComponents();
}
componentsToAdd.addAll(registrar.getComponents()); 就会回调每个模块的getComponents(),而getComponents()中通过factory字段声明了FirebaseCrashlytics的初始化方法是哪个。
回到FirebaseApp
firebaseApp.initializeAllApis();
private void initializeAllApis() {
componentRuntime.initializeEagerComponents(isDefaultApp());
}
public static final @NonNull String DEFAULT_APP_NAME = "[DEFAULT]";
public boolean isDefaultApp() {
return DEFAULT_APP_NAME.equals(getName());
}
initializeEagerComponents表示需要立刻初始化的模块,比如FirebaseCrashlytics。
isDefaultApp()用来判断该FirebaseApp是否通过默认初始化得到的实例。
回到ComponentRuntime
private void doInitializeEagerComponents(
Map<Component<?>, Provider<?>> componentsToInitialize, boolean isDefaultApp) {
for (Map.Entry<Component<?>, Provider<?>> entry : componentsToInitialize.entrySet()) {
Component<?> component = entry.getKey();
Provider<?> provider = entry.getValue();
// 关键这里判断,是否需要立刻初始化模块
if (component.isAlwaysEager() || (component.isEagerInDefaultApp() && isDefaultApp)) {
// 在这里会执行CrashlyticsRegistrar.buildCrashlytics()方法
provider.get();
}
}
eventBus.enablePublishingAndFlushPending();
}
所以可以知道,provider.get();实际就是调用了以下方法,获取factory字段并执行相应模块的初始化方法。
component
.getFactory()
.create(new RestrictedComponentContainer(component, this)))
结合CrashlyticsRegistrar的配置,可以看到buildCrashlytics被执行后,开始初始化FirebaseCrashlytics。
CrashlyticsRegistrar中.eagerInDefaultApp()方法很关键,这个就是标记了该模块被注册后,执行FirebaseCrashlytics模块的初始化。所以不需要像其他模块使用<provider> 引导初始化。
完整链路
FirebaseApp.initializeApp() -> new FirebaseApp() 解析<service> 标签的模块,完成注册 ->
firebaseApp.initializeAllApis()加载需要立刻初始化的模块 -> CrashlyticsRegistrar buildCrashlytics()构建Crashlytics模块 -> FirebaseCrashlytics.init()初始化
总结:
- AndroidManifest.xml中使用<service>注册了模块信息。
<service android:name="com.google.firebase.components.ComponentDiscoveryService" android:exported="false">
<meta-data android:name="com.google.firebase.components:com.google.firebase.crashlytics.CrashlyticsRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>
- 在对应的类CrashlyticsRegistrar中,字段factory告知FirebaseCrashlytics库的初始化方法,eagerInDefaultApp()方法标记该库在FirebaseApp初始化完成后,立刻执行该模块的初始化。
public class CrashlyticsRegistrar implements ComponentRegistrar {
@Override
public List<Component<?>> getComponents() {
return Arrays.asList(
Component.builder(FirebaseCrashlytics.class)
.factory(this::buildCrashlytics)
.eagerInDefaultApp()
.build(),
LibraryVersionComponent.create("fire-cls", BuildConfig.VERSION_NAME));
}
}
- 这一段流程,就说明了,FirebaseCrashlytics没有使用<provider> 引导初始化,依然能够自动执行初始化的原因。
2.FirebaseCrashlytics初始化发起网络请求
GET /spi/v2/platforms/android/
Host: firebase-settings.crashlytics.com
网络请求是在FirebaseCrashlytics初始化发生的,那具体是什么时候发生、怎么发生的?
static FirebaseCrashlytics init(@NonNull FirebaseApp app, @NonNull FirebaseInstallationsApi firebaseInstallationsApi, @NonNull Deferred<CrashlyticsNativeComponent> nativeComponent, @NonNull Deferred<AnalyticsConnector> analyticsConnector) {
// 创建
final SettingsController settingsController = SettingsController.create(context, googleAppId, idManager, new HttpRequestFactory(), appData.versionCode, appData.versionName, arbiter);
// 网络请求获取数据
settingsController.loadSettingsData(threadPoolExecutor).continueWith(threadPoolExecutor, ...);
}
创建一个SettingsController,配置管理类,用来获取后台配置
SettingsController
private static final String SETTINGS_URL_FORMAT = "https://firebase-settings.crashlytics.com/spi/v2/platforms/android/gmp/%s/settings";
public static SettingsController create(Context context, ...) {
final String settingsUrl = String.format(Locale.US, SETTINGS_URL_FORMAT, googleAppId);
// 网络请求类
final SettingsSpiCall settingsSpiCall =
new DefaultSettingsSpiCall(settingsUrl, httpRequestFactory);
}
设置配置
DefaultSettingsSpiCall
将上面的url保存起来,
DefaultSettingsSpiCall(String url, HttpRequestFactory requestFactory, Logger logger) {
if (url == null) {
throw new IllegalArgumentException("url must not be null.");
} else {
this.logger = logger;
this.requestFactory = requestFactory;
this.url = url;
}
}
// 设置请求头
protected HttpGetRequest createHttpGetRequest(Map<String, String> queryParams) {
HttpGetRequest httpRequest = this.requestFactory.buildHttpGetRequest(this.url, queryParams);
return httpRequest.header("User-Agent", "Crashlytics Android SDK/" + CrashlyticsCore.getVersion()).header("X-CRASHLYTICS-DEVELOPER-TOKEN", "470fa2b4ae81cd56ecbcda9735803434cec591fa");
}
// 请求
public JSONObject invoke(SettingsRequest requestData, boolean dataCollectionToken) {
HttpGetRequest httpRequest = this.createHttpGetRequest(queryParams);
httpRequest = this.applyHeadersTo(httpRequest, requestData);
// 发出请求
HttpResponse httpResponse = httpRequest.execute();
toReturn = this.handleResponse(httpResponse);
}
网络请求时机
FirebaseCrashlytics init
开始网络请求配置,通过settingsController
settingsController.loadSettingsData(threadPoolExecutor).continueWith(threadPoolExecutor, ...);
SettingsController
settingsController.loadSettingsData(threadPoolExecutor).continueWith(threadPoolExecutor, ...){
JSONObject settingsJson = SettingsController.this.settingsSpiCall.invoke(SettingsController.this.settingsRequest, true);
}
走到settingsSpiCall专门负责网络请求类,发起网络请求。得到的配置信息,会解析、缓存起来,第二次启动app就直接从缓存中获取,不发起网络请求。返回的消息如下:
{
"settings_version":3,
"cache_duration":86400,
"features":{
"collect_logged_exceptions":true,
"collect_reports":true,
"collect_analytics":false,
"prompt_enabled":false,
"push_enabled":false,
"firebase_crashlytics_enabled":false,
"collect_anrs":true,
"collect_metric_kit":false
},
"app":{
"status":"activated",
"update_required":false,
"report_upload_variant":2,
"native_report_upload_variant":2
},
"fabric":{
"org_id":"xxx",
"bundle_id":"xxx"
},
"on_demand_upload_rate_per_minute":10,
"on_demand_backoff_base":1.2,
"on_demand_backoff_step_duration_seconds":60
}
完整链路
FirebaseCrashlytics init() -> settingsController.loadSettingsData()加载配置数据 -> settingsSpiCall.invoke()调用接口,发生网络请求。
总结:发起网络的请求的目的,是为了获取后台配置,时机是在FirebaseCrashlytics.init()初始化时。所以延迟FirebaseCrashlytics初始化就能解决问题。
3.FirebaseCrashlytics是如何获取android id?
static FirebaseCrashlytics init(@NonNull FirebaseApp app, @NonNull FirebaseInstallationsApi firebaseInstallationsApi, @NonNull Deferred<CrashlyticsNativeComponent> nativeComponent, @NonNull Deferred<AnalyticsConnector> analyticsConnector) {
// 构建线程池服务
ExecutorService crashHandlerExecutor = ExecutorUtils.buildSingleThreadExecutorService("Crashlytics Exception Handler");
final CrashlyticsCore core = new CrashlyticsCore(app, ..., crashHandlerExecutor);
...
final boolean finishCoreInBackground = core.onPreExecute(appData, settingsController);
}
public static ExecutorService buildSingleThreadExecutorService(String name) {
ThreadFactory threadFactory = getNamedThreadFactory(name);
// 创建的线程使用BackgroundPriorityRunnable类执行
ExecutorService executor = newSingleThreadExecutor(threadFactory, new DiscardPolicy());
addDelayedShutdownHook(name, executor);
return executor;
}
创建线程执行工具
public static ThreadFactory getNamedThreadFactory(final String threadNameTemplate) {
final AtomicLong count = new AtomicLong(1L);
return new ThreadFactory() {
public Thread newThread(final Runnable runnable) {
// 线程工厂类Executors.defaultThreadFactory()
Thread thread = Executors.defaultThreadFactory().newThread(new BackgroundPriorityRunnable() {
public void onRun() {
runnable.run();
}
});
thread.setName(threadNameTemplate + count.getAndIncrement());
return thread;
}
};
}
crashHandlerExecutor是单一的线程池 ,作为参数传递给CrashlyticsCore。
CrashlyticsCore
public CrashlyticsCore(FirebaseApp app, ...,ExecutorService crashHandlerExecutor) {
this.app = app;
this.crashHandlerExecutor = crashHandlerExecutor;
this.backgroundWorker = new CrashlyticsBackgroundWorker(crashHandlerExecutor);
}
将创建好的线程池,传递进来并保存起来。交给CrashlyticsBackgroundWorker去管理线程池。接着再init()中执行core.onPreExecute()
public boolean onPreExecute(AppData appData, SettingsDataProvider settingsProvider) {
// 创建CrashlyticsController,并把上面的backgroundWorker传递过去
this.controller = new CrashlyticsController(this.context, this.backgroundWorker, ...);
this.controller.enableExceptionHandling(Thread.getDefaultUncaughtExceptionHandler(), settingsProvider);
}
创建CrashlyticsController,并把上面的backgroundWorker传递过去。
settingsProvider其实就是FirebaseCrashlytics.init()中的settingsController用来管理配置信息、从网络获取配置。
CrashlyticsBackgroundWorker类
后面会用到,这里不用细看,只是做个记录。
class CrashlyticsBackgroundWorker {
private final Executor executor;
private Task<Void> tail = Tasks.forResult((Object)null);
private final Object tailLock = new Object();
private final ThreadLocal<Boolean> isExecutorThread = new ThreadLocal();
public CrashlyticsBackgroundWorker(Executor executor) {
this.executor = executor;
executor.execute(new Runnable() {
public void run() {
CrashlyticsBackgroundWorker.this.isExecutorThread.set(true);
}
});
}
public <T> Task<T> submit(Callable<T> callable) {
synchronized(this.tailLock) {
Task<T> toReturn = this.tail.continueWith(this.executor, this.newContinuation(callable));
this.tail = this.ignoreResult(toReturn);
return toReturn;
}
}
}
CrashlyticsController
将传递过来的CrashlyticsBackgroundWorker保存起来,并作为执行线程的工具。
CrashlyticsController(Context context, CrashlyticsBackgroundWorker backgroundWorker, ...) {
this.context = context;
this.backgroundWorker = backgroundWorker;
}
void enableExceptionHandling(UncaughtExceptionHandler defaultHandler, SettingsDataProvider settingsProvider) {
// 关键步骤
this.openSession();
...
}
void openSession() {
this.backgroundWorker.submit(new Callable<Void>() {
public Void call() throws Exception {
CrashlyticsController.this.doOpenSession();
return null;
}
});
}
给线程池添加一个任务,用来执行doOpenSession()
private void doOpenSession() {
...
OsData osData = createOsData(this.getContext());
...
}
private static OsData createOsData(Context context) {
return OsData.create(VERSION.RELEASE, VERSION.CODENAME, CommonUtils.isRooted(context));
}
CommonUtils
public static boolean isRooted(Context context) {
boolean isEmulator = isEmulator(context);
}
public static boolean isEmulator(Context context) {
String androidId = Secure.getString(context.getContentResolver(), "android_id");
return Build.PRODUCT.contains("sdk") || Build.HARDWARE.contains("goldfish") || Build.HARDWARE.contains("ranchu") || androidId == null;
}
最终在isEmulator()方法中调用Secure.getString获取android id。作用是用来判断当前设备是否模拟器。
完整链路
/com/google/firebase/crashlytics/FirebaseCrashlytics.java, FirebaseCrashlytics.init()方法中new CrashlyticsCore(),并把crashHandlerExecutor线程池作为参数传递给CrashlyticsCore,在CrashlyticsCore中又把crashHandlerExecutor传递给CrashlyticsBackgroundWorker。init()方法结束前CrashlyticsCore.java core.onPreExecute(appData, settingsController) -> 在内部初始化了CrashlyticsController(),并调用了controller.enableExceptionHandling() -> openSession() 方法发起了一个后台任务backgroundWorker(CrashlyticsBackgroundWorker) ,在后台任务中执行doOpenSession() -> createOsData() -> CommonUtils.isRooted -> isEmulator() -> 获取android id
总结:这样就找到获取android id的地方,也就是在FirebaseCrashlytics.init()初始化时,有这样操作。