Android应用内升级App

Android应用内升级App

一、为什么需要应用内升级?

1、apk上架市场周期慢,无法回退

2、可以小规模实验以及试错(新功能实验,稳定性检测)

3、可以快速收敛版本(新功能覆盖、严重bug修复)

二、在app中存在的几种升级形式

1、应用启动时静默检测,提示更新

2、用户手动在设置页,点击检测更新

三、实现流程

应用内更新App流程图

四、案例实现步骤

1、网络模块设计

1)考虑通过接口隔离具体实现

好处:

(1)方便以后替换实现

(2)可以并行开发

2)使用okhttp完成接口实现,实现get请求,文件下载

2、UI实现

1)使用DialogFragment而不是使用Dialog

2)接入网络请求,进度回调

3、安装apk以及做一些细节处理

1)用户下载过程中cancel,如何及时的取消请求,中断下载

2)apk的完整性校验

4、适配

1)避免Android存储卡权限

使用应用内部的cache文件夹,避免涉及到存储卡权限

2)Android N FileProvider适配

应用安装,涉及到文件uri的传递,需要进行适配

3)Android O 对应用安装进行的权限的限制

需要引入安装权限

4)Android P 对http网络请求的约束

在Android P 上,默认不允许直接使用http的请求,需要使用https

五、具体实现

1、搭建网络访问模块

1)定义接口,共三个

网络访问接口,负责发起get请求、下载文件请求、取消

public interface INetManager {

/**

* 发起请求

*

* @param url 地址

* @param netCallback 处理返回的结果

* @param tag 标识当前的请求

*/

void get(String url, INetCallback netCallback, Object tag);

/**

* 下载

*

* @param url 资源地址

* @param targetFile 保存到:targetFile

* @param downloadCallback 下载结果回调

* @param tag 标识当前的下载请求

*/

void download(String url, File targetFile, IDownloadCallback downloadCallback, Object tag);

/**

* 取消数据请求

*

* @param tag 标识要取消的请求

*/

void cancel(Object tag);

}

处理网络请求结果的接口

public interface INetCallback {

/**

* 请求成功,再此进行处理

* @param response

*/

void onSuccess(String response);

/**

* 请求失败,在此进行处理

* @param throwable

*/

void onFailed(Throwable throwable);

}

处理下载结果的接口

public interface IDownloadCallback {

/**

* 下载成功,在此处理

* @param apkFile

*/

void onSuccess(File apkFile);

/**

* 下载进度,在此处理

* @param progress

*/

void progress(int progress);

/**

* 下载失败,在此处理

* @param throwable

*/

void onFailure(Throwable throwable);

}

2)接口实现类:

接口定义好了,自然就是实现了,这里使用Okhttp来完成网络的访问。

待会在业务代码:AppUpdater中就可以看到接口隔离实现的好处之一:

可以很方便的替换具体实现。当不想用Okhttp的时候,可以便捷的修改为其他网络访问框架。

public class OkHttpNetManager implements INetManager {

private static final String TAG = "OkHttpNetManager";

private static OkHttpClient sOkHttpClient;

private static Handler sHandler = new Handler(Looper.getMainLooper());

static {

OkHttpClient.Builder builder = new OkHttpClient.Builder();

builder.connectTimeout(15, TimeUnit.SECONDS);

sOkHttpClient = builder.build();

}

@Override

public void get(String url, final INetCallback netCallback, Object tag) {

Request.Builder builder = new Request.Builder();

Request request = builder.url(url).get().tag(tag).build();

Call call = sOkHttpClient.newCall(request);

call.enqueue(new Callback() {

@Override

public void onFailure(@NotNull Call call, @NotNull final IOException e) {

sHandler.post(new Runnable() {

@Override

public void run() {

netCallback.onFailed(e);

}

});

}

@Override

public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {

try {

final String string = response.body().string();

sHandler.post(new Runnable() {

@Override

public void run() {

netCallback.onSuccess(string);

}

});

} catch (final IOException e) {

e.printStackTrace();

sHandler.post(new Runnable() {

@Override

public void run() {

netCallback.onFailed(e);

}

});

}

}

});

}

@Override

public void download(String url, final File targetFile, final IDownloadCallback downloadCallback, Object tag) {

if (!targetFile.exists()) {

targetFile.getParentFile().mkdirs();

}

//发起请求

Request.Builder builder = new Request.Builder();

final Request request = builder.url(url).get().tag(tag).build();

Call call = sOkHttpClient.newCall(request);

call.enqueue(new Callback() {

@Override

public void onFailure(@NotNull Call call, @NotNull final IOException e) {

sHandler.post(new Runnable() {

@Override

public void run() {

downloadCallback.onFailure(e);

}

});

}

@Override

public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {

InputStream is = null;

OutputStream os = null;

try {

final long totalLen = response.body().contentLength();

is = response.body().byteStream();

os = new FileOutputStream(targetFile);

byte[] buffer = new byte[8 * 1024];

int bufferLen;

int curLen = 0;

while (!call.isCanceled() && (bufferLen = is.read(buffer)) != -1) {

os.write(buffer, 0, bufferLen);

os.flush();

curLen += bufferLen;

final int finalCurLen = curLen;

sHandler.post(new Runnable() {

@Override

public void run() {

downloadCallback.progress((int) (finalCurLen * 1.0f / totalLen * 100));

}

});

}

if (call.isCanceled()){

return;

}

sHandler.post(new Runnable() {

@Override

public void run() {

downloadCallback.onSuccess(targetFile);

}

});

} catch (final FileNotFoundException e) {

if (call.isCanceled()){

return;

}

e.printStackTrace();

sHandler.post(new Runnable() {

@Override

public void run() {

downloadCallback.onFailure(e);

}

});

} finally {

if (is != null) {

is.close();

}

if (os != null) {

os.close();

}

}

}

});

}

@Override

public void cancel(Object tag) {

List queuedCalls = sOkHttpClient.dispatcher().queuedCalls();

if (queuedCalls != null) {

for (Call call : queuedCalls) {

if (tag.equals(call.request().tag())) {

Log.d("cancel", "find call = " + tag);

call.cancel();

}

}

}

List runningCalls = sOkHttpClient.dispatcher().runningCalls();

if (runningCalls != null) {

for (Call call : runningCalls) {

if (tag.equals(call.request().tag())) {

Log.d("cancel", "find call = " + tag);

call.cancel();

}

}

}

}

}

