文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android中的全量更新、增量更新以及热更新

2023-09-14 15:48

关注

在客户端开发过程中,我们可能会遇到这样一种需求:点击某个按钮弹出一个弹窗,提示我们可以更新到apk的某个版本,或者我们可以通过服务端接口进行强制更新。在这种需求中,我们是不需要通过应用商店来更新我们的apk的,而是直接在apk内部进行版本更新。这次我们就来看看实现这种应用内更新的几种方式。当然,这种玩法只能在国内玩,海外的话会被Googleplay据审的。如果是海外的应用要更新apk,只能在GooglePlay上上传新版本的包。

全量更新

什么是全量更新呢?举个例子,假设现在用户手机上的apk是1.0版本,如果想要升级到2.0版本,全量更新的处理方式则是把2.0版本的apk全部下载下来进行覆盖安装。那么,我们该如果设计一个合理的全量更新方案呢?

做完上面这2点其实就可以实现一个较为完整的全量更新功能。

客户端核心代码如下:

package com.mvp.myapplication.update;import android.app.Service;import android.content.ComponentName;import android.content.Context;import android.content.Intent;import android.content.ServiceConnection;import android.content.pm.PackageManager;import android.content.pm.ProviderInfo;import android.net.Uri;import android.os.AsyncTask;import android.os.Binder;import android.os.Build;import android.os.Environment;import android.os.IBinder;import android.text.TextUtils;import android.util.Log;import androidx.core.content.FileProvider;import com.mvp.myapplication.utils.MD5Util;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;public class UpdateService extends Service {    public static final String KEY_MD5 = "MD5";    public static final String URL = "downloadUrl";    private boolean startDownload;//开始下载    public static final String TAG = "UpdateService";    private DownloadApk downloadApkTask;    private String downloadUrl;    private String mMd5;    private UpdateProgressListener updateProgressListener;    private LocalBinder localBinder = new LocalBinder();    public class LocalBinder extends Binder {        public void setUpdateProgressListener(UpdateProgressListener listener) {            UpdateService.this.setUpdateProgressListener(listener);        }    }    private void setUpdateProgressListener(UpdateProgressListener listener) {        this.updateProgressListener = listener;    }        private static String getFileProviderAuthority(Context context) {        try {            for (ProviderInfo provider : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS).providers) {                if (FileProvider.class.getName().equals(provider.name) && provider.authority.endsWith(".update_app.file_provider")) {                    return provider.authority;                }            }        } catch (PackageManager.NameNotFoundException ignore) {        }        return null;    }    private static Intent installIntent(Context context, String path) {        Intent intent = new Intent(Intent.ACTION_VIEW);        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);        intent.addCategory(Intent.CATEGORY_DEFAULT);        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {            Uri fileUri = FileProvider.getUriForFile(context, getFileProviderAuthority(context), new File(path));            intent.setDataAndType(fileUri, "application/vnd.android.package-archive");            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);        } else {            intent.setDataAndType(Uri.fromFile(new File(path)), "application/vnd.android.package-archive");        }        return intent;    }    public UpdateService() {    }    @Override    public int onStartCommand(Intent intent, int flags, int startId) {        if (!startDownload && intent != null) {            startDownload = true;            mMd5 = intent.getStringExtra(KEY_MD5);            downloadUrl = intent.getStringExtra(URL);            downloadApkTask = new DownloadApk(this, mMd5);            downloadApkTask.execute(downloadUrl);        }        return super.onStartCommand(intent, flags, startId);    }    @Override    public IBinder onBind(Intent intent) {        return localBinder;    }    @Override    public boolean onUnbind(Intent intent) {        return true;    }    @Override    public void onDestroy() {        if (downloadApkTask != null) {            downloadApkTask.cancel(true);        }        if (updateProgressListener != null) {            updateProgressListener = null;        }        super.onDestroy();    }    private static String getSaveFileName(String downloadUrl) {        if (downloadUrl == null || TextUtils.isEmpty(downloadUrl)) {            return System.currentTimeMillis() + ".apk";        }        return downloadUrl.substring(downloadUrl.lastIndexOf("/"));    }    private static File getDownloadDir(UpdateService service) {        File downloadDir = null;        if (Environment.getExternalStorageDirectory().equals(Environment.MEDIA_MOUNTED)) {            downloadDir = new File(service.getExternalCacheDir(), "update");        } else {            downloadDir = new File(service.getCacheDir(), "update");        }        if (!downloadDir.exists()) {            downloadDir.mkdirs();        }        return downloadDir;    }    private void start() {        if (updateProgressListener != null) {            updateProgressListener.start();        }    }    private void update(int progress) {        if (updateProgressListener != null) {            updateProgressListener.update(progress);        }    }    private void success(String path) {        if (updateProgressListener != null) {            updateProgressListener.success(path);        }        Intent i = installIntent(this, path);        startActivity(i);//自动安装        stopSelf();    }    private void error() {        if (updateProgressListener != null) {            updateProgressListener.error();        }        stopSelf();    }    private static class DownloadApk extends AsyncTask<String, Integer, String> {        private final String md5;        private UpdateService updateService;        public DownloadApk(UpdateService service, String md5) {            this.updateService = service;            this.md5 = md5;        }        @Override        protected void onPreExecute() {            super.onPreExecute();            if (updateService != null) {                updateService.start();            }        }        @Override        protected String doInBackground(String... strings) {            final String downloadUrl = strings[0];            final File file = new File(UpdateService.getDownloadDir(updateService),                    UpdateService.getSaveFileName(downloadUrl));            Log.d(TAG, "download url is " + downloadUrl);            Log.d(TAG, "download apk cache at " + file.getAbsolutePath());            File dir = file.getParentFile();            if (!dir.exists()) {                dir.mkdirs();            }            HttpURLConnection httpConnection = null;            InputStream is = null;            FileOutputStream fos = null;            long updateTotalSize = 0;            URL url;            try {                url = new URL(downloadUrl);                httpConnection = (HttpURLConnection) url.openConnection();                httpConnection.setConnectTimeout(20000);                httpConnection.setReadTimeout(20000);                Log.d(TAG, "download status code: " + httpConnection.getResponseCode());                if (httpConnection.getResponseCode() != 200) {                    return null;                }                updateTotalSize = httpConnection.getContentLength();                if (file.exists()) {                    if (updateTotalSize == file.length()) {                        // 下载完成                        if (TextUtils.isEmpty(md5) || MD5Util.getMD5String(file).toUpperCase().equals(md5.toUpperCase())) {return file.getAbsolutePath();                        }                    } else {                        file.delete();                    }                }                file.createNewFile();                is = httpConnection.getInputStream();                fos = new FileOutputStream(file, false);                byte buffer[] = new byte[4096];                int readSize = 0;                long currentSize = 0;                while ((readSize = is.read(buffer)) > 0) {                    fos.write(buffer, 0, readSize);                    currentSize += readSize;                    publishProgress((int) (currentSize * 100 / updateTotalSize));                }                // download success            } catch (Exception e) {                e.printStackTrace();                return null;            } finally {                if (httpConnection != null) {                    httpConnection.disconnect();                }                if (is != null) {                    try {                        is.close();                    } catch (IOException e) {                        e.printStackTrace();                    }                }                if (fos != null) {                    try {                        fos.close();                    } catch (IOException e) {                        e.printStackTrace();                    }                }            }            try {                if (TextUtils.isEmpty(md5) || MD5Util.getMD5String(file).toUpperCase().equals(md5.toUpperCase())) {                    return file.getAbsolutePath();                }            } catch (IOException e) {                e.printStackTrace();                return file.getAbsolutePath();            }            Log.e(TAG, "md5 invalid");            return null;        }        @Override        protected void onProgressUpdate(Integer... values) {            super.onProgressUpdate(values);            if (updateService != null) {                updateService.update(values[0]);            }        }        @Override        protected void onPostExecute(String s) {            super.onPostExecute(s);            if (updateService != null) {                if (s != null) {                    updateService.success(s);                } else {                    updateService.error();                }            }        }    }    public static class Builder {        private String downloadUrl;        private String md5;        private ServiceConnection serviceConnection;        protected Builder(String downloadUrl) {            this.downloadUrl = downloadUrl;        }        public static Builder create(String downloadUrl) {            if (downloadUrl == null) {                throw new NullPointerException("downloadUrl == null");            }            return new Builder(downloadUrl);        }        public String getMd5() {            return md5;        }        public Builder setMd5(String md5) {            this.md5 = md5;            return this;        }        public Builder build(Context context, UpdateProgressListener listener) {            if (context == null) {                throw new NullPointerException("context == null");            }            Intent intent = new Intent();            intent.setClass(context, UpdateService.class);            intent.putExtra(URL, downloadUrl);            intent.putExtra(KEY_MD5, md5);            UpdateProgressListener delegateListener = new UpdateProgressListener() {                @Override                public void start() {                    if (listener != null) {                        listener.start();                    }                }                @Override                public void update(int var1) {                    if (listener != null) {                        listener.update(var1);                    }                }                @Override                public void success(String path) {                    try {                        context.unbindService(serviceConnection);                    } catch (Throwable t) {                        Log.e("UpdateService", "解绑失败" + t.getMessage());                    }                    if (listener != null) {                        listener.success(path);                    }                }                @Override                public void error() {                    try {                        context.unbindService(serviceConnection);                    } catch (Throwable t) {                        Log.e("UpdateService", "解绑失败" + t.getMessage());                    }                    if (listener != null) {                        listener.error();                    }                }            };            serviceConnection = new ServiceConnection() {                @Override                public void onServiceConnected(ComponentName name, IBinder service) {                    LocalBinder binder = (LocalBinder) service;                    binder.setUpdateProgressListener(delegateListener);                }                @Override                public void onServiceDisconnected(ComponentName name) {                }            };            context.bindService(intent, serviceConnection, Context.BIND_IMPORTANT);            context.startService(intent);            return this;        }    }    public interface UpdateProgressListener {        void start();        void update(int var);        void success(String path);        void error();    }}
package com.mvp.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import com.mvp.myapplication.update.UpdateService;public class MainActivity extends AppCompatActivity {    private Button btnAllUpdate, btnAddUpdate, btnHotUpdate;    private String url,md5;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        btnAddUpdate = findViewById(R.id.btn_add_update);        btnAllUpdate = findViewById(R.id.btn_all_update);        btnHotUpdate = findViewById(R.id.btn_hot_update);        Log.e("MainActivity","onCreate");        btnAllUpdate.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                UpdateService.Builder.create(url)                        .setMd5(md5)                        .build(MainActivity.this, new UpdateService.UpdateProgressListener() {@Overridepublic void start() {    Log.e("MainActivity", "start");}@Overridepublic void update(int var) {    Log.e("MainActivity", "update ===> " + var);}@Overridepublic void success(String path) {    Log.e("MainActivity", "success ===> " + path);}@Overridepublic void error() {    Log.e("MainActivity", "error");}                        });            }        });    }}
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools">    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <application        android:allowBackup="true"        android:dataExtractionRules="@xml/data_extraction_rules"        android:fullBackupContent="@xml/backup_rules"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:roundIcon="@mipmap/ic_launcher_round"        android:supportsRtl="true"        android:theme="@style/Theme.MyApplication"        tools:targetApi="31">        <service            android:name=".update.UpdateService"            android:enabled="true"            android:exported="true"></service>        <activity            android:name=".MainActivity"            android:exported="true">            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>            <meta-data                android:name="android.app.lib_name"                android:value="" />        </activity>        <!-- Android7以上需要 -->        <provider            android:name="androidx.core.content.FileProvider"            android:authorities="${applicationId}.update_app.file_provider"            android:exported="false"            android:grantUriPermissions="true"            tools:replace="android:authorities">            <meta-data                android:name="android.support.FILE_PROVIDER_PATHS"                android:resource="@xml/update_app_path" />        </provider>    </application></manifest>
<?xml version="1.0" encoding="utf-8"?><paths>    <cache-path        name="update_app_cache_files"        path="/update" />    <external-path        name="update_app_external_files"        path="/" />    <external-cache-path        name="update_app_external_cache_files"        path="/update" /></paths>

