Android 增量更新(使用CMake编译)

784 阅读4分钟
原文链接: xiasuhuei321.com

前言

近期公司的 App 版本迭代比较快,经常发新版本,但实际上就我我自己来说,手机里的 App 不到不能用的时候我是不会更新的,我们前期的解决方案就是强制更新,不更新不让用,以前我们用户少的时候还能这么干,用户量上去还这么干,基本等于劝退那些不怎么愿意更新的用户。所以近期研究的技术点是 热修复 与 增量更新。热修复可以不用因为更改 Bug 而专门发布新版本,增量更新可以让用户在不得不更新的时候可以少花一些流量。热更新最近的接入测试流程都过的差不多了,比我想象的要顺利一些,不过我接入的是 Tinker,操作起来还是挺麻烦的,同事去看 Sophix 了,Sophix 是无侵入的接入,感觉打包什么的操作应该会简单很多。回归正题,本文是参考了很多文章最后经过实践得出的,因为一些文章的时间比较久远,我在实践的时候也碰到了很多问题,查资料也不太好查,最后想了想还是用 CMake 去编译,最后跑通了整个流程。

简介

增量更新实际上就是使用工具比对新老 apk,得出一个补丁,用户的是老版本的 apk,在下载完增量包之后与自身 apk 合成一个新的apk,然后再次安装(是的,需要重新安装,所以不是热修复,做不到无感)。整个过程实际上我们都是使用的现成的轮子,客户端需要做的就是:

  • 获得用户当前应用的 apk
  • 获取增量包
  • 将老 apk 与增量包合并生成新 apk
  • 合并完成后安装

其中生成增量包与合并是难点,但是并不需要我们自己去实现,已经有工具替我们实现了。就是 bsdiff 和 bzip,当然我在看 鸿洋大神 的这篇文章的时候,bzip 下载已经凉了,那个页面我也实在没看懂在哪下,最后是在鸿洋大神的代码仓库里复制出来的。

生成增量包

首先去下载工具:

下载完之后解压,使用终端(我是 Mac)进入解压后的文件夹,执行:

$ make

make 命令会读入所有的 Makefile 并执行,但是在执行目录下的 Makefile 会报错:

Makefile:13: *** missing separator.  Stop.

我搜到的解决方法是在倒数第一行和第三行加 TAB,我在网上搜到的资料说没有缩进会识别成条件判断符,有缩进会认为他是一个普通的 shell 脚本,当然到底是为什么,我这里并没有深追下去了。

继续执行 make 命令,会生成一个 bsdiff 的文件,之后会报错,我们这里只需要这个,没有生成的那个是合并老版本与增量包的东西。我们这里并不需要,因为合成新包的步骤是在客户端完成的。这里生成 bsdiff 就暂时告一段落,之后我们会利用这个工具来生成增量包,接下来在项目中引入 bzip,用于合成新的 apk。

集成合并工具

首先新建一个项目,勾上支持 Kotlin 和 C++,之后都选默认的,创建好项目之后是这样的:

目录

之前说了 bzip 这玩意我没弄懂怎么下源码,于是去鸿洋大神的 demo 里 copy 了一下,当然,这里我会放上我的 demo 地址,毕竟你是在看我的文,用我的 demo 不过分。将之前下载 bsdiff 中的 bspatch.c 拷贝到项目的 cpp 文件夹中,再将整个 bzip 拷贝到 cpp 文件夹中,由于我们这里是使用 CMake 来编译,所以需要在自动生成的 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)

include_directories(src/main/cpp/bzip2/bzlib.h
src/main/cpp/bzip2/bzlib_private.h
)

# 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.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp
             src/main/cpp/bspatch.c
             src/main/cpp/bzip2/blocksort.c
             src/main/cpp/bzip2/bzip2.c
             src/main/cpp/bzip2/bzip2recover.c
             src/main/cpp/bzip2/bzlib.c
             src/main/cpp/bzip2/compress.c
             src/main/cpp/bzip2/crctable.c
             src/main/cpp/bzip2/decompress.c
             src/main/cpp/bzip2/dlltest.c
             src/main/cpp/bzip2/huffman.c
             src/main/cpp/bzip2/mk251.c
             src/main/cpp/bzip2/randtable.c
             src/main/cpp/bzip2/spewG.c
             src/main/cpp/bzip2/unzcrash.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.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

