github地址
一、簡介:
在APP開發(fā)中,應用上線后公司肯定后期會對應用進行維護對一些Bug修復,這種情況就需要版本迭代了。檢測到服務器版本比本地手機版本高的時候,手機會詢問用戶是否要下載最新的app,然后下載apk下來,然后進行安裝。
備注:
也可以用第三方服務,比如騰訊的Bugly、Bmob云服務等,也挺方便的,不過apk要上傳到第三方的平臺上,如果公司要求在自己平臺上,就只能自己寫了。
二、實現(xiàn)步驟
上一張開發(fā)中的版本迭代的流程圖

具體來說大概是如下幾步:
1、每次啟動應用我們就獲取放在服務器上的版本信息,我們獲取到版本號與當前應用的版本好進行對比,這樣我們就可以知道應用是否更新了,版本信息一般包含如下內容:
{
"versionCode": "2", //版本號
"versionName": "2.0", //版本名稱
//服務器上最新版本的app的下載地址
"apkUrl": "http://oh0vbg8a6.bkt.clouddn.com/app-debug.apk",
"updateTitle": "更新提示" ,
"changeLog":"1.修復xxx Bug;2.更新了某某UI界面."
}
備注:
versionCode 2 //對用戶不可見,僅用于應用市場、程序內部識別版本,判斷新舊等用途。
versionName "2.0"http://展示給用戶,讓用戶會知道自己安裝的當前版本.
//versionCode的值,必須是int
2、獲取用戶當前使用的APP的versionCode(版本號)
/**
* 獲取當前APP版本號
* @param context
* @return
*/
public static int getPackageVersionCode(Context context){
PackageManager manager = context.getPackageManager();
PackageInfo packageInfo = null;
try {
packageInfo = manager.getPackageInfo(context.getPackageName(),0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if(packageInfo != null){
return packageInfo.versionCode;
}else{
return 1;
}
}
3、拿到本地的版本號后,與獲取到的服務器的最新的版本號做對比,如果比我們本地獲取的APP的versionCode 高,則就進行下一步
//如果當前版本小于新版本,則更新
//獲取當前app版本
int currVersionCode = AppUtils.getPackageVersionCode(MainActivity.this);
//newVersionCode自己通過網絡框架訪問服務器,解析數(shù)據(jù)得到
if(currVersionCode < newVersionCode){
Log.i("tag", "有新版本需要更新");
showHintDialog(); //彈出對話框,提示用戶更新APP
}
4、如果服務器有新的高版本,則彈出對話框提示用戶更新
//顯示詢問用戶是否更新APP的dialog
private void showHintDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.mipmap.ic_launcher)
.setMessage("檢測到當前有新版本,是否更新?")
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//取消更新,則跳轉到舊版本的APP的頁面
Toast.makeText(MainActivity.this, "暫時不更新app", Toast.LENGTH_SHORT).show();
}
})
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//6.0以下系統(tǒng),不需要請求權限,直接下載新版本的app
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
downloadApk();
} else {
//6.0以上,先檢查,申請權限,再下載
checkPermission();
}
}
}).create().show();
}
5、如果用戶選擇了更新APP,則對手機系統(tǒng)版本進行判斷<ul>
<li>6.0以下系統(tǒng),不需要請求權限,直接下載新版本的app</li>
<li>6.0以上,先檢查,申請權限,再下載</li>
</ul>
順便給出版本迭代需要的2個主要權限
<--網絡權限-->
<uses-permission android:name="android.permission.INTERNET" />
<--讀寫sdcard的權限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<--訪問網絡狀態(tài)的權限-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
6、檢查權限(6.0以上系統(tǒng))
筆者此處沒有使用原生的代碼,用的是第三方開源庫EasyPermission
https://github.com/googlesamples/easypermissions
//檢查權限
private void checkPermission() {
//app更新所需的權限
String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.INTERNET};
if (EasyPermissions.hasPermissions(this, permissions)) {
// Already have permission, do the thing
// ...
downloadApk();
} else {
// Do not have permissions, request them now(請求權限)
EasyPermissions.requestPermissions(this, "app更新需要讀寫sdcard的權限",
REQUEST_CODE_WRITE, permissions);
}
}
授權結果的回調:
//授權的結果的回調方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == REQUEST_CODE_WRITE){
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
downloadApk();
}
}
}
備注:
Manifest.permission.INTERNET完全可以不用寫,但是,我之前在比較復雜的測試中,遇到過問題,故此處就加上了。
權限申請,有時,用戶拒絕了授權,并且勾選了不再提示的選項,那么用戶會因為沒有授權而不能使用一些功能,這樣的用戶體驗是非常糟糕的,為了解決這個問題,我們可以通過彈出自定義的Dialog來讓用戶打開APP設置界面去手動開啟相應的權限,這樣才能完整的使用app,所以還需要實現(xiàn)EasyPermissions.PermissionCallbacks接口,重寫如下方法
/**
* 用戶同意授權了
*
* @param requestCode
* @param perms
*/
@Override
public void onPermissionsGranted(int requestCode, List<String> perms) {
downloadApk();
Log.i("tag","--------->同意授權");
}
/**
* 用戶拒絕了授權,則通過彈出對話框讓用戶打開app設置界面,
* 手動授權,然后返回app進行版本更新
*
* @param requestCode
* @param perms
*/
@Override
public void onPermissionsDenied(int requestCode, List<String> perms) {
Toast.makeText(this, "沒有同意授權", Toast.LENGTH_SHORT).show();
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
new AppSettingsDialog.Builder(this, "請設置權限")
.setTitle("設置對話框")
.setPositiveButton("設置")
.setNegativeButton("取消", null /* click listener */)
.setRequestCode(RC_SETTINGS_SCREEN)
.build()
.show();
}
}
···
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SETTINGS_SCREEN) {
// Do something after user returned from app settings screen, like showing a Toast.
Toast.makeText(this, "從app設置返回應用界面", Toast.LENGTH_SHORT)
.show();
downloadApk();
}
}
···
----------------------至此我們也已經把下載APK的的權限也搞定了-----------------
7、接下來只需要進行下載安裝即可,我們這時候就要判斷,是否處于WiFi狀態(tài)下,如果是WiFi情況下就直接進行更新,如果不是,再創(chuàng)建對話框,然后詢問用戶,是否確定需要通過流量來進行下載:(因為一般下載都是在后臺,所以都是放在Service中進行操作的。通過startService(new Intent(MainActivity.this, UpdateService.class));來啟動服務進行下載)
判斷是否處于WiFi狀態(tài)
/**
* 判斷是否處于WiFi狀態(tài)
* getActiveNetworkInfo 是可用的網絡,不一定是鏈接的,getNetworkInfo 是鏈接的。
*/
public static boolean isWifi(Context context) {
ConnectivityManager manager = (ConnectivityManager)context. getSystemService(CONNECTIVITY_SERVICE);
//NetworkInfo info = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
//處于WiFi連接狀態(tài)
if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return true;
}
return false;
}
新版app下載
//下載最新版的app
private void downloadApk() {
boolean isWifi = AppUtils.isWifi(this); //是否處于WiFi狀態(tài)
if (isWifi) {
startService(new Intent(MainActivity.this, UpdateService.class));
Toast.makeText(MainActivity.this, "開始下載。", Toast.LENGTH_LONG).show();
} else {
//彈出對話框,提示是否用流量下載
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示");
builder.setMessage("是否要用流量進行下載更新");
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
Toast.makeText(MainActivity.this, "取消更新。", Toast.LENGTH_LONG).show();
}
});
builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
startService(new Intent(MainActivity.this, UpdateService.class));
Toast.makeText(MainActivity.this, "開始下載。", Toast.LENGTH_LONG).show();
}
});
builder.setCancelable(false);
AlertDialog dialog = builder.create();
//設置不可取消對話框
dialog.setCancelable(false);
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
}
備注:
如果對service不是很理解的童鞋,可以看看這篇文章
<a >深入理解Service</a>
8、Service進行下載
這里是用DownloadManager進行下載的,下載完成后,點擊通知的圖標,可以自動安裝。
這里順便給出一個DownloadManager的鏈接,有需要的,可以自行閱讀
<a href="http://www.itdecent.cn/p/7ad92b3d9069 ">Android系統(tǒng)下載管理DownloadManager</a>
1)通過DownLoadManager來進行APK的下載,代碼如下:
//開始下載最新版本的apk文件
DownloadManager downloadManager = (DownloadManager)context.getSystemService(DOWNLOAD_SERVICE);
//DownloadManager實現(xiàn)下載
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(MainConstant.NEW_VERSION_APP_URL));
request.setTitle("文件下載")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,MainConstant.NEW_VERSION_APK_NAME)
//設置通知在下載中和下載完成都會顯示
//.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
//設置通知只在下載過程中顯示,下載完成后不再顯示
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
downloadManager.enqueue(request);
2)下載完畢,自動安裝的實現(xiàn)
當DownLoadManager下載完成后,會發(fā)送一個DownloadManager.ACTION_DOWNLOAD_COMPLETE的廣播,所以我們只要剛開始在啟動Service的時候,注冊一個廣播,監(jiān)聽
DownloadManager.ACTION_DOWNLOAD_COMPLETE,然后當下載完成后,在BroadcastReceiver中調用安裝APK的方法即可。
//廣播接收的注冊
public void receiverRegist() {
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//安裝apk
AppUtils.installApk(context);
stopSelf(); //停止下載的Service
}
};
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(receiver, filter); //注冊廣播
}
3)通過隱式意圖安裝apk
/**Apk的安裝
*
* @param context
*/
public static void installApk(Context context) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //這個必須有
intent.setDataAndType(
Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), MainConstant.NEW_VERSION_APK_NAME)),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
Service中的完整代碼
public class UpdateService extends Service {
public static final int NOTIFICATION_ID = 100;
private static final int REQUEST_CODE = 10; //PendingIntent中的請求碼
//下載的新版本的apk的存放路徑
public static final String destPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator + "newversion.apk";
private Context mContext = this;
private Notification mNotification;
private NotificationManager manager;
private NotificationCompat.Builder builder;
private RemoteViews remoteViews;
private BroadcastReceiver receiver;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
receiverRegist();
//下載apk文件
AppUtils.downloadApkByDownloadManager(this);
return Service.START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
//解除注冊
unregisterReceiver(receiver);
}
//廣播接收的注冊
public void receiverRegist() {
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//安裝apk
AppUtils.installApk(context);
stopSelf(); //停止下載的Service
}
};
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(receiver, filter); //注冊廣播
}
}
下面這段代碼是自己封裝的下載apk并實現(xiàn)自動安裝的功能,如有不妥之處,敬請 指出
public class UpdateService extends IntentService {
public static final int NOTIFICATION_ID = 100;
private static final int REQUEST_CODE = 10; //PendingIntent中的請求碼
public static final String destPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator + "newversion.apk";
private Context mContext = this;
private Notification mNotification;
private NotificationManager manager;
private NotificationCompat.Builder builder;
private RemoteViews remoteViews;
public UpdateService() {
super("UpdateService");
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
//開始下載最新版本的apk文件
initNotification();
download(MainConstant.NEW_VERSION_APP_URL);
}
}
private void download(String newVersionApkUrl) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
URL url = new URL(newVersionApkUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//設置連接的屬性
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
//如果響應碼為200
if (conn.getResponseCode() == 200) {
bis = new BufferedInputStream(conn.getInputStream());
bos = new BufferedOutputStream(new FileOutputStream(destPath));
int totalSize;
int count = 0; //讀取到的字節(jié)數(shù)的計數(shù)器
int progress; //當前進度
byte[] data = new byte[1024 * 1024];
int len;
//文件總的大小
totalSize = conn.getContentLength();
while ((len = bis.read(data)) != -1) {
count += len; //讀取當前總的字節(jié)數(shù)
bos.write(data, 0, len);
bos.flush();
progress = (int) ((count / (float) totalSize) * 100);
//progress = (count * 100) / totalSize; //當前下載的進度
//重新設置自定義通知的進度條的進度
remoteViews.setProgressBar(R.id.progressBar, 100, progress, false);
remoteViews.setTextViewText(R.id.tv_progress, "已經下載了:" + progress + "%");
//發(fā)送通知
manager.notify(NOTIFICATION_ID, mNotification);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//下載文件完成以后,執(zhí)行以下操作
Intent installIntent = new Intent();
/**啟動系統(tǒng)服務的Activity,用于顯示用戶的數(shù)據(jù)。
比較通用,會根據(jù)用戶的數(shù)據(jù)類型打開相應的Activity。
*/
installIntent.setAction(Intent.ACTION_VIEW);
installIntent.setDataAndType(Uri.fromFile(new File(destPath)), "application/vnd.android.package-archive");
//實例化延時的Activity
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, REQUEST_CODE, installIntent, PendingIntent.FLAG_ONE_SHOT);
builder.setContentTitle("文件下載完畢!")
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentText("已下載100%")
.setContentIntent(pendingIntent);
//點擊通知圖標,自動消失
Notification notification = builder.build();
notification.flags |= Notification.FLAG_AUTO_CANCEL;
manager.notify(NOTIFICATION_ID, notification);
}
//初始化通知
private void initNotification() {
builder = new NotificationCompat.Builder(mContext);
//自定義的Notification
remoteViews = new RemoteViews(getPackageName(), R.layout.layout_main_notification);
Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.stat_sys_download_anim0);
builder.setTicker("開始下載apk文件")
.setSmallIcon(R.drawable.stat_sys_download_anim5)
.setLargeIcon(largeIcon)
.setContent(remoteViews);
//實例化通知對象
mNotification = builder.build();
//獲取通知的管理器
manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
}