来了!今日头条 App 页面秒开方案详解

点击“开发者技术前线”,选择“星标?”

在看|星标|留言,  真爱


作者: 于卫国

https://www.jianshu.com/p/85e4f982cbdf


本文对Android H5秒开方案进行了调研,对今日头条App的秒开方案进行了分析。

本文首发:http://yuweiguocn.github.io/

背景

在回家的地铁上使用自家应用H5相关功能时,可能由于网络原因导致体验较差,在使用微信、今日头条App时,感觉很流畅,基本做到了秒开,然后就想了解下业内H5秒开方案。

问题原因

  • 文件下载耗时:包括html、css、js、图片等

  • 页面渲染耗时:页面渲染,解析js、css文件等

  • WebView创建耗时:首次创建WebView耗时大约需要500ms左右,第二次创建耗时大约需要20ms左右

常见解决方案

WebView缓存相关
  • 浏览器缓存机制,通过请求头控制缓存

  • Dom Storgage(Web Storage)存储机制

  • Web SQL Database 存储机制

  • Application Cache(AppCache)机制

  • Indexed Database (IndexedDB)

可通过以下代码实现:

WebSettings webSettings = myWebView.getSettings();webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);webSettings.setDomStorageEnabled(true);webSettings.setDatabaseEnabled(true);final String dbPath = getApplicationContext().getDir("db", Context.MODE_PRIVATE).getPath();webSettings.setDatabasePath(dbPath); webSettings.setAppCacheEnabled(true);final String cachePath = getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath();webSettings.setAppCachePath(cachePath);webSettings.setAppCacheMaxSize(5*1024*1024);webSettings.setJavaScriptEnabled(true);
开源方案
  • CacheWebView:通过拦截shouldInterceptRequest方法使用okhttp的缓存功能实现,使用简单可配置。

  • VasSonic:腾讯出品的一个轻量级的高性能的Hybrid框架,专注于提升页面首屏加载速度,完美支持静态直出页面和动态直出页面,支持预加载兼容离线包等方案。优点是性能好,速度快,大厂出品,缺点是配置复杂, 同时需要前后端接入。

今日头条方案

先来看下今日头条的效果,第二次断网打开页面做到了秒开的效果:

640?wx_fmt=png

今日头条针对自己平台的文章详情页做了很多优化,具体包括以下几点:

  • 内置文章详情页所需的css、js等文件,并可以控制版本

  • WebView预创建

  • 预加载包含文章详情页所需的css、js的空html

  • 在列表页预加载文章详情所需的内容使用LRU内存缓存并保存到本地数据库

  • 在文章详情页获取预创建的WebView(预加载了html),直接调用js设置页面内容

  • 通过js控制图片的显示,图片懒加载(当图片在可见区域或即将可见才会加载图片),点击加载图片等

  • Html中的图片通过ContentProvider获取使用Fresco下载的图片

内置所需文件


640?wx_fmt=other


WebView预创建,资源预加载
首次创建WebView要比第二次创建耗时慢很多,原因估计是WebView首次创建需要初始化一些静态资源,第二次创建时不需要初始化,所以第二次创建耗时要少很多。

使用Context包装类MutableContextWrapper传入Application预创建WebView对象,然后预加载一个使用java代码拼接的html,提前对js、css资源进行解析。等获取预创建的WebView时再替换为Activity的context。