热更新

严格意义上来说,个人认为热更新并不是用来进行包体升级,更多的用来进行修复bug的。例如,由于某个程序员的失误,在某个类中抛出了一个空指针异常,导致程序执行到该类后一直崩溃。这种情况下,其实就可以使用热更新来处理。因为,我们并没有大改app中的功能,只是某个类报错了。但个人认为热更新其实也不能解决所有的奔溃问题的,这些黑科技或多或少都是有一些兼容性的问题的,像Tinker就必须要冷启动才能修复,而且受限于Android的版本。
具体是技术实现方式可以参考笔者之前写的一篇博客:Android热修复1以及Android热更新十:自己写一个Android热修复

增量更新

什么是增量更新呢?举了例子,假设我们需要将apk从v1.0升级到v2.0,这时我们可以通过全量更新的方式,下载2.0版本的apk然后进行覆盖安装。但是,一般情况下2.0版本的apk往往包含了1.0版本的功能,理论上我们只需要下载二者的差分包,然后将差分包与1.0版本的包进行合并即可生成一个2.0版本的包。这样做的好处自然就是节约了流量了。像几乎所有的应用商店都使用增量更新的方式来更新apk。那么,我们该我们使用增量更新呢?这个就要借助一个工具:bsdiff
注意:如果想要使用增量更新,那么必须要有一个旧版本的apk,如果用户安装完apk后直接把旧版本的apk删掉了,那么还是老老实实使用全量更新的方式吧。

