最近就是在练习ndk开发,刚好遇到android增量更新的话题,主要是工具的运用,略带使用第三方so库的流程~~~话不多说,准备开车。
大家都会好奇,为啥我在应用市场上更新一个上百m的apk咋流量显示下载,只用了几十M,甚至可能更少,这不科学啊!事实上他就是这样子的。
其实是这样的,我们在应用市场只是下载了一个增量更新包,他会与当前我们使用版本的apk进行合并,而后再次提醒安装。(在这里,有些人会有疑问,如果旧版本被用户在sdcard清除掉了,是不是就没有办法跟服务器下载下来的差分包进行合并了,也就没办法就行更新了呢?其实还是有的补救的,就是这个apk会有一份拷贝在/data/app中,除非...就是某些人修改了机子的root权限,硬要到该目录把apk删掉....)
我们这里会使用到差分合并工具,工具在下面网址http://www.daemonology.net/bsdiff/
差分(bsdiff) 官网上说bsdiff is quite memory-hungry,是一个耗时操作,所以我们差分一般放在服务端进行。
合并(bpatch) 一般将服务端下载的差分包,然后在客户端合并
官网上有这么一句话bsdiff and bspatch use bzip2,所以这里我将bzip2的下载地址也发出来http://www.bzip.org/downloads.html
好的,开车!
1. 生成差分文件
Window环境下差分包 下载下来是这个玩意名字叫 bsdiff4.3-win32-src.zip
我们把刚刚下载的差分包解压一下,发现在bsdiff4.3-win32-src\Release路径下面有差分工具,我们这里可以用官方的,也可以自己来编译一下。
这里我深思熟虑了一下,那就用官方的吧,哈哈!
我们可以看到源码的bsdiff.cpp文件中对应找到main函数有这么一句代码
int main(int argc,char *argv[])
{
int fd;
u_char *old,*_new;
off_t oldsize,newsize;
off_t *I,*V;
off_t scan,pos,len;
off_t lastscan,lastpos,lastoffset;
off_t oldscore,scsc;
off_t s,Sf,lenf,Sb,lenb;
off_t overlap,Ss,lens;
off_t i;
off_t dblen,eblen;
u_char *db,*eb;
u_char buf[8];
u_char header[32];
FILE * pf;
BZFILE * pfbz2;
int bz2err;
if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
通过这里我们可以看出要传四个参数,argv[0]这个参数就是官方提供的差分工具的名字
bsdiff.exe,第二个参数就是旧版本apk目录,第三个参数就是新版本apk目录字,最后一个参数就是输出差分文件路径。这里我的操作和输出环境都在同一个目录。所以只需要在控制台输入对应命令就ok了。(命令如下)
bsdiff.exe old.apk new.apk apk.patch
好的差分包就生成完毕。
上述的操作都是人工操作的,我们可以在后台通过代码动态生成差分文件,大概操作方式是,这里后台用的是eclipse,我们在工程中new一个class,写一个native方法,然后控制台中使用javah命令,生成.h头文件,然后把差分源码还有刚刚的头文件都导入到VS中,把我们jin方法拷贝到bsdiff.cpp中进行实现,自己的jni再调用源码中的main方法,注意要吧这里的main方法改一下名字,然后在自己jin调用。生成一个dll动态库,然后放进java工程进行操作,后续的操作就跟我前面的jni编程文章一样,最后就是在java中实现生成代码。这里简单列一下cpp中代码.
JNIEXPORT void JNICALL Java_com_jni_demo_JniMain_diff
(JNIEnv * env, jobject jobj,jstring oldPath,jstring newpath,jstring pathPath){
int argc = 4;
char* argv[4];
char* oldPath_ch = (char*)env->GetStringUTFChars(oldPath,NULL);
char* newpath_ch = (char*)env->GetStringUTFChars(oldPath, NULL);
char* pathPath_ch = (char*)env->GetStringUTFChars(oldPath, NULL);
//我们既然动态调用,这里第一个参数随便传
argv[0] = "bsdiff";
argv[1] = oldPath_ch;
argv[2] = newpath_ch;
argv[3] = pathPath_ch;
//这里我们将main函数改名字了改成bsdif_main
bsdif_main(argc, argv);
//释放内存
env->ReleaseStringUTFChars(oldPath, oldPath_ch);
env->ReleaseStringUTFChars(newpath, newpath_ch);
env->ReleaseStringUTFChars(pathPath, pathPath_ch);
}
我们后台服务器有可能是Linux环境的,接下来再简单介绍一下在Linux下如何编译差分可执行文件
- 首先我们下载linux的bsdiff-4.3.tar包,解压。
- 这里我们做差分,所以我们这里只需要里面的bsdiff.c,将他拷贝出来放到自己新建目录文件夹
- 接着我们将下载的bzip2-1.0.6解压,将里面的.c和.h文件拷贝出来放到自己新建目录文件夹
- 将我们新建目录文件夹放到后台linux环境中
- 接下来就是在linux控制台中输入命令
- 我们先将我们刚刚上传的文件的执行权限改一下 chmod 777 ./*
- 这里我们可以通过linux自带的交叉编译工具gcc生成linux后台可用的执行文件(如果是需要编译成android平台能用的.so文件,则需要在linux平台安装ndk的支持)我们这里还是先讲生成linux平台能用的可执行文件 gcc -fPIC -shared blocksort.c decompress.c bsdiff.c randtable.c bzip2.c huffman.c compress.c bzlib.c crctable.c -o BsDiff 或者生成linux可用的动态库gcc -fPIC -shared blocksort.c decompress.c bsdiff.c randtable.c bzip2.c huffman.c compress.c bzlib.c crctable.c -o BsDiff.so
- 接下来会报一个错,bzlib.h找不到目录,我们只需将bsdiff.c中的尖括号改成双引号就ok了,这里的命令为vim bsdiff.c
- 接下来,我们编译上面命令还是会报错,错误说可执行文件只能有一个main方法,我们这里只需要留bsdiff.c的main方法即可,其他的main方法把名字改一下。
ok差分过程结束。接下来我们说一下终端的合并过程。
2 .合并差分文件
我们这里的环境是Android Stdio
这里前期操作跟上面的linux端操作类似,我们需要拷贝出来的是bspatch.c替换掉刚刚那个新目录文件夹中的bsdiff.c,然后把整个目录丢进android stdio下图目录。
接下来我们需要修改CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#修改一 :这里定义一个变量指定目录
file(GLOB my_c_path src/main/cpp/bzip2/*.c)
#修改二 :此处编译目录下的c文件
add_library( # Sets the name of the library.
bspatch
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${my_c_path}
src/main/cpp/bspatch.c )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
#修改三 :需要链接成so库的名字
bspatch
# Links the target library to the log library
# included in the NDK.
${log-lib} )
好的这里修改了3个地方,都在上面注释了。
然后我们build一下工程,就会在以下目录上生成so库,这里我没有指定生成so的平台目录,默认就是全平台生成的。
==注意这里要将所有的.c文件中的main函数重命名成别的名字==
好的我们继续!
这里我们新建一个java文件,写一个native方法。
public class BsPath {
public native static int patch(String oldApk,String newApk,String patch);
}
接下来我们用javah命令生成.h头文件,如下图所示
ok,接下来 我们编写bspatch.c文件
先引入我们刚刚生成的.h文件,这里我把文件丢进cpp目录里面了。
#include "kaka_com_patch_app_BsPath.h"
这里为了打印提示,我这里引入了log库和定义了几个宏定义
#include
#define TAG "JNI_LOG"
#define LOGD(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
然后我们将.h的jni方法复制到bspatch.c中进行实现,代码如下
/*
* Class: kaka_com_patch_app_BsPath
* Method: patch
* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_kaka_com_patch_1app_BsPath_patch
(JNIEnv *env, jclass jclass, jstring old_str, jstring new_str, jstring patch) {
int result = -1;
LOGD("patch begin");
int argc = 4;
char* argv[4];
char * old_ch = (*env)->GetStringUTFChars(env,old_str,JNI_FALSE);
char * new_ch = (*env)->GetStringUTFChars(env,new_str,JNI_FALSE);
char * patch_ch = (*env)->GetStringUTFChars(env,patch,JNI_FALSE);
argv[0] = "bspatch";
argv[1] =old_ch;
argv[1] =new_ch;
argv[1] =patch_ch;
//调用合并方法,成功时候返回0
result = bspatch_main(argc,argv);
//防止内存泄漏,释放
(*env)->ReleaseStringUTFChars(env,old_str,old_ch);
(*env)->ReleaseStringUTFChars(env,new_str,new_ch);
(*env)->ReleaseStringUTFChars(env,patch,patch_ch);
return result;
}
这里我们要记得加载我么的so库
public class BsPath {
static{
System.loadLibrary("bspatch");
}
public native static int patch(String oldApk,String newApk,String patch);
}
最后我们将几个util类写一下
ApkUtil类
public class ApkUtils {
//获取APK版本号 在公司实际开发中 是根据 key uuid判断(渠道 版本)
public static int getVersionCode (Context context, String packageName) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo info = pm.getPackageInfo(packageName, 0);
Log.d("Patch_App","getVersionCode = "+info.versionCode);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取已安装Apk文件的源Apk文件
* 如:/data/app/my.apk
*
* @param context
* @param packageName
* @return
*/
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 (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* 安装Apk
*
* @param context
* @param apkPath
*/
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);
}
}
Contants类
public class Contants {
public static final String PATCH_FILE = "apk.patch";
public static final String URL_PATCH_DOWNLOAD = "locahost:3000"+PATCH_FILE;
public static final String SD_CARD = Environment.getExternalStorageDirectory() + File.separator;
//新版本apk的目录
public static final String NEW_APK_PATH = SD_CARD+"apk_new.apk";
public static final String PATCH_FILE_PATH = SD_CARD+PATCH_FILE;
}
DownLoadUtils类
public class DownLoadUtils {
/**
* 下载差分包
* @param url
* @return
* @throws Exception
*/
public static File download(String url){
File file = null;
InputStream is = null;
FileOutputStream os = null;
try {
file = new File(Environment.getExternalStorageDirectory(),Contants.PATCH_FILE);
if (file.exists()) {
file.delete();
}
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setDoInput(true);
is = conn.getInputStream();
os = new FileOutputStream(file);
byte[] buffer = new byte[1*1024];
int len = 0;
while((len = is.read(buffer)) != -1){
Log.d("DownLoadUtils", String.valueOf(len));
os.write(buffer, 0, len);
}
} catch(Exception e){
e.printStackTrace();
}finally{
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
}
最后我们在MainActivity中调用一下
MainActivity类
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (ApkUtils.getVersionCode(this, getPackageName()) < 2.0) {
Log.d(TAG,"不是最新的版本号 开始更新 ");
new ApkUpdateTask().execute();
} else {
Log.d(TAG ," 最新版本号 无需更新");
}
}
class ApkUpdateTask extends AsyncTask {
@Override
protected Boolean doInBackground(Void... params) {
Log.d(TAG,"开始下载 。。。");
File patchFile = DownLoadUtils.download(Contants.URL_PATCH_DOWNLOAD) ;
Log.d(TAG,"下载完成 。。。");
String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
String newFile = Contants.NEW_APK_PATH;
String patchFileString = patchFile.getAbsolutePath();
Log.d(TAG,"开始合并");
int result = BsPath.patch(oldfile, newFile,patchFileString);
Log.d(TAG,"开始完成");
if (result == 0) {
return true;
} else {
return false;
}
}
@Override
protected void onPostExecute(Boolean aBoolean) {
if (aBoolean) {
Log.d(TAG,"合并成功 开始安装新apk");
ApkUtils.installApk(MainActivity.this, Contants.NEW_APK_PATH);
}
}
}
}
好的,大功告成!!!!终端的合并告一段落。
亲测一下木有问题。好的有点晚了,要睡觉了~~