Android8.0.0-r4的OTA升级流程

原网址:https://blog.csdn.net/dingfengnupt88/article/details/52875228
 Android系统进行升级的时候,有两种途径,一种是通过接口传递升级包路径自动升级(Android系统SD卡升级),升级完之后系统自动重启;另一种是手动进入recovery模式下,选择升级包进行升级,升级完成之后停留在recovery界面,需要手动选择重启。前者多用于手机厂商的客户端在线升级,后者多用于开发和测试人员。但不管哪种,原理都是一样的,都要在recovery模式下进行升级。
1、获取升级包,可以从服务端下载,也可以直接拷贝到SD卡中
2、获取升级包路径,验证签名,通过installPackage接口升级
3、系统重启进入Recovery模式
4、在install.cpp进行升级操作
5、try_update_binary执行升级脚本
6、finish_recovery,重启
一、获取升级包,可以从服务端下载,也可以直接拷贝到SD卡中
    假设SD卡中已有升级包update.zip
二、获取升级包路径,验证签名,通过installPackage接口升级

1、调用RecoverySystem类提供的verifyPackage方法进行签名验证

[java] view plain copy
  1. public static void verifyPackage(File packageFile,
  2. ProgressListener listener,
  3. File deviceCertsZipFile)
  4. throws IOException, GeneralSecurityException
    签名验证函数,实现过程就不贴出来了,参数,
        packageFile--升级文件
        listener--进度监督器
        deviceCertsZipFile--签名文件,如果为空,则使用系统默认的签名
    只有当签名验证正确才返回,否则将抛出异常。
    在Recovery模式下进行升级时候也是会进行签名验证的,如果这里先不进行验证也不会有什么问题。但是我们建议在重启前,先验证,以便及早发现问题。
    如果签名验证没有问题,就执行installPackage开始升级。
2、installPackage开始升级
    如果签名验证没有问题,就进行重启升级,
[java] view plain copy
  1. public static void installPackage(Context context, File packageFile)
  2. throws IOException {
  3. String filename = packageFile.getCanonicalPath();
  4. Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
  5. final String filenameArg = "--update_package=" + filename;
  6. final String localeArg = "--locale=" + Locale.getDefault().toString();
  7. bootCommand(context, filenameArg, localeArg);
  8. }

这里定义了两个参数,我们接着看,

[java] view plain copy
  1. private static void bootCommand(Context context, String... args) throws IOException {
  2. RECOVERY_DIR.mkdirs();  // In case we need it
  3. COMMAND_FILE.delete();  // In case it's not writable
  4. LOG_FILE.delete();
  5. FileWriter command = new FileWriter(COMMAND_FILE);
  6. try {
  7. for (String arg : args) {
  8. if (!TextUtils.isEmpty(arg)) {
  9. command.write(arg);
  10. command.write("\n");
  11. }
  12. }
  13. } finally {
  14. command.close();
  15. }
  16. // Having written the command file, go ahead and reboot
  17. PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
  18. pm.reboot(PowerManager.REBOOT_RECOVERY);
  19. throw new IOException("Reboot failed (no permissions?)");
  20. }
    创建目录/cache/recovery/,command文件保存在该目录下;如果存在command文件,将其删除;然后将上面一步生成的两个参数写入到command文件。
    最后重启设备,重启过程就不再详述了。
三、系统重启进入Recovery模式
    系统重启时会判断/cache/recovery目录下是否有command文件,如果存在就进入recovery模式,否则就正常启动。
    进入到Recovery模式下,将执行recovery.cpp的main函数,下面贴出关键代码片段,
[java] view plain copy
  1. int arg;
  2. while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
  3. switch (arg) {
  4. case 's': send_intent = optarg; break;
  5. case 'u': update_package = optarg; break;
  6. case 'w': wipe_data = wipe_cache = 1; break;
  7. case 'c': wipe_cache = 1; break;
  8. case 't': show_text = 1; break;
  9. case 'x': just_exit = true; break;
  10. case 'l': locale = optarg; break;
  11. case 'g': {
  12. if (stage == NULL || *stage == '\0') {
  13. char buffer[20] = "1/";
  14. strncat(buffer, optarg, sizeof(buffer)-3);
  15. stage = strdup(buffer);
  16. }
  17. break;
  18. }
  19. case 'p': shutdown_after = true; break;
  20. case 'r': reason = optarg; break;
  21. case '?':
  22. LOGE("Invalid command argument\n");
  23. continue;
  24. }
  25. }

    这是一个While循环,用来读取recoverycommand参数,OPTIONS的不同选项定义如下,

