Android 增量更新

    随着社会的发展,虽然对于一个用户而言,流量不值钱,每个用户的手机会有多少个G的流量,或者不限制流量,或者经常使用wifi。所以目前在Android端流量的优化,可能没有那么重要了。但是当我们的用户更新一个app的时候,每次都需要下载一个完整的app有可能要等几分钟,像王者荣耀这样大型的游戏app,下载完整的app至少几百兆,但是我们新的补丁包有可能只有几十兆,或者几兆,这样让用户使用新的功能,需要去下载整个新的app不合理,因此,增量更新就派上用处了额,增量更新可以减少服务器的压力。在很多应用市场上,比如我使用的小米应用市场,它就自己集成了bsdiff,在某些集成了bspatch的客户端,比如抖音,微信...它会自动使用增量更新。

Bsdiff

官网地址 :http://www.daemonology.net/bsdiff/

我这里使用的是centos来下载编译bsdiff的,下载解压bsdiff之后,我们可以看到解压之后的文件夹里面有Makefile文件,但是当我们

在bsdiff文件夹下执行make命令 发现有如下报错:

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

第13行报错,可以打开Makefile看看:

这是因为在第13行 .ifndef 语句之前缺少Tab键的空格,下面的.endif也是如此,加入Tab空格之后,重新执行make命令,有可能会提示你缺少bzlib.h的头文件,其实在bsdiff官网上面也提到了,使用bsdiff和bspatch需要用到bzip2,所有需要安装bzip2。在终端执行下面命令,来查找bzip2需要的安装包 yum search bzip2 可以得到下面的提示,然后选择安装你需要的就OK了

然后使用下面的命令安装(-y 代表下面的安装步骤都是选择yes):

yum install -y bzip2-devel.x86_64

安装好bzip2之后再执行make命令,就可以编译得到相关bsdiff和bspatch文件了,执行ls命令可以看到两个绿色的可执行文件

 使用以下命令可以得到bsdiff(同理bspatch也可以这样查看)的用法

可以看到bsdiff 需要传递三个参数,原文件  新的文件,差分文件,

Android项目当中的开发

在Android中,我们其实只需要将差分包 和 老版本的包进行合并就OK,因此我们使用到的是bspatch。差分工作是由服务器来完成(或者客户端完成之后上传到服务器,这个时候可以用脚本+工具结合使用)。那么下面进行客户端的开发。

使用Android Studio新建Native C++的项目,然后将bspatch.c拷贝到cpp目录下,之前编译的时候说过,bsdiff和bspatch依赖bzip2,打开bspatch.c其实也可以发现它引入了  #include <bzlib.h>  的头文件,所以这里我们也需要在项目当中引入bzip2的相关源代码,其实bzip2在我们的Android的源代码中已经存在,参见链接 。点开可以看到这里就是bzip2的源代码,里面有很多,但其实我们可以不用全部(当然也可以全部)拷贝到我们的代码中。可以在bzip2官网上下载最新的bzip2相关文件,我这里选择从bzip2的官网上下载,下载完成之后,我们看看Makefile文件:

这些 *.o文件其实就是我们需要生成的目标文件。这里那我们就将对应的*.c文件还有相关的依赖的*.h文件拷贝到我们的项目中就OK。我们新建一个bzip2的文件夹,将这些文件拷贝进去,最后如下图所示:

拷贝好之后就需要编写我们的CMakeLists.txt文件了,最后CMakeLists.txt的配置文件如下:

cmake_minimum_required(VERSION 3.4.1)
#引入头文件
include_directories(${CMAKE_SOURCE_DIR}/bzip2)
#批量导入对应的c文件
aux_source_directory(${CMAKE_SOURCE_DIR}/bzip2/ bzlibsrc)
add_library( 
        native-lib
        SHARED
        native-lib.cpp
        bspatch.c
        ${bzlibsrc})
target_link_libraries( 
        native-lib
        log)

写好CMakeLists.txt并且编译通过之后,就可以写Android端的java的代码了。

Android端:

首先增加相关的权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

这里使用将差分包放在sdcard的形式来模拟网络下载,所以有sdcard的读写权限,还有android高版本需要的安装apk的权限。

我们知道在Android7.0之后,通过FileProvider来实现应用之间共享文件:

 <!--${applicationId}-->
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.android.bsdiff.demo.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

上面的android:authorities后面的value值 一般都是你的包名称+fileProvider(也可以不是fileProvider,是其他字符串),在我测试的时候我使用android:authorities = "${applicationId}.provider" 这样动态去配置好像 增量更新的时候 一直出现安装包解析错误,所以我这里就写死了包名+fileProvider的形式。

在meta-data中设置访问的权限:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="path" path="." />
</paths>

具体的FileProvider可以参看网上很多博客,或者google的官方文档。

