code小生,一个专注 Android 领域的技术平台
公众号回复 Android 加入我的安卓技术群
作者:于卫国
链接:https://www.jianshu.com/p/85e4f982cbdf
声明:本文已获于卫国
授权发表,转发等请联系原作者授权
本文对Android H5秒开方案进行了调研,对今日头条App的秒开方案进行了详细分析。
本文首发:http://yuweiguocn.github.io/
《子夜吴歌·秋歌》
长安一片月,万户捣衣声。
秋风吹不尽,总是玉关情。
何日平胡虏,良人罢远征。
—唐,李白
背景
在回家的地铁上使用自家应用H5相关功能时,可能由于网络原因导致体验较差,在使用微信、今日头条App时,感觉很流畅,基本做到了秒开,然后就想了解下业内H5秒开方案。
问题原因
常见解决方案
WebView缓存相关
可通过以下代码实现:
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);
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);
开源方案
今日头条方案
先来看下今日头条的效果,第二次断网打开页面做到了秒开的效果:
今日头条针对自己平台的文章详情页做了很多优化,具体包括以下几点:
内置所需文件
![640?wx_fmt=other](https://i-blog.csdnimg.cn/blog_migrate/053811c362df99b0e0f828ac7f5fd633.jpeg)
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>\n"); builder.append("<html>\n"); builder.append("<head>\n"); builder.append("<meta charset=\"utf-8\">\n"); builder.append("<meta name=\"viewport\" content=\"initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n"); builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\""); builder.append("file:///android_asset/article/css/android.css"); builder.append("\">\n</head>\n"); 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>\n"); builder.append("</body>\n"); builder.append("</html>\n"); 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; }}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>\n");
builder.append("<html>\n");
builder.append("<head>\n");
builder.append("<meta charset=\"utf-8\">\n");
builder.append("<meta name=\"viewport\" content=\"initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n");
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"");
builder.append("file:///android_asset/article/css/android.css");
builder.append("\">\n</head>\n");
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>\n");
builder.append("</body>\n");
builder.append("</html>\n");
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](https://i-blog.csdnimg.cn/blog_migrate/1a90807654bbf1b2e400ddd0fde8498c.jpeg)
图片资源的显示
使用ContentProvider获取图片资源:
content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3/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) ; } ...}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 "";}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"] }}"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 秒开方案实现
百度APP-Android H5首屏优化实践
腾讯祭出大招VasSonic,让你的H5页面首屏秒开
Android WebView: 性能优化不得不说的事
推荐阅读
Android-春招-面试经历-2019年
无懈可击 Android 内部升级
人的认知往往与局部经验紧密相关
但你在开口下结论之前
一定要先想想有没有其他可能性