性能优化之合并多个FileProvider
Editor's Note
介绍启动优化的文章很多,但这一篇的内容你可能没看过,一起来学习一下?
The following article is from 字节小站 Author 字节小站
1. 前言
最近在做Android App启动优化的工作,目前在快速定位耗时方法和合并多个ContentProvider两个方面取得了不错效果。合并多个ContentProvider很容易,但是合并多个FileProvider却很难。我发现目前网上没有合并多个FileProvider的教程,所以本文的合并多个FileProvider的方案是全网首创,一定会让你耳目一新。如果你觉得文章写的不错,帮忙分享给你的Android同事和朋友们。
说到合并多个ContentProvider,大家一定会想到Jetpack组件中的App Startup。本文借助了该库完成了多个ContentProvider的合并,本文的侧重点不在于如何使用App Startup。而在于如何合并多个FileProvider。
本文核心提纲如下:
项目启动过程遇到的问题 简述ContentProvider 简述App Startup使用 重点讲解FileProvider原理 反射方式将Uri映射硬编码 定义中转ContentProvider分发多个FileProvider的openFile请求 使用Aspectjx插桩hook掉FileProvider的getUriFromFile返回值
2.问题
测量启动耗时过程中发现项目中所有ContentProvider初始化的耗时总时间在150ms左右。为了确定这个时长是否耗时,于是我写了只有一个ContentProvider的Demo,发现耗时在20ms左右。由于相差了7倍,因此我认为项目中ContentProvider的初始化耗时是有优化的空间。通过查看编译后的AndroidManifest文件,发现声明了14个ContentProvider。从App Startup的设计初衷我们知道,它可以帮项目中的多个ContentProvider瘦身,将多个ContentProvider合并成一个ContentProvider。
在合并之前,我们需要思考一个问题。
❝我们自己写的ContentProvider和第三方sdk中定义的ContentProvider,都可以被合并吗?如果不是,那么什么样的ContentProvider可以被合并,什么样的又不能被合并呢?
❞
尽管ContentProvider是古老的四大组件之一,已经有10多年的历史了,可能有的读者对它比较陌生。所以为了回答这个问题,我们得简单了解一下ContentProvider的设计目的和实现原理。
3. ContentProvider浅析
ContentProvider是Android系统中一个元老级的组件,Android系统诞生之初,它就存在了。它有以下几个特性
需要在AndroidManifest文件中注册 系统自动初始化ContentProvider,调用onCreate方法 支持进程间通信 支持增删改查操作,一般是数据库操作,但不限于此 支持调用自定义的方法
3.1 简单Demo
新建一个ByteStationContentProvider代码如下:
在AndroidManifest清单中注册:
我们注意到ByteStationContentProvider有如下方法:
onCreate insert、query、update、delete call
给出结论如下:
「如果项目中的ContentProvider只是重写了onCreate方法是可以被合并的。如果重写了增删改查和call方法,是不能被合并的。」
3.2 ContentProvider的滥用现象
自从square团队在Picasso图片加载库中使用ContentProovider自动初始化sdk后,广大的sdk开发者也学到了这招,尽量减少上层开发者的sdk初始化操作。在几年前,这也是一项黑科技,满满的逼格。但是随着项目规模的增大,对接了越来越多类似的sdk,导致启动时需要初始化越来越多的ContentProvider,拖累了App的启动速度。
Picasso使用ContentProvider自动初始化代码如下。
我们注意到Picasso的insert、query相关方法是默认空实现,没有任何的业务逻辑。那么该ContentProvider可以合并掉。
3.3 ContentProvider初始化时机
ContentProvider.onCreate方法调用介于Application.attachBaseContext和Application.onCreate之间。
代码调用图
3.4 计算ContentProvider耗时
从3.3章节可知,App中所有ContentProvider的耗时等价于Application.onCreate()开始执行的时间减去Application.attachBaseContext()执行结束的时间。
3.5 查看编译后的AndroidManifest文件
反编译查看 app/build/intermediates/merged_manifests/debug/AndroidManifest.xml
4. App Startup使用
App Startup库是一个简单而且高效的应用启动初始化组件,它是基于ContentProvider实现的。由于本文重点在FileProvider的合并。所以本章节只是简单介绍它的使用。
App Startup使用分为3步:
添加依赖 实现Initializer组件 AndroidManifest文件添加声明
4.1 添加依赖
4.2 实现Initializer组件
实现Initializer接口要求重写两个方法:
create()方法中,我们可以把原先在ContentProvider中初始化的代码,放在这里。 dependencies()方法表示当前初始化,是否依赖其它的Initializer组件,如果依赖的话,会先初始化它们。
4.3 AndroidManifest文件添加声明
使用还是蛮简单的。更多信息请查看官方文档。https://developer.android.com/topic/libraries/app-startup
4.4 移除第三方sdk中存在的ContentProvider
假设第三方sdk的AndroidManifest文件中声明了一个名叫ShareContentProvider的Provider
要移除它,需要在app项目中的AndroidManifest中声明一个相同的Provider,并加上tools:node="remove"。
5. FileProvider浅析
5.1 从调用安装界面讲起
「Android6.0以及之前版本安装apk代码如下」
「Android7.0+安装apk代码如下」
自定义FileProvider
res/xml文件夹新建toutiao.xml文件
调用安装程序
「我们可以看到它们的区别在于分别使用Uri.fromFile()和FIleProvider.getUriFromFile()获取文件的Uri。」
「打印结果对应的值如下表:」
方式 | 值 |
---|---|
Uri | file:///storage/emulated/0/toutiao/toutiao.apk |
FileProvider | content://com.toutiao.install/bytedance/toutiao.apk |
「Uri方式获取到的文件路径很容易被猜出文件所在位置,这样暴露给第三方程序,可能会带来风险。而FileProvider获取到的文件路径就不容易暴露文件所在位置。引入FileProvider机制的原因就是为安全考虑。」
5.2 xml的tag对应的文件存储位置
「下图表示各种tag对应的文件路径」
NAME | VALUE | PATH |
---|---|---|
TAG_ROOT_PATH | root-path | / |
TAG_FILES_PATH | files-path | /data/user/0/com.xxx/files |
TAG_CACHE_PATH | cache-path | /data/user/0/com.xxx/cache |
TAG_EXTERNAL | external-path | /storage/emulated/0 |
TAG_EXTERNAL_FILES | external-files-path | /storage/emulated/0/Android/data/com.xxx/files |
TAG_EXTERNAL_CACHE | external-cache-path | /storage/emulated/0/Android/data/com.xxx/cache |
5.3 xml中Uri和路径的映射表
FileProvider的sCache
FileProvider有一个静态变量sCache。key存放的是FileProvider的authority,value存放的PathStrategy代表的是FileProvider对应的xml中的内容。
SimplePathStrategy的mRoots
mRoots也是hashMap,key对应的是xml中的name节点。value对应的是tag+path的组合。
<files-path name="apk" path="."/>
❝key: apk
value: /data/user/0/com.xxx/files
❞
「sCache和mRoots对应关系如下图:」
5.4 FileProvider解析xml过程
FileProvider自动安装后调用attachInfo 调用parsePathStrategy解析xml getPathStrategy方法将解析的PathStrategy放入sCache
6. 合并FileProvider
合并FileProvider,我们需要将第三方sdk定义的FileProvider从AndroidManifest文件中移除掉。但是这样做我们将面临两个问题。
xml中的Uri和文件路径映射无法写入到FileProvider的sCache中 进程间文件共享,是通过寻找到Uri中的authority对应的ContentProvider。调用它的openFile方法实现的,如果不声明ContentProvider会导致文件共享失败
解决方案如下:
通过反射,将xml中各项映射,硬编码的方式写入到FileProvider的sCache中 定义一个中转ContentProvider,将它声明在AndroidManifest文件中,接管所有FileProvider的openFile方法 通过Aspect插桩方式,将所有FileProvider的getUriForFile()返回的Uri的authority hook成中转ContentProvider的authority
6.1 反射硬编码写入映射
通过反射将映射关系写入sCache中
将本应该AndroidManifest中注册的FileProvider的authority信息和xml信息硬编码注册到sCache中
6.2 定义中转ContentProvider
authorities是com.peter.dispatch。主要是中转作用
6.3 Aspect hook authority
6.3.1 根目录build.gradle添加aspectjx
6.3.2 app/build.gradle应用Aspect插件
6.3.3 hook FileProvider.getUriFromFile
将 content://com.toutiao.install/bytedance/toutiao.apk
「转换成」
content://com.peter.dispatch/com.toutiao.install/bytedance/toutiao.apk
6.3.4 重写中转CP的openFile
7.总结
本文主要讲解如何合并多个「FileProvider」优化App启动速度。下一篇我将讲解如何快速定位影响Application冷启动速度的慢方法。敬请关注,分享,点赞,留言。