2、AppUpdater类,为应用提供App更新的接口

默认使用OkHttpNetManager()实现类,当要换其他网络访问框架时,使用setINetManager更新即可。

public class AppUpdater {

private static AppUpdater sInstance = new AppUpdater();

public static AppUpdater getInstance() {

return sInstance;

}

/**

* 默认的网络访问方式:OkHttpNetManager

*/

private static INetManager sINetManager = new OkHttpNetManager();

public INetManager getINetManager() {

return sINetManager;

}

/**

* 指定网络访问方式

*

* @param netManager

*/

public void setINetManager(INetManager netManager) {

sINetManager = netManager;

}

3、使用网络模块请求数据并更新UI

1)发起获取新版本信息的请求,并根据结果做具体处理

btnCheckVersion.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

AppUpdater.getInstance().getINetManager().get(Constants.Url.appUpdaterJsonUrl, new INetCallback() {

@Override

public void onSuccess(String response) {

//TODO 分析结果,看是否要更新

//1、解析json

//2、做版本适配

//如果需要更新

//3、弹窗

//4、点击下载

AppVersionInfoBean appVersionInfoBean = AppVersionInfoBean.parse(response);

if (appVersionInfoBean == null){

Toast.makeText(SettingActivity.this, "版本检测接口返回数据异常", Toast.LENGTH_SHORT).show();

return;

}

// TODO 检测是否需要更新

try {

long versionCode = Long.parseLong(appVersionInfoBean.getVersionCode());

if (versionCode <= AppUtils.getVersionCode(SettingActivity.this)){

Toast.makeText(SettingActivity.this, "已经是最新版本,无需更新", Toast.LENGTH_SHORT).show();

return;

}

} catch (NumberFormatException e) {

e.printStackTrace();

Toast.makeText(SettingActivity.this, "版本检测接口返回版本号异常", Toast.LENGTH_SHORT).show();

return;

}

// TODO 弹出更新窗口

UpdateVersionShowDialog.show(SettingActivity.this,appVersionInfoBean);

}

@Override

public void onFailed(Throwable throwable) {

throwable.printStackTrace();

Toast.makeText(SettingActivity.this, "版本更新接口请求失败", Toast.LENGTH_SHORT).show();

}

},SettingActivity.this);

}

});

2)上面有一个AppVersionInfoBean类

我们把获取到的版本信息解析、封装成一个Bean类,用于版本验证和UI更新的数据来源。

这里有一个解析的小技巧:

把解析代码放到Bean类中。

