查看原文
其他

性能优化之合并多个FileProvider

AndroidPub 2023-02-21

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。

本文核心提纲如下:

  1. 项目启动过程遇到的问题
  2. 简述ContentProvider
  3. 简述App Startup使用
  4. 重点讲解FileProvider原理
  5. 反射方式将Uri映射硬编码
  6. 定义中转ContentProvider分发多个FileProvider的openFile请求
  7. 使用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系统诞生之初,它就存在了。它有以下几个特性

  1. 需要在AndroidManifest文件中注册
  2. 系统自动初始化ContentProvider,调用onCreate方法
  3. 支持进程间通信
  4. 支持增删改查操作,一般是数据库操作,但不限于此
  5. 支持调用自定义的方法

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文件

  1. 反编译查看
  2. app/build/intermediates/merged_manifests/debug/AndroidManifest.xml

4. App Startup使用

App Startup库是一个简单而且高效的应用启动初始化组件,它是基于ContentProvider实现的。由于本文重点在FileProvider的合并。所以本章节只是简单介绍它的使用。

App Startup使用分为3步:

  1. 添加依赖
  2. 实现Initializer组件
  3. AndroidManifest文件添加声明

4.1 添加依赖

4.2 实现Initializer组件

实现Initializer接口要求重写两个方法:

  1. create()方法中,我们可以把原先在ContentProvider中初始化的代码,放在这里。
  2. 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代码如下」
  1. 自定义FileProvider

2. AndroidManifest文件中注册FileProvider

  1. res/xml文件夹新建toutiao.xml文件

  2. 调用安装程序

「我们可以看到它们的区别在于分别使用Uri.fromFile()和FIleProvider.getUriFromFile()获取文件的Uri。」

「打印结果对应的值如下表:」

方式
Urifile:///storage/emulated/0/toutiao/toutiao.apk
FileProvidercontent://com.toutiao.install/bytedance/toutiao.apk

「Uri方式获取到的文件路径很容易被猜出文件所在位置,这样暴露给第三方程序,可能会带来风险。而FileProvider获取到的文件路径就不容易暴露文件所在位置。引入FileProvider机制的原因就是为安全考虑。」

5.2 xml的tag对应的文件存储位置

「下图表示各种tag对应的文件路径」

NAMEVALUEPATH
TAG_ROOT_PATHroot-path/
TAG_FILES_PATHfiles-path/data/user/0/com.xxx/files
TAG_CACHE_PATHcache-path/data/user/0/com.xxx/cache
TAG_EXTERNALexternal-path/storage/emulated/0
TAG_EXTERNAL_FILESexternal-files-path/storage/emulated/0/Android/data/com.xxx/files
TAG_EXTERNAL_CACHEexternal-cache-path/storage/emulated/0/Android/data/com.xxx/cache

5.3 xml中Uri和路径的映射表

  1. FileProvider的sCache

FileProvider有一个静态变量sCache。key存放的是FileProvider的authority,value存放的PathStrategy代表的是FileProvider对应的xml中的内容。

  1. 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过程

  1. FileProvider自动安装后调用attachInfo
  2. 调用parsePathStrategy解析xml
  3. getPathStrategy方法将解析的PathStrategy放入sCache

6. 合并FileProvider

合并FileProvider,我们需要将第三方sdk定义的FileProvider从AndroidManifest文件中移除掉。但是这样做我们将面临两个问题。

  1. xml中的Uri和文件路径映射无法写入到FileProvider的sCache中
  2. 进程间文件共享,是通过寻找到Uri中的authority对应的ContentProvider。调用它的openFile方法实现的,如果不声明ContentProvider会导致文件共享失败

解决方案如下:

  1. 通过反射,将xml中各项映射,硬编码的方式写入到FileProvider的sCache中
  2. 定义一个中转ContentProvider,将它声明在AndroidManifest文件中,接管所有FileProvider的openFile方法
  3. 通过Aspect插桩方式,将所有FileProvider的getUriForFile()返回的Uri的authority hook成中转ContentProvider的authority

6.1 反射硬编码写入映射

  1. 通过反射将映射关系写入sCache中

  2. 将本应该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冷启动速度的慢方法。敬请关注,分享,点赞,留言。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存