前言:本文以Bmob作为数据存储的服务器来实现版本更新,使用其它服务器亦可,思路通用。数据的增删改查请查阅其官方文档:http://doc.bmob.cn/data/android/develop_doc/。涉及Bmob的数据部分不再赘述。文后有 demo
先上图,成功更新版本后的样子:
一、预备知识:VersionCode ,VersionName
Google为APK定义了两个关于版本属性:VersionCode和VersionName,用途各异:
VersionCode :版本号。对用户不可见,仅用于应用市场、程序内部识别版本,判断新旧等用途。
Integer类型,系统默认该值为1。每次发布更新版本时,递增该值
VersionName:版本名。展示给用户,用户通过它认知自己安装的版本
String类型,一般和VersionCode成对出现。
二、思路
每次进行版本更新时,VersionCode版本号递增1,需要开发者修改服务器后台版本数值。app从服务器上获取版本号,并与本地版本号进行比对,若大于本地,则提示用户升级。此属性值是版本更新的依据。
另一属性值versionName,用于向用户展示版本变化幅度。例如从1.0.1变到1.0.2,只是修改了一个很小的bug;若变到1.1.0,可能是修改了某些功能; 再如变到2.0.0,便是进行了大幅修改,比如UI界面改变,功能的增删等。此属性值不作为判断版本升级的依据,只是告知用户版本做了一定程度调整。
三、申请静态、动态各类权限,至少应该包括网络、读写权限
app判断需进行版本更新,访问服务器自动下载新版本apk文件到本地,然后进行静默安装。我的demo是copy以前代码,申请了一些无关的权限,请自行甄别
四、遇到的问题
调试中最易犯的错误是用debug版运行,版本如有更新,会自动下载release的新版本apk到本地。这样往往造成签名不一致,导致静默安装失败。解决方案:在app下build.gradle文件中,统一debug、release签名
//签名配置
signingConfigs {
config {
keyAlias 'chat'
keyPassword '123456'
storeFile file('/src/main/jdk/mychat.jks')
storePassword '123456'
}
}
buildTypes {
//打包配置
release {
//清理无用资源
//shrinkResources true
//是否启动ZipAlign压缩
zipAlignEnabled true
//是否混淆
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
//签名
signingConfig signingConfigs.config
}
debug {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
//签名
signingConfig signingConfigs.config
}
}
注意: 签名配置signingConfigs 时一定要放在打包配置buildTypes 之前,因为脚本是按顺序执行的
不重复造轮子了,部分方法参考网络,感谢分享!
五、关键代码实现
整体思路:手动申请权限成功后,获取后台最新版本号,并与本地版本号比对,大于则弹出对话框提示更新下载然后进行静默安装。
一> 模拟数据
1) 新建一个MyTestData 数据类,继承BmobObject ,根据需要自行增减字段
public class MyTestData extends BmobObject {
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
2)BmobManger中增加批量上传数据的方法
/**
* 同时新增多条测试数据
*/
public void uploadTestData( QueryListListener<BatchResult> listener) {
BmobBatch batch = new BmobBatch();
//批量添加
List<BmobObject> categoriesSave = new ArrayList<>();
for (int i = 0; i < 60; i++) {
MyTestData category = new MyTestData();
category.setDesc("这是测试: " + i);
categoriesSave.add(category);
}
//执行批量操作
batch.insertBatch(categoriesSave);
batch.doBatch(listener);
}
3)上传测试数据。该方法会自动在Bmob后台新建一个MyTestData表, 并产生模拟数据
BmobManager.getInstance().uploadTestData(new QueryListListener<BatchResult>(){
@Override
public void done(List<BatchResult> results, BmobException e) {
if (e == null) {
//返回结果的results和上面提交的顺序是一样的,请一一对应
for (int i = 0; i < results.size(); i++) {
BatchResult result = results.get(i);
BmobException ex = result.getError();
//只有批量添加才返回objectId
if (ex == null) {
Loggerr.i("第" + i + "个数据批量操作成功:" + result.getCreatedAt() + "," + result.getObjectId() + "," + result.getUpdatedAt());;
//模拟数据已写入,下次不会再写
SpUtils.getInstance().putBoolean("sp_is_first_send", false);
if (BmobManager.getInstance().isLogin()) {
//跳转到登录页
startActivity(new Intent(IndexActivity.this, LoginActivity.class));
} else {
//跳转到注册页
startActivity(new Intent(IndexActivity.this, RegisterActivity.class));
}
finish();
} else {
Loggerr.i("第" + i + "个数据批量操作失败:" + ex.getMessage() + "," + ex.getErrorCode());;
}
}
} else {
Loggerr.i("失败:" + e.getMessage() + "," + e.getErrorCode());
}
}
});
4)查看Bmob后台MyTestData表,新增了模拟测试数据
二) android 6.0后手动权限申请
1)清单文件 AdroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STAT E" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.CALL_PHONE" />
2)BaseAcitity中申请。整个BaseAcitity类代码:
public class BaseActivity extends AppCompatActivity {
//申请运行时权限的Code
private static final int PERMISSION_REQUEST_CODE = 1000;
//申请窗口权限的Code
public static final int PERMISSION_WINDOW_REQUEST_CODE = 1001;
//申明所需权限
private String[] mStrPermission = {
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA,
Manifest.permission.READ_CONTACTS,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CALL_PHONE,
Manifest.permission.ACCESS_FINE_LOCATION
};
//保存没有同意的权限
private List<String> mPerList = new ArrayList<>();
//保存没有同意的失败权限
private List<String> mPerNoList = new ArrayList<>();
private OnPermissionsResult permissionsResult;
/**
* 一个方法请求权限
*/
protected void request(OnPermissionsResult permissionsResult) {
Log.i("register","base activity request 请求权限:" );
if (!checkPermissionsAll()) {
requestPermissionAll(permissionsResult);
}
}
/**
* 判断单个权限
*/
protected boolean checkPermissions(String permissions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int check = checkSelfPermission(permissions);
return check == PackageManager.PERMISSION_GRANTED;
}
return false;
}
/**
* 判断是否需要申请权限
*/
protected boolean checkPermissionsAll() {
mPerList.clear();
for (int i = 0; i < mStrPermission.length; i++) {
boolean check = checkPermissions(mStrPermission[i]);
//如果不同意则请求
if (!check) {
mPerList.add(mStrPermission[i]);
}
}
return mPerList.size() > 0 ? false : true;
}
/**
* 请求权限
*/
protected void requestPermission(String[] mPermissions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(mPermissions, PERMISSION_REQUEST_CODE);
}
}
/**
* 申请所有权限
*/
protected void requestPermissionAll(OnPermissionsResult permissionsResult) {
this.permissionsResult = permissionsResult;
requestPermission((String[]) mPerList.toArray(new String[mPerList.size()]));
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
mPerNoList.clear();
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
//你有失败的权限
mPerNoList.add(permissions[i]);
}
}
if (permissionsResult != null) {
if (mPerNoList.size() == 0) {
permissionsResult.OnSuccess();
} else {
permissionsResult.OnFail(mPerNoList);
}
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
protected interface OnPermissionsResult {
void OnSuccess();
void OnFail(List<String> noPermissions);
}
/**
* 判断窗口权限
* @return
*/
protected boolean checkWindowPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(this);
}
return true;
}
/**
* 请求窗口权限
*/
protected void requestWindowPermissions() {
Toast.makeText(this, "申请窗口权限,暂时没做UI交互", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION
, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, PERMISSION_WINDOW_REQUEST_CODE);
}
}
3)TestAcivity继承BaseAcitivity,申请权限
/**
* 权限申请成功后再去获取版本更新信息。因为是异步,会出现权限没有申请成功就去下载的情况,最终导致失败
*/
private void requestPermiss(){
request(new BaseActivity.OnPermissionsResult(){
@Override
public void OnSuccess() {
Loggerr.i("权限成功:" );
//检查更新
new UpdateHelper(TestActivity.this).updateApp(new UpdateHelper.OnUpdateAppListener(){
@Override
public void OnUpdate(boolean isUpdate) {
if(isUpdate){
Loggerr.i("版本更新了");
}else{
Loggerr.i("版本没有更新了");
}
}
});
}
@Override
public void OnFail(List<String> noPermissions) {
}
}
);
}
三)版本更新数据类
1)数据类
public class UpdateSet extends BmobObject {
//描述
private String desc;
//下载地址
private String path;
//版本号
private int versionCode;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getVersionCode() {
return versionCode;
}
public void setVersionCode(int versionCode) {
this.versionCode = versionCode;
}
}
2)版本更新帮助类 - UpdateHelper类
public class UpdateHelper {
private Context mContext;
private DialogView mUpdateView;
private TextView tv_desc;
private TextView tv_confirm;
private TextView tv_cancel;
private String TAG="register";
private ProgressDialog mProgressDialog;
public UpdateHelper(Context mContext) {
this.mContext = mContext;
}
public void updateApp(final OnUpdateAppListener listener) {
BmobManager.getInstance().queryUpdateSet(new FindListener<UpdateSet>() {
@Override
public void done(List<UpdateSet> list, BmobException e) {
if (e == null) {
//每次只读取UpdateSet表中最新的一条数据
if (CommonUtils.isEmpty(list)) {
UpdateSet updateSet = list.get(0);
//获取自己的VersionCode
try {
int AppCode = mContext.getPackageManager().
getPackageInfo(mContext.getPackageName(), 0).versionCode;
Loggerr.i("当前版本AppCod="+AppCode+",网络上versioncode="+updateSet.getVersionCode());
//有更新
if (listener != null) {
listener.OnUpdate(updateSet.getVersionCode() > AppCode ? true : false);
}
if (updateSet.getVersionCode() > AppCode) {
//检测到有更新比对版本
createUpdateDialog(updateSet);
}
} catch (PackageManager.NameNotFoundException ex) {
ex.printStackTrace();
}
}
}else{
Loggerr.i("bmob后台没有版本更新数据");
}
}
});
}
```
四) 生成更新版realase .apk
versionCode 初始默认值为1,现将版本号递增1。每次更新,版本号都必须比前次大
versionCode 2
versionName "2.0 release版本测试"
创建存放.jks文件的文件夹,我的签名配置中需要创建jdk文件夹
//签名配置
signingConfigs {
config {
keyAlias 'chat'
keyPassword '123456'
storeFile file('/src/main/jdk/mychat.jks')
storePassword '123456'
}
}
限于篇幅,如何生成正式签名的apk,请看这篇:https://blog.csdn.net/u010475354/article/details/106899320
假设已生成app-release.apk文件
五) 上传更新版本信息到Bmob后台。绑定独立域名的可通过代码上传新版.apk文件,没有绑定的手动修改。下列1) ,2)方法,根据情况选择1种即可
1)已绑定的可直接上传.apk文件到Bmob后台
*将打包好的更新版 .apk 文件拷贝到手机中
*上传至Bmob服务器,成功后会返回真实下载地址
*上传版本信息、apk下载路径到后台UpdateSet 表中
//上传.apk文件,并将文件下载地址保存到后台UpdateSet表中
private void uploadApk(){
//改成你手机存放需更新的 .apk文件地址
String apkPath = "/sdcard/zzd/app-release.apk" ;
File uploadFile = new File(apkPath);
if (uploadFile != null) {
//上传.apk文件
final BmobFile bmobFile = new BmobFile(uploadFile);
bmobFile.uploadblock(new UploadFileListener() {
@Override
public void done(BmobException e) {
if (e == null) {
Loggerr.i("文件真实下载地址是:" + bmobFile.getFileUrl());
//更新版描述
String desc = "这是 2.0版本";
int versionCode = 2;
//后台修改UpdateSet表
BmobManager.getInstance().pushVersionUpdate(bmobFile.getFileUrl(), desc, versionCode, new SaveListener<String>() {
@Override
public void done(String s, BmobException e) {
if (e == null) {
Loggerr.i("上传版本信息成功" );
}else{
Loggerr.i("上传版本信息失败" );
}
}
});
}else{
Loggerr.i("上传apk失败" );
}
}
@Override
public void onProgress(Integer value) {
// 返回的上传进度(百分比)
}
});
}
}
上传成功后,后台UpdateSet表:
2)未绑定独立域名的,手动操作后台:
1.后台新建UpdateSet表,表中字段:
desc - String类型
paht - String 类型
versionCode - Number类型
apk -file类型
2. 单击左上角添加行,单击空白行apk列,上传需更新的.apk文件 。
3. apk字段右击,将Copy link address 得到的apk真实下载地址填入到path字段。填写其它各个字段值,特别是versionCode,比上个版本多1
UpdateSet表最后的样子:
六)检测
将app下build.gradle 文件 版本属性值 改成默认值1
versionCode 1
versionName "1.0 debug版本测试"
运行,检测到版本更新:
下载后静默安装,会重启app。红色划线部分清晰显示当前版本为2,版本更新成功:
demo已上传csdn,不需要积分,等待核准。现把工程压缩,上传到Bmob,可以下载啦: