Recovery系统升级(3)---软件流程

系统升级软件流程

本章节结合源码剖析Recovery系统升级流程,流程中相关技术难点或者细节会单独文章介绍,文中相应位置会附上链接。

从APP检测到服务器推送OTA升级包到设备启动到新版本系统的整个软件流程如下图所示,
文章将围绕图中涉及到的模块详细讲解。
在这里插入图片描述

1. App下载升级包并调用RecoverySystem接口

检测是否有OTA推送并从服务器下载升级包的业务逻辑由oem厂商自行实现,下面从触发升级开始分析。
App下载完升级包后调用 framework RecoverySystem 类的 installPackage 接口传入下载好的升级包路径,app的任务到此即结束。

// android.os.RecoverySystem
RecoverySystem.installPackage(Context context, File packageFile)

2. Framework RecoverySystem 触发升级

Google AOSP RecoverySystem
函数 installPackage()

// frameworks/base/core/java/android/os/RecoverySystem.java
public static void installPackage(Context context, File packageFile, boolean processed)
        throws IOException {
   
    synchronized (sRequestLock) {
   
        /* 1. 构造固定格式的 recovery 升级指令 */
        LOG_FILE.delete();
        // Must delete the file in case it was created by system server.
        UNCRYPT_PACKAGE_FILE.delete();

        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");

		//  如果升级包存储于data分区,则需要对升级包特殊处理,原因和原理见下文介绍。
        // 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 {
   
            	// 升级包预处理是由服务 uncryptd 完成的,其输入为文件 UNCRYPT_PACKAGE_FILE,
            	// 输出为文件 BLOCK_MAP_FILE, 此处初始化这两个文件。 uncryptd 详见下文介绍。
                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();
            }
			
			// 预处理的升级包参数改为 "@+BLOCK_MAP_FILE(/cache/recovery/block.map)",
			// 为什么这么做,仅仅是约定而已,原理见下文介绍 uncryptd 。
            // 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;
        }

        /* 2. 通过 RECOVERY_SERVICE 把升级指令写入到 BCB(也就是misc分区头部)*/
        RecoverySystem rs = (RecoverySystem) context.getSystemService(
                Context.RECOVERY_SERVICE);
        if (!rs.setupBcb(command)) {
   
            throw new IOException("Setup BCB failed");
        }

        /* 3. 通过 POWER_SERVICE 触发重启 */
        // 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?)");
    }
}

分析 installPackage 一共干了以下3件事:

2.1 构造 Recovery 升级指令

通过升级包存储位置的绝对路径判断存储设备是 data 分区还是其他存储介质(U盘、TF卡等)决定是否对升级包预处理 。
如果需要预处理,则把参数 update_package 的值改为固定值 “@/cache/recovery/block.map”,同时把参数 update_package、locale、security 等格式化成固定格式的升级指令字符串。

1)uncryptd 为什么预处理升级包?
Android data 分区的数据会被加密(FDE/FBE),AOSP recovery 没有实现分区解密功能,因此 recovery 无法访问data分区的数据,也就无法从 data 分区文件系统直接 load 升级包。(recovery模式下:FDE加密的设备无法挂载data分区,FBE加密的设备看到的data分区文件内容是乱码)。

  • 所以在进入recovery 前先把升级包数据解密,解密后会把升级包在存储介质中的存储信息写入固定文件 /cache/recovery/block.map 中,进入recovery后,从 block.map 文件中解析出升级包位置信息即可 load 升级包数据。
  • 当然如果升级包保存在未加密的TF卡、U盘中,那么无需多升级包做额外处理,recovery可以从存储器文件系统直接 load 数据)

2)uncryptd 如何预处理升级包?
输入: UNCRYPT_PACKAGE_FILE 输入参数。
输出:处理结果写入到文件BLOCK_MAP_FILE(/cache/recovery/block.map)。
当然此处只是准备好输入输出文件,预处理操作是在下文第 2.3 步重启设备时执行的,见下文介绍。
UNCRYPT_PACKAGE_FILE :升级包的实际在文件系统的路径;
BLOCK_MAP_FILE :升级包数据在存储器中块分布信息(主要就是块号);

3)升级指令字符串中各个参数的格式为 –key=value 或者 –key,同时以换行符分隔。

4)函数参数 processed 的作用
uncryptd 在关机时对升级包做预处理解密,当升级包的 size 比较大时会造成关机耗时,因此可以事先预处理好升级包,再调用 installPackage 时processed 置为 true,那么在关机时就不会启动 uncryptd,从而不影响关机速度。

2.2 通过 RECOVERY_SERVICE 把升级指令写入到BCB

把格式化后的指令字符串写入 misc 分区头部的 BCB (bootloader control block)区域。

RecoverySystem

// frameworks/base/core/java/android/os/RecoverySystem.java
// 1. RecoverySystem 调用 setupBcb
public static void installPackage(Context context, File packageFile, boolean processed) {
   
	...
	RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
	rs.setupBcb(command)
	...
}

// 2. 调 RecoverySystemService 的 setupBcb 接口
private boolean setupBcb(String command) {
   
	return mService.setupBcb(command);
}

RecoverySystemService

// frameworks/base/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
// 1. 调用 setupOrClearBcb 把升级指令字符串 command 写入 BCB
public boolean setupBcb(String command) {
   
    if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
    return setupOrClearBcb(true, command);
}