[java] view plain copy
  1. static const struct option OPTIONS[] = {
  2. { "send_intent", required_argument, NULL, 's' },
  3. { "update_package", required_argument, NULL, 'u' },
  4. { "wipe_data", no_argument, NULL, 'w' },
  5. { "wipe_cache", no_argument, NULL, 'c' },
  6. { "show_text", no_argument, NULL, 't' },
  7. { "just_exit", no_argument, NULL, 'x' },
  8. { "locale", required_argument, NULL, 'l' },
  9. { "stages", required_argument, NULL, 'g' },
  10. { "shutdown_after", no_argument, NULL, 'p' },
  11. { "reason", required_argument, NULL, 'r' },
  12. { NULL, 0, NULL, 0 },
  13. };
    显然,根据第二步写入的命令文件内容,将为update_package 赋值。
    接着看,
[java] view plain copy
  1. if (update_package) {
  2. // For backwards compatibility on the cache partition only, if
  3. // we're given an old 'root' path "CACHE:foo", change it to
  4. // "/cache/foo".
  5. if (strncmp(update_package, "CACHE:", 6) == 0) {
  6. int len = strlen(update_package) + 10;
  7. char* modified_path = (char*)malloc(len);
  8. strlcpy(modified_path, "/cache/", len);
  9. strlcat(modified_path, update_package+6, len);
  10. printf("(replacing path \"%s\" with \"%s\")\n",
  11. update_package, modified_path);
  12. update_package = modified_path;
  13. }
  14. }

兼容性处理。

[java] view plain copy
  1. int status = INSTALL_SUCCESS;
  2. if (update_package != NULL) {
  3. status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
  4. if (status == INSTALL_SUCCESS && wipe_cache) {
  5. if (erase_volume("/cache")) {
  6. LOGE("Cache wipe (requested by package) failed.");
  7. }
  8. }
  9. if (status != INSTALL_SUCCESS) {
  10. ui->Print("Installation aborted.\n");
  11. // If this is an eng or userdebug build, then automatically
  12. // turn the text display on if the script fails so the error
  13. // message is visible.
  14. char buffer[PROPERTY_VALUE_MAX+1];
  15. property_get("ro.build.fingerprint", buffer, "");
  16. if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
  17. ui->ShowText(true);
  18. }
  19. }
  20. } else if (wipe_data) {
  21. if (device->WipeData()) status = INSTALL_ERROR;
  22. if (erase_volume("/data")) status = INSTALL_ERROR;
  23. if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
  24. if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR;
  25. if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
  26. } else if (wipe_cache) {
  27. if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
  28. if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
  29. } else if (!just_exit) {
  30. status = INSTALL_NONE;  // No command specified
  31. ui->SetBackground(RecoveryUI::NO_COMMAND);
  32. }
    update_package不为空,执行install_package方法。
    我们也可以看到擦除数据、缓存的实现也是在这个里执行的,这里就不展开了。
四、在install.cpp进行升级操作
    具体的升级过程都是在install.cpp中执行的,先看install_package方法,
[java] view plain copy
  1. int
  2. install_package(const char* path, int* wipe_cache, const char* install_file,
  3. bool needs_mount)
  4. {
  5. FILE* install_log = fopen_path(install_file, "w");
  6. if (install_log) {
  7. fputs(path, install_log);
  8. fputc('\n', install_log);
  9. } else {
  10. LOGE("failed to open last_install: %s\n", strerror(errno));
  11. }
  12. int result;
  13. if (setup_install_mounts() != 0) {
  14. LOGE("failed to set up expected mounts for install; aborting\n");
  15. result = INSTALL_ERROR;
  16. } else {
  17. result = really_install_package(path, wipe_cache, needs_mount);
  18. }
  19. if (install_log) {
  20. fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);
  21. fputc('\n', install_log);
  22. fclose(install_log);
  23. }
  24. return result;
  25. }

这个方法中首先创建了log文件,升级过程包括出错的信息都会写到这个文件中,便于后续的分析工作。继续跟进,really_install_package,

