H Builder 浅谈移动App升级更新

最近项目用到H Builder开发,从安卓原生开发转到H5,让人产生不少新鲜感,同时感慨下,先不说体验方面(0_0),Html语言的简单易用性确实比基于JAVA的安卓要快捷。var这种弱类型,让你不用纠结于到底要用哪种数据类型定义和接收(我是用上瘾了= =),vue.js基于MVVM的开源框架使H5的开发简洁到极致。基于H5开发,每个模块的实现就可以统一,不用安卓一版,iOS一版,降低了开发成本。官网开放的一些API也很方便,包含一些对原生应用层的封装。今天来聊聊基于H Builder开发的App的差量升级。


官网提供了3种升级方式:

 1整包升级

如果你需要新增5+模块时必须用此类更新升级,如果不这样,会导致项目报模块未添加错误。

应用资源升级

即wgt升级

应用资源差量升级

为了解决没必要的资源更新的升级,只需要添加配置文件,配置从A版本升级到B版本的差异描述,wgtu格式。


看起来这三种升级基本满足了开发者的需求,但是本人在开发过程中遇到了不愉快的升级体验。当我需要升级,但是有些资源文件(尤其是图片)太大,让我考虑用差量升级的时候,我发现,我需要打的差量升级包随着版本号的增多而增多,如果当前版本是第n个版本,那我就需要打n-1个差量升级包wgtu。what if n 特别大?

无奈只能用wgt升级。但是这样又给用户带来了不好的体验,下载大量不必要的资源文件。基于此,本人考虑了一种改进方案解决这样的问题,即自己做个差量升级插件,实现无版本号比对的差量升级,升级包也是压缩资源,可以自定义后缀格式。升级包的内容,除去不需要更新的资源,其他的都包括。但是拿数据说话,项目的其他资源一共也没有图片资源的1/4大。此外,如果图片需要更新,则只需要将对应目录下的图片添加上即可。

思路有了,但是怎么干呢。那就是先捋一遍官网差量升级的思路:

H Builder 浅谈移动App升级更新_第1张图片

 注意:当通过官网提供的下载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

1.在Android工程的assets\data\properties.xml文件中声明插件类别名和Native层扩展插件类的对应关系

H Builder 浅谈移动App升级更新_第2张图片

踩坑点:JS调用的方法不能有静态方法,我在这卡了1小时,= =!

2.update.js中声明方法,注册方法:

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);

3.UpdateUtil.java文件中进行文件的解压以及替换操作。

因为没涉及到对比差异,所以没有解压到当前目录,直接解压到了项目中,对文件进行替换。这个过程,注意一个问题,资源文件不要带有中文名字,这样进行解压会报  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]);
				}

			});
		}

为了直观些,撸上过程图:

H Builder 浅谈移动App升级更新_第3张图片

可以看到新版本的图片依然存在。

到此,差量更新问题算是有一段落了。考虑有时间进行优化,如果有宝贵意见,可以和我一起探讨。

iOS实现思路:

本人不是iOS开发的,同事遇到问题是,当替换文件完成,通过plus.runtime.restart()进行项目重启后,不会重新获取manifest.json中的version信息,拿到的是之前旧版本的version。针对这个问题,只能通过将获取到的version存到本地缓存中,然后取出来和线上的比较,检查到比线上版本低则进行更新,更新成功之后,将线上返回的最新version版本存到本地缓存,以供重启后读取再和线上的比较。这样能解决问题。


项目下载地址




你可能感兴趣的:(android,开发,h-builder)