本文基于Android R。高通平台。

设置中点击“恢复出厂设置”即可清楚用户数据。查看代码发现其只是发送了一个广播而已。

                Intent intent = new Intent(Intent.ACTION_FACTORY_RESET);intent.setPackage("android");intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);intent.putExtra(Intent.EXTRA_REASON,"CryptKeeper.showFactoryReset() corrupt=" + corrupt);sendBroadcast(intent);
public static final String ACTION_FACTORY_RESET = "android.intent.action.FACTORY_RESET";

/framework/base/core/res/AndroidManifest.xml中静态注册了这个广播的接收者MasterClearReceiver。

        <receiver android:name="com.android.server.MasterClearReceiver"android:permission="android.permission.MASTER_CLEAR"><intent-filterandroid:priority="100" ><!-- For Checkin, Settings, etc.: action=FACTORY_RESET --><action android:name="android.intent.action.FACTORY_RESET" /><!-- As above until all the references to the deprecated MASTER_CLEAR get updated toFACTORY_RESET. --><action android:name="android.intent.action.MASTER_CLEAR" /><!-- MCS always uses REMOTE_INTENT: category=MASTER_CLEAR --><action android:name="com.google.android.c2dm.intent.RECEIVE" /><category android:name="android.intent.category.MASTER_CLEAR" /></intent-filter></receiver>

查看MasterClearReceiver代码,onReceive接收到广播时,判断如果广播是Intent.ACTION_FACTORY_RESET,且factoryResetPackage不为空,就重新将广播发出去,return退出。从此可以看到MasterClearReceiver支持供应商重载“恢复出厂设置”操作,只需要将com.android.internal.R.string.config_factoryResetPackage值overlay就可以了。否则继续执行默认的恢厂操作。

        final String factoryResetPackage = context.getString(com.android.internal.R.string.config_factoryResetPackage);if (Intent.ACTION_FACTORY_RESET.equals(intent.getAction())&& !TextUtils.isEmpty(factoryResetPackage)) {intent.setPackage(factoryResetPackage).setComponent(null);context.sendBroadcastAsUser(intent, UserHandle.SYSTEM);return;}

新启线程执行操作:

RecoverySystem.rebootWipeUserData(context, shutdown, reason, forceWipe, mWipeEsims);

RecoverySystem的rebootWipeUserData方法中调用bootCommand方法,传入参数"–wipe_data"和当前local参数(当前地区语言)、reasonArg、shutdownArg。

String shutdownArg = null;
if (shutdown) {shutdownArg = "--shutdown_after";
}String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {String timeStamp = DateFormat.format("yyyy-MM-ddTHH:mm:ssZ", System.currentTimeMillis()).toString();reasonArg = "--reason=" + sanitizeArg(reason + "," + timeStamp);
}final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);

最终会调用到RecoverySystemService的rebootRecoveryWithCommand方法。

    @Override // Binder callpublic void rebootRecoveryWithCommand(String command) {if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");synchronized (sRequestLock) {if (!setupOrClearBcb(true, command)) {return;}// Having set up the BCB, go ahead and reboot.PowerManager pm = mInjector.getPowerManager();pm.reboot(PowerManager.REBOOT_RECOVERY);}}

可以看到rebootRecoveryWithCommand方法只执行了两个操作:

  1. setupOrClearBcb(true, command)
  2. pm.reboot(PowerManager.REBOOT_RECOVERY);

setupOrClearBcb将之前传递过来的参数写入BCB中,然后重启进入recovery模式中。

setupBCB

    private boolean setupOrClearBcb(boolean isSetup, String command) {mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);final boolean available = checkAndWaitForUncryptService();if (!available) {Slog.e(TAG, "uncrypt service is unavailable.");return false;}if (isSetup) {mInjector.systemPropertiesSet("ctl.start", "setup-bcb");} else {mInjector.systemPropertiesSet("ctl.start", "clear-bcb");}// 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 {// Send the BCB commands if it's to setup BCB.if (isSetup) {socket.sendCommand(command);}// 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();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);return false;}} catch (IOException e) {Slog.e(TAG, "IOException when communicating with uncrypt:", e);return false;} finally {socket.close();}return true;}

uncrypt

