NDK学习笔记-增量更新

虽然现在有插件化开发和热修复,但为何还需要增量更新?插件化开发和热修复依赖于宿主程序,增量更新适合更新宿主程序。

差分包生成的前提

差分包的生成依赖于BsDiff开源项目,而BsDiff又依赖于Bzip2
BsDiff源代码下载地址:BsDiff
Bzip2源代码下载地址:Bzip2

Window服务器端配置

新建Java Web项目

  • new -> Web -> Dynamic Web Project
    由于我本地装的是tomcat 7,这里就选择Apache Tomcat v7.0
    创建Windows服务器差分
  • 在src目录下生成三个类,用于生成差分包
    路径类(Constants.java
public class Constants {
	public static final String OLD_APK_PATH = "E:\workspace\android\appupdatetest\AppUpdate_old.apk";
	public static final String NEW_APK_PATH = "E:\workspace\android\appupdatetest\AppUpdate_new.apk";
	public static final String PATCH_PATH = "E:\workspace\android\appupdatetest\apk.patch";
}

native方法类(BsDiff.java

public class BsDiff {
	public native static void diff(String oldfile, String newfile, String patchfile);
	static {
		System.loadLibrary("bsdiff");
	}
}

主方法类(BsDiffTest.java

public class BsDiffTest {
	public static void main(String[] args) {
		BsDiff.diff(Constants.OLD_APK_PATH, Constants.NEW_APK_PATH, Constants.PATCH_PATH);
	}
}

生成Windows平台下的dll动态库(VS)

  • 新建空项目 -> 将原代码添加到项目(包含c,cpp,h) -> 移除bspatch.cpp(Server端不需要合并)
  • 去除警告(项目右键 -> 属性 -> 配置属性 -> C/C++ -> 命令行 -> 添加指令)

-D _CRT_SECURE_NO_WARNINGS -D _CRT_NONSTDC_NO_DEPRECATE

  • 去除严格语法检查(配置属性 -> C/C++ -> 常规 -> SDL检查(否))
  • 生成dll动态库(配置属性 -> 常规 -> 配置类型 -> dll动态库)
  • 生成x64平台dll(Debug -> 配置管理器 -> win32 -> x64)
  • 将bsdiff.cpp中的main改为bsdiff_main,方便JNI调用
  • 将编写好的native方法类生成头文件,并在项目中添加进来
  • VS中引入头文件jni.h和jni_md.h,并将头文件包含#includ <jni.h>改为#include "jni.h"
  • 在bsdiff.cpp文件中实现native方法(注意在这里要统一所有源文件的编码格式,否可能找不都头文件)
//JNI调用
JNIEXPORT void JNICALL Java_com_cj5785_appuodateserver_bsdiff_BsDiff_diff
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr)
{
	int argc = 4;
	char *oldfile = (char *)env->GetStringUTFChars(oldfile_jstr, NULL);
	char *newfile = (char *)env->GetStringUTFChars(newfile_jstr, NULL);
	char *patchfile = (char *)env->GetStringUTFChars(patchfile_jstr, NULL);
	//参数,第一个参数无效,第二个参数为源文件路径,第三个参数为新文件路径,第四个参数为差分包路径
	char *argv[4] = { "bsdiff" , oldfile, newfile, patchfile};
	bsdiff_main(argc, argv);
	env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
	env->ReleaseStringUTFChars(newfile_jstr, newfile);
	env->ReleaseStringUTFChars(patchfile_jstr, patchfile);
}
  • 此时如果生成,会报错(“DWORD FormatMessageW(DWORD,LPCVOID,DWORD,DWORD,LPWSTR,DWORD,va_list *)”: 无法将参数 5 从“char [1024]”转换为“LPWSTR”),此处将lastErrorTxt强转为LPWSTR即可((LPWSTR)lastErrorTxt)
  • 去除错误,编译即可生成dll动态库

生成差分包

  • 将生成的dll放入web项目根中
  • 运行web程序,生成差分包
  • 将生成的差分包放在服务器Webcontent(网页根目录)下

Android端配置

在Android端,最主要的就是bspatch.c文件,这个文件用于差分包的合成
在这里通过演示一个前台的活动去更新软件,实际开发中一般放在后台,通过每次启动区服务端检查有无更新,从而决定时候下载差分包

调用差分合成的native类(BsPatch.java)

public class BsPatch {
	public static native void patch(String oldfile, String newfile, String patchfile);
	static {
		System.loadLibrary("bspatch");
	}
}

根据BsPatch.java,使用javah生成头文件
新建jni文件夹,将头文件拷贝至jni文件夹,添加本地支持(具体操作步骤参考之前的NDK开发流程一文)
在bspatch.c中实现头文件声明的函数,同时还需要导入依赖的Bzip2中用到的C文件
同时将main改为bspatch_main,方便jni调用
其实现类似于服务端,在此不再赘述

JNIEXPORT void JNICALL Java_com_cj5785_appupdate_BsPatch_patch
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr)
{
	int argc = 4;
	char *oldfile = (char *)(*env)->GetStringUTFChars(env, oldfile_jstr, NULL);
	char *newfile = (char *)(*env)->GetStringUTFChars(env, newfile_jstr, NULL);
	char *patchfile = (char *)(*env)->GetStringUTFChars(env, patchfile_jstr, NULL);
	//参数,第一个参数无效,第二个参数为源文件路径,第三个参数为新文件路径,第四个参数为差分包路径
	char *argv[4] = { "bspatch" , oldfile, newfile, patchfile};
	bspatch_main(argc, argv);
	(*env)->ReleaseStringUTFChars(env, oldfile_jstr, oldfile);
	(*env)->ReleaseStringUTFChars(env, newfile_jstr, newfile);
	(*env)->ReleaseStringUTFChars(env, patchfile_jstr, patchfile);
}

常量类(Constants.java)

此处使用本地tomcat服务器测试,实际中使用实际主机的IP

import java.io.File;

import android.os.Environment;

public class Constants {
	public static final String PATCH_FILE = "apk.patch";
	public static final String URL_PATCH_DOWNLOAD = "http://192.168.1.3:8080/" + PATCH_FILE;
	public static final String PACKAGE_NAME = "com.cj5785.appupdate";
	public static final String SD_CARD = Environment.getExternalStorageDirectory().toString() + File.separatorChar;
	public static final String NEW_APK_PATH = SD_CARD + "apk_new_test.apk";
	public static final String PATCH_FILE_PATH = SD_CARD + PATCH_FILE;
}

下载工具类(DownloadUtils.java)

主要用于下载差分包

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import android.os.Environment;

public class DownloadUtils {
	public static File download(String url) {
		File file = null;
		InputStream iStream = null;
		FileOutputStream oStream = null;
		try {
			file = new File(Environment.getExternalStorageDirectory(), Constants.PATCH_FILE);
			if(file.exists()) {
				file.delete();
			}
			HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
			conn.setDoInput(true);
			iStream = conn.getInputStream();
			oStream = new FileOutputStream(file);
			byte[] buf = new byte[1024];
			int len = 0;
			while((len = iStream.read(buf)) != -1) {
				oStream.write(buf, 0, len);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				iStream.close();
			} catch (Exception e2) {
				e2.printStackTrace();
			}
			try {
				oStream.close();
			} catch (Exception e2) {
				e2.printStackTrace();
			}
		}		
		return file;
	}
}

apk工具类(ApkUtils.java)

此工具类主要用于apk的安装

import java.io.File;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.text.TextUtils;

public class ApkUtils {

	public static String getSourceApkPath(Context context, String packageName) {
		if(TextUtils.isEmpty(packageName)) {
			return null;
		}
		try {
			ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
			return appInfo.sourceDir;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public static void installApk(Context context, String apkPath) {
		Intent intent = new Intent(Intent.ACTION_VIEW);
		intent.setDataAndType(Uri.parse("file://" + apkPath), "application/vnd.android.package-archive");
		context.startActivity(intent);
	}
}

主活动(MainActivity.java)

import java.io.File;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		new ApkUpdateTask().execute();
	}
	
	class ApkUpdateTask extends AsyncTask<Void, Void, Boolean>{

		@Override
		protected Boolean doInBackground(Void... params) {
			try {
				//下载差分包
				File patchFile = DownloadUtils.download(Constants.URL_PATCH_DOWNLOAD);
				//获取当前应用的apk文件
				String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
				//和并得到最新版的APK文件
				String newfile = Constants.NEW_APK_PATH;
				String patchfile = patchFile.getAbsolutePath();
				BsPatch.patch(oldfile, newfile, patchfile);
			} catch (Exception e) {
				e.printStackTrace();
				return false;
			}
			return true;
		}

		@Override
		protected void onPostExecute(Boolean result) {
			super.onPostExecute(result);
			//安装apk
			if(result) {
				ApkUtils.installApk(MainActivity.this, Constants.NEW_APK_PATH);
			}
		}
	}
}

其他

布局文件并没有与项目有关的地方,这里就不用贴出来了
清单文件与项目有关的地方有两个,一个是versionCode和versionName,这个地方主要是用来做安装校验的,现在的代码在安装的时候并没有做校验,所以还存在一些问题,即安装校验和文件删除
还有一个就是用户权限

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

至此,Android的核心代码就已经贴完了

Linux服务器配置

Windows服务端搞定了,那么Linux服务端也顺便搞一搞

准备源代码

将所需的bsdiff.c源文件和bzip2相关源文件以及Linux下的jni.hjni_md整理出来,这里我直接提取了Linux端的java目录下的jni.hjni_md.h
修改bsdiff.c源文件,添加JNI头文件,使其能被JNI调用
同时引入bsdiff.c所需文件
bsdiff.c中的main改为bsdiff_main
bsdiff.c中调用bsdiff_main函数(即实现JNI头函数)
此处和windows类似,可以参考Windows下的dll编译

//JNI调用
JNIEXPORT void JNICALL Java_com_cj5785_appuodateserver_bsdiff_BsDiff_diff
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr)
{
	int argc = 4;
	char *oldfile = (char *)env->GetStringUTFChars(oldfile_jstr, NULL);
	char *newfile = (char *)env->GetStringUTFChars(newfile_jstr, NULL);
	char *patchfile = (char *)env->GetStringUTFChars(patchfile_jstr, NULL);
	//参数,第一个参数无效,第二个参数为源文件路径,第三个参数为新文件路径,第四个参数为差分包路径
	char *argv[4] = { "bsdiff" , oldfile, newfile, patchfile};
	bsdiff_main(argc, argv);
	env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
	env->ReleaseStringUTFChars(newfile_jstr, newfile);
	env->ReleaseStringUTFChars(patchfile_jstr, patchfile);
}

编译生成动态库

gcc -fPIC -shared blocksort.c decompress.c bsdiff.c randtable.c bzip2.c huffman.c compress.c bzlib.c crctable.c -o bsdiff.so

Linux下的jar包生成

将生成的.so动态库放入根目录,其代码与Windows服务端代码类似
修改Contants.java下的路径,使其为Linux目录
修改BsDiff.java文件,指定动态库路径(这里有两种做法,不修改其路径,将动态库放入系统动态库目录,不建议这么做,建议放在自定义目录,使用System.load加载)

static {
    System.load("/home/ubuntu/bsdiff.so");
}

导出jar包
根据Contants.java路径放入旧文件和新文件
运行jar包,生成差分包

java -jar jarname.jar

差分算法简单分析

  • 不同部分用Bzip压缩
  • 型旧版本重复越多,差分包越小
  • 新旧版本重复越少,差分包越大

差分运用

无论是Windows还是Linux,在使用时候都是类似的
由原代码的情况下,可以编译出很多可用的版本
命令行的C语言代码
可视化的C++代码都是可以的

原文地址:https://www.cnblogs.com/cj5785/p/10664669.html