Android 启动优化案例:WebView非预期初始化排查
去年年底做启动优化时,有个比较好玩的 case 给大家分享下,希望大家能从我的分享里 get 到我在做一些问题排查修复时是怎么看上去又low又土又高效的。
1. 现象
在我们使用 Perfetto 进行app 启动过程性能观测时,在 UI 线程发现了一段 几十毫秒接近百毫秒 的非预期Webview初始化的耗时(机器环境:小米10 pro),在线上用户机器上这段代码执行时间可能会更长。

为什么说非预期呢:
- 首页没有WebView的使用、预加载
- X5内核的初始化也在启动流程之后
2. 顺藤摸瓜
一般当我们发现了这种问题后,我们应该如何应对呢?
- 搞懂流程,如果在排查启动性能时,发现了不符合预期的主(子)线程耗时,第一步就是摸清楚这段耗时代码是怎么被调用的。
- 见招拆招,当我们知道代码如何被调用的之后,就可以想办法进行修复工作,如果是因为项目代码在错误的时机被调用,那就延后或者去除相关调用
WebViewChromiumAwInit.java
那我们开始第一步,搞懂流程,我们能看到图中耗时代码块被调用的系统方法是:
WebViewChromiumAwInit.startChromiumLocked,由于 Perfetto 并看不到 App 相关的堆栈信息,所以我们无法直接知道到底是哪行代码引起的。
那我们就去跟跟 webview 源码,看看具体情况,点进 WebViewChromiumAwInit.java 页面看相关代码,发现 startChromiumLocked 是被 ensureChromiumStartedLocked 方法调用的:
// This method is not private only because the downstream subclass needs to access it,
// it shouldn't be accessed from anywhere else.
/* package */
void ensureChromiumStartedLocked(boolean fromThreadSafeFunction) {assert Thread.holdsLock(mLock);if (mInitState == INIT_FINISHED) { // Early-out for the common case.return;}if (mInitState == INIT_NOT_STARTED) {// If we're the first thread to enter ensureChromiumStartedLocked, we need to determine// which thread will be the UI thread; declare init has started so that no other thread// will try to do this.mInitState = INIT_STARTED;setChromiumUiThreadLocked(fromThreadSafeFunction);}if (ThreadUtils.runningOnUiThread()) {// If we are currently running on the UI thread then we must do init now. If there was// already a task posted to the UI thread from another thread to do it, it will just// no-op when it runs.startChromiumLocked();return;}mIsPostedFromBackgroundThread = true;// If we're not running on the UI thread (because init was triggered by a thread-safe// function), post init to the UI thread, since init is *not* thread-safe.AwThreadUtils.postToUiThreadLooper(new Runnable() {@Overridepublic void run() {synchronized (mLock) {startChromiumLocked();}}});// Wait for the UI thread to finish init.while (mInitState != INIT_FINISHED) {try {mLock.wait();} catch (InterruptedException e) {// Keep trying; we can't abort init as WebView APIs do not declare that they throw// InterruptedException.}}}
那么 ensureChromiumStartedLocked 方法又是被谁调用的呢?我们在 WebViewChromiumAwInit.java 文件里大致找一下就能找到以下嫌疑人,第一反应是“这也太多了吧,这咋排查啊”。
-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabasepublic class WebViewChromiumAwInit {public AwTracingController getAwTracingController() {synchronized (mLock) {if (mAwTracingController == null) {ensureChromiumStartedLocked(true);}}return mAwTracingController;}public AwProxyController getAwProxyController() {synchronized (mLock) {if (mAwProxyController == null) {ensureChromiumStartedLocked(true);}}return mAwProxyController;}void startYourEngines(boolean fromThreadSafeFunction) {synchronized (mLock) {ensureChromiumStartedLocked(fromThreadSafeFunction);}}public SharedStatics getStatics() {synchronized (mLock) {if (mSharedStatics == null) {ensureChromiumStartedLocked(true);}}return mSharedStatics;}public GeolocationPermissions getDefaultGeolocationPermissions() {synchronized (mLock) {if (mDefaultGeolocationPermissions == null) {ensureChromiumStartedLocked(true);}}return mDefaultGeolocationPermissions;}public AwServiceWorkerController getDefaultServiceWorkerController() {synchronized (mLock) {if (mDefaultServiceWorkerController == null) {ensureChromiumStartedLocked(true);}}return mDefaultServiceWorkerController;}public android.webkit.WebIconDatabase getWebIconDatabase() {synchronized (mLock) {ensureChromiumStartedLocked(true);if (mWebIconDatabase == null) {mWebIconDatabase = new WebIconDatabaseAdapter();}}return mWebIconDatabase;}public WebStorage getDefaultWebStorage() {synchronized (mLock) {if (mDefaultWebStorage == null) {ensureChromiumStartedLocked(true);}}return mDefaultWebStorage;}public WebViewDatabase getDefaultWebViewDatabase(final Context context) {synchronized (mLock) {ensureChromiumStartedLocked(true);if (mDefaultWebViewDatabase == null) {mDefaultWebViewDatabase = new WebViewDatabaseAdapter(mFactory,HttpAuthDatabase.newInstance(context, HTTP_AUTH_DATABASE_FILE),mDefaultBrowserContext);}}return mDefaultWebViewDatabase;}
}
WebViewChromiumFactoryProvider.java
经过上面对的简单分析,我们大概知道了WebViewChromiumAwInit.startChromiumLocked是被 ensureChromiumStartedLocked 方法调用的,而ensureChromiumStartedLocked 方法会被以下几个方法调用,那我们接下来的工作就需要找到下面这几个方法到底被谁调用了。
-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase
到这里,分享我的一个土方法,我们要找到底哪个地方会调用这些方法,那就找一个不认识的、看上去不会被别人提起的方法,进行 google,我们一眼就选中了getDefaultServiceWorkerController 方法,没办法,谁叫我不认识你呢。虽然方法笨,但是架不住效率啊。于是乎,我们把它揪出来了 - WebViewChromiumFactoryProvider.java

我们大概了解一下 WebViewChromiumFactoryProvider 大概是个什么角色,WebViewChromiumFactoryProvider 实现了 WebViewFactoryProvider 接口,简单理解就是 WebView 的工厂,App 如果要创建 WebView,就会通过 WebViewFactoryProvider 接口的实现类进行 createWebView,所以其实就是个工厂模式。通过抽象规范 api,保证兼容性和可移植性可扩展性。
我们在这个文件中也如愿以偿的看到了上面列出来的几个方法的调用。WebViewChromiumFactoryProvider 在接口方法的实现中,调用了WebViewChromiumAwInit 里的一系列方法,如下:
//WebViewChromiumFactoryProvider.java
@Override
public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}//我们截取一段@Overridepublic GeolocationPermissions getGeolocationPermissions() {return mAwInit.getDefaultGeolocationPermissions();}@Overridepublic CookieManager getCookieManager() {return mAwInit.getDefaultCookieManager();}@Overridepublic ServiceWorkerController getServiceWorkerController() {synchronized (mAwInit.getLock()) {if (mServiceWorkerController == null) {mServiceWorkerController = new ServiceWorkerControllerAdapter(mAwInit.getDefaultServiceWorkerController());}}return mServiceWorkerController;}@Overridepublic TokenBindingService getTokenBindingService() {return null;}@Overridepublic android.webkit.WebIconDatabase getWebIconDatabase() {return mAwInit.getWebIconDatabase();}@Overridepublic WebStorage getWebStorage() {return mAwInit.getDefaultWebStorage();}@Overridepublic WebViewDatabase getWebViewDatabase(final Context context) {return mAwInit.getDefaultWebViewDatabase(context);}WebViewDelegate getWebViewDelegate() {return mWebViewDelegate;}WebViewContentsClientAdapter createWebViewContentsClientAdapter(WebView webView,Context context) {try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped("WebViewChromiumFactoryProvider.insideCreateWebViewContentsClientAdapter")) {return new WebViewContentsClientAdapter(webView, context, mWebViewDelegate);}}void startYourEngines(boolean onMainThread) {try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped("WebViewChromiumFactoryProvider.startYourEngines")) {mAwInit.startYourEngines(onMainThread);}}boolean hasStarted() {return mAwInit.hasStarted();}
3. 确定问题
我们上面通过阅读 WebViewChromiumFactoryProvider.java、WebViewChromiumAwInit.java 这两个文件具体代码实现,有了一个比较清晰的思路。
App 在初始化过程中,调用到了 WebViewFactoryProvider 接口实现类的某个方法,这个方法调用了 WebViewChromiumAwInit 的下面方法中的其中一个或者多个。那其实问题就清晰了,我们只需要找到,我们 app 启动阶段到底哪行代码,会调用到 WebViewFactoryProvider 接口某个接口方法就行。
-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase
由于 WebView 的代码并不会打包进 app 里,App 用的 WebView 内核都是用的 Android 系统负责内置、升级的 WebView 内核代码,所以通过 transform 的方式进行 hook 调用是不行的,这里我们采用动态代理的方式,对 WebViewFactoryProvider 接口方法进行 hook,我们通过动态代理生成一个 proxy 对象,通过反射的方式,替换掉 android.webkit.WebViewFactory 的 sProviderInstance 对象。
##WebViewFactory@SystemApipublic final class WebViewFactory{//...@UnsupportedAppUsageprivate static WebViewFactoryProvider sProviderInstance;//...}##动态代理try {Class clas = Class.forName("android.webkit.WebViewFactory");Method method = clas.getDeclaredMethod("getProvider");method.setAccessible(true);Object obj = method.invoke(null);Object hookService = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getSuperclass().getInterfaces(),new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Log.d("zttt", "hookService method: " + method.getName());new RuntimeException(method.getName()).printStackTrace();return method.invoke(obj, args);}});Field field = clas.getDeclaredField("sProviderInstance");field.setAccessible(true);field.set(null, hookService);} catch (Exception e) {e.printStackTrace();}
替换掉 sProviderInstance 之后,我们就可以在我们的代理逻辑中,加上断点来进行调试,最终找到了造成 WebView非预期初始化的始作俑者:WebSettings.getDefaultUserAgent。

4. 解决问题
事情到这里就好解决了,只需要对 WebSettings.getDefaultUserAgent 进行编译期的Hook,重定向到带缓存defaultUserAgent 的相关方法就行了,本地有缓存则直接读取,本地没有则立即读取,得益于之前我在项目中实现的使用方便的 配置化 Hook 框架,这种小打小闹的 Hook 工作不到一分钟就能完成。


当然,这里还需要考虑一个问题,那就是当用户机器的 defaultUserAgent 发生变化之后,怎么才能及时的更新本地缓存以及网络请求中用上新的defaultUserAgent。我们的做法是:
-
当本地没有缓存时,立刻调用
WebSettings.getDefaultUserAgent拿值并更新缓存; -
每次App启动阶段结束之后,会在子线程中去调用
WebSettings.getDefaultUserAgent拿值并更新缓存。
这样处理之后,将 defaultUserAgent 发生变化之后的影响最小化,系统 WebView 升级本身就是极度不频繁的事情,在这种 case 下我们舍弃了下一次 App 打开前几个网络请求的 defaultUserAgent 正确性也是合理的,这也是我们考量 「风险收益比」的一个经典case。
5. 确认问题被解决
通过上述 hook,我们重新打包 run 一遍 app,在启动阶段已经观察不到相关耗时了。
搞定,收工,不仅解决问题效率高,写博客也效率高,一会就整完了,简直就像是季度绩效考核前的产品,出方案和上线的效率就一个字,嗖。
为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还含底层逻辑):https://qr18.cn/FVlo89
性能优化核心笔记:https://qr18.cn/FVlo89
启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化:https://qr18.cn/FVlo89

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能监控框架》:https://qr18.cn/FVlo89

