基础知识

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

2020-06-01  本文已影响0人  JasonChen8888

前言

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

常见的版本更新方式

增量更新

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

文件的拆分

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

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

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

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
  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项目为例,来处理客户端的的文件合并

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

上一篇 下一篇

猜你喜欢

热点阅读