前言
大部分Android設備出廠時的軟件大都是帶着bug風險(低風險)出貨的,後期再通過OTA的方式去升級修訂補丁。在滿足主要功能正常使用的情況下產品搶先出貨,其他小功能再通過迭代更新。這個功能的重要性不言而喻。今天就來看看Android系統的升級流程。
概述
一般Android升級流程是,由軟件發放端推送軟件到服務器,然後由服務器向Android設備推送升級包。在Android設備中,一般會有一個系統服務用於檢測是否有版本更新,如果有更新包,則下載下來,下載完成並校驗成功後,通過調用系統的接口進入升級流程。本文就是從調用這個接口開始的。
我們先來看看升級服務下載好安裝包後,調用了什麼接口進行升級:
RecoverySystem.installPackage(context, file);
installPackage的實現如下:
/frameworks/base/core/java/android/os/RecoverySystem.java
@SystemApi
@RequiresPermission(android.Manifest.permission.RECOVERY)
public static void installPackage(Context context, File packageFile, boolean processed)
throws IOException {
synchronized (sRequestLock) {
LOG_FILE.delete();
// Must delete the file in case it was created by system server.
UNCRYPT_PACKAGE_FILE.delete(); //先刪除"/cache/recovery/uncrypt_file"
String filename = packageFile.getCanonicalPath(); //獲取絕對路徑
Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
// If the package name ends with "_s.zip", it's a security update.
boolean securityUpdate = filename.endsWith("_s.zip"); //必須以zip結尾
// If the package is on the /data partition, the package needs to
// be processed (i.e. uncrypt'd). The caller specifies if that has
// been done in 'processed' parameter.
if (filename.startsWith("/data/")) {
if (processed) {
if (!BLOCK_MAP_FILE.exists()) {
Log.e(TAG, "Package claimed to have been processed but failed to find "
+ "the block map file.");
throw new IOException("Failed to find block map file");
}
} else {
FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
try {
uncryptFile.write(filename + "\n");
} finally {
uncryptFile.close();
}
// UNCRYPT_PACKAGE_FILE needs to be readable and writable
// by system server.
if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
|| !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
}
BLOCK_MAP_FILE.delete();
}
// If the package is on the /data partition, use the block map
// file as the package name instead.
filename = "@/cache/recovery/block.map";
}
final String filenameArg = "--update_package=" + filename + "\n";
final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
final String securityArg = "--security\n";
String command = filenameArg + localeArg;
if (securityUpdate) {
command += securityArg;
}
RecoverySystem rs = (RecoverySystem) context.getSystemService(
Context.RECOVERY_SERVICE);
if (!rs.setupBcb(command)) {
throw new IOException("Setup BCB failed");
}
// Having set up the BCB (bootloader control block), go ahead and reboot
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
String reason = PowerManager.REBOOT_RECOVERY_UPDATE;
// On TV, reboot quiescently if the screen is off
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wm.getDefaultDisplay().getState() != Display.STATE_ON) {
reason += ",quiescent";
}
}
pm.reboot(reason);
throw new IOException("Reboot failed (no permissions?)");
}
}
如上方法中主要做的事如下:
1.判斷升級包是否在data分區,如果在data分區,因爲data分區加密過了,recovery並不能直接從data分區中獲取升級包,需要進行解析,具體解析放到了processPackage方法中,下面會說到;
2.調用setupBcb,將升級包的路徑以及升級指令寫入到BCB中,系統重啓的時候,就會去這個地方檢測是否有recovery標誌,如有,則進入recovery模式,並觸發升級,但此時如果沒找到升級包,則會出現error,且系統馬上重啓;
3.調用pm服務,進行reboot;
該方法向系統提出了升級的需求,並告知了升級包的路徑,接下來就是進入到了PowerManagerService的reboot流程,注意這裏調用到的reason,它的值是REBOOT_RECOVERY_UPDATE,我們接下來分析的時候會用到。先進入PowerManager:
/frameworks/base/core/java/android/os/PowerManager.java
* @param confirm If true, shows a reboot confirmation dialog.
* @param reason The reason for the reboot, or null if none.
* @param wait If true, this call waits for the reboot to complete and does not return.
public void reboot(String reason) {
try {
mService.reboot(false, reason, true);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
這裏的代碼很簡單,直接調用電源管理服務的reboot接口進入到下一個流程,需要注意的是此時傳入的參數,第一個參數爲false,表示不需要彈窗確認,第三個參數爲true表示一直等待直到關機流程完成。
mService是一個PowerManagerService,方法實現如下:
/frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
if (PowerManager.REBOOT_RECOVERY.equals(reason)
|| PowerManager.REBOOT_RECOVERY_UPDATE.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
final long ident = Binder.clearCallingIdentity();
try {
shutdownOrRebootInternal(HALT_MODE_REBOOT, confirm, reason, wait);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
這裏針對進入Recovery模式進行了權限檢測,調用者需具備permission.RECOVERY權限。之後就進入到了shutdownOrRebootInternal方法中:
private void shutdownOrRebootInternal(final @HaltMode int haltMode, final boolean confirm,
final String reason, boolean wait) {
....
Runnable runnable = new Runnable() {
@Override
public void run() {
synchronized (this) {
if (haltMode == HALT_MODE_REBOOT_SAFE_MODE) {
ShutdownThread.rebootSafeMode(getUiContext(), confirm);
} else if (haltMode == HALT_MODE_REBOOT) {
ShutdownThread.reboot(getUiContext(), reason, confirm);
} else {
ShutdownThread.shutdown(getUiContext(), reason, confirm);
}
}
}
};
// ShutdownThread must run on a looper capable of displaying the UI.
Message msg = Message.obtain(UiThread.getHandler(), runnable);
msg.setAsynchronous(true);
UiThread.getHandler().sendMessage(msg);
....
}
這裏創建了一個線程,調用ShutdownThread類的reboot方法繼續跑重啓流程。對於這裏創建新線程的目的,ShutdownThread必須在能夠顯示UI的循環程序上運行。在這裏面,需要彈窗顯示升級流程或者關機流程等。我們繼續往下看:
/frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java
public static void reboot(final Context context, String reason, boolean confirm) {
mReboot = true;
mRebootSafeMode = false;
mRebootHasProgressBar = false;
mReason = reason;
shutdownInner(context, confirm);
}
這裏代碼簡單,不做解釋,繼續往下看:
private static void shutdownInner(final Context context, boolean confirm) {
// ShutdownThread is called from many places, so best to verify here that the context passed
// in is themed.
context.assertRuntimeOverlayThemable();
// ensure that only one thread is trying to power down.
// any additional calls are just returned
synchronized (sIsStartedGuard) {
if (sIsStarted) {
Log.d(TAG, "Request to shutdown already running, returning.");
return;
}
}
final int longPressBehavior = context.getResources().getInteger(
com.android.internal.R.integer.config_longPressOnPowerBehavior);
final int resourceId = mRebootSafeMode
? com.android.internal.R.string.reboot_safemode_confirm
: (longPressBehavior == 2
? com.android.internal.R.string.shutdown_confirm_question
: com.android.internal.R.string.shutdown_confirm);
Log.d(TAG, "Notifying thread to start shutdown longPressBehavior=" + longPressBehavior);
if (confirm) {
final CloseDialogReceiver closer = new CloseDialogReceiver(context);
if (sConfirmDialog != null) {
sConfirmDialog.dismiss();
}
sConfirmDialog = new AlertDialog.Builder(context)
.setTitle(mRebootSafeMode
? com.android.internal.R.string.reboot_safemode_title
: com.android.internal.R.string.power_off)
.setMessage(resourceId)
.setPositiveButton(com.android.internal.R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
beginShutdownSequence(context);
}
})
.setNegativeButton(com.android.internal.R.string.no, null)
.create();
closer.dialog = sConfirmDialog;
sConfirmDialog.setOnDismissListener(closer);
sConfirmDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
sConfirmDialog.show();
} else {
beginShutdownSequence(context);
}
}
這裏主要做了如下事情:
1.確認上下文環境是否符合要求,如果調用到了系統資源則會拋出異常;
2.判斷是否有多方調用關機流程,如果有則直接返回;
3.獲取用戶關機 Behavior。
4.如果需要顯示關機彈窗,則創建各類資源進行彈窗,然後根據用戶選擇進入不同的流程;
5.如果不需要關機彈窗,則直接進入關機流程;
是否需要彈窗,則由所傳入的參數confirm所決定,confirm在PowerManager中傳入,傳入的值爲false,所以這裏我們就直接進入關機流程,不創建確認彈窗:
private static void beginShutdownSequence(Context context) {
synchronized (sIsStartedGuard) {
if (sIsStarted) {
Log.d(TAG, "Shutdown sequence already running, returning.");
return;
}
sIsStarted = true;
}
sInstance.mProgressDialog = showShutdownDialog(context);
sInstance.mContext = context;
sInstance.mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
// make sure we never fall asleep again
sInstance.mCpuWakeLock = null;
try {
sInstance.mCpuWakeLock = sInstance.mPowerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG + "-cpu");
sInstance.mCpuWakeLock.setReferenceCounted(false);
sInstance.mCpuWakeLock.acquire();
} catch (SecurityException e) {
Log.w(TAG, "No permission to acquire wake lock", e);
sInstance.mCpuWakeLock = null;
}
// also make sure the screen stays on for better user experience
sInstance.mScreenWakeLock = null;
if (sInstance.mPowerManager.isScreenOn()) {
try {
sInstance.mScreenWakeLock = sInstance.mPowerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK, TAG + "-screen");
sInstance.mScreenWakeLock.setReferenceCounted(false);
sInstance.mScreenWakeLock.acquire();
} catch (SecurityException e) {
Log.w(TAG, "No permission to acquire wake lock", e);
sInstance.mScreenWakeLock = null;
}
}
if (SecurityLog.isLoggingEnabled()) {
SecurityLog.writeEvent(SecurityLog.TAG_OS_SHUTDOWN);
}
// start the thread that initiates shutdown
sInstance.mHandler = new Handler() {
};
sInstance.start();
}
beginShutdownSequence方法中,先是判斷是否已經開始關機,如果是,則直接返回,避免多個關機case。然後拿鎖,避免系統休眠,並且保持屏幕常亮。最後調用start開啓關機線程:
sInstance.start();
ShutdownThread本身就是繼承Thread的線程,我們看看它的run實現:
public void run() {
....
if (mRebootHasProgressBar) {
sInstance.setRebootProgress(MOUNT_SERVICE_STOP_PERCENT, null);
// If it's to reboot to install an update and uncrypt hasn't been
// done yet, trigger it now.
uncrypt();
}
shutdownTimingLog.traceEnd(); // SystemServerShutdown
metricEnded(METRIC_SYSTEM_SERVER);
saveMetrics(mReboot, mReason);
// Remaining work will be done by init, including vold shutdown
rebootOrShutdown(mContext, mReboot, mReason);
}
前面代碼量太多,不詳細列出,簡單介紹下這個線程做的事情:
1.發送關機廣播;
2.調用ActivityManager的shutdown方法,關閉activity管理器;
3.調用PackageManagerService的shutdown方法,關閉包管理器;
4.關閉radios;
5.如果是升級而觸發的重啓,則通過調用uncrypt方法解析升級包;
6.調用rebootOrShutdown方法繼續關機流程;
接下來的rebootOrShutdown實現如下:
public static void rebootOrShutdown(final Context context, boolean reboot, String reason) {
if (reboot) {
Log.i(TAG, "Rebooting, reason: " + reason);
PowerManagerService.lowLevelReboot(reason);
Log.e(TAG, "Reboot failed, will attempt shutdown instead");
reason = null;
} else if (SHUTDOWN_VIBRATE_MS > 0 && context != null) {
...
}
...
}
我們這裏跑的分支是reboot,調用PowerManagerService的lowLevelReboot方法:
public static void lowLevelReboot(String reason) {
...
if (reason.equals(PowerManager.REBOOT_QUIESCENT)) {
sQuiescent = true;
reason = "";
} else if (reason.endsWith("," + PowerManager.REBOOT_QUIESCENT)) {
sQuiescent = true;
reason = reason.substring(0,
reason.length() - PowerManager.REBOOT_QUIESCENT.length() - 1);
}
if (reason.equals(PowerManager.REBOOT_RECOVERY)
|| reason.equals(PowerManager.REBOOT_RECOVERY_UPDATE)) {
reason = "recovery";
}
if (sQuiescent) {
// Pass the optional "quiescent" argument to the bootloader to let it know
// that it should not turn the screen/lights on.
reason = reason + ",quiescent";
}
SystemProperties.set("sys.powerctl", "reboot," + reason);
try {
Thread.sleep(20 * 1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Slog.wtf(TAG, "Unexpected return from lowLevelReboot!");
}
這裏想列出這個方法,主要是看到了一個靜默重啓的配置,想拿出來講講,之前以爲Android沒做這個,還曾經自己實現了。沒想到Android本身已經有實現。主要是通過reason值傳給bootloader,bootloader再根據該值來決定要不要亮屏。如果reason是recovery相關,則賦值reason爲【reason】,然後通過屬性服務啓動reboot。
結語
本文大概分析了從升級服務調用了installPackage方法後發生的一系列的事。閱讀源碼果然是一條讓人瞭解Android的最佳途徑,在本次的源碼閱讀中,有一個意外收貨就是靜默重啓,記得Android 5.0 沒有這個功能的。後面再遇到類似的靜默重啓需求就可以直接用上了。
再者,本來此次是要寫關於Android升級流程的文章,沒想到寫着寫着就變成了Android的關機流程了。這裏就當做Android升級流程的上篇吧,下篇再寫關於Android重啓進入recovery時所發生的事。
整個流程分析下來,竟不知如何分小標題,感覺在哪裏拆分都會破壞流程的流暢性。這裏就不加小標題了。
最後
我在微信公衆號也有寫文章,更新比較及時,有興趣者可以掃描如下二維碼,或者微信搜索【Android系統實戰開發】,關注有驚喜哦!