[java] view plain copy
  1. static int
  2. really_install_package(const char *path, int* wipe_cache, bool needs_mount)
  3. {
  4. ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
  5. ui->Print("Finding update package...\n");
  6. // Give verification half the progress bar...
  7. ui->SetProgressType(RecoveryUI::DETERMINATE);
  8. ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
  9. LOGI("Update location: %s\n", path);
  10. // Map the update package into memory.
  11. ui->Print("Opening update package...\n");
  12. if (path && needs_mount) {
  13. if (path[0] == '@') {
  14. ensure_path_mounted(path+1);
  15. } else {
  16. ensure_path_mounted(path);
  17. }
  18. }
  19. MemMapping map;
  20. if (sysMapFile(path, &map) != 0) {
  21. LOGE("failed to map file\n");
  22. return INSTALL_CORRUPT;
  23. }
  24. // 装入签名文件
  25. int numKeys;
  26. Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
  27. if (loadedKeys == NULL) {
  28. LOGE("Failed to load keys\n");
  29. return INSTALL_CORRUPT;
  30. }
  31. LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);
  32. ui->Print("Verifying update package...\n");
  33. // 验证签名
  34. int err;
  35. err = verify_file(map.addr, map.length, loadedKeys, numKeys);
  36. free(loadedKeys);
  37. LOGI("verify_file returned %d\n", err);
  38. // 签名失败的处理
  39. if (err != VERIFY_SUCCESS) {
  40. LOGE("signature verification failed\n");
  41. sysReleaseMap(&map);
  42. return INSTALL_CORRUPT;
  43. }
  44. /* Try to open the package.
  45. */
  46. // 打开升级包
  47. ZipArchive zip;
  48. err = mzOpenZipArchive(map.addr, map.length, &zip);
  49. if (err != 0) {
  50. LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");
  51. sysReleaseMap(&map);
  52. return INSTALL_CORRUPT;
  53. }
  54. /* Verify and install the contents of the package.
  55. */
  56. ui->Print("Installing update...\n");
  57. ui->SetEnableReboot(false);
  58. // 执行升级脚本文件,开始升级
  59. int result = try_update_binary(path, &zip, wipe_cache);
  60. ui->SetEnableReboot(true);
  61. ui->Print("\n");
  62. sysReleaseMap(&map);
  63. return result;
  64. }
    该方法主要做了三件事
1、验证签名
[java] view plain copy
  1. int numKeys;
  2. Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
  3. if (loadedKeys == NULL) {
  4. LOGE("Failed to load keys\n");
  5. return INSTALL_CORRUPT;
  6. }

装载签名文件,如果为空 ,终止升级;

[java] view plain copy
  1. int err;
  2. err = verify_file(map.addr, map.length, loadedKeys, numKeys);
  3. free(loadedKeys);
  4. LOGI("verify_file returned %d\n", err);
  5. // 签名失败的处理
  6. if (err != VERIFY_SUCCESS) {
  7. LOGE("signature verification failed\n");
  8. sysReleaseMap(&map);
  9. return INSTALL_CORRUPT;
  10. }
    调用verify_file进行签名验证,这个方法定义在verifier.cpp文件中,此处不展开,如果验证失败立即终止升级。
2、读取升级包信息
[java] view plain copy
  1. ZipArchive zip;
  2. err = mzOpenZipArchive(map.addr, map.length, &zip);
  3. if (err != 0) {
  4. LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");
  5. sysReleaseMap(&map);
  6. return INSTALL_CORRUPT;
  7. }

执行mzOpenZipArchive方法,打开升级包并扫描,将包的内容拷贝到变量zip中,该变量将作为参数用来执行升级脚本。

3、执行升级脚本文件,开始升级

[java] view plain copy
  1. int result = try_update_binary(path, &zip, wipe_cache);

try_update_binary方法用来处理升级包,执行制作升级包中的脚本文件update_binary,进行系统更新。

五、try_update_binary执行升级脚本