通过设置属性启动 setup-bcb 程序,通过socket与 uncrypt通信,将之前的参数发送给 uncrypt。(关于“ctl.start”属性启动程序的细节可以参考https://blog.csdn.net/u010223349/article/details/8876232)

setup-bcb,clear-bcb,uncrypt都是指同一程序,代码位置位于 /bootable/recovery/uncrypt/。

Android.bp:

cc_binary {name: "uncrypt",srcs: ["uncrypt.cpp",],cflags: ["-Wall","-Werror",],shared_libs: ["libbase","libbootloader_message","libcutils","libfs_mgr",],static_libs: ["libotautil",],init_rc: ["uncrypt.rc",],
}

uncrypt.rc:

service uncrypt /system/bin/uncryptclass mainsocket uncrypt stream 600 system systemdisabledoneshotservice setup-bcb /system/bin/uncrypt --setup-bcbclass mainsocket uncrypt stream 600 system systemdisabledoneshotservice clear-bcb /system/bin/uncrypt --clear-bcbclass mainsocket uncrypt stream 600 system systemdisabledoneshot

uncrypt.cpp对程序启动的参数分别处理,当参数为“setup-bcb”时调用setup_bcb()方法。

static bool setup_bcb(const int socket) {// c5. receive message lengthint length;if (!android::base::ReadFully(socket, &length, 4)) {PLOG(ERROR) << "failed to read the length";return false;}length = ntohl(length);// c7. receive messagestd::string content;content.resize(length);if (!android::base::ReadFully(socket, &content[0], length)) {PLOG(ERROR) << "failed to read the message";return false;}LOG(INFO) << "  received command: [" << content << "] (" << content.size() << ")";std::vector<std::string> options = android::base::Split(content, "\n");std::string wipe_package;for (auto& option : options) {if (android::base::StartsWith(option, "--wipe_package=")) {std::string path = option.substr(strlen("--wipe_package="));if (!android::base::ReadFileToString(path, &wipe_package)) {PLOG(ERROR) << "failed to read " << path;return false;}option = android::base::StringPrintf("--wipe_package_size=%zu", wipe_package.size());}}// c8. setup the bcb commandstd::string err;if (!write_bootloader_message(options, &err)) {LOG(ERROR) << "failed to set bootloader message: " << err;write_status_to_socket(-1, socket);return false;}if (!wipe_package.empty() && !write_wipe_package(wipe_package, &err)) {PLOG(ERROR) << "failed to set wipe package: " << err;write_status_to_socket(-1, socket);return false;}// c10. send "100" statuswrite_status_to_socket(100, socket);return true;
}

write_bootloader_message()方法将之前传递过来的参数写入BCB。由bootloader_message.cpp中write_misc_partition()方法实现。(关于BCB的更多介绍参考https://blog.csdn.net/Android_2016/article/details/98959849)

bool write_misc_partition(const void* p, size_t size, const std::string& misc_blk_device,size_t offset, std::string* err) {android::base::unique_fd fd(open(misc_blk_device.c_str(), O_WRONLY));if (fd == -1) {*err = android::base::StringPrintf("failed to open %s: %s", misc_blk_device.c_str(),strerror(errno));return false;}if (lseek(fd, static_cast<off_t>(offset), SEEK_SET) != static_cast<off_t>(offset)) {*err = android::base::StringPrintf("failed to lseek %s: %s", misc_blk_device.c_str(),strerror(errno));return false;}if (!android::base::WriteFully(fd, p, size)) {*err = android::base::StringPrintf("failed to write %s: %s", misc_blk_device.c_str(),strerror(errno));return false;}if (fsync(fd) == -1) {*err = android::base::StringPrintf("failed to fsync %s: %s", misc_blk_device.c_str(),strerror(errno));return false;}return true;
}

setup_bcb()方法在简单处理从socket接收的消息后,返回状态值“100”就结束了。

关于write_misc_partition()方法中变量misc_blk_device,misc_blk_device由方法get_misc_blk_device()初始化。查找/misc分区。

static std::string get_misc_blk_device(std::string* err) {Fstab fstab;if (!ReadDefaultFstab(&fstab)) {*err = "failed to read default fstab";return "";}auto record = GetEntryForMountPoint(&fstab, "/misc");if (record == nullptr) {*err = "failed to find /misc partition";return "";}return record->blk_device;
}

ReadDefaultFstab()方法读取设备分区信息,返回挂载点。

// Loads the fstab file and combines with fstab entries passed in from device tree.
bool ReadDefaultFstab(Fstab* fstab) {Fstab dt_fstab;ReadFstabFromDt(&dt_fstab, false);*fstab = std::move(dt_fstab);std::string default_fstab_path;// Use different fstab paths for normal boot and recovery boot, respectivelyif (access("/system/bin/recovery", F_OK) == 0) {default_fstab_path = "/etc/recovery.fstab";} else {  // normal bootdefault_fstab_path = GetFstabPath();}Fstab default_fstab;if (!default_fstab_path.empty()) {ReadFstabFromFile(default_fstab_path, &default_fstab);} else {LINFO << __FUNCTION__ << "(): failed to find device default fstab";}for (auto&& entry : default_fstab) {fstab->emplace_back(std::move(entry));}return !fstab->empty();
}

查看recovery.fstab文件,可以看到关于/misc分区的一些信息:

/dev/block/bootdevice/by-name/misc /misc emmc defaults defaults

recovery模式

pm.reboot(PowerManager.REBOOT_RECOVERY)重启进入recovery模式。recovery相关代码位于/bootable/recovery/目录。

启动recovery程序,首先进入recovery_main.cpp中的main()方法。其中"…"表示对代码进行了删减,这里只关注“恢复出厂设置”的处理逻辑。

int main(int argc, char** argv) {...load_volume_table();std::string stage;std::vector<std::string> args = get_args(argc, argv, &stage);auto args_to_parse = StringVectorToNullTerminatedArray(args);static constexpr struct option OPTIONS[] = {{ "fastboot", no_argument, nullptr, 0 },{ "locale", required_argument, nullptr, 0 },{ "reason", required_argument, nullptr, 0 },{ "show_text", no_argument, nullptr, 't' },{ nullptr, 0, nullptr, 0 },};bool show_text = false;bool fastboot = false;std::string locale;std::string reason;// The code here is only interested in the options that signal the intent to start fastbootd or// recovery. Unrecognized options are likely meant for recovery, which will be processed later in// start_recovery(). Suppress the warnings for such -- even if some flags were indeed invalid, the// code in start_recovery() will capture and report them.opterr = 0;int arg;int option_index;while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,&option_index)) != -1) {switch (arg) {case 't':show_text = true;break;case 0: {std::string option = OPTIONS[option_index].name;if (option == "locale") {locale = optarg;} else if (option == "reason") {reason = optarg;LOG(INFO) << "reason is [" << reason << "]";} else if (option == "fastboot" &&android::base::GetBoolProperty("ro.boot.dynamic_partitions", false)) {fastboot = true;}break;}}}optind = 1;opterr = 1;static constexpr const char* kDefaultLibRecoveryUIExt = "librecovery_ui_ext.so";// Intentionally not calling dlclose(3) to avoid potential gotchas (e.g. `make_device` may have// handed out pointers to code or static [or thread-local] data and doesn't collect them all back// in on dlclose).void* librecovery_ui_ext = dlopen(kDefaultLibRecoveryUIExt, RTLD_NOW);using MakeDeviceType = decltype(&make_device);MakeDeviceType make_device_func = nullptr;if (librecovery_ui_ext == nullptr) {printf("Failed to dlopen %s: %s\n", kDefaultLibRecoveryUIExt, dlerror());} else {reinterpret_cast<void*&>(make_device_func) = dlsym(librecovery_ui_ext, "make_device");if (make_device_func == nullptr) {printf("Failed to dlsym make_device: %s\n", dlerror());}}Device* device;...BootState boot_state(reason, stage);  // recovery_main owns the state of boot.device->SetBootState(&boot_state);...while (true) {...auto ret = fastboot ? StartFastboot(device, args) : start_recovery(device, args);if (ret == Device::KEY_INTERRUPTED) {ret = action.exchange(ret);if (ret == Device::NO_ACTION) {continue;}}switch (ret) {case Device::SHUTDOWN:ui->Print("Shutting down...\n");Shutdown("userrequested,recovery");break;case Device::REBOOT_BOOTLOADER:ui->Print("Rebooting to bootloader...\n");Reboot("bootloader");break;case Device::REBOOT_FASTBOOT:ui->Print("Rebooting to recovery/fastboot...\n");Reboot("fastboot");break;case Device::REBOOT_RECOVERY:ui->Print("Rebooting to recovery...\n");Reboot("recovery");break;case Device::ENTER_FASTBOOT:if (android::fs_mgr::LogicalPartitionsMapped()) {ui->Print("Partitions may be mounted - rebooting to enter fastboot.");Reboot("fastboot");} else {LOG(INFO) << "Entering fastboot";fastboot = true;}break;case Device::ENTER_RECOVERY:LOG(INFO) << "Entering recovery";fastboot = false;break;case Device::REBOOT:ui->Print("Rebooting...\n");Reboot("userrequested,recovery");break;default:ui->Print("Rebooting...\n");Reboot("unknown" + std::to_string(ret));break;...}}// Should be unreachable.return EXIT_SUCCESS;
}

load_volume_table()加载挂载分区,从打印的log中查看到挂载的分区信息:

0 /system  ext4 system_a 0
1 /system_ext  ext4 system_ext_a 0
2 /product  ext4 product_a 0
3 /vendor  ext4 vendor_a 0
4 /odm  ext4 odm_a 0
5 /metadata  ext4 /dev/block/bootdevice/by-name/metadata 0
6 /data  f2fs /dev/block/bootdevice/by-name/userdata 0
7 /sdcard  vfat /dev/block/mmcblk1p1 0
8 /boot  emmc /dev/block/bootdevice/by-name/boot 0
9 /misc  emmc /dev/block/bootdevice/by-name/misc 0
10 /tmp  ramdisk ramdisk 0

get_args(argc, argv, &stage)方法从/misc分区读取之前写入到BCB中的信息,赋值给变量args,在log中打印args:

/system/bin/recovery
--wipe_data
--reason=MasterClearConfirm,2021-03-29T09:02:07Z
--locale=zh

可以看到就是之前RecoverySystemService的rebootRecoveryWithCommand()方法通过socket发送给uncrypt的消息内容。

对args信息进行一些处理后,判断是否是StartFastboot()或则start_recovery(),这里选择的是start_recovery(),while(true)循环体中判断start_recovery()返回参数,以此执行下一步操作。

recovery.cpp–>start_recovery(),同样“…”处对代码进行了删减。

Device::BuiltinAction start_recovery(Device* device, const std::vector<std::string>& args) {static constexpr struct option OPTIONS[] = {{ "fastboot", no_argument, nullptr, 0 },{ "install_with_fuse", no_argument, nullptr, 0 },{ "just_exit", no_argument, nullptr, 'x' },{ "locale", required_argument, nullptr, 0 },{ "prompt_and_wipe_data", no_argument, nullptr, 0 },{ "reason", required_argument, nullptr, 0 },{ "rescue", no_argument, nullptr, 0 },{ "retry_count", required_argument, nullptr, 0 },{ "security", no_argument, nullptr, 0 },{ "show_text", no_argument, nullptr, 't' },{ "shutdown_after", no_argument, nullptr, 0 },{ "sideload", no_argument, nullptr, 0 },{ "sideload_auto_reboot", no_argument, nullptr, 0 },{ "update_package", required_argument, nullptr, 0 },{ "wipe_ab", no_argument, nullptr, 0 },{ "wipe_cache", no_argument, nullptr, 0 },{ "wipe_data", no_argument, nullptr, 0 },{ "wipe_package_size", required_argument, nullptr, 0 },{ nullptr, 0, nullptr, 0 },};...auto args_to_parse = StringVectorToNullTerminatedArray(args);int arg;int option_index;// Parse everything before the last element (which must be a nullptr). getopt_long(3) expects a// null-terminated char* array, but without counting null as an arg (i.e. argv[argc] should be// nullptr).while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,&option_index)) != -1) {switch (arg) {case 't':// Handled in recovery_main.cppbreak;case 'x':just_exit = true;break;case 0: {std::string option = OPTIONS[option_index].name;if (option == "install_with_fuse") {install_with_fuse = true;} else if (option == "locale" || option == "fastboot" || option == "reason") {// Handled in recovery_main.cpp} else if (option == "prompt_and_wipe_data") {should_prompt_and_wipe_data = true;} else if (option == "rescue") {rescue = true;} else if (option == "retry_count") {android::base::ParseInt(optarg, &retry_count, 0);} else if (option == "security") {security_update = true;} else if (option == "sideload") {sideload = true;} else if (option == "sideload_auto_reboot") {sideload = true;sideload_auto_reboot = true;} else if (option == "shutdown_after") {shutdown_after = true;} else if (option == "update_package") {update_package = optarg;} else if (option == "wipe_ab") {should_wipe_ab = true;} else if (option == "wipe_cache") {should_wipe_cache = true;} else if (option == "wipe_data") {should_wipe_data = true;} else if (option == "wipe_package_size") {android::base::ParseUint(optarg, &wipe_package_size);}break;}case '?':LOG(ERROR) << "Invalid command argument";continue;}}optind = 1;// next_action indicates the next target to reboot into upon finishing the install. It could be// overridden to a different reboot target per user request.Device::BuiltinAction next_action = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;...if (update_package != nullptr) {...} else if (should_wipe_data) {save_current_log = true;CHECK(device->GetReason().has_value());bool convert_fbe = device->GetReason().value() == "convert_fbe";if (!WipeData(device, convert_fbe)) {status = INSTALL_ERROR;}} ...error:if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {ui->SetBackground(RecoveryUI::ERROR);if (!ui->IsTextVisible()) {sleep(5);}}// Determine the next action.//  - If the state is INSTALL_REBOOT, device will reboot into the target as specified in//    `next_action`.//  - If the recovery menu is visible, prompt and wait for commands.//  - If the state is INSTALL_NONE, wait for commands (e.g. in user build, one manually boots//    into recovery to sideload a package or to wipe the device).//  - In all other cases, reboot the device. Therefore, normal users will observe the device//    rebooting a) immediately upon successful finish (INSTALL_SUCCESS); or b) an "error" screen//    for 5s followed by an automatic reboot.if (status != INSTALL_REBOOT) {if (status == INSTALL_NONE || ui->IsTextVisible()) {auto temp = PromptAndWait(device, status);if (temp != Device::NO_ACTION) {next_action = temp;}}}// Save logs and clean up before rebooting or shutting down.FinishRecovery(ui);return next_action;
}

和上面的main()方法一样,args参数被处理赋值给args_to_parse变量,通过getopt_long()对参数进行判断选择对应的操作,option == "wipe_data"时,should_wipe_data = true,之后通过判断变量should_wipe_data调用WipeData(device, convert_fbe)方法清楚用户数据:

constexpr const char* CACHE_ROOT = "/cache";
constexpr const char* DATA_ROOT = "/data";
constexpr const char* METADATA_ROOT = "/metadata";bool WipeData(Device* device, bool convert_fbe) {RecoveryUI* ui = device->GetUI();ui->Print("\n-- Wiping data...\n");if (!FinishPendingSnapshotMerges(device)) {ui->Print("Unable to check update status or complete merge, cannot wipe partitions.\n");return false;}bool success = device->PreWipeData();if (success) {success &= EraseVolume(DATA_ROOT, ui, convert_fbe);bool has_cache = volume_for_mount_point("/cache") != nullptr;if (has_cache) {success &= EraseVolume(CACHE_ROOT, ui, false);}if (volume_for_mount_point(METADATA_ROOT) != nullptr) {success &= EraseVolume(METADATA_ROOT, ui, false);}}if (success) {success &= device->PostWipeData();}ui->Print("Data wipe %s.\n", success ? "complete" : "failed");return success;
}

WipeData()方法对/data,/cache,/metadata分区进行格式化操作。之后start_recovery()返回Device::REBOOT,设备重启。

安卓恢复出厂设置过程详解相关推荐

  1. 如何给惠普计算机主机解还原,hp电脑怎么恢复出厂设置 hp电脑恢复出厂设置教程详解...

    hp电脑怎么恢复出厂设置?通常大家遇到某些故障问题时,都会选择重装系统,如果不想进行修复系统的话,大家可以选择恢复出厂设置.而且目前市面上的电脑都具备恢复出厂设置功能,那hp电脑如何恢复出厂设置呢?大 ...

  2. 联想计算机系统还原怎么弄,电脑恢复出厂设置,手把手教你联想电脑怎么恢复出厂设置...

    电脑使用久了,难免会遇到系统出现一些故障的时候,当我们遇到一些比较棘手的时候该怎么办呢?其实除了重装系统外,我们或许可以选择电脑恢复出厂设置的方法,让坏的系统重新恢复过来,为此,小编就给大家带来了联想 ...

  3. 华擎主板bios设置图解_[华擎主板bios设置图解]详解华擎主板bios恢复出厂设置

    [华擎主板bios设置图解]详细说明华擎主板bios修复系统恢复2019 现在有很多人全是应用华擎主板的,可是了解华擎主板bios如何恢复数据设定的人确是很少.因而,对于这个问题,今日网编就来给大伙说 ...

  4. idata界面_iData手持移动终端组合键恢复出厂设置教程

    本文详细介绍iData手持移动终端恢复出厂设置过程,让手持机处于出厂时的"状态",以解决因为手机使用过程中出现的各种问题.比如因为忘记解锁图案多次输入造成的"图案解锁尝试 ...

  5. 路由器如何恢复出厂设置

    路由器在什么情况下恢复出厂设置? 1.路由器登录密码忘记的情况下需要恢复出厂设置. 2.电脑无法登录到路由器管理页面情况下可以恢复出厂设置. 3.设置路由器上网,在宽带账号和密码都正确的情况下,WAN ...

  6. ac1900 linksys 恢复_AC1900路由器怎么恢复出厂设置?

    请问大家:如何把AC1900路由器恢复出厂设置? 答:如果想把你的AC1900路由器恢复出厂设置,可以按照下面的方法来操作: 1.首先,在你的AC1900路由器机身找到复位按钮,复位按钮通常在电源接口 ...

  7. android 恢复出厂设置流程分析,基于Android系统快速恢复出厂设置方法实现.doc

    基于Android系统快速恢复出厂设置方法实现 基于Android系统快速恢复出厂设置方法实现 摘 要:针对使用Android系统的智能电视进行恢复出厂设置时重置速度慢的情况进行了研究和分析,从其重置 ...

  8. 笔记本电脑计算机恢复出厂设置密码,笔记本电脑如何恢复出厂设置

    我们使用电脑遇到一些难以解决的问题可以选择将电脑恢复出厂设置来解决问题,当笔记本电脑出现问题的时候也可以这么解决.那下面就以win10系统为例,给大家讲讲笔记本电脑怎么恢复出厂设置吧. 注意:请在开始 ...

  9. ac1900 linksys 恢复_AC1900路由器怎么恢复出厂设置? | 192路由网

    请问大家:如何把AC1900路由器恢复出厂设置? 答:如果想把你的AC1900路由器恢复出厂设置,可以按照下面的方法来操作: 1.首先,在你的AC1900路由器机身找到复位按钮,复位按钮通常在电源接口 ...

最新文章

  1. Python中的线程、进程、协程以及区别
  2. Android平台类加载流程源码分析
  3. BOOST_PREDEF_TESTED_AT宏相关的测试程序
  4. 通过粘性仙人掌基元进行延迟加载和缓存
  5. dos系统功能调用的屏幕显示字符是( )号调用_四、WIN10模拟DOS环境之8086汇编实战...
  6. 基于FFmpeg的音频编码(PCM数据编码成AAC android)
  7. TME上市两周年|为2020甜蜜发糖,收获2021的希望
  8. 使用Java的代理机制进行日志输出
  9. 写一个方法,用一个for循环打印九九乘法表
  10. php 重新组合数组_PHP数组组合
  11. Typecho中的gravatar头像无法加载
  12. [Vue warn]: Invalid prop: custom validator check failed for prop xxx.
  13. 使用Ant打包java程序
  14. java开发16g内存够吗_Java 内存模型 ,一篇就够了!
  15. Windows NT各版本对应关系
  16. 海关179对接问题及解决办法大集锦
  17. layui tree组件更改图标
  18. python打砖块游戏算法设计分析_基于pygame的打砖块游戏,做到一半,不带做了
  19. 企业级反向代理 Haproxy
  20. EverEdit - 值得关注的国产原创开发的免费高效优秀的文本与代码编辑器

热门文章

  1. 南京理工大学博士毕业指南(不定期整理更新)
  2. 【转载】如何把产品做简单
  3. VMware安装Win10
  4. Win10无法连接打印机怎么办?不能使用打印机的解决方法
  5. 提升IE或WoW的安装或更新速度
  6. AbsoluteLayout绝对布局 实现登录界面
  7. 《星际争霸》单位语音中英文完全版(zt)
  8. Java--初识CSS
  9. 真实案例分享:网络推广执行力超强名人
  10. Linux阅码场 - Linux内核月报(2020年12月)