Android开发——增量更新实战总结

0.   前言

增量更新的原理还是比较简单的,但是真的实现起来遇到的坑简直让人吐血,我是在windows下实现的,各种各样的坑,各种各样的错误,折腾了整整两天多才完成了,因此记录下来。

先介绍下什么是增量更新和为什么我们需要增量更新,当我们发布新版本的时候,一些用户升级并不是很积极,反正我个人是不太喜欢更新手机上的APP,不知道大家是什么样的习惯,这就造成了新版本的升级率并不高。增量更新就是解决这个问题的途径之一。增量更新的实现原理是将下载下来的增量包与手机上的APK合并形成新的包,然后再次重新安装,相比下载整个安装包的做法大大减少了用户下载等待的时间。

 

1.  增量包的生成

首先第一步肯定是要生成增量包,所谓增量包就是对比并抽取出new.apkold.apk不一样的地方形成的类似于补丁性质的文件。好在这个工作已经有bsdiff工具帮我们实现了,可以直接点击下载,里面也包含了下面要用到的bspatch工具。

将两个版本的apk文件置于目录下并执行bsdiff old.apk new.apk patch.patch命令即可生成如下图所示的patch.patch文件,这就是我们所需要的增量包。有了增量包,当然得和old.apk合体才能完成它的人生意义,增量包和old.apk生成new.apk可以使用bspatch工具实现,命令为bspatch old.apk new.apk patch.patch,这里就不具体演示了,如果想实验先把该文件夹下已有的new.apk移出去。有兴趣可以比对新生成的new.apk和原来new.apkMD5值,肯定是一样的。

 

2.   获取本地apk

PC端实现增量包和old.apk的合体不能完成我们的需求,因为增量包经过网络被用户下载后是在手机端完成合体的,那么如何在手机端完成这个任务呢?

首先肯定是提取应用本地旧的apk,这比较简单,这里为了演示就不判空了,现实应用中如果检测不到old.apk(可能是被删除了)就没必要再给用户传输patch了,那么更新只能传完整包了。获取到old.apk路径的代码如下:

ApplicationInfo applicationInfo =getApplicationContext().getApplicationInfo();
String apkPath = applicationInfo.sourceDir;

3.  增量包和old.apk合体

接下来就是在手机端将其和增量包合并。这个合并任务肯定会用到bsdiff以及bzip的源码,是需要native方法去做的native方法的实现就是工具的源码,再编译成so包供Java使用。下面介绍一下详细的流程。

3.1  声明native方法

首先声明一个PatchUtil类,并声明一个native方法:

public class BsPatchUtil {
    static {
        System.loadLibrary("apkpatch");
    }
    public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}

执行Make Project进行编译,将会生成.class文件。.class的路径为app\build\intermediates\classes\debug\Java类路径,在main目录下执行javah命令即可生成jni目录下的.h文件备用。

javah -d jni -classpath [你的sdk路径]\platforms\android-22\android.jar;..\..\build\intermediates\classes\debug [你的包名+包含native方法的类(中间都用点隔开)] 

3.2  导入源码

在实现BsPatchUtil之前,我们需要将bspatch.c以及bzip的相关源码拷贝到jni目录下,bzip的源码只保留.h头文件和.c文件,并将bspatch.c中的main()方法名修改为executePatch()。并且修改其中bzip的引入头为#include "bzip2/bzlib.h"

文件结构如下图所示:

3.3  实现bspatch_util.c

新建一个bspatch_util.c,把3.1中生成的.h文件include进去,下面源码的第9行就是在调用我们在bspatch.c中原来的main函数,只是被我们手动改名为executePatch()