public class AppVersionInfoBean implements Serializable {

private String title;

private String content;

private String url;

private String md5;

private String versionCode;

private AppVersionInfoBean(String title, String content, String url, String md5, String versionCode) {

this.title = title;

this.content = content;

this.url = url;

this.md5 = md5;

this.versionCode = versionCode;

}

/**

* 把response转换为AppVersionInfoBean。

*

* @param response

* @return

*/

public static AppVersionInfoBean parse(String response) {

try {

JSONObject responseJson = new JSONObject(response);

String title = responseJson.optString("title");

String content = responseJson.optString("content");

String url = responseJson.optString("url");

String md5 = responseJson.optString("md5");

String versionCode = responseJson.optString("versionCode");

//TODO 是否需要对获取到的值进行检验

// 不应该在这里检测,检测属于使用这个bean,不适合在这里处理

return new AppVersionInfoBean(title,content,url,md5,versionCode);

} catch (JSONException e) {

e.printStackTrace();

}

return null;

}

public String getTitle() {

return title;

}

public String getContent() {

return content;

}

public String getUrl() {

return url;

}

public String getMd5() {

return md5;

}

public String getVersionCode() {

return versionCode;

}

}

3)UI模块以及安装apk

使用的是一个DialogFragment。

在这里发起了下载Apk的请求,并对请求结果做处理。

public class UpdateVersionShowDialog extends DialogFragment {

private static final String TAG = "UpdateVersionShowDialog";

private static final String KEY_APP_VERSION_INFO_BEAN = "app_version_info_bean";

/**

* 版本更新信息bean,由show方法传入

*/

private AppVersionInfoBean appVersionInfoBean;

@Override

public void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

Bundle arguments = getArguments();

if (arguments != null) {

appVersionInfoBean = (AppVersionInfoBean) arguments.getSerializable(KEY_APP_VERSION_INFO_BEAN);

}

}

@Nullable

@Override

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.dialog_update_app_version, container, false);

bindView(view);

return view;

}

private void bindView(View view) {

TextView tvTitle = view.findViewById(R.id.tv_title);

TextView tvContent = view.findViewById(R.id.tv_content);

final TextView tvUpdate = view.findViewById(R.id.tv_update);

tvTitle.setText(appVersionInfoBean.getTitle());

tvContent.setText(appVersionInfoBean.getContent());

tvUpdate.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(final View v) {

v.setEnabled(false);

//安装包的下载地址,选择getCacheDir路径,可以避免存储权限的处理

final File targetFile = new File(getActivity().getCacheDir(), "target.apk");

AppUpdater.getInstance().getINetManager().download(appVersionInfoBean.getUrl(), targetFile, new IDownloadCallback() {

@Override

public void onSuccess(File apkFile) {

v.setEnabled(true);

dismiss();

//下载成功

Log.d(TAG, "success = " + apkFile.getAbsolutePath());

//TODO check MD5

String fileMd5 = AppUtils.getFileMd5(targetFile);

Log.d(TAG, "md5 = " + fileMd5);

if (fileMd5 != null && fileMd5.equals(appVersionInfoBean.getMd5())) {

//校验成功,安装

Toast.makeText(getActivity(), "开始安装", Toast.LENGTH_SHORT).show();

AppUtils.installApk(getActivity(), apkFile);

} else {

Toast.makeText(getActivity(), "md5检测失败", Toast.LENGTH_SHORT).show();

}

}

@Override

public void progress(int progress) {

Log.d(TAG, "progress = " + progress);

tvUpdate.setText(progress + "%");

}

@Override

public void onFailure(Throwable throwable) {

v.setEnabled(true);

throwable.printStackTrace();

Toast.makeText(getActivity(), "文件下载失败", Toast.LENGTH_SHORT).show();

}

}, UpdateVersionShowDialog.this);

}

});

}

@Override

public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {

super.onViewCreated(view, savedInstanceState);

getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);

getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

}

@Override

public void onDismiss(@NonNull DialogInterface dialog) {

super.onDismiss(dialog);

Log.d("tag", "onDismiss: ");

AppUpdater.getInstance().getINetManager().cancel(this);

}

public static void show(FragmentActivity fragmentActivity, AppVersionInfoBean appVersionInfoBean) {

Bundle bundle = new Bundle();

bundle.putSerializable(KEY_APP_VERSION_INFO_BEAN, appVersionInfoBean);

UpdateVersionShowDialog updateVersionShowDialog = new UpdateVersionShowDialog();

updateVersionShowDialog.setArguments(bundle);

updateVersionShowDialog.show(fragmentActivity.getSupportFragmentManager(), "updateVersionShowDialog");

}

}

4)最后,就是一个工具类 AppUtils

