Android增量更新

最近就是在练习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下图目录。

Android增量更新_第1张图片
image

接下来我们需要修改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的平台目录,默认就是全平台生成的。

Android增量更新_第2张图片
image

==注意这里要将所有的.c文件中的main函数重命名成别的名字==

好的我们继续!

这里我们新建一个java文件,写一个native方法。

public class BsPath {
    public native static int patch(String oldApk,String newApk,String patch);
}

接下来我们用javah命令生成.h头文件,如下图所示

Android增量更新_第3张图片
image

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

}

好的,大功告成!!!!终端的合并告一段落。

亲测一下木有问题。好的有点晚了,要睡觉了~~

你可能感兴趣的:(Android增量更新)