Android的系统升级,Android的线刷,卡刷,格式化究竟有什么奇妙的地方呢?它又是怎么实现的呢?今天我将为大家揭开它的面纱!我们以Tiny4412的Recovery源代码为例,虽然4412并没有支持Recovery系统,但弄明白它的原理,我们也可以开发或者移植一个出来。其实,在recovery.cpp中开头就已经做了详细的说明,我们来看看。
1/* 2 * The recovery tool communicates with the main system through /cache files. 3 * /cache/recovery/command - INPUT - command line for tool, one arg per line 4 * /cache/recovery/log - OUTPUT - combined log file from recovery run(s) 5 * /cache/recovery/intent - OUTPUT - intent that was passed in 6 * 7 * The arguments which may be supplied in the recovery.command file: 8 * --send_intent=anystring - write the text out to recovery.intent 9 * --update_package=path - verify install an OTA package file 10 * --wipe_data - erase user data (and cache), then reboot 11 * --wipe_cache - wipe cache (but not user data), then reboot 12 * --set_encrypted_filesystem=on|off - enables / diasables encrypted fs 13 * --just_exit - do nothing; exit and reboot 14 * 15 * After completing, we remove /cache/recovery/command and reboot. 16 * Arguments may also be supplied in the bootloader control block (BCB). 17 * These important scenarios must be safely restartable at any point: 18 * 19 * FACTORY RESET 20 * 1. user selects "factory reset" 21 * 2. main system writes "--wipe_data" to /cache/recovery/command 22 * 3. main system reboots into recovery 23 * 4. get_args() writes BCB with "boot-recovery" and "--wipe_data" 24 * -- after this, rebooting will restart the erase -- 25 * 5. erase_volume() reformats /data 26 * 6. erase_volume() reformats /cache 27 * 7. finish_recovery() erases BCB 28 * -- after this, rebooting will restart the main system -- 29 * 8. main() calls reboot() to boot main system 30 * 31 * OTA INSTALL 32 * 1. main system downloads OTA package to /cache/some-filename.zip 33 * 2. main system writes "--update_package=/cache/some-filename.zip" 34 * 3. main system reboots into recovery 35 * 4. get_args() writes BCB with "boot-recovery" and "--update_package=..." 36 * -- after this, rebooting will attempt to reinstall the update -- 37 * 5. install_package() attempts to install the update 38 * NOTE: the package install must itself be restartable from any point 39 * 6. finish_recovery() erases BCB 40 * -- after this, rebooting will (try to) restart the main system -- 41 * 7. ** if install failed ** 42 * 7a. prompt_and_wait() shows an error icon and waits for the user 43 * 7b; the user reboots (pulling the battery, etc) into the main system 44 * 8. main() calls maybe_install_firmware_update() 45 * ** if the update contained radio/hboot firmware **: 46 * 8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache" 47 * -- after this, rebooting will reformat cache & restart main system -- 48 * 8b. m_i_f_u() writes firmware image into raw cache partition 49 * 8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache" 50 * -- after this, rebooting will attempt to reinstall firmware -- 51 * 8d. bootloader tries to flash firmware 52 * 8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache") 53 * -- after this, rebooting will reformat cache & restart main system -- 54 * 8f. erase_volume() reformats /cache 55 * 8g. finish_recovery() erases BCB 56 * -- after this, rebooting will (try to) restart the main system -- 57 * 9. main() calls reboot() to boot main system 58 */
在这段英文注释里,详细的说明了factory_reset(Android的恢复出厂设置功能)的流程以及OTA系统更新的流程。在这段注释得最前面说得很明白,我们只要往/cache/recovery/command中写入相应的命令即可完成对应的功能。
1* The arguments which may be supplied in the recovery.command file: 2 * --send_intent=anystring - write the text out to recovery.intent 3 * --update_package=path - verify install an OTA package file 4 * --wipe_data - erase user data (and cache), then reboot 5 * --wipe_cache - wipe cache (but not user data), then reboot 6 * --set_encrypted_filesystem=on|off - enables / diasables encrypted fs 7 * --just_exit - do nothing; exit and reboot
比如写入:
--update_package=path(对应的OTA更新的路径)
例如:
--update_package=/mnt/external_sd/xxx.zip
将这条命令写入后,再重启Android系统,recovery检测到有这个命令存在,就会去搜索这个路径,然后将这个路径做路径转换,接下来获取转换后的路径后,就挂载这个路径,然后挂载这个路径,获取OTA包,解包,校验,然后最后实现真正的更新。
如果我们往这个文件写入: --wipe_data那么就会做出厂设置,格式化/data分区的内容。接下来,我们来看看代码,从main函数开始分析:进入main函数后,会将recovery产生的log信息重定向到/tmp/recovery.log这个文件里,具体代码实现如下:
1//重定向标准输出和标准出错到/tmp/recovery.log 这个文件里 2//static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log"; 3redirect_stdio(TEMPORARY_LOG_FILE);
redirect_stdio函数源代码实现:
1static void redirect_stdio(const char* filename) { 2 // If these fail, there's not really anywhere to complain... 3 freopen(filename, "a", stdout); setbuf(stdout, NULL); 4 freopen(filename, "a", stderr); setbuf(stderr, NULL); 5}
我们看到,所有产生来自stdout和stderr的信息会使用freopen这个函数重定向到/tmp/recovery.log这个文件里。stdout就是标准输出,stdout就是标准出错。标准输出就是我们平时使用的printf输出的信息。
当然也可以使用fprintf(stdout,"hello world\n");也是一样的标准出错就是fprintf(stderr,"hello world!\n");类似的代码。接下下来,将会判断是否使用adb的sideload来传入,通过参数--adbd来判断:
1 // If this binary is started with the single argument "--adbd", 2 // instead of being the normal recovery binary, it turns into kind 3 // of a stripped-down version of adbd that only supports the 4 // 'sideload' command. Note this must be a real argument, not 5 // anything in the command file or bootloader control block; the 6 // only way recovery should be run with this argument is when it 7 // starts a copy of itself from the apply_from_adb() function. 8 if (argc == 2 && strcmp(argv[1], "--adbd") == 0) { 9 adb_main(); 10 return 0; 11 }
做完这些步骤以后,会初始化并装载recovery的分区表recovery.fstab,然后挂载/cache/recovery/last_log这个文件,用来输出log。
1 printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start)); 2 //装载recovery的分区表recovery.fstab 3 load_volume_table(); 4 //在recovery中挂载/cache/recovery/last_log这个文件 5 //#define LAST_LOG_FILE "/cache/recovery/last_log" 6 ensure_path_mounted(LAST_LOG_FILE); 7 rotate_last_logs(KEEP_LOG_COUNT);
这里主要看如何装载分区表的流程,先来看看recovery.fstab
1/dev/block/by-name/boot /boot emmc defaults defaults 2/dev/block/by-name/recovery /recovery emmc defaults defaults 3/dev/block/by-name/splashscreen /splashscreen emmc defaults defaults 4/dev/block/by-name/fastboot /fastboot emmc defaults defaults 5/dev/block/by-name/misc /misc emmc defaults defaults 6/dev/block/by-name/system /system ext4 ro,noatime wait 7/dev/block/by-name/cache /cache ext4 nosuid,nodev,noatime,barrier=1,data=ordered wait,check 8/dev/block/by-name/userdata /data ext4 nosuid,nodev,noatime,discard,barrier=1,data=ordered,noauto_da_alloc wait,check 9/dev/block/by-name/factory /factory ext4 nosuid,nodev,noatime,barrier=1,data=ordered wait
1void load_volume_table() 2{ 3 int i; 4 int ret; 5 //读recovery.fstab 这个分区表 6 fstab = fs_mgr_read_fstab("/etc/recovery.fstab"); 7 if (!fstab) { 8 LOGE("failed to read /etc/recovery.fstab\n"); 9 return; 10 } 11 //将对应的信息加入到一条链表中 12 ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk"); 13 //如果load到的分区表为空,后面做释放操作 14 if (ret < 0 ) { 15 LOGE("failed to add /tmp entry to fstab\n"); 16 fs_mgr_free_fstab(fstab); 17 fstab = NULL; 18 return; 19 } 20 21 printf("recovery filesystem table\n"); 22 printf("=========================\n"); 23 //到这一步,打印分区表信息,这类信息在 24 //recovery启动的时候的log可以看到 25 //分别是以下 26 //编号| 挂载节点| 文件系统类型| 块设备| 长度 27 for (i = 0; i < fstab->num_entries; ++i) { 28 Volume* v = &fstab->recs[i]; 29 printf(" %d %s %s %s %lld\n", i, v->mount_point, v->fs_type, 30 v->blk_device, v->length); 31 } 32 printf("\n"); 33}
挂载完相应的分区以后,就需要获取命令参数,因为只有挂载了对应的分区,才能访问到前面要写入command的这个文件,这样我们才能正确的打开文件,如果分区都没找到,那么当然就找不到分区上的文件,上面这个步骤是至关重要的。
1//获取参数 2 //这个参数也可能是从/cache/recovery/command文件中得到相应的命令 3 //也就是可以往command这个文件写入对应的格式的命令即可 4 get_args(&argc, &argv); 5 6 const char *send_intent = NULL; 7 const char *update_package = NULL; 8 int wipe_data = 0, wipe_cache = 0, show_text = 0; 9 bool just_exit = false; 10 bool shutdown_after = false; 11 12 int arg; 13 //参数有擦除分区,OTA更新等 14 while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) { 15 switch (arg) { 16 case 's': send_intent = optarg; break; 17 case 'u': update_package = optarg; break; 18 case 'w': wipe_data = wipe_cache = 1; break; 19 case 'c': wipe_cache = 1; break; 20 case 't': show_text = 1; break; 21 case 'x': just_exit = true; break; 22 case 'l': locale = optarg; break; 23 case 'g': { 24 if (stage == NULL || *stage == '\0') { 25 char buffer[20] = "1/"; 26 strncat(buffer, optarg, sizeof(buffer)-3); 27 stage = strdup(buffer); 28 } 29 break; 30 } 31 case 'p': shutdown_after = true; break; 32 case 'r': reason = optarg; break; 33 case '?': 34 LOGE("Invalid command argument\n"); 35 continue; 36 } 37 }
获取到对应的命令,就会执行对应的标志,后面会根据标志来执行对应的操作。
做完以上的流程后,下面就是创建设备,设置语言信息,初始化recovery的UI界面,设置Selinux权限,代码如下:
1//设置语言 2 if (locale == NULL) { 3 load_locale_from_cache(); 4 } 5 printf("locale is [%s]\n", locale); 6 printf("stage is [%s]\n", stage); 7 printf("reason is [%s]\n", reason); 8 //创建设备 9 Device* device = make_device(); 10 //获取UI 11 ui = device->GetUI(); 12 //设置当前的UI 13 gCurrentUI = ui; 14 //设置UI的语言信息 15 ui->SetLocale(locale); 16 //UI初始化 17 ui->Init(); 18 19 int st_cur, st_max; 20 if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) { 21 ui->SetStage(st_cur, st_max); 22 } 23 //设置recovery的背景图 24 ui->SetBackground(RecoveryUI::NONE); 25 //设置界面上是否能够显示字符,使能ui->print函数开关 26 if (show_text) ui->ShowText(true); 27 //设置selinux权限,一般我会把selinux 给disabled 28 struct selinux_opt seopts[] = { 29 { SELABEL_OPT_PATH, "/file_contexts" } 30 }; 31 32 sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1); 33 34 if (!sehandle) { 35 ui->Print("Warning: No file_contexts\n"); 36 } 37 //虚函数,没有做什么流程 38 device->StartRecovery(); 39 40 printf("Command:"); 41 for (arg = 0; arg < argc; arg++) { 42 printf(" \"%s\"", argv[arg]); 43 } 44 printf("\n");
接下来是重要的环节,这个环节将会根据上面命令参数来做真正的事情了,比如恢复出厂设置,OTA更新等。
1//如果update_package(也就是要升级的OTA包)不为空的情况下 2 //这里要对升级包的路径做一下路径转换,这里可以自由定制自己升级包的路径 3 if (update_package) { 4 // For backwards compatibility on the cache partition only, if 5 // we're given an old 'root' path "CACHE:foo", change it to 6 // "/cache/foo". 7 8 //这里就是做转换的方法 9 //先比较传进来的recovery参数的前6个byte是否是CACHE 10 //如果是将其路径转化为/cache/CACHE: ...... 11 if (strncmp(update_package, "CACHE:", 6) == 0) { 12 int len = strlen(update_package) + 10; 13 char* modified_path = (char*)malloc(len); 14 strlcpy(modified_path, "/cache/", len); 15 strlcat(modified_path, update_package+6, len); 16 printf("(replacing path \"%s\" with \"%s\")\n", 17 update_package, modified_path); 18 //这个update_package就是转换后的路径 19 update_package = modified_path; 20 } 21 } 22 printf("\n"); 23 property_list(print_property, NULL); 24 //获取属性,这里应该是从一个文件中找到ro.build.display.id 25 //获取recovery的版本信息 26 property_get("ro.build.display.id", recovery_version, ""); 27 printf("\n"); 28 29 //定义一个安装成功的标志位INSTALL_SUCCESS ----> 其实是个枚举,值为0 30 int status = INSTALL_SUCCESS; 31 //判断转换后的OTA升级包的路径是否不为空,如果不为空 32 //执行install_package 函数进行升级 33 if (update_package != NULL) { 34 status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true); 35 //判断是否升级成功 36 if (status == INSTALL_SUCCESS && wipe_cache) { 37 //擦除这个路径,相当于删除了这个路径下的OTA升级包 38 if (erase_volume("/cache")) { 39 LOGE("Cache wipe (requested by package) failed."); 40 } 41 } 42 //如果安装不成功 43 if (status != INSTALL_SUCCESS) { 44 ui->Print("Installation aborted.\n"); 45 46 // If this is an eng or userdebug build, then automatically 47 // turn the text display on if the script fails so the error 48 // message is visible. 49 char buffer[PROPERTY_VALUE_MAX+1]; 50 property_get("ro.build.fingerprint", buffer, ""); 51 if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) { 52 ui->ShowText(true); 53 } 54 } 55 } 56 //如果跑的是格式化数据区,那么就走这个流程 57 else if (wipe_data) { 58 if (device->WipeData()) status = INSTALL_ERROR; 59 //格式化/data分区 60 if (erase_volume("/data")) status = INSTALL_ERROR; 61 if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR; 62 if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR; 63 if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n"); 64 } 65 //格式化cache分区 66 else if (wipe_cache) { 67 if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR; 68 if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n"); 69 } 70 else if (!just_exit) { 71 status = INSTALL_NONE; // No command specified 72 ui->SetBackground(RecoveryUI::NO_COMMAND); 73 } 74 //如果安装失败或者。。。 75 if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) { 76 copy_logs(); 77 //显示错误的LOGO 78 ui->SetBackground(RecoveryUI::ERROR); 79 } 80 Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT; 81 if (status != INSTALL_SUCCESS || ui->IsTextVisible()) { 82 Device::BuiltinAction temp = prompt_and_wait(device, status); 83 if (temp != Device::NO_ACTION) after = temp; 84 } 85 86 // Save logs and clean up before rebooting or shutting down. 87 //完成recovery升级 88 finish_recovery(send_intent); 89 90 switch (after) { 91 case Device::SHUTDOWN: 92 ui->Print("Shutting down...\n"); 93 property_set(ANDROID_RB_PROPERTY, "shutdown,"); 94 break; 95 96 case Device::REBOOT_BOOTLOADER: 97 ui->Print("Rebooting to bootloader...\n"); 98 property_set(ANDROID_RB_PROPERTY, "reboot,bootloader"); 99 break; 100 101 default: 102 ui->Print("Rebooting...\n"); 103 property_set(ANDROID_RB_PROPERTY, "reboot,"); 104 break; 105 } 106 sleep(5); // should reboot before this finishes 107 return EXIT_SUCCESS;
在这里面,我们最常用的即是OTA更新和恢复出厂设置,先来说说恢复出厂设置,这个功能就是所谓的手机双清,众所周知,Android手机在使用很久后,由于垃圾数据,以及其它的因素会导致手机的反应越来越慢,这让人烦恼不已,所以就需要双清,双清一般就是清除/data分区和/cache分区,代码流程很详细,有兴趣可以自己去分析。
接下来看看OTA是如何实现更新的,我们看到install_ota_package这个函数,执行到这个函数,看到源码:
1//安装更新包 2int 3install_package(const char* path, int* wipe_cache, const char* install_file, 4 bool needs_mount) 5{ 6 FILE* install_log = fopen_path(install_file, "w"); 7 if (install_log) { 8 fputs(path, install_log); 9 fputc('\n', install_log); 10 } else { 11 LOGE("failed to open last_install: %s\n", strerror(errno)); 12 } 13 int result; 14 //设置安装挂载对应的节点 15 //这一步是关键 16 if (setup_install_mounts() != 0) { 17 LOGE("failed to set up expected mounts for install; aborting\n"); 18 result = INSTALL_ERROR; 19 } else { 20 //到这里才是真正的去安装OTA包 21 result = really_install_package(path, wipe_cache, needs_mount); 22 } 23 //如果返回结果为0,那么安装就成功了 24 if (install_log) { 25 fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log); 26 fputc('\n', install_log); 27 fclose(install_log); 28 } 29 return result; 30}
其实到了really_install_package这一步,才是真正做到OTA更新,但是在OTA更新之前至关重要的一步就是设置安装挂载对应的节点了,我曾经掉入此坑,现在拿出来分析一下,我们来看看setup_install_mounts这个函数:
1//设置安装挂载的节点 2int setup_install_mounts() { 3 if (fstab == NULL) { 4 LOGE("can't set up install mounts: no fstab loaded\n"); 5 return -1; 6 } 7 for (int i = 0; i < fstab->num_entries; ++i) { 8 Volume* v = fstab->recs + i; 9 //如果判断挂载的路径是/tmp 或者/cache 10 //那么就挂载对应的节点,而其它的节点都不会去挂载 11 if (strcmp(v->mount_point, "/tmp") == 0 || 12 strcmp(v->mount_point, "/cache") == 0) { 13 if (ensure_path_mounted(v->mount_point) != 0) { 14 LOGE("failed to mount %s\n", v->mount_point); 15 return -1; 16 } 17 18 } 19 //如果不是/tmp或者/cache这两个节点,则默认就会卸载所有的挂载节点 20 else { 21 //卸载所有的挂载节点 22 if (ensure_path_unmounted(v->mount_point) != 0) { 23 LOGE("failed to unmount %s\n", v->mount_point); 24 return -1; 25 } 26 } 27 } 28 return 0; 29}
如果在安装更新的时候,OTA包经过路径转换后不是放在/tmp和/cache这个路径下的时候,那么就会走else分支,从而卸载所有的挂载节点,这样就会导致,传的路径正确,却OTA更新不成功,如果是做自己定制的路径,这一步一定要小心,我们可以在这里继续添加定制的挂载点。那么,执行完设置挂载节点的函数后,接下来就是执行真正的OTA更新了,我们来看看:
1static int 2really_install_package(const char *path, int* wipe_cache, bool needs_mount) 3{ 4 //设置更新时的背景 5 ui->SetBackground(RecoveryUI::INSTALLING_UPDATE); 6 ui->Print("Finding update package...\n"); 7 // Give verification half the progress bar... 8 //设置进度条的类型 9 ui->SetProgressType(RecoveryUI::DETERMINATE); 10 //显示进度条 11 ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME); 12 LOGI("Update location: %s\n", path); 13 //在屏幕上打印 Opening update package.. 14 // Map the update package into memory. 15 ui->Print("Opening update package...\n"); 16 //patch是OTA的路径,need_mount参数表示是否需要挂载,1挂载,0,不挂载 17 if (path && needs_mount) { 18 if (path[0] == '@') { 19 ensure_path_mounted(path+1); 20 } else { 21 //挂载OTA升级包的路径------> 一般是执行这个流程 22 ensure_path_mounted(path); 23 } 24 } 25 26 MemMapping map; 27 if (sysMapFile(path, &map) != 0) { 28 LOGE("failed to map file\n"); 29 return INSTALL_CORRUPT; 30 } 31 32 int numKeys; 33 //获取校验公钥文件 34 Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys); 35 if (loadedKeys == NULL) { 36 LOGE("Failed to load keys\n"); 37 return INSTALL_CORRUPT; 38 } 39 LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE); 40 41 ui->Print("Verifying update package...\n"); 42 43 int err; 44 //校验文件 45 err = verify_file(map.addr, map.length, loadedKeys, numKeys); 46 free(loadedKeys); 47 LOGI("verify_file returned %d\n", err); 48 //如果校验不成功 49 if (err != VERIFY_SUCCESS) { 50 //打印签名失败 51 LOGE("signature verification failed\n"); 52 sysReleaseMap(&map); 53 return INSTALL_CORRUPT; 54 } 55 56 /* Try to open the package. 57 */ 58 //尝试去打开ota压缩包 59 ZipArchive zip; 60 err = mzOpenZipArchive(map.addr, map.length, &zip); 61 if (err != 0) { 62 LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad"); 63 sysReleaseMap(&map); 64 return INSTALL_CORRUPT; 65 } 66 67 /* Verify and install the contents of the package. 68 */ 69 //开始安装升级包 70 ui->Print("Installing update...\n"); 71 ui->SetEnableReboot(false); 72 int result = try_update_binary(path, &zip, wipe_cache); 73 //安装成功后自动重启 74 ui->SetEnableReboot(true); 75 ui->Print("\n"); 76 77 sysReleaseMap(&map); 78 //返回结果 79 return result; 80}
关于recovery的大致流程,我们分析至此,关于如何像MTK平台一样,定制recovery,这就需要读者能够读懂recovery的流程,然后加入自己的代码进行定制,当然我们也会看到,一些recovery花样百出,很多UI做了自己的,而不是用安卓系统原生态的,安卓系统recovery原生态的UI如下:
如何定制相应的UI,后续我们会对recovery源代码中的UI显示做进一步的分析。。。。