[java] view plain copy
  1. // If the package contains an update binary, extract it and run it.
  2. static int
  3. try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache) {
  4. // 检查update-binary是否存在
  5. const ZipEntry* binary_entry =
  6. mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);
  7. if (binary_entry == NULL) {
  8. mzCloseZipArchive(zip);
  9. return INSTALL_CORRUPT;
  10. }
  11. const char* binary = "/tmp/update_binary";
  12. unlink(binary);
  13. int fd = creat(binary, 0755);
  14. if (fd < 0) {
  15. mzCloseZipArchive(zip);
  16. LOGE("Can't make %s\n", binary);
  17. return INSTALL_ERROR;
  18. }
  19. // update-binary拷贝到"/tmp/update_binary"
  20. bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);
  21. close(fd);
  22. mzCloseZipArchive(zip);
  23. if (!ok) {
  24. LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME);
  25. return INSTALL_ERROR;
  26. }
  27. // 创建管道,用于下面的子进程和父进程之间的通信
  28. int pipefd[2];
  29. pipe(pipefd);
  30. // When executing the update binary contained in the package, the
  31. // arguments passed are:
  32. //
  33. //   - the version number for this interface
  34. //
  35. //   - an fd to which the program can write in order to update the
  36. //     progress bar.  The program can write single-line commands:
  37. //
  38. //        progress <frac> <secs>
  39. //            fill up the next <frac> part of of the progress bar
  40. //            over <secs> seconds.  If <secs> is zero, use
  41. //            set_progress commands to manually control the
  42. //            progress of this segment of the bar
  43. //
  44. //        set_progress <frac>
  45. //            <frac> should be between 0.0 and 1.0; sets the
  46. //            progress bar within the segment defined by the most
  47. //            recent progress command.
  48. //
  49. //        firmware <"hboot"|"radio"> <filename>
  50. //            arrange to install the contents of <filename> in the
  51. //            given partition on reboot.
  52. //
  53. //            (API v2: <filename> may start with "PACKAGE:" to
  54. //            indicate taking a file from the OTA package.)
  55. //
  56. //            (API v3: this command no longer exists.)
  57. //
  58. //        ui_print <string>
  59. //            display <string> on the screen.
  60. //
  61. //   - the name of the package zip file.
  62. //
  63. const char** args = (const char**)malloc(sizeof(char*) * 5);
  64. args[0] = binary;
  65. args[1] = EXPAND(RECOVERY_API_VERSION);   // defined in Android.mk
  66. char* temp = (char*)malloc(10);
  67. sprintf(temp, "%d", pipefd[1]);
  68. args[2] = temp;
  69. args[3] = (char*)path;
  70. args[4] = NULL;
  71. // 创建子进程。负责执行binary脚本
  72. pid_t pid = fork();
  73. if (pid == 0) {
  74. umask(022);
  75. close(pipefd[0]);
  76. execv(binary, (char* const*)args);// 执行binary脚本
  77. fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno));
  78. _exit(-1);
  79. }
  80. close(pipefd[1]);
  81. *wipe_cache = 0;
  82. // 父进程负责接受子进程发送的命令去更新ui显示
  83. char buffer[1024];
  84. FILE* from_child = fdopen(pipefd[0], "r");
  85. while (fgets(buffer, sizeof(buffer), from_child) != NULL) {
  86. char* command = strtok(buffer, " \n");
  87. if (command == NULL) {
  88. continue;
  89. } else if (strcmp(command, "progress") == 0) {
  90. char* fraction_s = strtok(NULL, " \n");
  91. char* seconds_s = strtok(NULL, " \n");
  92. float fraction = strtof(fraction_s, NULL);
  93. int seconds = strtol(seconds_s, NULL, 10);
  94. ui->ShowProgress(fraction * (1-VERIFICATION_PROGRESS_FRACTION), seconds);
  95. } else if (strcmp(command, "set_progress") == 0) {
  96. char* fraction_s = strtok(NULL, " \n");
  97. float fraction = strtof(fraction_s, NULL);
  98. ui->SetProgress(fraction);
  99. } else if (strcmp(command, "ui_print") == 0) {
  100. char* str = strtok(NULL, "\n");
  101. if (str) {
  102. ui->Print("%s", str);
  103. } else {
  104. ui->Print("\n");
  105. }
  106. fflush(stdout);
  107. } else if (strcmp(command, "wipe_cache") == 0) {
  108. *wipe_cache = 1;
  109. } else if (strcmp(command, "clear_display") == 0) {
  110. ui->SetBackground(RecoveryUI::NONE);
  111. } else if (strcmp(command, "enable_reboot") == 0) {
  112. // packages can explicitly request that they want the user
  113. // to be able to reboot during installation (useful for
  114. // debugging packages that don't exit).
  115. ui->SetEnableReboot(true);
  116. } else {
  117. LOGE("unknown command [%s]\n", command);
  118. }
  119. }
  120. fclose(from_child);
  121. int status;
  122. waitpid(pid, &status, 0);
  123. if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
  124. LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status));
  125. return INSTALL_ERROR;
  126. }
  127. return INSTALL_SUCCESS;
  128. }

