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.WebViewFactorysProviderInstance 对象。

    ##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

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/177627.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年【电工(高级)】考试报名及电工(高级)考试试卷

题库来源:安全生产模拟考试一点通公众号小程序 2023年【电工(高级)】考试报名及电工(高级)考试试卷,包含电工(高级)考试报名答案和解析及电工(高级)考试试卷…

isomorphic-fetch库代码示例

isomorphic-fetch库的爬虫程序。 typescript // 引入isomorphic-fetch库 import fetch from isomorphic-fetch; // 设置 const proxy ; // 定义视频URL const url ; // 使用fetch获取视频数据 fetch(url, { method: GET, headers: { Accept: application/json, …

2023 PostgreSQL 数据库生态大会:解读拓数派大数据计算系统及其云存储底座

11月3日-5日,由中国开源软件推进联盟 PostgreSQL 分会主办的中国 PostgreSQL 数据库生态大会在北京中科院软件所隆重举行。大会以”极速进化融合新生”为主题,从线下会场和线上直播两种方式展开,邀请了数十位院士、教授、高管和社群专家&…

YOLOv8优化策略:全新的聚焦式线性注意力模块Focused Linear Attention | ICCV2023

🚀🚀🚀本文改进:深入分析了现有线性注意力方法的缺陷,并提出了一个全新的聚焦的线性注意力模块(Focused Linear Attention),同时具有高效性和很强的模型表达能力。 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻…

Cesium 展示——坐标间的转换

文章目录 需求1. 点击位置会后获取的地球坐标2. 笛卡尔坐标(Cartesian3)3. 地理坐标系分析转换关系如下需求 坐标间的转换 1. 点击位置会后获取的地球坐标 Cesium点击位置会后获取的地球坐标。 2. 笛卡尔坐标(Cartesian3) 笛卡尔坐标系中,表示一个在 x 轴上、y轴上、…

Fedora Linux 39 正式版官宣 11 月 发布

导读Fedora Linux 39 正式版此前宣布将于 10 月底发布,不过这款 Linux 发行版面临了一些延期,今天开发团队声称,Fedora Linux 39 正式版将于 11 月 7 日发布。 过查询得知,在近日的 "Go / No-Go" 会议上,开…

轻量封装WebGPU渲染系统示例<28>- MRT纹理(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/rendering/src/voxgpu/sample/MRT.ts 当前示例运行效果: 此示例基于此渲染系统实现,当前示例TypeScript源码如下: const colorRTTTex { diffuse: { uuid: "colorRTT", rtt…

若依前后分离版框架下Springboot java引入Mqtt接受发送消息

**这只是其中一种而且是粗浅的接、发消息。 同步机制还要跟搞物联网的同事沟通确认去看看能不能实现 或者是设备比较多的情况下 不会去使用同步机制 首先pom文件 引入依赖 ** <dependency><groupId>org.eclipse.paho</groupId><artifactId>org.eclipse…

全方位移动机器人 Stanley 轨迹跟踪 Gazebo 仿真

全方位移动机器人 Stanley 轨迹跟踪 Gazebo 仿真 本来打算今天出去跑一下 GPS&#xff0c;但是下雨&#xff0c;作罢 添加参考轨迹信息 以下三个功能包不需要修改&#xff1a; mrobot&#xff1a;在 Rviz 和 Gazebo 中仿真机器人cmd_to_mrobot&#xff1a;运动学解算&#…

最佳实践-使用Github Actions来构建跨平台容器镜像

公众号「架构成长指南」&#xff0c;专注于生产实践、云原生、分布式系统、大数据技术分享。 前言 最近在写K8s的相关系列文章&#xff0c;因为有涉及到镜像构建&#xff0c;发现在Mac m1的Arm架构下构建的部分镜像&#xff0c;没法在X86架构下使用&#xff0c;不兼容。 尝试…

Xocde 升级15 或者 iOS17报错:

错误&#xff1a; Assertion failed: (false && "compact unwind compressed function offset doesnt fit in 24 bits"), function operator(), file Layout.cpp, line 5758. 翻译&#xff1a; 断言失败&#xff1a;&#xff08;false&&“压缩展开…

【仿真】ruckig在线轨迹生成器示例

该场景说明了使用 CoppeliaSim 中提供的 Ruckig 在线轨迹生成功能的各种方法&#xff1a; 1. 在线程脚本内使用单个阻塞函数&#xff08;红色&#xff09; 2. 在线程脚本中使用多个非阻塞函数&#xff08;黄色&#xff09; 3. 在非线程脚本中使用多个非阻塞函数&#xff08;…