具体的操作逻辑在 doPatchNative 方法的实现中,该方法传递3个参数,原文件(老版本),新版本文件,差分文件,最后原文件+差分文件合并成 新版本文件,我们对新版本文件进行安装操作:

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.tv_version);
        tv.setText("当前版本:" + BuildConfig.VERSION_NAME);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.REQUEST_INSTALL_PACKAGES};
            if (checkSelfPermission(
                    perms[0]) == PackageManager.PERMISSION_DENIED || checkSelfPermission(
                    perms[1]) == PackageManager.PERMISSION_DENIED) {
                requestPermissions(perms, 200);
            }
        }
    }

    public void onUpdate(View view) {
        //网络请求下载查分包(省略。直接拷贝查分包到sd卡)

        new AsyncTask<Void, Void, File>() {

            @Override
            protected File doInBackground(Void... voids) {
                //bspatch 做合成 得到新版本的apk文件
                //sz: linux>windows
                //rz: windows>linux
                String patch = new File(Environment.getExternalStorageDirectory(),
                        "patch.diff").getAbsolutePath();
                File newApk = new File(Environment.getExternalStorageDirectory(), "new.apk");
                if (!newApk.exists()) {
                    try {
                        newApk.createNewFile();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                String oldApk = getApplicationInfo().sourceDir;
                doPatchNative(oldApk, newApk.getAbsolutePath(), patch);
                return newApk;
            }

            @Override
            protected void onPostExecute(File apkFile) {
                //安装
                if (!apkFile.exists()) {
                    Log.e("TAG","new file not exist");
                    return;
                }
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                if (Build.VERSION.SDK_INT >= 24) { //Android 7.0及以上
                    // 参数2 清单文件中provider节点里面的authorities ; 参数3  共享的文件,即apk包的file类
                    Uri apkUri = FileProvider.getUriForFile(MainActivity.this,
                             "com.android.bsdiff.demo.fileProvider", apkFile);
                    //对目标应用临时授权该Uri所代表的文件
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
                } else {
                    intent.setDataAndType(Uri.fromFile(apkFile),
                            "application/vnd.android.package-archive");
                }
                startActivity(intent);
            }
        }.execute();

    }

    private native void doPatchNative(String oldApk, String newApk, String patch);

}

看native_lib中关于文件合并的调用:

extern "C"{
    extern int bspatch_main(int argc,char * argv[]);  //函数的申明
}

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

    char *argv[] = {
            "bspatch",
            const_cast<char *>(oldApk),
            const_cast<char *>(newApk),
            const_cast<char *>(patch)
    };
    bspatch_main(4,argv);

    env->ReleaseStringUTFChars(oldApk_, oldApk);
    env->ReleaseStringUTFChars(newApk_, newApk);
    env->ReleaseStringUTFChars(patch_, patch);
}

在bspatch.c文件中,其实原本作者定义合成文件的函数名称为main,如下:

作为bsdiff的设计者将合成文件的方法名称命名为main,这个无可厚非,但是我们的项目中,可能用到很多个第三方的c文件,有可能作者设计入口函数都是main,所以在调用的时候我们都调用main读起来有点不清晰,所以在此就将main方法修改一个名称(这也是得到源代码的好处,可以随意修改)

看见标记红色的部分,其实就是上文在centos中调用./bspatch --help时候的提示语,这也反映这个函数就是合成时候调用的函数。

在native-lib.cpp需要调用bspatch.c里面的函数,但是一个是.cpp一个是.c文件,所以在调用.c文件的时候,我们需要在extern “C”中进行声明。如下:

extern "C"{
    extern int bspatch_main(int argc,char * argv[]);  //函数的申明
}

这样我们声明了一个名称为bspatch_main的函数,但是这个函数在外部实现,在Android Studio中

我们其实可以点击红色框框的地方,看看这个函数的实现在什么地方,其实最后定位到的就是bspatch中的这个方法,这样,我们就可以在native-lib中去调用bspatch_main这个方法了。

在本文的Demo中,我们的old.apk和new.apk的区别就是下面

就是版本号的区别,将new.apk中的versionCode修改为 2 ,versionName修改为 "2.0",那么就需要使用上面编译生成的bsdiff(绿色的)可执行文件来生成差分文件。

在linux系统中,可以使用rz,或者sz  向linux服务器上传文件,或者从linux服务器下载文件,但是使用这个命令必须是安装了终端,像windows中xshell, mac系统(自带的终端不行)中的FinalShell才能使用。将编译出的old.apk,new.apk上传上去后,调用

./bsdiff old.apk new.apk patch.diff

之后就可以生成差分文件patch.diff,然后将差分文件上传到服务器,就可以供用户下载 合成,并且安装咯。在Demo中,我将打包后的old.apk  new.apk patch.diff放在assets文件夹下,下载之后,可以将对应的patch.diff拷贝到sdk,然后安装old.apk,就可以体验增量更新了。

问题:在Android,我们可能有很多个应用市场,或者很多个推广渠道,每次Android打线上包的时候,都需要打上百个android的apk包,这样,就导致我们做增量更新的时候,不可能每次都对这个100多个渠道进行增量更新,就算是100多个渠道好实现, 那么某个渠道可能有很多个版本的用户都在使用,这时候,需要对某个渠道的某个版本做差分包,这样就有点麻烦了。我们可以选择某个渠道,比如官网渠道,或者看看网上”多渠道包的增量更新“怎么做的。

Demo传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值