最近项目用到H Builder开发,从安卓原生开发转到H5,让人产生不少新鲜感,同时感慨下,先不说体验方面(0_0),Html语言的简单易用性确实比基于JAVA的安卓要快捷。var这种弱类型,让你不用纠结于到底要用哪种数据类型定义和接收(我是用上瘾了= =),vue.js基于MVVM的开源框架使H5的开发简洁到极致。基于H5开发,每个模块的实现就可以统一,不用安卓一版,iOS一版,降低了开发成本。官网开放的一些API也很方便,包含一些对原生应用层的封装。今天来聊聊基于H Builder开发的App的差量升级。
官网提供了3种升级方式:
如果你需要新增5+模块时必须用此类更新升级,如果不这样,会导致项目报模块未添加错误。
即wgt升级
为了解决没必要的资源更新的升级,只需要添加配置文件,配置从A版本升级到B版本的差异描述,wgtu格式。
看起来这三种升级基本满足了开发者的需求,但是本人在开发过程中遇到了不愉快的升级体验。当我需要升级,但是有些资源文件(尤其是图片)太大,让我考虑用差量升级的时候,我发现,我需要打的差量升级包随着版本号的增多而增多,如果当前版本是第n个版本,那我就需要打n-1个差量升级包wgtu。what if n 特别大?
无奈只能用wgt升级。但是这样又给用户带来了不好的体验,下载大量不必要的资源文件。基于此,本人考虑了一种改进方案解决这样的问题,即自己做个差量升级插件,实现无版本号比对的差量升级,升级包也是压缩资源,可以自定义后缀格式。升级包的内容,除去不需要更新的资源,其他的都包括。但是拿数据说话,项目的其他资源一共也没有图片资源的1/4大。此外,如果图片需要更新,则只需要将对应目录下的图片添加上即可。
思路有了,但是怎么干呢。那就是先捋一遍官网差量升级的思路:
注意:当通过官网提供的下载API 即plus.downloader.createDownload(url, options,function)进行下载时,如果options中未设置下载路径filename:“_doc/”,则默认下载到:sdcard/Android/data/[项目包名]/downloads/目录下,如果指定路径filename:“_doc/update/”,则下载到sdcard/Android/data/[项目包名]/apps/[appid]/doc/update/目录下。(用H Builder直接运行的项目,多了一层.HBuilder的隐藏文件夹)
首先官网通过检测manifest.json中的version版本号,判断是否升级。如果需要升级,则下载差量升级包wgtu,暂存到目录sdcard/Android/data/[项目包名]/apps/appId/[自己指定的下载路径]/目录下,然后解压,然后读取配置文件update.xml,通过配置文件对sdcard/Android/data/[项目包名]/apps/www下的文件进行增删改操作,操作完毕,重启app,OK,搞定。
那就知道自己该怎么干了,根据需要压缩资源包,定义成.wgtzu格式,无需配置文件,app端下载完成解压,将图片资源直接进行添加到对应目录下(强制替换同名文件),其他资源文件则强制替换。待替换完成,重启。
下面就开始真正实现差量更新插件了:
官网提供了第三方插件开发的教程:http://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/66
先集成离线打包,可以参考我之前的文章:http://blog.csdn.net/qq_14859923/article/details/53189869
踩坑点:JS调用的方法不能有静态方法,我在这卡了1小时,= =!
document.addEventListener('plusready', function() {
//插件别名
var _LCUPDATER = 'lcUpdater';
var B = window.plus.bridge;
//声明函数原型
var lcUpdater = {
//解压
upZipFile: function(newDirPath, originPath, successCallback, failCallback) {
var success = typeof successCallback !== 'function' ? null : function(args) {
successCallback(args);
};
var fail = typeof failCallback !== 'function' ? null : function(args) {
failCallback(args);
};
callbackID = B.callbackId(success, fail);
return B.exec(_LCUPDATER, "upZipFile", [callbackID, newDirPath, originPath]);
}
};
window.plus.lcUpdater = lcUpdater;
console.log("window.plus.lcUpdater=" + JSON.stringify(window.plus.lcUpdater));
}, true);
因为没涉及到对比差异,所以没有解压到当前目录,直接解压到了项目中,对文件进行替换。这个过程,注意一个问题,资源文件不要带有中文名字,这样进行解压会报 java.lang.IllegalArgumentException: MALFORMED 错误。原因是系统提供的Zip压缩文件处理没有处理中文,如果非要用中文可以用ant.jar处理。
/**
* Java utils 实现的Zip工具
*
* @author zxr
*/
public class UpdateUtil extends StandardFeature {
static final String TAG = UpdateUtil.class.getSimpleName();
private final int BUFF_SIZE = 10 * 1024;
// APPID
private static final String APP_ID = "H5C22DBD6";
public void onStart(Context pContext, Bundle pSavedInstanceState,
String[] pRuntimeArgs) {
}
/**
* 如:sdcard/Android/data/com.lc.android.lcuploader/
*
* @return
*/
public static String getRootPath() {
File sd = Environment.getExternalStorageDirectory();
Log.d(TAG, "sd=" + sd.getAbsolutePath());
String proRootPath = sd.getAbsolutePath()
+ File.separator
+ "Android"
+ File.separator
+ "data"
+ File.separator
+ DCloudApplication.getInstance().getApplicationContext()
.getPackageName() + File.separator;
return proRootPath;
}
public static String getProPath() {
return getRootPath() + "apps" + File.separator + APP_ID
+ File.separator + "www" + File.separator;
}
/**
* 解压是耗时操作,所以放到线程中, 在子线程中传递给JS的回调,DCloud已经给回调到主线程中,所以不用担心线程异常。 解压缩一个文件
* 考虑线程池的问题,但是目前就这个功能用到,所以感觉没必要维护线程池,暂时不加了。
*
* @param zipFile
* 压缩文件
* @param folderPath
* 解压缩的目标目录
* @throws IOException
* 当解压缩过程出错时抛出
*/
public void upZipFile(final IWebview pWebview, final JSONArray array) {
new Thread(new Runnable() {
@Override
public void run() {
String zipFilePath = getRootPath() + array.optString(1);
String folderPath = getProPath();
Log.d(TAG, "压缩文件:" + zipFilePath);
Log.d(TAG, "目标文件:" + folderPath);
File zipFile = new File(zipFilePath);
File desDir = new File(folderPath);
Log.d(TAG, "desDir.exists()=" + desDir.exists());
Log.d(TAG, "zipFile.exists()=" + zipFile.exists());
if (!desDir.exists()) {
desDir.mkdirs();
}
if (!zipFile.exists()) {
Log.d(TAG, "压缩文件不存在!");
return;
}
boolean isSuccess = true;
ZipFile zf = null;
try {
zf = new ZipFile(zipFile);
for (Enumeration> entries = zf.entries(); entries
.hasMoreElements();) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
if (!entry.isDirectory()) {
InputStream in = zf.getInputStream(entry);
String str = folderPath + File.separator
+ entry.getName();
str = new String(str.getBytes("8859_1"), "GB2312");
Log.d(TAG, "str=" + str);
File desFile = new File(str);
if (!desFile.exists()) {
File fileParentDir = desFile.getParentFile();
if (!fileParentDir.exists()) {
fileParentDir.mkdirs();
}
desFile.createNewFile();
}
Log.d(TAG, "" + desFile.getName());
OutputStream out = new FileOutputStream(desFile);
byte buffer[] = new byte[BUFF_SIZE];
int realLength;
while ((realLength = in.read(buffer)) > 0) {
out.write(buffer, 0, realLength);
}
out.close();
in.close();
}
}
} catch (IOException e) {
e.printStackTrace();
isSuccess = false;
} finally {
if (null != zf) {
try {
zf.close();
} catch (IOException e) {
e.printStackTrace();
isSuccess = false;
}
}
}
final JSONArray newArray = new JSONArray();
newArray.put(isSuccess);
newArray.put(isSuccess ? "文件更新成功!" : "文件更新失败!");
JSUtil.execCallback(pWebview, array.optString(0), newArray,
isSuccess ? JSUtil.OK : JSUtil.ERROR, false);
}
}).start();
}
}
4. h5程序调用测试:
先用H Builder打了个wgt包(也可以用压缩软件直接打zip包),然后将里面的图片资源全部删除,改为wgtzu格式,放到自己搭建的本地服务器上。
var fileName = '';
document.addEventListener('plusready', function() {
}, false);
function download() {
plus.nativeUI.showWaiting("正在下载...");
var dtask = plus.downloader.createDownload("http://192.168.0.106:8080/MyProject/aV1.0.0.wgtzu", {
}, function(d, status) {
plus.nativeUI.closeWaiting();
// 下载完成
if(status == 200) {
//默认路径有 _downloads/aV1.0.0.wgtzu,去掉_
fileName = d.filename.substring(1, d.filename.length);
console.log(fileName);
upzip();
} else {
alert("Download failed: " + status);
}
});
console.log(JSON.stringify(dtask));
//dtask.addEventListener( "statechanged", onStateChanged, false );
dtask.start();
}
var PKG_NAME = 'com.lc.android.lcuploader';
function upzip() {
var newFilePath = fileName;
console.log(JSON.stringify(plus.lcUpdater));
plus.lcUpdater.upZipFile(newFilePath, function(res) {
var isSuccess = res[0];
if(isSuccess) {
plus.nativeUI.alert(res[1], function() {
plus.runtime.restart();
});
} else {
alert(res[1]);
}
});
}
为了直观些,撸上过程图:
到此,差量更新问题算是有一段落了。考虑有时间进行优化,如果有宝贵意见,可以和我一起探讨。
iOS实现思路:
本人不是iOS开发的,同事遇到问题是,当替换文件完成,通过plus.runtime.restart()进行项目重启后,不会重新获取manifest.json中的version信息,拿到的是之前旧版本的version。针对这个问题,只能通过将获取到的version存到本地缓存中,然后取出来和线上的比较,检查到比线上版本低则进行更新,更新成功之后,将线上返回的最新version版本存到本地缓存,以供重启后读取再和线上的比较。这样能解决问题。
项目下载地址