Android系統升級流程---上篇

前言

大部分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系統實戰開發】,關注有驚喜哦!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章