美文网首页基础知识
Android 增量更新之文件的拆分和合并

Android 增量更新之文件的拆分和合并

作者: JasonChen8888 | 来源:发表于2020-06-01 18:14 被阅读0次

    前言

    正常一个项目的版本更新,很多情况下是进行apk包的新版本发布,让用户下载更新,但是有个弊端就是如果包体很大,这样就耗时又费流量。

    常见的版本更新方式

    • 热修复(热更新)
      热修复是修改线上版本的bug,用技术去实现不更新整个apk的条件下,修改掉bug。针对的是类的层级面
    • 插件化
      插件化是想把需要实现的模块或功能当做一个独立的模块提取出来,只需要去下载当前的模块的apk包或者dex包就可以了。针对的是功能模块层级面
    • 增量更新
      增量更新是针对新旧Apk文件对比,拆分出(.patch)的更新文件,(.patch)文件包含的是新包相对旧包没有的内容,然后由客户端进行合并成新的Apk。针对的是应用全局层级面。

    增量更新

    • 文件的拆分
      文件的拆分是通常是由服务端来完成的,一般是作为实时操作生成不同版本的差异的(.patch)文件,最后改文件放在服务端,让客户端下载合并更新。
    • 文件的合并
      文件合并是由客户端来完成的,通常是将旧的apk和(.patch)文件进行合并,生成新的apk,然后进行重新安装。

    Apk文件的拆分和合并需要用bsdiff和bzip2这两个工具

    文件的拆分

    Apk的文件拆分,将新版本的apk和旧版本的apk,差异的内容进行分解出来,生成.patch文件

    • 使用现成的可执行文件进行拆分


      拆分命令.png

      cmd命令:

    bsdiff.exe  appOld.apk  appNew.apk  apk.patch
    

    命令行说明:
    第一个是拆分的可执行的文件名
    第二个是旧文件的名称
    第三个是新文件的名称
    第四个是拆分(.patch)文件名

    • 利用下载下来的源码实现自己的可执行文件或者dll包
      这边以生产dll动态库,在java工程调用为例
      工具:vs2015
      创建一个win32的工程项目,将bsdiff-win的.c和.h文件复制到项目工程下,进行编译


      工程源码.png

      直接看bsdiff.cpp文件的源码:

    int main(int argc,char *argv[])
    {
          ..............省略代码................
    
        if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
    
         ...........省略代码................
    }
    

    有个main方法,如果直接编译是可以生产exe文件,这边将main方法进行修改为bsdiff_main,采用jni的形式进行调用
    以静态的native注册为例, 关于native的动态注册,可以参考https://www.jianshu.com/p/3aeabe2b5744

    JNIEXPORT void JNICALL Java_jason_DiffUtil_diffApk
    (JNIEnv *env, jclass jclz, jstring oldPath_jst, jstring newPath_jst, jstring patchPath_jst) {
    
        char * oldPath = (char*)env->GetStringUTFChars(oldPath_jst, NULL);
        char * newPath = (char*)env->GetStringUTFChars(newPath_jst, NULL);
        char * patchPath = (char*)env->GetStringUTFChars(patchPath_jst, NULL);
    
        int argc = 4;
        char *argv[4];
    
        argv[0] = "bsdiff";
        argv[1] = oldPath;
        argv[2] = newPath;
        argv[3] = patchPath;
    
        bsdiff_main(argc, argv);
    
        env->ReleaseStringUTFChars(oldPath_jst, oldPath);
        env->ReleaseStringUTFChars(newPath_jst, newPath);
        env->ReleaseStringUTFChars(patchPath_jst, patchPath);
    }
    

    新建的项目工程默认都是生成exe,修改下输出类型,属性---->常规---->项目默认值:配置类型


    修改输出类型.png

    默认打出来的dll包是32位的,如果是64的系统环境,修改一下项目配置,vs工具栏--->生成--->配置管理器,如下图:


    修改方案平台.png

    最后生成解决方案


    生成Dll.png
    • vs上运行项目出现的错误和解决方案:
      问题一: 在导入的时候自己创建目录存放文件,并不是放在跟生成的代码文件一起,会出现include 找不到文件
      解决方法:右键工程 ---> 属性 ---> c++ -----> 附含包目录(我们所放置代码的目录)
      问题二:To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. 出现CRT的警告。
      解决方法:右键工程 ---> 属性 ---> c++ -----> 命令行 添加 -D _CRT_SECURE_NO_WARNINGS
      问题三:'setmode': The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name: _setmode. See online help for details. 这个是sdl 安全检查
      解决方法:关闭sdl 安全检查,右键工程 ---> 属性 ---> c++------>常规 ----> SDL检查 否
      问题四:"char *" 类型的实参与 "LPWSTR" 类型的形参不兼容 BsDiffUtil e:\VSProject\BsDiffUtil\BsDiffUtil\BsDiffUtil\source\bsdiff.cpp 55
      解决方法:右键工程 ---> 属性 ---> 常规 ---->项目默认值 ---->字符集 修改为 使用多字节字符集
      问题五:预编译头文件来自编译器的早期版本,或者预编译头为 C++ 而在 C 中使用它(或相反)问题的解决方案
      解决方法:右键工程 ---> 属性 ---> c++------>预编译头 ----> 预编译头 不使用预编译头
      注:vs 切换平台之后 需要重新配置上述依赖

    • 在java工程调用生成的dll文件,实现文件拆分

    1. 将生成的BsDiffUtil.dll的文件复制到java项目工程


      项目结构图.png
    2. native的方法(这边提早定义了,因为需要静态注册,生成.h的头文件)
    public class DiffUtil {
    
        static{
            System.loadLibrary("BsDiffUtil");
        }
    
        public static native void diffApk(String oldPath, String newPath, String patch);
        
    }
    
    1. 掉进行文件拆分
    public class MainTest {
    
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            String oldPath = "F:\\Test\\appNew.apk";
            String newPath = "F:\\Test\\appOld.apk";
            String patch = "F:\\Test\\apk.patch";
            System.out.println("bsdiff start");
            DiffUtil.diffApk(oldPath, newPath, patch);
            System.out.println("bsdiff end");
        }
    }
    

    文件的拆分就先介绍到这。

    文件的合并

    文件的合并,指的是旧的Apk文件合并.patch文件,成为新的Apk文件。
    采用Android studio项目为例,来处理客户端的的文件合并

    • 复制bsdiff的bspatch.c文件和bzip2的.c和.h文件copy到项目的cpp目录下
      由于Android是基于linux内核的,所以这边采用的是linux包的bspatch.c的代码:


      bsdiff的源码.png

      同样需要bzip2的源代码:


      bzip2的源码文件.png
      将bsdiff的bspatch.c文件和bzip2的.c和.h文件copy到项目的cpp目录下(为了将bzip和bspatch分开,单独创建个目录存放)
      as下的cpp文件.png
    • 配置CMakeList.txt

    cmake_minimum_required(VERSION 3.4.1)
    
    file(GLOB my_c_path bzip2/*.c)
    add_library( # Sets the name of the library.
                 my-test
    
                 # Sets the library as a shared library.
                 SHARED
    
                 # Provides a relative path to your source file(s).
                 ${my_c_path}
                 bspatch.c)
    
    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 )
    
    target_link_libraries( # Specifies the target library.
                           my-test
    
                           # Links the target library to the log library
                           # included in the NDK.
                           ${log-lib} )
    
    • 编写jni方法去调用bspatch的api
    1. 将bspatch的main方修改为bspatch_main;
    2. 创建一个在java文件中创建native方法
    public class BsPatch {
        public native static int patch(String oldfile, String newFile, String patchFile);
    }
    
    1. 在bspatch.c中生成jni方法,对bspatch的bspatch_main方法进行调用
    JNIEXPORT jint JNICALL
    Java_com_ndk_so_generator_BsPatch_patch(JNIEnv *env, jclass clazz, jstring oldfile,
                                            jstring new_file, jstring patch_file) {
        int ret= -1;
        LOGD(" jni patch begin");
    
        char *oldPath = (char *) (*env)->GetStringUTFChars(env, oldfile, JNI_FALSE);
        char *newPath = (char *) (*env)->GetStringUTFChars(env, new_file, JNI_FALSE);
        char *patchPath = (char *) (*env)->GetStringUTFChars(env, patch_file, JNI_FALSE);
    
        int argc = 4;
        char *argv[4];
    
        argv[0] = "TimBsPatch";
        argv[1] = oldPath;
        argv[2] = newPath;
        argv[3] = patchPath;
    
        //如果成功,ret等于0
        ret = bspatch_main(argc,argv);
    
        (*env) -> ReleaseStringUTFChars(env, oldfile, oldPath);
        (*env) -> ReleaseStringUTFChars(env, new_file, newPath);
        (*env) -> ReleaseStringUTFChars(env, patch_file, patchPath);
    }
    
    1. 在MainActivity中,存储权限申请,实现版本判断,进行更新逻辑实现
      (.patch)文件是服务端生成的,提供给客户端下载,去进行合并。(这边是没有做下载,直接向文件放置到外置存储卡)
      如果要将(.patch)文件和旧版本APK合成新版本的Apk,那么问题来了,旧的apk去哪里获取?
      关键点:我们在安装apk的时候,Android系统会将所要安装的apk文件copy到/data/app/目录下
    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;
        }
    

    MainActivity的代码:

    public class MainActivity extends AppCompatActivity {
    
      public static final String TAG = "chenby";
    
      private static int REQ_PERMISSION_CODE = 1001;
    
      private static final String[] PERMISSIONS = { Manifest.permission.READ_EXTERNAL_STORAGE,
          Manifest.permission.WRITE_EXTERNAL_STORAGE};
    
      // Used to load the 'native-lib' library on application startup.
      static {
        System.loadLibrary("my-test");
      }
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        // Example of a call to a native method
        checkAndRequestPermissions();
      }
    
      private void init() {
        TextView tv = findViewById(R.id.sample_text);
        if (ApkUtils.getVersionCode(this, getPackageName()) < 2.0) {
          tv.setText("不是最新的版本号 开始更新 ");
          new ApkUpdateTask().execute();
        } else {
          tv.setText(" 最新版本号 无需更新");
        }
      }
    
      /**
       * 权限检测以及申请
       */
      private void checkAndRequestPermissions() {
        // Manifest.permission.WRITE_EXTERNAL_STORAGE 和  Manifest.permission.READ_PHONE_STATE是必须权限,允许这两个权限才会显示广告。
    
        if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
            && hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
          init();
        } else {
          ActivityCompat.requestPermissions(this, PERMISSIONS, REQ_PERMISSION_CODE);
        }
      }
    
      /**
       * 权限判断
       * @param permissionName
       * @return
       */
      private boolean hasPermission(String permissionName) {
        return ActivityCompat.checkSelfPermission(this, permissionName)
            == PackageManager.PERMISSION_GRANTED;
      }
    
      @Override
      public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    
        if (requestCode == REQ_PERMISSION_CODE) {
          checkAndRequestPermissions();
        }
    
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
      }
    
      public void onOpen(View view) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
      }
    
      class ApkUpdateTask extends AsyncTask<Void, Void, Boolean> {
    
    
        @Override
        protected Boolean doInBackground(Void... params) {
          
          String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
    
          String newFile = Contants.NEW_APK_PATH;
    
    
          String patchFileString = Contants.PATCH_FILE_PATH;
    
          File patchFile = new File(patchFileString);
          if(!patchFile.exists()) {
            return false;
          }
    
          Log.d(TAG,"开始合并");
          int ret = BsPatch.patch(oldfile, newFile,patchFileString);
          Log.d(TAG,"开始完成");
    
          if (ret == 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);
          }
        }
      }
    }
    

    7.0以上安卓 apk的问题

    1. 测试运行
      先运行一个apk,然后升级版本号,再增加一些资源文件,或者代码页面。将新和旧的apk进行拆分出apk。patch文件,然后将apk.patch放置外置存储卡,安装就版本的apk, 运行进行升级。

    结语

    以上就是一个简单的增量更新过程:主要的内容是在服务端对apk文件进行拆分出(.patch)文件,然后再客户端将旧版本apk和服务端下载下来(.patch)进行合并出新版本apk,进行新版本安装更新。
    项目源码
    https://github.com/jasonkevin88/BsDiffUtil
    https://github.com/jasonkevin88/BsDiff
    https://github.com/jasonkevin88/SoGenerator

    相关文章

      网友评论

        本文标题:Android 增量更新之文件的拆分和合并

        本文链接:https://www.haomeiwen.com/subject/htajzhtx.html