Android incremental update (bsdiff use)

Simple to understand

  • What incremental updates are?
    The client does not need to download a new version apk when APP update, just download subcontracting difference (difference between the two apk).

  • Poor subcontractors how to get? How subcontracting new apk apk synthesis of old and poor?
    Use bsdiff .

  • bsdiff how it works?
    Compare two binary files, specific implementation are interested you can go to dig deep.
    For example easy to understand: Git, you can record every time you modify, add, delete, and not in file units can be specific to a particular line of a file each time the code is actually only difference upload upload.

  • bsdiff ye use?
    First download bsdiff , have what look unzip the file:

    2580435-32cfe504b24400bf.png

    bsdiff.c generate a difference is calculated using the difference subcontracting, bspatch is of synthesizing a new apk.


Server configuration (Linux)

  1. Download bsdiff: wget http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
  2. Decompression bsdiff: tar -xvf bsdiff-4.3.tar.gz
  3. ls cd to the next bsdiff, with unzipped seen as: bsdiff.1 bsdiff.c bspatch.1 bspatch.c Makefile
  4. Direct the make command in the directory, result:
    Makefile: 13: *** Stop Missing Separator..
    Just in case there is a pit, you need to modify the Makefile
  5. Modify Makefile: vim Makefile
    Open Makefile can see the 13 line format error, less indentation, line 15 also have the same problem, modify the finished as follows:
CFLAGS          +=      -O3 -lbz2                                                               
                                                                                                
PREFIX          ?=      /usr/local                                            
INSTALL_PROGRAM ?=      ${INSTALL} -c -s -m 555                               
INSTALL_MAN     ?=      ${INSTALL} -c -m 444                                  
                                                                              
all:            bsdiff bspatch                                                
bsdiff:         bsdiff.c                                                      
bspatch:        bspatch.c                                                     
                                                                               