这里需要在 bspatch.c 中加入我们的一些东西:

JNIEXPORT jint
JNICALL Java_com_xiasuhuei321_incrementapk2_MainActivity_bspatch
        (JNIEnv *env, jclass cls,
         jstring old, jstring new, jstring patch) {
    int argc = 4;
    char *argv[argc];
    argv[0] = "bspatch";
    argv[1] = (char *) ((*env)->GetStringUTFChars(env, old, 0));
    argv[2] = (char *) ((*env)->GetStringUTFChars(env, new, 0));
    argv[3] = (char *) ((*env)->GetStringUTFChars(env, patch, 0));


    int ret = patchMethod(argc, argv);

    (*env)->ReleaseStringUTFChars(env, old, argv[1]);
    (*env)->ReleaseStringUTFChars(env, new, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
    return ret;
}

这里的 Java_com_xiasuhuei321_incrementapk2_MainActivity_bspatch 是有一定命名规律的,用过 jni 的同学应该知道这玩意可以通过 javah -d 的命令来为带有 native 的 class 文件生成 .h 的头文件。这里不深究,总是就是我在这个包底下的 MainActivity 里写了一个 native(kotin 中是 extern)方法叫 bspatch,方法里拿到了传入的三个参数,调用了 bspatch 的 patchMethod,最后释放了字符串资源。这里的方法名一定要写对,不然运行的时候会报错,大意是没有人实现你的 native 方法。接下来看 MainActivity 代码:

package com.xiasuhuei321.incrementapk2

import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v4.content.FileProvider
import android.support.v7.app.AppCompatActivity
import com.tbruyelle.rxpermissions2.RxPermissions
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .subscribe{
                }


        val destApk = File(Environment.getExternalStorageDirectory(), "dest.apk")
        val patch = File(Environment.getExternalStorageDirectory(), "xxx/PATCH.patch")

        patchApkBtn.setOnClickListener {
            Thread {
                bspatch(applicationInfo.sourceDir, destApk.absolutePath, patch.absolutePath)
                runOnUiThread { installApk(destApk) }
            }.start()
        }
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    external fun stringFromJNI(): String

    external fun bspatch(oldPth: String, newPath: String, path: String): Int

    companion object {

        // Used to load the 'native-lib' library on application startup.
        init {
            System.loadLibrary("native-lib")
        }
    }

    private fun installApk(file: File) {
        try {
            val f = file
//            val f = File("sdcard/remeet/apk/remeet.apk")
            val intent = Intent(Intent.ACTION_VIEW)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                val apkUri = FileProvider.getUriForFile(this, "${applicationInfo.packageName}.installapk.provider", f)
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
            } else {
                intent.setDataAndType(Uri.fromFile(f), "application/vnd.android.package-archive")
            }

            startActivity(intent)
        } catch (e: Exception) {
        } finally {

        }
    }
}

比较简单,界面一共就一个按钮和一个文字,文字显示版本(我自己乱填的),按钮点击会触发合并补丁的事件。当然,因为我自己的手机是8.0的,我要看效果是能简单适配一下了,各位如果代码跑不起来可以看看我的清单文件申请的权限,和对于7.0的适配。然后自己这里打两个不同的包,一个是 old 一个是 new ,之后进入 bsdiff 的目录,执行命令:

$ ./bsdiff old.apk new.apk PATCH.patch

之后将这个 PATCH.patch 用 adb push 命令推到自己在代码中指定的目录,就可以开始尝试合成补丁了~

最后是跑起来了:

跑起来了

当然,这只是一个 Demo,需要你自己去完善整个流程,我这里只是演示了合成补丁包并重新安装的部分。