Android实现应用的增量更新\升级
2016-10-17 16:29
651 查看
转载请注明出处:http://blog.csdn.net/yyh352091626/article/details/50579859
GitHub更新:https://github.com/smuyyh/IncrementallyUpdate
增量升级的背景
虽然很多App的版本更新并不频繁,但是一个App基本上也有几兆到几十兆不等,在没有Wifi的条件下,更新App是非常耗流量的。说到这个就必须得吐槽一下三大网络运营商,4G网络是变快了,但是流量确没有多,流量仍然不够用,治标不治本,并没什么卵用。
随着各类App版本的不断更新和升级,App体积也逐渐变大,用户升级成了一个比较棘手的问题,Google很快就意识到了这一点,在IO大会上提出了增量升级,国内诸如小米应用商店也实现了应用的增量升级,减少用户流量的损耗。
增量更新原理
增量更新的原理也很简单,就是将手机上已安装的旧版本apk与服务器端新版本apk进行二进制对比,并得到差分包(patch),用户在升级更新应用时,只需要下载差分包,然后在本地使用差分包与旧版的apk合成新版apk,然后进行安装。这个原理就很想微软更新漏洞打补丁一样,其实都是一个道理。差分包文件的大小,那就远比APK小得多了,这样也便于用户进行应用升级。旧版的APK可以在/data/app/%packagename%底下找到。
差分包的生成和新的APK的合成,需要用到NDK环境,没接触过的那就先学一下,当然,我后面会提供编译好的so库,直接放倒libs/armeabi下调用也是可以的。制作差分包的工具为bsdiff,这是一个非常牛的二进制查分工具,bsdiff源代码在android的源码目录下
\external\bsdiff这边也可以找到。另外还需要bzlib来进行打包。在安全性方面,补丁和新旧版APK最好都要进行MD5验证,以免被篡改,对此我暂不进行叙述。
增量更新存在的不足
1、增量升级是以两个应用版本之间的差异来生成补丁的,但是我们无法保证用户每次的及时升级到最新,也就是在更新前,新版和旧版只差一个版本,所以必须对你所发布的每一个版本都和最新的版本作差分,以便使所有版本的用户都可以差分升级,这样相对就比较繁琐了。解决方法也有,可以通过Shell脚本来实现批量生成。
2.增量升级能成功的前提是,从手机端能够获得旧版APK,并且与服务端的APK签名是一样的,所以像那些破解的APP就无法实现更新。前面也提到了,为了安全性,防止补丁合成错误,最好在补丁合成前对旧版本的apk进行sha1或者MD5校验,保证基础包的一致性,这样才能顺利的实现增量升级。
C语言实现的主要代码
这是在jni上实现差分包的生成与合并,当然,差分包一般是在服务端生成的,在服务端,那就需要把com_yyh_lib_bsdiff_DiffUtils.c以及bzip2库,编译成动态链接库,供Java调用。windows下生成的动态链接库为.dll文件,Unix-like下生成的为.so文件,因为Android是基于Linux内核的,所以也是.so文件,Mac OSX下生成的动态链接库为.dylib文件。
所以后面需要DaemonProcess-1.apk(旧版) DaemonProcess-2.apk(新版)这两个APK,我放在assets文件夹下,来生成差分包。这两个文件就自行拷贝到SD卡/yyh文件夹下,或者按需修改。
Java代码主要实现部分
DiffUtils.java
PatchUtils.java
MainActivity.java
这样就实现了差分包的生成与新的APK的合成,那么我们得到新的APK之后,就调用以下代码进行安装。
无需root实现apk的静默安装
对于应用商店来说,App就不仅一个,想要得到所有旧版APK,就可以遍历所有的包名,通过context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir获取。相关代码如下
之前在实现的过程中,还碰到过一个问题,就是差分包可以生成,但是合成的时候就出错了,最后还是没搞懂是为什么。有解决过类似问题的,还希望多交流一下~~
以上主要就是进行差分包的生成与新的APK的合成,关键技术都实现了,调试了两天,终于把它搞定了。其他扩展的功能,大家自行实现。上效果~点击bsdiff进行差分包生成,然后点击bspatch进行合并。
Android 实现应用的增量更新和升级
服务端通过新版本APK和旧版本APK生成patch补丁(也成为差分包),客户端更新的时候只需要下载差分包到本地,然后从system/app取出旧版本APK,通过差分包来合成新版本的APK,这个过程实际上就是打补丁。
首先来看看这四个文件的作用
srcDir:旧版本apk路径。也就是已安装的旧版应用的APK地址。为了便于演示,这边直接写死路径。若想真正获取旧版apk地址,可通过下面代码实现:
destDir1:新版本的apk路径。
destDir2:新版本的apk路径。通过差分包+旧版本APK合成新版本APK。
patchDir:差分包。通过旧版本APK+新版本APK生成差分包。
若需自己编译jni代码,则下载NDK,并在local.properties下配置自己ndk路径
build.gradle加入以下内容:
若不想编译jni资源,也可直接使用项目提供的so库。在build.gradle配置so库路径,去掉jni编译相关脚本,sync now...
调用生成差分包及合成APK的native方法。
服务端工具以及源码位于Server目录下。目前只在Linux64位的系统下编译,其他系统大家可自行编译。Linux下的可直接修改makefile,windows下可用VC编译。
Diff工具:生成差分包
Patch工具:合并
GitHub更新:https://github.com/smuyyh/IncrementallyUpdate
增量升级的背景
虽然很多App的版本更新并不频繁,但是一个App基本上也有几兆到几十兆不等,在没有Wifi的条件下,更新App是非常耗流量的。说到这个就必须得吐槽一下三大网络运营商,4G网络是变快了,但是流量确没有多,流量仍然不够用,治标不治本,并没什么卵用。
随着各类App版本的不断更新和升级,App体积也逐渐变大,用户升级成了一个比较棘手的问题,Google很快就意识到了这一点,在IO大会上提出了增量升级,国内诸如小米应用商店也实现了应用的增量升级,减少用户流量的损耗。
增量更新原理
增量更新的原理也很简单,就是将手机上已安装的旧版本apk与服务器端新版本apk进行二进制对比,并得到差分包(patch),用户在升级更新应用时,只需要下载差分包,然后在本地使用差分包与旧版的apk合成新版apk,然后进行安装。这个原理就很想微软更新漏洞打补丁一样,其实都是一个道理。差分包文件的大小,那就远比APK小得多了,这样也便于用户进行应用升级。旧版的APK可以在/data/app/%packagename%底下找到。
差分包的生成和新的APK的合成,需要用到NDK环境,没接触过的那就先学一下,当然,我后面会提供编译好的so库,直接放倒libs/armeabi下调用也是可以的。制作差分包的工具为bsdiff,这是一个非常牛的二进制查分工具,bsdiff源代码在android的源码目录下
\external\bsdiff这边也可以找到。另外还需要bzlib来进行打包。在安全性方面,补丁和新旧版APK最好都要进行MD5验证,以免被篡改,对此我暂不进行叙述。
增量更新存在的不足
1、增量升级是以两个应用版本之间的差异来生成补丁的,但是我们无法保证用户每次的及时升级到最新,也就是在更新前,新版和旧版只差一个版本,所以必须对你所发布的每一个版本都和最新的版本作差分,以便使所有版本的用户都可以差分升级,这样相对就比较繁琐了。解决方法也有,可以通过Shell脚本来实现批量生成。
2.增量升级能成功的前提是,从手机端能够获得旧版APK,并且与服务端的APK签名是一样的,所以像那些破解的APP就无法实现更新。前面也提到了,为了安全性,防止补丁合成错误,最好在补丁合成前对旧版本的apk进行sha1或者MD5校验,保证基础包的一致性,这样才能顺利的实现增量升级。
C语言实现的主要代码
<span style="font-size:14px;">/** * 生成差分包 */ JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_DiffUtils_genDiff(JNIEnv *env, jclass cls, jstring old, jstring new, jstring patch) { int argc = 4; char * argv[argc]; argv[0] = "bsdiff"; argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0)); argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0)); argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0)); printf("old apk = %s \n", argv[1]); printf("new apk = %s \n", argv[2]); printf("patch = %s \n", argv[3]); int ret = genpatch(argc, argv); printf("genDiff result = %d ", ret); (*env)->ReleaseStringUTFChars(env, old, argv[1]); (*env)->ReleaseStringUTFChars(env, new, argv[2]); (*env)->ReleaseStringUTFChars(env, patch, argv[3]); return ret; } /** * 差分包合成新的APK */ JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_PatchUtils_patch (JNIEnv *env, jclass cls, jstring old, jstring new, jstring patch){ int argc = 4; char * argv[argc]; argv[0] = "bspatch"; argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0)); argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0)); argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0)); printf("old apk = %s \n", argv[1]); printf("patch = %s \n", argv[3]); printf("new apk = %s \n", argv[2]); int ret = applypatch(argc, argv); printf("patch result = %d ", ret); (*env)->ReleaseStringUTFChars(env, old, argv[1]); (*env)->ReleaseStringUTFChars(env, new, argv[2]); (*env)->ReleaseStringUTFChars(env, patch, argv[3]); return ret; }</span>
这是在jni上实现差分包的生成与合并,当然,差分包一般是在服务端生成的,在服务端,那就需要把com_yyh_lib_bsdiff_DiffUtils.c以及bzip2库,编译成动态链接库,供Java调用。windows下生成的动态链接库为.dll文件,Unix-like下生成的为.so文件,因为Android是基于Linux内核的,所以也是.so文件,Mac OSX下生成的动态链接库为.dylib文件。
所以后面需要DaemonProcess-1.apk(旧版) DaemonProcess-2.apk(新版)这两个APK,我放在assets文件夹下,来生成差分包。这两个文件就自行拷贝到SD卡/yyh文件夹下,或者按需修改。
Java代码主要实现部分
DiffUtils.java
<span style="font-size:14px;">package com.yyh.lib.bsdiff; /** * APK Diff工具类 * * @author yuyuhang * @date 2016-1-26 下午1:10:18 */ public class DiffUtils { static DiffUtils instance; public static DiffUtils getInstance() { if (instance == null) instance = new DiffUtils(); return instance; } static { System.loadLibrary("ApkPatchLibrary"); } /** * native方法 比较路径为oldPath的apk与newPath的apk之间差异,并生成patch包,存储于patchPath * * 返回:0,说明操作成功 * * @param oldApkPath * 示例:/sdcard/old.apk * @param newApkPath * 示例:/sdcard/new.apk * @param patchPath * 示例:/sdcard/xx.patch * @return */ public native int genDiff(String oldApkPath, String newApkPath, String patchPath); } </span>
PatchUtils.java
<span style="font-size:14px;">package com.yyh.lib.bsdiff; /** * APK Patch工具类 * * @author yuyuhang * @date 2016-1-26 下午1:10:40 */ public class PatchUtils { static PatchUtils instance; public static PatchUtils getInstance() { if (instance == null) instance = new PatchUtils(); return instance; } static { System.loadLibrary("ApkPatchLibrary"); } /** * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath * * 返回:0,说明操作成功 * * @param oldApkPath * 示例:/sdcard/old.apk * @param newApkPath * 示例:/sdcard/new.apk * @param patchPath * 示例:/sdcard/xx.patch * @return */ public native int patch(String oldApkPath, String newApkPath, String patchPath); } </span>
MainActivity.java
<span style="font-size:14px;">package com.yyh.activity; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Looper; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.Toast; import com.example.bsdifflib.R; import com.yyh.lib.bsdiff.DiffUtils; import com.yyh.lib.bsdiff.PatchUtils; import com.yyh.utils.ApkUtils; import com.yyh.utils.SignUtils; @SuppressWarnings("unchecked") public class MainActivity extends Activity { Button btnstart; private ArrayList<ResolveInfo> mApps; private PackageManager pm; // 成功 private static final int WHAT_SUCCESS = 1; // 合成失败 private static final int WHAT_FAIL_PATCH = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pm = getPackageManager(); // initApp(); } public void bsdiff(View view) { new DiffTask().execute(); } public void bspatch(View view) { new PatchTask().execute(); } /** * 生成差分包 * * @author yuyuhang * @date 2016-1-25 下午12:24:34 */ private class DiffTask extends AsyncTask<String, Void, Integer> { @Override protected void onPreExecute() { super.onPreExecute(); } @Override protected Integer doInBackground(String... params) { String appDir, newDir, patchDir; try { appDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-1.apk"; newDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-2.apk"; patchDir = Environment.getExternalStorageDirectory() + "/yyh/YixinCall.patch"; int result = DiffUtils.getInstance().genDiff(appDir, newDir, patchDir); if (result == 0) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "差分包已生成", Toast.LENGTH_SHORT).show(); } }); return WHAT_SUCCESS; } else { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "差分包生成失败", Toast.LENGTH_SHORT).show(); } }); return WHAT_FAIL_PATCH; } } catch (Exception e) { e.printStackTrace(); } return WHAT_FAIL_PATCH; } } /** * 差分包合成APK * * @author yuyuhang * @date 2016-1-25 下午12:24:34 */ private class PatchTask extends AsyncTask<String, Void, Integer> { @Override protected void onPreExecute() { super.onPreExecute(); } @Override protected Integer doInBackground(String... params) { String appDir, newDir, patchDir; try { // 指定包名的程序源文件路径 appDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-1.apk"; newDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-3.apk"; patchDir = Environment.getExternalStorageDirectory() + "/yyh/YixinCall.patch"; int result = PatchUtils.getInstance().patch(appDir, newDir, patchDir); if (result == 0) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "合成APK成功", Toast.LENGTH_SHORT).show(); } }); return WHAT_SUCCESS; } else { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "合成APK失败", Toast.LENGTH_SHORT).show(); } }); return WHAT_FAIL_PATCH; } } catch (Exception e) { e.printStackTrace(); } return WHAT_FAIL_PATCH; } } /** * 初始化app列表 */ private void initApp() { // 获取android设备的应用列表 Intent intent = new Intent(Intent.ACTION_MAIN); // 动作匹配 intent.addCategory(Intent.CATEGORY_LAUNCHER); // 类别匹配 mApps = (ArrayList<ResolveInfo>) pm.queryIntentActivities(intent, 0); // 排序 Collections.sort(mApps, new Comparator<ResolveInfo>() { @Override public int compare(ResolveInfo a, ResolveInfo b) { // 排序规则 PackageManager pm = getPackageManager(); return String.CASE_INSENSITIVE_ORDER.compare(a.loadLabel(pm).toString(), b.loadLabel(pm).toString()); // 忽略大小写 } }); for (ResolveInfo ri : mApps) { Log.i("test", ri.activityInfo.packageName); } } } </span>
这样就实现了差分包的生成与新的APK的合成,那么我们得到新的APK之后,就调用以下代码进行安装。
<span style="font-size:14px;">Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); startActivity(intent); </span>或者如果需要静默安装的话,可以参考我的另一篇博客:Android
无需root实现apk的静默安装
对于应用商店来说,App就不仅一个,想要得到所有旧版APK,就可以遍历所有的包名,通过context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir获取。相关代码如下
<span style="font-size:14px;">package com.yyh.utils; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.text.TextUtils; import java.util.Iterator; import java.util.List; /** * Apk工具类 * * @author yuyuhang * @date 2016-1-25 下午12:07:09 */ public class ApkUtils { /** * 获取已安装apk的PackageInfo * * @param context * @param packageName * @return */ public static PackageInfo getInstalledApkPackageInfo(Context context, String packageName) { PackageManager pm = context.getPackageManager(); List<PackageInfo> apps = pm.getInstalledPackages(PackageManager.GET_SIGNATURES); Iterator<PackageInfo> it = apps.iterator(); while (it.hasNext()) { PackageInfo packageinfo = it.next(); String thisName = packageinfo.packageName; if (thisName.equals(packageName)) { return packageinfo; } } return null; } /** * 判断apk是否已安装 * * @param context * @param packageName * @return */ public static boolean isInstalled(Context context, String packageName) { PackageManager pm = context.getPackageManager(); boolean installed = false; try { pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); installed = true; } catch (Exception e) { e.printStackTrace(); } return installed; } /** * 获取已安装Apk文件的源Apk文件 * * @param context * @param packageName * @return */ 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 (NameNotFoundException e) { e.printStackTrace(); } return null; } /** * 安装Apk * * @param context * @param apkPath */ public static void installApk(Context context, String apkPath) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse("file://" + apkPath), "application/vnd.android.package-archive"); context.startActivity(intent); } }</span>
之前在实现的过程中,还碰到过一个问题,就是差分包可以生成,但是合成的时候就出错了,最后还是没搞懂是为什么。有解决过类似问题的,还希望多交流一下~~
以上主要就是进行差分包的生成与新的APK的合成,关键技术都实现了,调试了两天,终于把它搞定了。其他扩展的功能,大家自行实现。上效果~点击bsdiff进行差分包生成,然后点击bspatch进行合并。
Android 实现应用的增量更新和升级
原理
服务端通过新版本APK和旧版本APK生成patch补丁(也成为差分包),客户端更新的时候只需要下载差分包到本地,然后从system/app取出旧版本APK,通过差分包来合成新版本的APK,这个过程实际上就是打补丁。步骤 | 内容 |
---|---|
拷贝资源 | 拷贝旧版本APK以及新版本APK到SD卡。为了后面进行生成差分包 |
安装旧版本APK | 安装旧版本的APK |
生成补丁 | 生成差分包。这个实际上应该是在服务端完成 |
打补丁 | 通过差分包及旧版本APK生成新版本APK |
安装新版本APK | 安装生成的新版本APK |
获取某个应用的APK安装文件 | 在真正的增量更新过程中,旧版本Apk应该从/data/app底下获取,拷贝到SD卡,进行打补丁。当然,也可以不拷贝,直接使用该路径。 |
String srcDir = Environment.getExternalStorageDirectory().toString() + "/DaemonProcess-1.apk"; String destDir1 = Environment.getExternalStorageDirectory().toString() + "/DaemonProcess-2.apk"; String destDir2 = Environment.getExternalStorageDirectory().toString() + "/DaemonProcess-3.apk"; String patchDir = Environment.getExternalStorageDirectory().toString() + "/DaemonProcess.patch";
首先来看看这四个文件的作用
srcDir:旧版本apk路径。也就是已安装的旧版应用的APK地址。为了便于演示,这边直接写死路径。若想真正获取旧版apk地址,可通过下面代码实现:
String appDir = getPackageManager().getApplicationInfo(packageName, 0).sourceDir;
destDir1:新版本的apk路径。
destDir2:新版本的apk路径。通过差分包+旧版本APK合成新版本APK。
patchDir:差分包。通过旧版本APK+新版本APK生成差分包。
NDK配置
若需自己编译jni代码,则下载NDK,并在local.properties下配置自己ndk路径ndk.dir=/Users/yuyuhang/Documents/Android/android-ndk-r10c
build.gradle加入以下内容:
android { defaultConfig { ndk{ moduleName "ApkPatchLibrary" abiFilters "armeabi", "armeabi-v7a", "x86" } } sourceSets { main { jni.srcDirs = ['src/main/jni', 'src/main/jni/'] } } }
若不想编译jni资源,也可直接使用项目提供的so库。在build.gradle配置so库路径,去掉jni编译相关脚本,sync now...
sourceSets { main { // jni.srcDirs = ['src/main/jni', 'src/main/jni/'] jniLibs.srcDirs = ['libs'] // 若不想编译jni代码,可直接引用so库,ndk编译相关脚本注释掉 } }
使用
调用生成差分包及合成APK的native方法。package com.yyh.lib.bsdiff; public class DiffUtils { static DiffUtils instance; public static DiffUtils getInstance() { if (instance == null) instance = new DiffUtils(); return instance; } static { System.loadLibrary("ApkPatchLibrary"); } /** * 比较oldapk与newapk之间差异,并生成patch包,存储于patchPath * * @param oldApkPath 示例:/sdcard/old.apk * @param newApkPath 示例:/sdcard/new.apk * @param patchPath 示例:/sdcard/xx.patch * @return 0:成功 非0:失败 */ public native int genDiff(String oldApkPath, String newApkPath, String patchPath); }
package com.yyh.lib.bsdiff; public class PatchUtils { static PatchUtils instance; public static PatchUtils getInstance() { if (instance == null) instance = new PatchUtils(); return instance; } static { System.loadLibrary("ApkPatchLibrary"); } /** * 使用oldApk与patch补丁包,合成新的apk,存储于newApkPath * * @param oldApkPath 示例:/sdcard/old.apk * @param newApkPath 示例:/sdcard/new.apk * @param patchPath 示例:/sdcard/xx.patch * @return 0:成功 非0:失败 */ public native int patch(String oldApkPath, String newApkPath, String patchPath); }
服务端
服务端工具以及源码位于Server目录下。目前只在Linux64位的系统下编译,其他系统大家可自行编译。Linux下的可直接修改makefile,windows下可用VC编译。Diff工具:生成差分包
<!--命令 oldApk newApk patch--> ./linux-x86_64/Diff DaemonProcess-1.apk DaemonProcess-2.apk dp.patch
Patch工具:合并
<!--命令 oldApk newApk patch--> ./linux-x86_64/Patch DaemonProcess-1.apk DaemonProcess-3.apk dp.patch
相关文章推荐
- Android实现应用的增量更新\升级---其一
- [置顶] Android实现应用的增量更新\升级
- [置顶] Android实现应用的增量更新\升级 标签: 增量更新Androidbsdiffpatch增量升级 2016-01-25 16:01
- Android实现应用的增量更新\升级---其二
- Android应用开发笔记(12):Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现完整方案+参考程序代吗
- 【转】Android应用的自动升级、更新模块的实现 (2)
- Android 应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- 对“Android应用的自动升级、更新模块的实现”的一些改进
- Android应用开发笔记(12):Android应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- Android 应用的自动升级、更新模块的实现
- Android应用的自动升级、更新模块的实现
- 增量升级(省流量更新)的Android客户端实现