Android 利用ContentProvider初始化组件踩到的坑

项目描述

先简单描述一下遇到的问题。

项目比较庞大是以组件化的形式进行构建的,记录崩溃日志是由专门的一个组件去做,这里且叫它crash吧。而crash的核心逻辑如下:

//伪代码
public class MyCrash implements UncaughtExceptionHandler {
	
	private static UncaughtExceptionHandler defaultUncaughtExceptionHandler;
	
    public static void init(String path) {
    	...
    	//获取到默认的ExceptionHandler
    	defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        //设置自己的ExceptionHandler
        Thread.setDefaultUncaughtExceptionHandler(new MyCrash());
    }
    @Override    
    public void uncaughtException(Thread t, Throwable e) {
        try {
            //日志记录逻辑
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        	//回调默认的ExceptionHandler
            if (defaultUncaughtExceptionHandler != null) {
                defaultUncaughtExceptionHandler.uncaughtException(t, e);
            }
        }
    }
}

然后该组件利用ContentProvider进行初始化,大概如下所示:

class MyContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        //伪代码,初始化。
        MyCrash.init("")
        return true
    }
    ......
}

问题排查

收到反馈说,部分手机不会记录崩溃日志。这就是很奇怪了,因为理论上来说,只要设置了ExceptionHandle都会捕获到传过来的异常呀。难道是没有设置到ExceptionHandle

后经过断点排查,不会上传崩溃日志的手机,在运行阶段Thread持有的defaultUncaughtExceptionHandler,不是我们设置的MyCrash,而是一个三方组件设置他们自己的CrashExceptionHandle且没有回调我们的MyCrash,而他们也是利用ContentProvider初始化的。

所以这时候就牵扯到ContentProvider的初始化流程了,具体在ActivityThread中,下面放一下伪代码。

ActivityThread
private void handleBindApplication(AppBindData data) {
	...
	//1.获取到Application
	app = data.info.makeApplication(data.restrictedBackupMode, null);
	...
	//2.初始化ContentProvider
	installContentProviders(app, data.providers);
	...
	//3.调用Application的onCreate
	mInstrumentation.callApplicationOnCreate(app);
	...
}

整体顺序是获取Application->初始化ContentProvider->调用Application#onCreate。也就是说ContentProvider的初始化是要在Application之前的。其中ContentProvider的初始化就是循环便利储存ContentProvider的集合调用它的onCreate方法。

private void installContentProviders(Context context, List<ProviderInfo> providers) {
    ...
    for (ProviderInfo cpi : providers) {
       //这里会获取到ContentProvider,最终会调用到ContentProvider的attachInfo,在attachInfo中调用了onCreate
    }
    ...
}

ContentProvider的初始化顺序就很清晰明了了。而我们的问题是部分手机记录不了,也就是说ContentProvider在集合中的顺序是不可保证的,这样才能解释部分手机有问题,部分手机正常,那这个顺序是怎么来的呢?

起初我想到顺序是不是和合并后的AndroidManifest.xml文件里面注册的ContentProvider节点顺序有关系,随后就将该想法排除,因为生成的APK是一样的,所以AndroidManifest.xml文件里面注册的ContentProvider节点顺序是一定的。而有问题的是部分手机,所以一定不是这里的问题,没办法只能继续查看源码,看看ContentProvider究竟是如何读到内存中的。

经过一番查找,发现ContentProvider的集合是从ComponentResolverprivate final ArrayMap<ComponentName, ParsedProvider> mProviders = new ArrayMap<>();取得。由于涉及到得源码比较多,这里就不一一列举了,下面放上源码大致的调用链

ActivityManagerService#attachApplicationLocked -> ActivityManagerService#generateApplicationProvidersLocked -> PackageManagerService#queryContentProviders -> ComponentResolver#queryProviders -> ActivityThread#bindApplication -> ActivityThread#handleBindApplication

我们注意一下重点,ContentProvider得信息是被储存在ArrayMap中得,而ArrayMap肯定是无法保证顺序的呀。不了解ArrayMap的下面我简单介绍一下,

ArrayMapGoogle专门提供的key-value映射集合,主要为了解决HashMap浪费控件的问题,在小数据量上性能不错,但是它底层是用数组来着,利用二分查找,而二分查找的顺序是根据hash值来的,默认的hash值是通过System.identityHashCode(key)来进行获取的,而这玩意儿又和对象的地址有关系。所以不同的手机顺序肯定就不一样了。

到这里,问题就分析结束了,最终解决方案是,去除了利用ContentProvider的初始化机制,改在Application中直接进行初始化。

总结

上面的问题虽然解决了,但是利用ContentProvider解耦初始化组件真的好吗?直观的有以下几个问题。

  1. 内存泄漏。初始化完成之后ContentProvider会被系统直接持有,无用,但也不删除
  2. 无法保证组件初始化的顺序。这个就是我们上面分析的问题
  3. 会拉长启动时间。上面我们看到了,ContentProvider循环初始化完成之后,才会进行Application#onCreate的调用,所以对于一些非必要在主线程初始化的组件,这无疑会拉长启动时间。

不过如果非要去解耦组件初始化,可以看一看Jetpack startup组件,它也是利用ContentProvider去初始化的,但是它利用AndroidManifest.xml合并的功能最终会合并成一个ContentProvider,而且内存维持有集合可以保证组件初始化顺序。

总之一句话,不要滥用ContentProvider仅仅去做一个初始化。

创作不易,如有帮助一键三连咯🙆‍♀️。欢迎技术探讨噢!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值