try_update_binary函数,是真正实现读取升级包中的脚本文件并执行相应的函数的地方。在此函数中,通过调用fork函数创建出一个子进程,在子进程中开始读取并执行升级脚本文件。在此需要注意的是函数fork的用法,fork被调用一次,将做两次返回,在父进程中返回的是子进程的进程ID,为正数;而在子进程中,则返回0。子进程创建成功后,开始执行升级代码,并通过管道与父进程交互,父进程则通过读取子进程传递过来的信息更新UI。

六、finish_recovery,重启
    上一步完成之后,回到main函数,
[java] view plain copy
  1. // Save logs and clean up before rebooting or shutting down.
  2. finish_recovery(send_intent);
    保存升级过程中的log,清除临时文件,包括command文件(不清除的话,下次重启还会进入recovery模式),最后重启。
   以上就是升级的一个流程。
补充:

手动升级的流程也基本差不多,通过power key + volume上键组合,进入recovery模式,进入prompt_and_wait函数等待用户按键事件。

recovery.cpp的main函数,

[java] view plain copy
  1. Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
  2. if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
  3. Device::BuiltinAction temp = prompt_and_wait(device, status);
  4. if (temp != Device::NO_ACTION) after = temp;
  5. }

根据用户选择进入到相应的分支进行处理,如下图,

[java] view plain copy
  1. int chosen_item = get_menu_selection(headers, device->GetMenuItems(), 0, 0, device);
  2. // device-specific code may take some action here.  It may
  3. // return one of the core actions handled in the switch
  4. // statement below.
  5. Device::BuiltinAction chosen_action = device->InvokeMenuItem(chosen_item);

当我们选择从外置sdcard升级,进入如下分支中,

[java] view plain copy
  1. case Device::APPLY_EXT: {
  2. ensure_path_mounted(SDCARD_ROOT);
  3. char* path = browse_directory(SDCARD_ROOT, device);
  4. if (path == NULL) {
  5. ui->Print("\n-- No package file selected.\n", path);
  6. break;
  7. }
  8. ui->Print("\n-- Install %s ...\n", path);
  9. set_sdcard_update_bootloader_message();
  10. void* token = start_sdcard_fuse(path);
  11. int status = install_package(FUSE_SIDELOAD_HOST_PATHNAME, &wipe_cache,
  12. TEMPORARY_INSTALL_FILE, false);
  13. finish_sdcard_fuse(token);
  14. ensure_path_unmounted(SDCARD_ROOT);
  15. if (status == INSTALL_SUCCESS && wipe_cache) {
  16. ui->Print("\n-- Wiping cache (at package request)...\n");
  17. if (erase_volume("/cache")) {
  18. ui->Print("Cache wipe failed.\n");
  19. } else {
  20. ui->Print("Cache wipe complete.\n");
  21. }
  22. }
  23. if (status >= 0) {
  24. if (status != INSTALL_SUCCESS) {
  25. ui->SetBackground(RecoveryUI::ERROR);
  26. ui->Print("Installation aborted.\n");
  27. } else if (!ui->IsTextVisible()) {
  28. return Device::NO_ACTION;  // reboot if logs aren't visible
  29. } else {
  30. ui->Print("\nInstall from sdcard complete.\n");
  31. }
  32. }
  33. break;
  34. }

    char* path = browse_directory(SDCARD_ROOT, device);这个函数浏览SD card下的文件并把路径记录下来,然后根据名称排序并处理用户按键。

  ·当用户选择第一个条目“../”,直接跳转到上级目录,并且继续浏览文件

  ·当用户选择的条目以"/"开头,直接进入子目录

  ·其它情况表明,该条目就是zip.写入BCB,copy 更新包至临时目录直接转入install_package

选择zip包后,同样也是执行install_package函数,后面与自动升级的流程是一样的。

[java] view plain copy
  1. int status = install_package(FUSE_SIDELOAD_HOST_PATHNAME, &wipe_cache,
  2. TEMPORARY_INSTALL_FILE, false);

