Android货拉拉H5离线包方案
先看一下离线包的整体实现思路: H5离线包的基本原理是将html、js、css、图片等静态资源打包成压缩包,然后下载到客户端并解压,H5加载时直接从本地读取静态资源文件,减少网络请求,提高速度。
从何下手
1.找到离线包
源码中有宝藏,在assets目录中看见一个zip包,解压看看:
就是它了,下面研究下加载机制。
2.离线包加载机制
源码中有宝藏,贴心的给我们准备了一个测试用http server,在lib_web module中。
- HttpServerStarter启动了一个10线程的HttpServer,这里端口号小改一下,8888太容易冲突了。
- 接下来看下readme的攻略: (1).本地服务IP修改:ServerConstant中LOCALHOST修改为自己的ip地址(mac 获取 终端输入命令行:ifconfig | grep "inet") (2).Coverage模式运行HttpServerStarter (3).电脑浏览器测试两个接口 http://127.0.0.1:8888/queryOffline?bisName=act3-2108-turntable&offlineZipVer=ab http://127.0.0.1:8888/package?bisName=act3-2108-turntable 照着操作就好了,很贴心。 3.服务跑起来后可以在浏览器里访问测试下,当请求package接口时,会直接将第一步的离线包下载下来。 queryOffline:拉取离线包信息处理接口。 package:获取信息接口判定需要升级离线包,通过该接口进行下载。
3.客户端使用离线包机制
看一下简易初始化的逻辑即可。
public void init(Context context) {
OfflineConfig offlineConfig = new OfflineConfig.Builder(true)
// .addDisable("act3-2108-turntable")//禁用业务名称
.addPreDownload("uappweb-offline")//预加载业务名称
.build();
OfflineWebClient.init(context.getApplicationContext(),
new OfflineParams()
.config(offlineConfig)//必须
.requestServer(new DefaultLocalRequest())//必须
// .requestServer(new DefaultRequest())
.isDebug(BuildConfig.DEBUG)
);
}
对于配置preDownload的业务,会在应用启动阶段进行离线包的加载。 否则加载机制在进入该web页面生效,且缓存会在下一次生效。
public static void init(Context context, OfflineParams offlineParams) {
OfflineWebManager.getInstance().init(context, offlineParams);
if (OfflineWebManager.getInstance().isInit()) {
OfflineTaskManager.startInitTask();
}
}
初始化的配置参数设置给OfflineWebManager,并进行初始化。
/**
* 检查上次安装的离线包,进行改名替换删除等逻辑
*/
static void checkAllVersion() {
OfflineWebManager.getInstance().getExecutor().execute(new CheckVersionTask());
}
先来看下创建的线程池:
@SuppressLint("NewApi")
private void initThreadPool() {
final AtomicInteger mAtomicInteger = new AtomicInteger(1);
SecurityManager var1 = System.getSecurityManager();
final ThreadGroup group = var1 != null ? var1.getThreadGroup() : Thread.currentThread().getThreadGroup();
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
return new Thread(group, runnable, "track io-pool-thread-" + mAtomicInteger.getAndIncrement(), 0);
// return new Thread(group, runnable, "track io-pool-thread-" + mAtomicInteger.getAndIncrement(), 1024 * 256);
}
};
mThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, Math.max(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE),
TIME_OUT, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(128),
threadFactory);
mThreadPoolExecutor.allowCoreThreadTimeOut(true);
mThreadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
super.rejectedExecution(r, e);
}
});
}
这个线程池执行的第一个任务是CheckVersionTask。
可以看出这个任务执行是和保证离线包解压完整性相关的,不做太多研究。 离线包下载后保存到私有目录offline_web中,按照业务为维度划分目录存储。
接下来看下preDownload机制获取离线包的流程,可以看出如果有多个业务的离线包需要加载,采用并行的机制,单个业务的整个流程采用串行的机制:
/**
* 根据业务名称拉取离线包
*
* @param bisName 业务名
*/
static void checkPackage(final String bisName,final ResourceFlow.FlowListener listener) {
if (TextUtils.isEmpty(bisName)) {
if (listener!=null) {
listener.error(null,new IllegalStateException("bisName == null"));
}
return;
}
OfflineWebManager.getInstance().getExecutor().execute(new CheckAndUpdateTask(bisName,listener));
}
执行了CheckAndUpdateTask,整个流程通过XXXFlow连接起来。 1.ResourceFlow:获取,下载,解压,替换离线包流程管理类。 2.FetchPackageFlow:获取更新包。 3.DownloadFlow:下载包。 4.ParsePackageFlow:解析离线包。 5.ReplaceResFlow:替换离线包。 除ResourceFlow外,其他Flow实现IFlow接口。
public interface IFlow {
void process() throws FlowException;
}
每个Flow执行其process方法,将整个离线包获取流程串联起来,整个机制是一个简单的责任链模式。
FetchPackageFlow 请求第二步服务端提供的queryOffline接口,获取到离线包相关信息,主要有: 1.url 离线包下载地址。 2.version 离线包版本。 剩余就是离线包相关的一些控制和降级策略的字段。
DownloadFlow 服务端返回有新的离线包,则进行下载。
ParsePackageFlow ReplaceResFlow 这两步对离线包进行解压和替换,不论是本次生效还是下次启动时生效,成功的zip包解压后的位置为://data/data/pkgName/offline_web/bisname/cur
3.webview相关
离线包相关使用webview为OfflineWebView。
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
可以从使用上看出,主要是拦截了加载url的方法,进行了特殊处理。
private void initProxy() {
mOfflineWebViewProxy = OffWebProxyFactory.getProxy(this);
}
@Override
public void loadUrl(String url) {
super.loadUrl(mOfflineWebViewProxy.loadUrl(url));
}
@Override
public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
super.loadUrl(mOfflineWebViewProxy.loadUrl(url), additionalHttpHeaders);
}
请求加载处理被代理给OfflineWebViewProxy。
下面找下OfflineWebViewProxy loadUrl的逻辑:
看图说话,最初请求的url,取query的offweb,得到bizName,解析出来后映射到本地地址,这样就找到了离线包的入口页面:index.html。
最终将http协议的在线url转化为了file协议的本地url:
file:///data/user/0/com.lalamove.huolala.client.offline_web/offline_web/act3-2108-turntable/cur/index.html?offweb=act3-2108-turntable&offweb_host=www.baidu.com
到这里为止,基本上吧这个离线包的主流程研究明白了,其他支线如稳定性保障,降级策略也了解了个7788,需要的时候再详细对下就可以了。
3.前端相关
(1)有个细节是前端页面的所有绝对路径需要修改为相对路径,如主页面的资源和js都通过前端的打包平台适配为相对路径。
(2)由于不是搞前端的,对于跨域问题也只是略知一二。 看官方说明,只有对请求的跨域进行了适配,在离线包页面中进行请求时,由于是本地文件,请求header里的refer会变为null,这需要后端在网关层面进行支持。
这里可以预习下前端对于请求跨域的通用处理方案,CORS,全称是"跨域资源共享"(Cross-origin resource sharing)。 【1】在请求报文中,增加origin字段。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。 (1)Access-Control-Allow-Origin 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。 2)Access-Control-Allow-Credentials 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
转载自:https://juejin.cn/post/7380771735681548307