#include "com_example_patch_BsPatchUtil.h"
JNIEXPORT jint JNICALL Java_com_example_patch_BsPatchUtil_patch
        (JNIEnv *env, jclass clazz, jstring old, jstring new, jstring patch){
        int args=4; char *argv[args];
    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 = executePatch(args, argv);
    (*env)->ReleaseStringUTFChars(env, old, argv[1]);
    (*env)->ReleaseStringUTFChars(env, new, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
    return ret;
}

3.4  修改Gradle的配置

先在gradle.properties文件中添加NDK使用声明,android.useDeprecatedNdk=true

接下来在build.gradle中添加NDK配置,如下图所示,

其中apkpatch就是我们命名的so库,如果想只生成指定CPU类型的so文件,可以进行如下配置。

ndk{
    moduleName "apkpatch"
    abiFilters "armeabi", "armeabi-v7a","x86"
}

3.5  生成so

进行Rebuild Project操作,不出意外会遇到Multiple define of main错误,这个解决起来也比较容易,直接把报错的c文件中对应的main函数注释即可。重新编译即可生成so库:

3.5  调用so

我们在BsPatchUtil静态代码块中的System.loadLibrary("apkpatch")就是对so库的加载

接下来就是Java逻辑了,这里我们方便演示直接把old.apkpatch文件置于SD卡中,直接把两者合体成新apk就达到我们的目的了,真正应用中是需要提取本地apk的。下面代码也比较简单,就是单纯的调用native方法,生成新的apk之后关闭自身并重新安装。

final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk");
final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");
String str = Environment.getExternalStorageDirectory()+"/"+"old.apk";
BsPatchUtil.patch(str, destApk.getAbsolutePath(), patch.getAbsolutePath());
if (destApk.exists())
   install(this, destApk.getAbsolutePath());
private void install(Context context, String apkPath) {
   Intent i = new Intent(Intent.ACTION_VIEW);
   i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   i.setDataAndType(Uri.fromFile(new File(apkPath)), "application/vnd.android.package-archive");
   context.startActivity(i);
   android.os.Process.killProcess(android.os.Process.myPid());
}

3.6  结果演示

点击按钮进行合体操作,关闭当前应用并重新安装新apk的应用。可以看到按钮上的字改成了“Hello New Word”,算是完成了增量更新的操作。

4.  关于增量更新需要注意的地方

1增量更新最大的优点就是不需要用户下载新的完整安装包,因为实在是太大了。但是合体之前一定要注意old.apk是否存在,也要对下载下来的差分包做合法性验证,防止中间人攻击。

2增量更新和热修复完全不是一个东西。区别是增量更新需要重新安装apk,而热修复不需要。

(3)最后说一下增量更新的缺点,不仅客户端需要维护关于增量更新的逻辑,服务器端更是如此。每次发布新版本,服务端都需要为以前所有的老版本生成对应的差分包,维护会变得复杂,好在可以用脚本去做。

5.  期间遇到的问题

做个貌似简单的增量更新其实想一次做到还是比较难的,期间也遇到了挺多坑,在Android开发——增量更新实战中遇到的问题中总结了,其中cannot locate symbol"signal" referenced by "libbsdiff.so"错误大概花了两天的时间才得以解决,如果有同学需要可以参考,避免被坑。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于您的问题,我会尽力回答。在Android中,JNI可以用来与C/C++代码进行交互,我们可以利用JNI来读写SD卡。下面是一个简单的例子: 1. 首先,在您的Java代码中定义native方法: ``` public class MainActivity extends Activity { static { System.loadLibrary("native-lib"); } public native String readFromSDCard(String filePath); public native boolean writeToSDCard(String filePath, String content); } ``` 2. 然后,在您的C/C++代码中实现这些方法: ``` #include <jni.h> #include <string.h> #include <stdio.h> #include <android/log.h> JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_readFromSDCard(JNIEnv *env, jobject instance, jstring filePath) { const char *path = env->GetStringUTFChars(filePath, 0); FILE *file = fopen(path, "r"); if (file == NULL) { __android_log_print(ANDROID_LOG_ERROR, "JNI", "readFromSDCard: fopen failed"); return NULL; } fseek(file, 0, SEEK_END); long size = ftell(file); fseek(file, 0, SEEK_SET); char *buffer = new char[size + 1]; fread(buffer, 1, size, file); buffer[size] = '\0'; fclose(file); env->ReleaseStringUTFChars(filePath, path); return env->NewStringUTF(buffer); } JNIEXPORT jboolean JNICALL Java_com_example_myapplication_MainActivity_writeToSDCard(JNIEnv *env, jobject instance, jstring filePath, jstring content) { const char *path = env->GetStringUTFChars(filePath, 0); const char *str = env->GetStringUTFChars(content, 0); FILE *file = fopen(path, "w"); if (file == NULL) { __android_log_print(ANDROID_LOG_ERROR, "JNI", "writeToSDCard: fopen failed"); return false; } fwrite(str, 1, strlen(str), file); fclose(file); env->ReleaseStringUTFChars(filePath, path); env->ReleaseStringUTFChars(content, str); return true; } ``` 3. 最后,在您的AndroidManifest.xml文件中添加以下权限: ``` <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ``` 这样,您就可以在Java代码中调用这些native方法来读写SD卡了。 至于Android NDK的使用实例——增量更新实战,这是一个比较复杂的话题,如果您有相关的需求,可以提出具体的问题,我会尽力为您解答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值