Android 8.0 (35)----Android8.0.0-r4的OTA升级流程相关推荐

  1. Android10.0 OTA升级流程分析

    原文地址:https://skytoby.github.io/2019/Android%20OTA%E5%8D%87%E7%BA%A7%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E ...

  2. android ota升级服务,android 标准OTA升级流程

    标准的OTA升级流程包括一下几个步骤: 1.Android设备首先会与OTA服务器进行交互,如果有更新会推送给客户.推送的信息常常会包含OTA更新包的下载地址和一些版本信息. 2.Update程序会将 ...

  3. Android系统OTA升级流程

    转自: https://www.2cto.com/kf/201610/558070.html Android系统进行升级的时候,有两种途径,一种是通过接口传递升级包路径自动升级,升级完之后系统自动重启 ...

  4. android指纹解锁动画,Android8.1 SystemUI Keyguard之指纹解锁流程

    手指在指纹传感器上摸一下就能解锁,Keyguard是怎么做到的呢? 下面我们就跟着源码,解析这整个过程. 何时开始监听指纹传感器? 先来看下IKeyguardService这个binder接口有哪些回 ...

  5. android ota 方案实战,Android 系统OTA升级流程

    Android系统进行升级的时候,有两种途径,一种是通过接口传递升级包路径自动升级(Android系统SD卡升级),升级完之后系统自动重启:另一种是手动进入recovery模式下,选择升级包进行升级, ...

  6. 【小题目】输入一个数字表示重量,如果重量<=20,则每千克收费0.35元;如果超过20千克不超过100千克的范围,则超过的部分按照每千克0.5元收费;如果超过100千克,则超过的范围按照每千克0.8元

    import java.util.Scanner; public class IfElseExer2 {public static void main(String[] args){Scanner s ...

  7. Android SDK Manager 无法下载Android8.1.0(API 27) SDK Platform的解决方案

    在Android SDK Manager 中安装Android 8.1.0 SDK Platform时报错导致无法安装. 错误信息:Downloading SDK Platform Android 8 ...

  8. android8 华为,重磅!华为多款手机可直升Android 8.0,EMUI 8.0完美适配

    原标题:重磅!华为多款手机可直升Android 8.0,EMUI 8.0完美适配 众望所归,华为多款手机终于可以直接升级安卓8.0系统了. 其实,自谷歌推出安卓8.0系统后,国门很多主流厂商也在不断地 ...

  9. android9是最新版本,Android9.0正式版发布,你的手机升级到主流Android8.0系

    原标题:Android9.0正式版发布,你的手机升级到主流Android8.0系 今天凌晨,谷歌正式推送Android9.0更新,这款最新的安卓系统被命名为Android Pie,Pie意义为&quo ...

最新文章

  1. Complete C# Unity Game Developer 2D
  2. R语言dplyr包移除dataframe数据列实战(Remove Columns)
  3. Cadence入门笔记(1):创建元件库的基本操作!
  4. Web应用验证码方面总结(ASP.NET版)
  5. python学成需要多久-小白学python怎么快速入门?多久能完成一个项目?
  6. 挑战程序猿---三角形
  7. 弱网环境测试-Charles学习
  8. ise verilog多模块编译_如何使用ISE高效开发Verilog项目(新手)
  9. (4)HTML标签补充和HTML转义字符
  10. groovy 和 java的区别_Groovy和JAVA的区别
  11. templateref html内容,angular4中的ElemenetRef和TemplateRef之间的区别
  12. JS遮罩效果 (很强)
  13. 如何注册一个免费的iTunes帐号(Apple ID)
  14. 定风波·三月七日(苏轼)
  15. The7 v.10.2.0-中文汉化主题/可视化拖拽编辑的WordPress主题企业外贸商城网站模板
  16. 百度×TCL丨鸿鹄语音芯片首次在家电行业量产!
  17. 03Blender基本修改器,渲染基础知识
  18. 发那科机器人GI分配_发那科机器人应用-运动指令入门(1)
  19. Android中获取屏幕信息的几种方式
  20. Go语言环境安装与试运行

热门文章

  1. 【STM32】HAL库 STM32CubeMX教程七---PWM输出(呼吸灯)
  2. java 6和_java都到6了 有什么不同 哦????
  3. 【LeetCode】【HOT】437. 路径总和 III(DFS)
  4. 【数据库】第二章 基础函数、聚合函数、条件查询、子查询和多表查询
  5. apache 设置缓存
  6. 数据库SQL优化总结
  7. clion上添加程序的预定添加程序的命令行
  8. 第二阶段冲刺 每日站立会议 1/4
  9. 关于火狐浏览器在ubuntu和安卓手机上的同步
  10. Hadoop 2.x简介