install:                                                                       
        ${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin                        
        .ifndef WITHOUT_MAN                                                                     
        ${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1                     
        .endif 
  1. Then make: bsdiff.c: 33: 19: fatal error: bzlib.h: No such file or directory
    , said the lack of bzip library, bzip is a compression tool, you can go to download bzip source code is compiled here by way of direct mounting system environment:
    Ubuntu: APT install libbz2-dev
    Centos: yum -y install bzip2-devel.x86_64
    Mac: BREW install bzip2
  2. Amassing make, OJBK! ls it: bsdiff bsdiff.1 bsdiff.c bspatch bspatch.1 bspatch.c Makefile
    more than two tools: bsdiff, bspatch
  3. Server configuration is complete, for a return of 7 bsdiff use tools to generate a difference subcontractors

Client Configuration

Apk file server via two will survive relatively poor subcontractor, the client got poor subcontractor certainly keep the old apk (which is now installed apk) to synthesize new apk, so the client have to do is synthesized installation.

  1. Creating a direct C ++ project (apk synthesis in the native layer is dry)


    2580435-a0a6311f2a5653f0.png
  2. Bspatch.c put under the cpp directory (Mentioned above bsdiff with open differential, with open bspatch synthesis, the client need synthesis operations), also requires bzlib.h bspatch.c
  3. Download bzip , bzip new folder in cpp, the result of the decompression bzlib copy into it. These files have some use less, you can find which files useful bzlib of the makefile, makefile can see this content:
libbz2.a: $(OBJS)

...

OBJS= blocksort.o  \
      huffman.o    \
      crctable.o   \
      randtable.o  \
      compress.o   \
      decompress.o \
      bzlib.o

So keep only the corresponding .c files can, src directory is deleted after completion:


2580435-3ae51f585fad8170.png

Bzlib.h and bzlib_private.h which is used in bzlib.c, you want to keep.

  1. 修改项目的CMakeLists.txt,引入bzip:
    • 定义file(GLOB bzip bzip/*.c)
    • 在add_library中添加:
      ${bzip}
      bspatch.c
  2. run一下试试:bspatch.c:31:10: fatal error: 'bzlib.h' file not found
    打开看到Bspatch.c中引入头文件是这样写的:#include <bzlib.h>
    <>是引入系统头文件,可以改成 #include “bzip/bzlib.h”
    或者在cmake文件中添加:include_directories(bzip)
    最终的cmake文件:
cmake_minimum_required(VERSION 3.4.1)

file(GLOB bzip bzip/*.c)

add_library(
        native-lib

        SHARED

        native-lib.cpp

        ${bzip}

        bspatch.c
)

include_directories(bzip)

find_library(
        log-lib

        log)

target_link_libraries(
        native-lib

        ${log-lib})

客户端编码

服务端和客户端都配置好了,之后是客户端完成合成、安装的操作,这里就不写下载过程了,直接把一会服务端生成的差分包放到sdcard目录下。
安装apk没什么说的,主要是怎么调用bspatch去合成,打开bspatch.c文件,总共就俩函数,很明显应该用这个叫main的函数(名字有点特别,叫什么没关系,可以随便改),看看参数:

int main(int argc,char * argv[])
{
    
    ...

    if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
  
    ...
}

这个函数需要传入一个数组,第一个参数是数组长度(长度必须是4),数组第一元素是个字符串,用来输出log的,没什么真正用处,后面三个元素依次是oldfile、newfile、patchfile,也就是现有apk、新apk、差分包,现有apk和差分包我们可以拿到,新apk指的合成之后的存放在哪。看到这可以开撸了:

  1. 在MainActivity创建参数相对应的native函数:
// 参数顺序不用太在意,知道对应关系就行
private native void bsPatch(String oldApk, String patch, String output);
  1. 直接用AS提示生成native方法:
extern "C"
JNIEXPORT void JNICALL
Java_com_yu_mybsdiff_MainActivity_bsPatch(JNIEnv *env, jobject instance, jstring oldApk_,
                                          jstring patch_, jstring output_) {
    const char *oldApk = env->GetStringUTFChars(oldApk_, 0);
    const char *patch = env->GetStringUTFChars(patch_, 0);
    const char *output = env->GetStringUTFChars(output_, 0);

    //TODO

    env->ReleaseStringUTFChars(oldApk_, oldApk);
    env->ReleaseStringUTFChars(patch_, patch);
    env->ReleaseStringUTFChars(output_, output);
}

上面代码是自动生成的,不用管,只要在TODO里调用bspatch的main函数就可以了,参数刚才分析过了,调用简单的很:

    const char *argv[] = {"", oldApk, output, patch};
    main(4, argv);

直接这样写会发现找不到main函数,因为没有引入bspatch头文件,include半天发现bspatch根本没有.h文件,无法include, 我们直接在上面加上main函数的声明就可以了,因为C工程中的函数不能重名,所以这俩mian函数就是一个声明一个实现,最终的native-lib.cpp内容:

#include <jni.h>
#include <string>

extern "C" {
    extern int main(int argc, const char *argv[]);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_yu_mybsdiff_MainActivity_bsPatch(JNIEnv *env, jobject instance, jstring oldApk_,
                                          jstring patch_, jstring output_) {
    const char *oldApk = env->GetStringUTFChars(oldApk_, 0);
    const char *patch = env->GetStringUTFChars(patch_, 0);
    const char *output = env->GetStringUTFChars(output_, 0);

    const char *argv[] = {"", oldApk, output, patch};
    main(4, argv);

    env->ReleaseStringUTFChars(oldApk_, oldApk);
    env->ReleaseStringUTFChars(patch_, patch);
    env->ReleaseStringUTFChars(output_, output);
}

到这里合成apk的native代码就实现完成,就三四句代码。

  1. 在写MainActivity中的逻辑之前先看下布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"/>

    <Button
        android:onClick="update"
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=" 更新 "
        android:layout_marginTop="10dp"/>

</LinearLayout>
2580435-0b7035c321e10c93.png
  1. MainActivity中获取bspatch中所需参数的代码:
public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ...
    }



    public void update(View view) {
        new AsyncTask<Void, Void, File>() {
            @Override
            protected File doInBackground(Void... voids) {

                // 获得bspatch中要传入的参数
                String patch = new File(Environment.getExternalStorageDirectory(), "patch").getAbsolutePath();
                String oldApk = getApplicationInfo().sourceDir;
                String output = createNewApk().getAbsolutePath();

                if (!new File(patch).exists()) {
                    return null;
                }
                bsPatch(oldApk, patch, output);
                return new File(output);
            }

            @Override
            protected void onPostExecute(File file) {
                super.onPostExecute(file);
                //安装新apk
               ...
            }
        }.execute();
    }


    /**
     * 创建合成后的新版本apk文件
     *
     * @return
     */
    private File createNewApk() {
        File newApk = new File(Environment.getExternalStorageDirectory(), "bsdiff.apk");
        if (!newApk.exists()) {
            try {
                newApk.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return newApk;
    }


    private native void bsPatch(String oldApk, String patch, String output);
}
  1. 记得添加权限:
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
  1. 安装apk代码,不多说什么了,不熟悉的话一搜一堆,直接贴出流程:
  • res/xml目录下创建file_paths.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<paths>

    <root-path
        name="root"
        path="" />

    <files-path
        name="files"
        path="." />

    <cache-path
        name="cache"
        path="." />

    <external-path
        name="external"
        path="." />

    <external-files-path
        name="external_file_path"
        path="." />

    <external-cache-path
        name="external_cache_path"
        path="." />

</paths>
  • AndroidManifest.xml中添加
       <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.yu.mybsdiff.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
  • 含有安装逻辑的MainActivity代码:
public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = findViewById(R.id.sample_text);
        // 显示当前版本号
        tv.setText(BuildConfig.VERSION_NAME);

        // 运行时权限申请
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String perms[] = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
            if (checkSelfPermission(perms[0]) == PackageManager.PERMISSION_DENIED) {
                requestPermissions(perms, 1000);
            }
        }
    }



    public void update(View view) {
        new AsyncTask<Void, Void, File>() {
            @Override
            protected File doInBackground(Void... voids) {

                String patch = new File(Environment.getExternalStorageDirectory(), "patch").getAbsolutePath();
                String oldApk = getApplicationInfo().sourceDir;
                String output = createNewApk().getAbsolutePath();

                if (!new File(patch).exists()) {
                    return null;
                }
                bsPatch(oldApk, patch, output);
                return new File(output);
            }

            @Override
            protected void onPostExecute(File file) {
                super.onPostExecute(file);
                //安装新apk
                if (file != null) {
                    if (!file.exists()) return;
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        Uri fileUri = FileProvider.getUriForFile(MainActivity.this, MainActivity.this.getApplicationInfo().packageName + ".fileprovider", file);
                        intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
                    } else {
                        intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
                    }
                    MainActivity.this.startActivity(intent);
                } else {
                    Toast.makeText(MainActivity.this, "差分包不存在!", Toast.LENGTH_LONG).show();
                }
            }
        }.execute();
    }


    /**
     * 创建合成后的新版本apk文件
     *
     * @return
     */
    private File createNewApk() {
        File newApk = new File(Environment.getExternalStorageDirectory(), "bsdiff.apk");
        if (!newApk.exists()) {
            try {
                newApk.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return newApk;
    }


    private native void bsPatch(String oldApk, String patch, String output);
}

生成差分包,演示更新

  1. 上面的客户端代码就完成了,可以build之后在outputs中取出这个apk先,当前版本号默认为1.0:
        versionCode 1
        versionName "1.0"

为了展示更新流程,还需要一个新的apk,这里修改一下布局文件,在里面加一个图片,然后把版本改成2.0:

        versionCode 2
        versionName "2.0"

再build一下,取出2.0的apk
分别命名为app_1_0.apk、app_2_0.apk

  1. Two apk files ready, the server have also configured the bsdiff, and now these two apk uploaded to the server, directory server bsdiff now is this use scp command:
    bsdiff.1
    bsdiff.c
    bspatch. 1
    bspatch.c
    Makefile
    app_1_0.apk
    app_2_0.apk
    execution ./bsdiff app_1_0.apk app_2_0.apk patch directly in the directory, which means calling bsdiff generation old apk (app_1_0.apk) and new apk (app_1_0.apk) difference package patch. So the addition of patch files directory, download the file, put down the phone sdcard directory.

  2. App_1_0.apk installed on your phone, click Update:


    2580435-c19a7129abeba992.gif
  3. ⚠️ Note: build a debug environment under the apk if you can not be installed directly to the phone via adb install, update failed to install when the situation occurs, we need to add in gradle.propertoes in:

android.injected.testOnly=false

On it.

  1. Finally, a look at a few File size:


    2580435-98b5a1fb86f6cfb2.png

Only 73KB of downloaded files, version upgrade is complete! Although the size difference between the subcontractor will follow the content increases and becomes larger, but if the project is large, apk size and poor subcontractors should not be just a few times.


Project Address
If a problem occurs during operation, there is no generation look sdcard directory bsdiff.apk (the name is in createNewApk own approach taken, this project called bsdiff.apk), if there is to put it out directly adb guide see if I can install on the installation.

Guess you like

Origin blog.csdn.net/weixin_34279246/article/details/90945106