Android 启动优化案例:WebView非预期初始化排查

news2024/11/20 14:37:52

去年年底做启动优化时,有个比较好玩的 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() {
            @Override
            public 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
-getDefaultWebViewDatabase

public 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);
}

//我们截取一段
    @Override
    public GeolocationPermissions getGeolocationPermissions() {
        return mAwInit.getDefaultGeolocationPermissions();
    }
    @Override
    public CookieManager getCookieManager() {
        return mAwInit.getDefaultCookieManager();
    }
    @Override
    public ServiceWorkerController getServiceWorkerController() {
        synchronized (mAwInit.getLock()) {
            if (mServiceWorkerController == null) {
                mServiceWorkerController = new ServiceWorkerControllerAdapter(
                        mAwInit.getDefaultServiceWorkerController());
            }
        }
        return mServiceWorkerController;
    }
    @Override
    public TokenBindingService getTokenBindingService() {
        return null;
    }
    @Override
    public android.webkit.WebIconDatabase getWebIconDatabase() {
        return mAwInit.getWebIconDatabase();
    }
    @Override
    public WebStorage getWebStorage() {
        return mAwInit.getDefaultWebStorage();
    }
    @Override
    public 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.javaWebViewChromiumAwInit.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
    @SystemApi
    public final class WebViewFactory{
        //...
        @UnsupportedAppUsage
        private 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() {
                    @Override
                    public 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.coloradmin.cn/o/1031759.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

当语文课本上的古诗词遇上拓世AI,文生图绘就东方美学画卷

东方美学自古以来一直以其深刻的内涵和精致的表现形式,吸引着世界各地的文化爱好者。在中国的文学传统中,古诗词扮演着不可或缺的角色。诗词是我国文学宝库中的瑰宝,也是我们民族的文化精髓。如今随着人工智能技术的崛起,古诗词与…

基于微信小程序付费自习室系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言运行环境说明用户微信端的主要功能有:管理员的主要功能有:具体实现截图详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考论文参考源码获取 前言 💗博主介绍&#x…

文心一言 VS 讯飞星火 VS chatgpt (98)-- 算法导论9.3 4题

四、用go语言,对一个包含n个元素的集合,假设一个算法只使用比较来确定第i小的元素,证明:无需额外的比较操作,它也能找到第 i-1 小的元素和第 n-i大的元素。 文心一言: 在这个问题中,我们要使用一个称为&q…

做影视特效本地电脑配置不够怎么办?

影视特效对电脑要求高,往往本地电脑配置不足的情况下,会导致电脑卡顿等造成工作效率低下等问题,再加上现在异地协同的云电脑需求越来越高,更多的企业和个人开始选择做影视特效的云电脑,那么今天就来看看租一台云电脑如…

Swift SwiftUI 修改 List 背景颜色

Preview: Code: .listRowBackground(Color(.yellow)).scrollContentBackground(.hidden) .background(.linearGradient(colors: [.white, .accentColor], startPoint: .top, endPoint: .bottom))喜欢或对你有帮助,点个赞吧,自己先点个嘿嘿。 有错误或者…

【面试题】面试官问你前端性能优化时,他想问什么?

一直以来,前端性能优化都是面试过程中考察的热点题目。 相关的技术博客也层不出穷,我们总是能找到很多这样的文章, 从一个应用的各个层面开始分析,优化的种种手段,取得的种种效果。 往往篇幅越长,讲得越…

MySQL 远程连接1130问题

通过后台进入mysql 1,切换到mysql库 2.查询user表信息 3.更新你想远程登录的用户的host信息,我这里想用root进行远程登录,所以修改如下 4.刷新权限 5.大功告成 快来和博主打成一片吧^_^

使用人工智能聊天机器人时要注意这些!(配提问技巧)

这两年ChatGPT可谓是火遍了互联网,在微博等社交平台上能看到很多网友晒出了与ChatGPT对话的截图,精准、恰当的回答让网友们都倍感新鲜。但是有很多人把人工智能聊天机器人当成玩具,有很多经典犯错回答,不少网友戏称是“人工智障”…

如何优雅的退出线程(condition_variable )

C多线程并发编程入门&#xff08;目录&#xff09; 使用条件变量来实现在析构函数中再次向线程发出一次notify&#xff0c;之后join 线程&#xff0c;等等线程安全退出。 #pragma once #include <iostream> #include <fstream> #include <string> #include …

PerformanceOne一站式性能测试平台

PerformanceOne&#xff08;简称&#xff1a;P-One&#xff09;是泽众软件自主研发的一套一站式性能测试平台软件产品。 该产品采用 B/S 架构开发&#xff0c;实现了集管理、设计、压测、监控以及分析于一体的全方位性能测试解决方案。可有效提升性能测试技术能力&#xff0c;…

拥抱数字化时代SOP电子作业指导书系统助力企业差异化竞争

在如今的竞争激烈的市场环境中&#xff0c;企业要想在同等条件下脱颖而出&#xff0c;差异化竞争成为了关键。然而&#xff0c;与硬件相比&#xff0c;软件的差异化更具有决定性的作用。而软件的差异化往往体现在细节上&#xff0c;而不是大的战略方面。而如何将这些细节进行量…

深入理解C#中委托的使用及不同类型委托的应用示例

在C#中&#xff0c;委托是一种强大而灵活的机制&#xff0c;可以引用一个或多个方法&#xff0c;并允许以类似函数指针的方式进行调用。委托在事件处理、回调函数和多线程编程等场景中非常有用。本文将深入探讨C#中委托的使用&#xff0c;并介绍不同类型委托的应用示例。 目录…

怎么解决IT运维管理痛点?运维工单系统如何提高企业运营效率?

随着企业信息化的飞速发展&#xff0c;IT系统的规模和复杂性也在迅速增长。然而&#xff0c;传统的IT运维管理方式往往难以满足现代企业的需求&#xff0c;存在着许多痛点。   首先&#xff0c;传统的运维管理方式效率低下&#xff0c;缺乏有效的IT资产管理和监控手段&#x…

高效畅通的iOS平台S5配置指南

在iOS平台上&#xff0c;使用S5代理ip访问互联网是一种非常有用的技巧。无论是为了保证隐私安全&#xff0c;还是解决网络限制问题&#xff0c;S5代理ip都能为您提供更快、更稳定的互联网访问体验。本文将为您详细介绍如何在iOS平台上配置和使用S5代理ip&#xff0c;让您的网络…

jarvisoj_level3_x64

jarvisoj_level3_x64 Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)64位&#xff0c;只开了nx ssize_t vulnerable_function() {char buf[128]; // [rsp0h] [rbp-80h] BYREFwrite(1, "Inp…

铁路设备屡遭破坏!RFID电子锁实现铁路防护网破坏实时报警管理

铁路防护网是铁路运输中保障安全的重要组成部分&#xff0c;然而&#xff0c;铁路设备被破坏的情况时有发生&#xff0c;给铁路运输带来了严重的安全隐患和经济损失。 一、铁路防护网面临的挑战 铁路防护网作为铁路运输的重要保障措施&#xff0c;时刻面临着破坏行为的威胁。…

win10 关闭edge跳转IE浏览器

按下windows键&#xff0c;搜索控制面板 右上角输入IE 点击IE 高级中取消下红框选择即可

mapbox鼠标滑过高亮要素

成果图 实现方法 这里借鉴了官网这个例子 https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/ 这里是图层的样式配置&#xff0c;通过改变select的true和false&#xff0c;来控制渲染的颜色和宽度 paint: {line-opacity: 1,line-color: [case,[boolean, [feature-st…

算法通过村第七关-树(递归/二叉树遍历)黄金笔记|迭代遍历

文章目录 前言1. 迭代法实现前序遍历2. 迭代法实现中序遍历3. 迭代法实现后序遍历总结 前言 提示&#xff1a;在一个信息爆炸却多半无用的世界&#xff0c;清晰的见解就成了一种力量。 --尤瓦尔赫拉利《今日简史》 你是不是觉得上一关特别简单&#xff0c;代码少&#xff0c;背…

MySQL 高级语句 Part1(进阶查询语句+MySQL数据库函数+连接查询)

高级语句 第一部分 一、MySQL进阶查询语句1.1 select ----显示表格中一个或数个字段的所有数据记录1.2 distinct ----不显示重复的数据记录1.3 where ----有条件查询1.4 and or ----且 或1.5 in----显示已知的值的数据记录1.6 between----显示两个值范围内的数据记录1.7 通配符…