// 2. setupOrClearBcb 实际上是启动 native 服务 setup-bcb 并通过它把指令字符串写入 BCB
private boolean setupOrClearBcb(boolean isSetup, String command) {
   
	// 2.1 检查 uncrypt/setup-bcb/clear-bcb 服务是否正在运行,
	// 如果处于runing状态则说明在这之前已经触发工作了,中止本次操作。
    final boolean available = checkAndWaitForUncryptService();
    if (!available) {
   
        Slog.e(TAG, "uncrypt service is unavailable.");
        return false;
    }
	
	// 2.2 通过 isSetup 判断往 BCB 写入还是擦除参数,启动不同的服务
	// (本质上 uncrypt/setup-bcb/clear-bcb 都是同一个binary,
	// 只是传入不同参数执行不同任务而已,详见下文讲解)
    if (isSetup) {
   
        mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
    } else {
   
        mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
    }
	
	// 2.3 启动 setup-bcb 或者 clear-bcb 服务后通过socket 与其通信
    // Connect to the uncrypt service socket.
    UncryptSocket socket = mInjector.connectService();
    if (socket == null) {
   
        Slog.e(TAG, "Failed to connect to uncrypt socket");
        return false;
    }

    try {
   
    	// 如果是写 BCB 参数则把升级指令通过 socket 传输给服务 setup-bcb
        // Send the BCB commands if it's to setup BCB.
        if (isSetup) {
   
            socket.sendCommand(command);
        }

		// 从 socket 读取 setup-bcb/clear-bcb 执行的结果
        // Read the status from the socket.
        int status = socket.getPercentageUncrypted();

        // Ack receipt of the status code. uncrypt waits for the ack so
        // the socket won't be destroyed before we receive the code.
        socket.sendAck();
		
		// setup-bcb/clear-bcb 定义好的成功返回值,执行成功返回100
		// (100仅仅是 setup-bcb 服务定义的正确返回值,无计量等特殊含义)
        if (status == 100) {
   
            Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
                    + " bcb successfully finished.");
        } else {
   
            // Error in /system/bin/uncrypt.
            Slog.e(TAG, "uncrypt failed with status: " + status);
        }
    }
}

此处不展开介绍 native 服务 setup-bcb/clear-bcb 如何写入和擦除BCB数据,详见文章:(待续)

2.3 通过 POWER_SERVICE 触发重启设备

该步骤的重点是启动 uncryptd 预处理升级包。

RecoverySystem:

// frameworks/base/core/java/android/os/RecoverySystem.java
public static void installPackage(Context context, File packageFile, boolean processed) {
   
	...
	PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
	String reason = PowerManager.REBOOT_RECOVERY_UPDATE;
	pm.reboot(reason);
}

PowerManager:

// frameworks/base/core/java/android/os/PowerManager.java
public void reboot(@Nullable String reason) {
   
	mService.reboot(false, reason, true);
 }

PowerManagerService:

// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
public void reboot(boolean confirm, @Nullable String reason, boolean wait) {
   
	shutdownOrRebootInternal(HALT_MODE_REBOOT, confirm, reason, wait);
}

private void shutdownOrRebootInternal(final @HaltMode int haltMode, final boolean confirm,
        @Nullable final String reason, boolean wait) {
   
	...
	// 启动关机线程 ShutdownThread
    Runnable runnable = new Runnable() {
   
        @Override
        public void run() {
   
	        if (haltMode == HALT_MODE_REBOOT) {
   
	            ShutdownThread.reboot(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);

    // PowerManager.reboot() is documented not to return so just wait for the inevitable.
    if (wait) {
   
	     while (true) {
   
         	runnable.wait();
	     }
    }
}

ShutdownThread:

// frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java

public final class ShutdownThread extends Thread {
   

	ShutdownThread sInstance = new ShutdownThread()
	// 1. 重启机器
	public static void reboot(final Context context, String reason, boolean confirm) {
   
	    mReboot = true;
	    mRebootSafeMode = false;
	    mRebootHasProgressBar = false;
	    mReason = reason;
	    shutdownInner(context, confirm);
	}

	// 2. 弹出关机进度条弹窗(uncryptd 处理升级包比较耗时)
   private static void shutdownInner(final Context context, boolean confirm) {
   
		beginShutdownSequence(context) {
   
		    sInstance.mProgressDialog = showShutdownDialog(context);
			sInstance.start()
		}
	}
	
	// 3. 如上文所述,UNCRYPT_PACKAGE_FILE 存在以及 BLOCK_MAP_FILE 不存在 则说明需要uncryptd 
	// 预处理升级包,此时标记本次重启需要给用户进度条弹窗,同时该标记 mRebootHasProgressBar 在
	// 下文也会作为是否启动 uncryptd 的标志。
	private static ProgressDialog showShutdownDialog(Context context) {
   
		// mReason could be "recovery-update" or "recovery-update,quiescent".
		if (mReason != null && mReason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) {
   
		   // We need the progress bar if uncrypt will be invoked during the
		   // reboot, which might be time-consuming.
		   mRebootHasProgressBar = RecoverySystem.UNCRYPT_PACKAGE_FILE.exists()
		           && !(RecoverySystem.BLOCK_MAP_FILE.exists());
		}
		...
	}

	// 4. ShutdownThread 线程的任务实现
    /**
     * Makes sure we handle the shutdown gracefully.
     * Shuts off power regardless of radio state if the allotted time has passed.
     */
    public void run() {
   
    	// 记录本次重启原因
		{
   
			 String reason = (mReboot ? "1" : "0") + (mReason != null ? mReason : "");
			 SystemProperties.set(SHUTDOWN_ACTION_PROPERTY, reason);
		}
		// 此处进入 uncryptd 开始预处理升级包
        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();
        }
        // 最后关机 or 重启
        rebootOrShutdown(mContext, mReboot, mReason);
    }
    
    // 5. 
	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;
	    } 
	    ...
	}
}

函数 uncrypt()
通过 RecoverySystem 启动 uncryptd 预处理升级包,同时监听处理进度,更新弹窗显示的进度条。

// frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java
private 
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值