《Android Framework学习手册》:https://qr18.cn/AQpN4J
- 开机Init 进程
- 开机启动 Zygote 进程
- 开机启动 SystemServer 进程
- Binder 驱动
- AMS 的启动过程
- PMS 的启动过程
- Launcher 的启动过程
- Android 四大组件
- Android 系统服务 - Input 事件的分发过程
- Android 底层渲染 - 屏幕刷新机制源码分析
- Android 源码分析实战

相关文章:
Android 启动优化案例:WebView非预期初始化排查
去年年底做启动优化时,有个比较好玩的 case 给大家分享下,希望大家能从我的分享里 get 到我在做一些问题排查修复时是怎么看上去又low又土又高效的。 1. 现象 在我们使用 Perfetto 进行app 启动过程性能观测时,在 UI 线程发现了一段 几十毫…...
20230919后台面经整理
1.你认为什么是操作系统,操作系统有哪些功能 os是:管理资源、向用户提供服务、硬件机器的扩展 1.进程线程管理:状态、控制、通信等 2.存储管理:分配回收、地址转换 3.文件管理:目录、操作、磁盘、存取 4.设备管理&…...
画一个时钟(html+css+js)
这是一个很简约的时钟。。。。。。。 效果: 代码: <template><div class"demo-box"><div class"clock"><ul class"mark"><liv-for"(rotate, index) in rotatedAngles":key"i…...
红 黑 树
文章目录 一、红黑树的概念二、红黑树的实现1. 红黑树的存储结构2. 红黑树的插入 一、红黑树的概念 在 AVL 树中删除一个结点,旋转可能要持续到根结点,此时效率较低 红黑树也是一种二叉搜索树,通过在每个结点中增加一个位置来存储红色或黑色…...
掷骰子的多线程应用程序1(复现《Qt C++6.0》)
说明:复现的代码来自《Qt C6.0》P496-P500。在复现时完全按照代码,出现了两处报错: (1)ui指针(2)按钮的响应函数。下面程序对以上问题进行了修改。除了图片、清空、关闭功能外,其他…...
【vue2第十八章】VueRouter 路由嵌套 与 keep-alive缓存组件(activated,deactivated)
VueRouter 路由嵌套 在使用vue开发中,可能会碰到使用多层级别的路由。比如: 其中就包含了两个主要页面,首页,详情,但是首页的下面又包含了列表,喜欢,收藏,我的四个子路由。 此时就…...
如何确保亚马逊、速卖通等平台测评补单的环境稳定性和安全性?
做亚马逊、速卖通等平台测评补单时,确保环境的安全性和稳定性是非常重要的。稳定的环境是测评的基础,如果无法解决安全性问题,那么测评就不值得进行。为了确保稳定的环境系统,需要考虑物理环境和IP环境两个方面。 首先࿰…...
echarts图表 实现高度按照 内容撑起来或者超出部分滚动展示效果
背景:因为数据不固定 高度写死导致数据显示不全,所以图表高度要根据内容计算 实现代码如下: <divv-if"showCharts"id"business-bars"class"chart":style"{ height: chartHeight px }"></d…...
【论文阅读】检索增强发展历程及相关文章总结
文章目录 前言Knn-LMInsightMethodResultsDomain AdaptionTuning Nearest Neighbor Search Analysis REALMInsightsMethodKnowledge RetrieverKnowledge-Augmented Encoder ExpResultAblation StudyCase Study DPRInsightMethodExperimentsResults RAGInsightRAG-Sequence Mode…...
【漏洞复现系列】二、weblogic-cve_2020_2883(RCE/反序列化)
Key words:T3协议,weblogic Server,反序列化 2.1、漏洞原理 cve_2020_2883 远程代码执行漏洞存在于 WebLogic Server 核心组件中,允许未经身份验证的攻击者通过 T3 协议网络访问并破坏易受攻击的 WebLogic Server,成功的漏洞利…...
算法通关村-----LRU的设计与实现
LRU 缓存 问题描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存。int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值&…...
王江涛十天搞定考研词汇
学习目标: 考研词汇 学习内容: 2023-9-17 第一天考研词汇 学习时间: 开始:2023-9-17 结束:进行中 学习产出: 2023-9-17intellect智力;知识分子intellectual智力的;聪明的intellectualize使...理智化&a…...
算法(二)——数组章节和链表章节
数组章节 (1)二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 class Solution {public i…...
Android:ListView在Fragment中的使用
一、前言: 因为工作一直在用mvvm框架,因此这篇文章是基于mvvm框架写的。在Fragment复制之前一定要谨记项目可以跑起来。确保能跑起来之后直接复制就行。 二、代码展示: 页面布局 ?xml version"1.0" encoding"utf-8"…...
BIGEMAP在土地规划中的应用
工具 Bigemap gis office地图软件 BIGEMAP GIS Office-全能版 Bigemap APP_卫星地图APP_高清卫星地图APP 1.使用软件一般都用于套坐标,比如我们常见的(kml shp CAD等土建规划图纸)以及一些项目厂区红线,方便于项目选址和居民建…...
软件测试常见术语和名词解释
1. Unit testing (单元测试):指一段代码的基本测试,其实际大小是未定的,通常是一个函数或子程序,一般由开发者执行。 2. Integration testing (集成测试):被测试系统的所有组件都集成在一起,找出被测试系统…...
prometheus+process_exporter进程监控
一、需要监控进程的服务器上配置 1、进入到临时工作目录,传入process_exporter包 [root Nginx1 ~]# cd work/ [root Nginx1 work]# rz 2、解压,并移动至/usr/local/目录下 [root Nginx1 work]# tar xzf process-exporter-0.7.5.linux-amd64.tar.gz [root…...
四川玖璨电子商务有限公司专注抖音电商运营
四川玖璨电商是一个靠谱的抖音培训公司,在电商行业内有着广泛的知名度和良好的口碑。该公司通过多年的发展,形成了独特的运营理念和有效的运营策略,为商家提供了一站式的抖音电商运营服务。 首先,四川玖璨电子商务有限公司注重与…...
python LeetCode 刷题记录 83
题目 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 代码 class Solution:def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:if head:# 判断非空链表current he…...
Grom 如何解决 SQL 注入问题
什么是 SQL 注入 SQL 注入是一种常见的数据库攻击手段, SQL 注入漏洞也是网络世界中最普遍的漏洞之一。 SQL 注入就是恶意用户通过在表单中填写包含 SQL 关键字的数据来使数据库执行非常规代码的过程。 这个问题的来源就是, SQL 数据库的操作是通过 SQ…...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...
免费数学几何作图web平台
光锐软件免费数学工具,maths,数学制图,数学作图,几何作图,几何,AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
