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…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...

宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
SQL Server 触发器调用存储过程实现发送 HTTP 请求
文章目录 需求分析解决第 1 步:前置条件,启用 OLE 自动化方式 1:使用 SQL 实现启用 OLE 自动化方式 2:Sql Server 2005启动OLE自动化方式 3:Sql Server 2008启动OLE自动化第 2 步:创建存储过程第 3 步:创建触发器扩展 - 如何调试?第 1 步:登录 SQL Server 2008第 2 步…...
vue3 daterange正则踩坑
<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...