public class AppUtils {

/**

* 获取当前App的版本号

*

* @return 版本号

*/

public static long getVersionCode(Context context) {

PackageManager packageManager = context.getPackageManager();

try {

PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {

long longVersionCode = packageInfo.getLongVersionCode();

return longVersionCode;

}else {

return packageInfo.versionCode;

}

} catch (PackageManager.NameNotFoundException e) {

e.printStackTrace();

}

return -1;

}

/**

* MD5校验

*

* @param targetFile 要校验md5的文件

* @return 文件的md5

*/

public static String getFileMd5(File targetFile) {

if (targetFile == null || !targetFile.isFile()){

return null;

}

MessageDigest digest;

FileInputStream fis = null;

byte[] buffer = new byte[1024];

try {

digest = MessageDigest.getInstance("MD5");

fis = new FileInputStream(targetFile);

int bufferLen;

while ((bufferLen = fis.read(buffer)) != -1){

digest.update(buffer,0,bufferLen);

}

} catch (NoSuchAlgorithmException e) {

e.printStackTrace();

return null;

} catch (IOException e) {

e.printStackTrace();

return null;

}finally {

if (fis != null){

try {

fis.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

byte[] result = digest.digest();

BigInteger bigInteger = new BigInteger(1,result);

return bigInteger.toString(16);

}

/**

* 安装apk

*

* @param activity

* @param apkFile

*/

public static void installApk(FragmentActivity activity, File apkFile) {

//文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装

//因此,可能会存在权限问题,需要做下面的设置

//如果文件是sdcard上的,就不需要这个操作了

try {

apkFile.setExecutable(true, false);

apkFile.setReadable(true, false);

apkFile.setWritable(true, false);

} catch (Exception e) {

e.printStackTrace();

}

Intent intent = new Intent();

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

intent.setAction(Intent.ACTION_VIEW);

Uri uri;

//TODO N FileProvider

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){

uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", apkFile);

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

}else {

uri = Uri.fromFile(apkFile);

}

intent.setDataAndType(uri,"application/vnd.android.package-archive");

activity.startActivity(intent);

//TODO 0 INSTALL PERMISSION

//在AndroidManifest中加入权限即可

}

}

4、适配与问题处理

1)N FileProvider

android:name="androidx.core.content.FileProvider"

android:authorities="${applicationId}.fileprovider"

android:exported="false"

android:grantUriPermissions="true">

android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/fileproviderpath" />

xml/fileproviderpath:

name="files"

path="."/>

name="cache"

path="."/>

name="external"

path="."/>

name="external_cache"

path="."/>

name="external_file"

path="."/>

2)O INSTALL PERMISSION

//在AndroidManifest中加入权限:

3)问题记录:java.net.UnknownServiceException: CLEARTEXT communication to 59.110.162.30 not permitted by network security policy

解决:

(1)在res/xml中新建:network_security_config

(2)在AndroidManifest.xml的application中:

android:networkSecurityConfig="@xml/network_security_config"

六、总结

其实应用内更新的基本逻辑很简单,就是获取一个Apk,然后安装。

重要的是学习,如何构建整个功能模块的思路及其思考:

1、要获取Apk,需要用到网络吧?

所以得构建网络访问框架。

2、网络访问时,http/https可能会带来什么问题?如何处理呢?

3、下载apk后,存储策略是什么?是存在sdcard还是应用内部的cache?

4、如果是cache,那么要交给系统程序去安装,就涉及到文件的跨进程传递了?要如何处理?

5、O以后涉及到了安装权限问题

除了上面,我们还有如下思考:大文件,如何下载?

1、断点续下,分区间下载

原理:http,head中有range,可以指定下载一个文件的:起始字节和终止字节

实现:

如果target.apk有300字节,所以我们可以用多个线程去下载:

线程1:0,100

线程2:101,200

线程3:201,300

最后,在本地合并,使用RandomAccessFile进行seek操作。

2、使用增量更新

apk1 本地

apk2 server

apk diff apk2 --> patch

download patch

涉及到算法 bsdiff。

Android应用内升级该思考的问题

参考:慕课网视频

写于:

2020/09/10

相关推荐

英雄聯盟fps怎麼顯示
office365登陆账号没有反应

英雄聯盟fps怎麼顯示

📅 07-29 👁️ 7992
十万个为什么
约彩365苹果在线安装

十万个为什么

📅 07-26 👁️ 9565
笔记本电脑多少年更换一台(笔记本电脑多久换一个新的比较合适?)
office365登陆账号没有反应

笔记本电脑多少年更换一台(笔记本电脑多久换一个新的比较合适?)

📅 07-08 👁️ 8054
CICC科普栏目|解析3G、4G、5G 背后的科学意义
约彩365苹果在线安装

CICC科普栏目|解析3G、4G、5G 背后的科学意义

📅 07-23 👁️ 2624
阿根廷6场比赛获4个点球 追平单届世界杯点球纪录
外勤365官方网站

阿根廷6场比赛获4个点球 追平单届世界杯点球纪录

📅 07-10 👁️ 7495
魔羯男突然冷淡
约彩365苹果在线安装

魔羯男突然冷淡

📅 10-10 👁️ 9250