今日头条App 页面秒开方案详解
热文导读| 点击标题阅读
互联网寒冬下,程序员如何突围提升自己?
Google发力了,Flutter突飙猛进,在鸿蒙OS的威胁下,FuchsiaOS API也发布了
全网第一篇整理的Flutter 面试知识点集锦
作者: 于卫国
来源: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框架,专注于提升页面首屏加载速度,完美支持静态直出页面和动态直出页面,支持预加载兼容离线包等方案。优点是性能好,速度快,大厂出品,缺点是配置复杂, 同时需要前后端接入。
今日头条方案
先来看下今日头条的效果,第二次断网打开页面做到了秒开的效果:
今日头条针对自己平台的文章详情页做了很多优化,具体包括以下几点:
内置文章详情页所需的css、js等文件,并可以控制版本
WebView预创建
预加载包含文章详情页所需的css、js的空html
在列表页预加载文章详情所需的内容使用LRU内存缓存并保存到本地数据库
在文章详情页获取预创建的WebView(预加载了html),直接调用js设置页面内容
通过js控制图片的显示,图片懒加载(当图片在可见区域或即将可见才会加载图片),点击加载图片等
Html中的图片通过ContentProvider获取使用Fresco下载的图片
内置所需文件
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; }}
本地数据库缓存
使用数据库进行持久化。
图片资源的显示
使用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对象即可。效果如下图所示:
通过以上优化点实现的最终效果如下:
总结
通过对今日头条app的分析,针对平台特有文章可以采用类似头条方案对数据预加载以提升用户体验。由于时间关系,就不再对微信进行分析,猜测也是采用了类似方案实现。通过WebView提供的缓存功能和拦截资源方法进行缓存体验上还是不尽如人意。也许等5G普及会好许多。
参考
Android Webview H5 秒开方案实现
https://juejin.im/post/5b94ca52e51d450e7d097f38
百度APP-Android H5首屏优化实践
https://mp.weixin.qq.com/s/AqQgDB-0dUp2ScLkqxbLZg
腾讯祭出大招VasSonic,让你的H5页面首屏秒开
https://segmentfault.com/a/1190000010711024
Android WebView: 性能优化不得不说的事
https://www.jianshu.com/p/95d4d73be3d1
更多学习和讨论,欢迎加入我们的知识星球,这里有1000+小伙伴,让你的学习不寂寞~·
看完本文有收获?请转发分享给更多人
我们的知识星球第三期开期了,已达到1100人了,能连续做三期已很不容易了,有很多老用户续期,目前续期率达到50%,说明了大家对我们的知识星球还是很认可的,欢迎大家加入尽早我们的知识星球,更多星球信息参见:
欢迎加入Java和Android架构社群
如何进阶成为Java的Android版和架构师?
说两件事
微信扫描或者点击上方二维码领取的Android \ Python的\ AI \的Java等高级进阶资源
更多学习资料点击下面的“阅读原文 ”获取
谢谢老板,点个好看↓
今日头条App 页面秒开方案详解相关推荐
- 今日头条推荐算法原理全文详解之一
本次分享将主要介绍今日头条推荐系统概览以及内容分析.用户标签.评估分析,内容安全等原理. 今日头条推荐算法原理全文详解 今日头条 数据分析 产品经理 产品 好文分享 第1张 一.系统概览 推荐系统,如 ...
- 今日头条推荐算法原理全文详解之四
三.用户标签 内容分析和用户标签是推荐系统的两大基石.内容分析涉及到机器学习的内容多一些,相比而言,用户标签工程挑战更大. 今日头条推荐算法原理全文详解 今日头条 数据分析 产品经理 产品 好文分享 ...
- 【推荐系统】今日头条推荐算法原理全文详解
如今,算法分发已经逐步成为信息平台.搜索引擎.浏览器.社交软件等几乎所有软件的标配,但同时也开始面临各种不同的质疑.挑战与误解. 2018年1月,今日头条资深算法架构师曹欢欢博士,首次公开今日头条的算 ...
- 仿今日头条APP页面切换
先展示效果 下面状态栏切换就省略了,用的是Fragment切换,主要看上面切换 需要加的权限 <uses-permission android:name="android.permis ...
- 方案详解|AARRR+八角行为分析=用游戏化思维实现用户增长
我们需要一套基于AARRR模型,围绕增长成本.效率.质量三个话题来针对每一层转化漏斗提炼可操作的运营方案,以AARRR模型+八角行为分析法为理论框架的,游戏化运营增长策略应运而生.随着互联网线上流量的 ...
- Android 换肤方案详解(一)
引言 在我们的开发中,也许有些项目会有换肤的需求,这个时候会比较头疼怎么做才能做到一键换肤呢?大家肯定是希望只要一行代码就能调用最好.下面我们先分析一下换肤的本质是什么? 原理 换肤,其本质无非就是更 ...
- python爬取今日头条手机app广告_今日头条App广告采集器的实现
应客户需求,要对今日头条App内出现的广告商品进行提取,获取商品名称.价格区间.当前销量.卖家数据等信息.之前他们都是通过人工手动去提取的,效率很低,而且容易遗漏.现在需要通过程序来自动化采集以提高效 ...
- mysql千万级数据怎么删除,MySQL 快速删除大量数据(千万级别)的几种实践方案详解...
这篇文章主要介绍了MySQL 快速删除大量数据(千万级别)的几种实践方案详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 笔者 ...
- 爬虫实战(二)—利用requests、selenium爬取王者官网、王者营地APP数据及pymongo详解
概述 可关注微信订阅号 loak 查看实际效果. 代码已托管github,地址为:https://github.com/luozhengszj/LOLGokSpider ,包括了项目的所有代码. 本文 ...
最新文章
- [转] C#中绘制矢量图形
- Flash Socket安全问题的全面解析 服务器端代码
- Leetcode 204. 计数质数 解题思路及C++实现
- web页面到ajax,页面使用ajax加载页面后如果运行其中的js,webpack如何多页面展示...
- 2021-10-11 二叉树,二叉搜索树及其相关23个操作 C++实现笔记(复习用,含C指针复习)
- 第九届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
- JSON学习笔记(六)- JSONP
- python反编译exe_实战 Python3.7+64位 Exe 反编译
- 电子商务应用课程知识整理 第二章-电子商务相关知识与技术
- python人像精细分割_人像抠图 - 发丝级人像分割 - 照片人物特效 - 极链科技
- 深度学习笔记(38) 非极大值抑制
- 你值得拥有的 11 个前端开发利器
- Luakit的前世今生
- MATLAB-典型动态系统建模与仿真
- php工程师绩效考核表_如何对程序员绩效考核?
- (每日一练c++)CC114 有效的数独
- TCPIP------慢启动与拥塞避免
- 金庸《倚天屠龙记》中的一个瑕疵
- 双硬盘好还是纯固态好学计算机专业,固态硬盘虽好,但是和机械硬盘相比还是有其“不足之处”...
- scratch编程——画笔模块画各种同心图案
热门文章
- 2098799-77-8,Thalidomide-O-PEG4-Propargyl炔丙基在铜催化剂存在下与叠氮化物分子反应
- 【操作系统】CSAPP学习笔记
- 轻松解决 f2pool鱼池ethermine连接失败antpool矿池连接不上的问题E池连接超时
- 百钱百鸡:公鸡5元一只,母鸡3元一只,小鸡1元3只,100元要买100只鸡,共有几种情况
- Flash数据读取和保存
- 用 LCD1602 显示的时钟
- 手把手教你下载在线地图?!再也不用下了
- 镭速raysync介绍文件传输软件的进史
- vue监听滚动到底部加载更多
- 用计算机技术辅助语文教学,计算机技术相关毕业论文致谢,关于计算机辅助语文教学相关论文范文文献...