bsdiff oldfile newfile1 patchfile

bspatch oldfile newfile2 patchfile

使用上面两步便可以完成差分包的拆分与合并,新生成的newfile2 与newfile1的md5是一致的。但是上面这两步法我们是在pc端进行的,我们该如何在代码中实现上面的逻辑呢?首先,拆分的逻辑还是在pc端中进行,客户端只需要关注如何合并差分包。
首先,我们需要导入bspatch相关的类
在这里插入图片描述
接着,我们新建一个类用于调用c相关的代码:

package com.mvp.myapplication.utils;public class BSPatchUtil {    // Used to load the 'native-lib' library on application startup.    static {        System.loadLibrary("bspatch");    }        public static native int bspatch(String oldApkPath, String outputApkPath,       String patchPath);}
package com.mvp.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.os.Environment;import android.util.Log;import android.view.View;import android.widget.Button;import com.mvp.myapplication.update.UpdateService;import com.mvp.myapplication.utils.BSPatchUtil;import java.io.File;public class MainActivity extends AppCompatActivity {    private Button btnAllUpdate, btnAddUpdate, btnHotUpdate;    private String url, md5;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        btnAddUpdate = findViewById(R.id.btn_add_update);        btnAllUpdate = findViewById(R.id.btn_all_update);        btnHotUpdate = findViewById(R.id.btn_hot_update);        Log.e("MainActivity", "onCreate");        btnAllUpdate.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                UpdateService.Builder.create(url)                        .setMd5(md5)                        .build(MainActivity.this, new UpdateService.UpdateProgressListener() {@Overridepublic void start() {    Log.e("MainActivity", "start");}@Overridepublic void update(int var) {    Log.e("MainActivity", "update ===> " + var);}@Overridepublic void success(String path) {    Log.e("MainActivity", "success ===> " + path);}@Overridepublic void error() {    Log.e("MainActivity", "error");}                        });            }        });        btnAddUpdate.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                genNewApk();            }        });    }    private void genNewApk() {        String oldpath = getApplicationInfo().sourceDir;        String newpath = (this.getCacheDir().getAbsolutePath()+ File.separator                + "composed_hivebox_apk.apk");        String patchpath = (this.getCacheDir().getAbsolutePath()+ File.separator                + "bs_patch");        Log.e("MainActivity", "oldpath is " + oldpath + "\n newpath is " + newpath + "\n patchpath is " + patchpath);        BSPatchUtil.bspatch(oldpath, newpath, patchpath);    }}

注意:需要修改bspatch.c文件中的Java_com_mvp_myapplication_utils_BSPatchUtil_bspatch方法签名:改为BSPatchUtil 的包名,例如BSPatchUtil对应的路径为com.dxl.testbatch.util.BSPatchUtil,那么native方法签名就是Java_com_mvp_myapplication_utils_BSPatchUtil_bspatch
这样便可以通过jni调用到c层面的代码。

接着,修改build.gradle文件,添加下面圈中的闭包
在这里插入图片描述
最后,我们执行Make Project命令,正常情况下便可以生成如下几个so库
在这里插入图片描述
最后,我们在把so库放入jniLibs文件夹中,然后build下生成apk包
在这里插入图片描述

然后,我们将差分包bs_patch放入手机的data/data目录下,点击按钮就会生成composed_hivebox_apk.apk这个apk包,将其与v2.0的包进行MD5对比,发现是一致的。如此,我们便实现了一个简单的增量更新逻辑。
Demo地址:https://gitee.com/hzulwy/add_-update/tree/master/MyApplication

来源地址:https://blog.csdn.net/qq_36828822/article/details/129852751

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