public class PreloadWebView {    private PreloadWebView(){}    private static final int CACHED_WEBVIEW_MAX_NUM = 2;    private static final Stack<WebView> mCachedWebViewStack = new Stack<>();    public static PreloadWebView getInstance(){        return Holder.INSTANCE;    }    private static class Holder{        private static final PreloadWebView INSTANCE = new PreloadWebView();    }    /**     * 创建WebView实例     * 用了applicationContext     */    public void preload() {        L.d("webview preload");        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {            @Override            public boolean queueIdle() {                if (mCachedWebViewStack.size() < CACHED_WEBVIEW_MAX_NUM) {                    mCachedWebViewStack.push(createWebView());                }                return false;            }        });    }    private WebView createWebView() {        WebView webview = new WebView(new MutableContextWrapper(App.getApp()));        webview.getSettings().setJavaScriptEnabled(true);        webview.loadDataWithBaseURL("file:///android_asset/article/?item_id=0&token=0",getHtml(),"text/html","utf-8","file:///android_asset/article/?item_id=0&token=0");        return webview;    }    private static String getHtml() {        StringBuilder builder = new StringBuilder();        builder.append("<!DOCTYPE html>");        builder.append("<html>");        builder.append("<head>");        builder.append("<meta charset="utf-8">");        builder.append("<meta name="viewport" content="initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">");        builder.append("<link rel="stylesheet" type="text/css" href="");        builder.append("file:///android_asset/article/css/android.css");        builder.append(""></head>");        builder.append("<body class="font_m"><header></header><article></article><footer></footer>");        builder.append("<script type="text/javascript" src="");        builder.append("file:///android_asset/article/js/lib.js");        builder.append(""></script>");        builder.append("<script type="text/javascript" src="");        builder.append("file:///android_asset/article/js/android.js");        builder.append("" ></script>");        builder.append("</body>");        builder.append("</html>");        return builder.toString();    }    /**     * 从缓存池中获取合适的WebView     *     * @param context activity context     * @return WebView     */    public WebView getWebView(Context context) {        // 为空,直接返回新实例        if (mCachedWebViewStack == null || mCachedWebViewStack.isEmpty()) {            WebView web = createWebView();            MutableContextWrapper contextWrapper = (MutableContextWrapper) web.getContext();            contextWrapper.setBaseContext(context);            return web;        }        WebView webView = mCachedWebViewStack.pop();        // webView不为空,则开始使用预创建的WebView,并且替换Context        MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();        contextWrapper.setBaseContext(context);        return webView;    }}
本地数据库缓存

使用数据库进行持久化。


640?wx_fmt=other


图片资源的显示

使用ContentProvider获取图片资源:

content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3

上面的ContentProvider的uri会调用对应ContentProvider的openFile方法,别忘了在清单文件中注册。

public class ImageProvider extends ContentProvider {  ...  public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {    File file = getFile(uri);    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) ;  }  ...}

中间字符串使用zip压缩,使用下面的代码解压zip数据的代码:

static final byte[] buffer = new byte[4096];public static final String unzip(String str) {    try {        Inflater inflater = new Inflater();        inflater.setInput(Base64.decode(str, 8));        int size = inflater.inflate(buffer);        inflater.end();        String temp = new String(buffer, 0, size, "UTF-8");        return temp;    } catch (Exception e) {        e.printStackTrace();    }    return "";}

解压后的数据如下:

{    "origin": {        "uri": "large/pgc-image/8e72c19ce0f2456880947531d5bbb230",        "urls": ["http://p1-tt.byteimg.com/large/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p1-tt.byteimg.com/large/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p3-tt.byteimg.com/large/pgc-image/8e72c19ce0f2456880947531d5bbb230"]    },    "webp_origin": {        "uri": "details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp",        "urls": ["http://p99.pstatp.com/details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p6-tt.byteimg.com/details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p1-tt.byteimg.com/details/v0/w640/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp"]    },    "thumb": {        "uri": "thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230",        "urls": ["http://p9-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p3-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230", "http://p1-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230"]    },    "webp_thumb": {        "uri": "thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp",        "urls": ["http://p1-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p3-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp", "http://p6-tt.byteimg.com/thumb/pgc-image/8e72c19ce0f2456880947531d5bbb230.webp"]    }}

uri的最后两个片段表示文章id及图片索引,用于通过js通知页面图片加载完成。通过解析content的uri中的数据获取Fresco下载的缓存文件,返回一个ParcelFileDescriptor对象即可。效果如下图所示:

640?wx_fmt=png

通过以上优化点实现的最终效果如下:

640?wx_fmt=png


640?wx_fmt=png

总结

通过对今日头条app的分析,针对平台特有文章可以采用类似头条方案对数据预加载以提升用户体验。由于时间关系,就不再对微信进行分析,猜测也是采用了类似方案实现。通过WebView提供的缓存功能和拦截资源方法进行缓存体验上还是不尽如人意。也许等5G普及会好许多。

在公众号,在后台回复关键字:666,可以获取一份程序员大礼包!

参考

Android Webview H5 秒开方案实现

https://juejin.im/post/5b94ca52e51d450e7d097f38

百度 APP 首屏优化实践

腾讯祭出大招VasSonic,让你的H5页面首屏秒开

https://segmentfault.com/a/1190000010711024

支付宝 App 启动性能优化

---END---


选择”开发者技术前线 “星标?,内容一触即达。点击原文更多惊喜!

开发者技术前线 汇集技术前线快讯和关注行业趋势,大厂干货,是开发者经历和成长的优秀指南。


历史推荐


充气娃娃什么感觉?Python 告诉你!

逆袭Flutter? Facebook 发布全新跨平台引擎 Hermes!

中国 GitHub 霸榜乱象刷赞,高仿,骂街和版聊!

640?

点个在看,解锁更多惊喜!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值