Android啟動篇 — init原理(二)

来源:http://www.cnblogs.com/pepsimaxin/archive/2017/04/21/6740413.html
-Advertisement-
Play Games

基於Android 7.0源碼,深入解析init進程及main函數的邏輯功能 ...


========================================================          ========================================================

=              【原創文章】:參考部分博客內容,學習之餘進行了大量的篩減細化分析                        =          =                          【特殊申明】:避諱抄襲侵權之嫌疑,特此說明,歡迎轉載!                           =   
========================================================          ========================================================

【前言】

  Android啟動篇 — init原理(一)中講解分init進程分析init創建系統目錄並掛在相應系統文件、初始化屬性域、設置系統屬性、啟動配置屬性服務端等一系列複雜工作,很多工作和知識點跟Linux關係很大,所以沒有作過多介紹,而本此對於init.rc的解析則是重中之重,所以單獨拿出來進行詳細分析。

int main(int argc, char** argv) {
    /* 01. 創建文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工作 */•
    /* 05. 重新設置屬性 */
    /* 06. 創建epoll句柄 */
    /* 07. 裝載子進程信號處理器 */
    /* 08. 設置預設系統屬性 */
    /* 09. 啟動配置屬性的服務端 */
    /* 10. 匹配命令和函數之間的對應關係 */
------------------------------------------------------------------------------------------- // Android啟動篇 — init原理(一)中講解
/* 11. 解析init.rc */
Parser& parser = Parser::GetInstance(); // 構造解析文件用的parser對象 // 增加ServiceParser為一個section,對應name為service parser.AddSectionParser("service",std::make_unique<ServiceParser>()); // 增加ActionParser為一個section,對應name為action parser.AddSectionParser("on", std::make_unique<ActionParser>()); // 增加ImportParser為一個section,對應name為service parser.AddSectionParser("import", std::make_unique<ImportParser>()); parser.ParseConfig("/init.rc"); // 開始實際的解析過程

【正文】

  init.rc是一個配置文件,內部由Android初始化語言編寫(Android Init Language)編寫的腳本,主要包含五種類型語句:Action、Command、Service、Option和Import,在分析代碼的過程中我們會詳細介紹。

  init.rc的配置代碼在:system/core/rootdir/init.rc 中

  init.rc文件是在init進程啟動後執行的啟動腳本,文件中記錄著init進程需執行的操作。

  init.rc文件大致分為兩大部分,一部分是以“on”關鍵字開頭的動作列表(action list):

on early-init      // Action類型語句
    # Set init and its forked children's oom_adj.     // #:註釋符號
    write /proc/1/oom_score_adj -1000
    ... ...
    start ueventd

  Action類型語句格式:

on <trigger> [&& <trigger>]*     // 設置觸發器  
   <command>  
   <command>      // 動作觸發之後要執行的命令

  另一部分是以“service”關鍵字開頭的服務列表(service list):  如 Zygote

service ueventd /sbin/ueventd
    class core
    critical
    seclabel u:r:ueventd:s0

  Service類型語句格式:

service <name> <pathname> [ <argument> ]*   // <service的名字><執行程式路徑><傳遞參數>  
   <option>       // option是service的修飾詞,影響什麼時候、如何啟動services  
   <option>  
   ...

  藉助系統環境變數或Linux命令,動作列表用於創建所需目錄,以及為某些特定文件指定許可權,而服務列表用來記錄init進程需要啟動的一些子進程。如上面代碼所示,service關鍵字後的第一個字元串表示服務(子進程)的名稱,第二個字元串表示服務的執行路徑。

  值得一提的是在Android 7.0中對init.rc文件進行了拆分,每個服務一個rc文件。我們要分析的zygote服務的啟動腳本則在init.zygoteXX.rc中定義。

  在init.rc的import段我們看到如下代碼:

import /init.${ro.zygote}.rc     // 可以看出init.rc不再直接引入一個固定的文件,而是根據屬性ro.zygote的內容來引入不同的文件

  說明:

  從android5.0開始,android開始支持64位的編譯,zygote本身也就有了32位和64位的區別,所以在這裡用ro.zygote屬性來控制啟動不同版本的zygote進程。

  init.rc位於/system/core/rootdir下。在這個路徑下還包括四個關於zygote的rc文件。分別是Init.zygote32.rc,Init.zygote32_64.rc,Init.zygote64.rc,Init.zygote64_32.rc,由硬體決定調用哪個文件。

  這裡拿32位處理器為例,init.zygote32.rc的代碼如下所示:

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main         # class是一個option,指定zygote服務的類型為main
    socket zygote stream 660 root system          # socket關鍵字表示一個option,創建一個名為dev/socket/zygote,類型為stream,許可權為660的socket
    onrestart write /sys/android_power/request_state wake          # onrestart是一個option,說明在zygote重啟時需要執行的command
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

  “service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server”

  在Init.zygote32.rc中,定義了一個zygote服務:zygote,由關鍵字service告訴init進程創建一個名為zygote的進程,這個進程要執行的程式是:/system/bin/app_process,給這個進程四個參數:

    · -Xzygote:該參數將作為虛擬機啟動時所需的參數

    · /system/bin:代表虛擬機程式所在目錄

    · --zygote:指明以ZygoteInit.java類中的main函數作為虛擬機執行入口

    · --start-system-server:告訴Zygote進程啟動SystemServer進程

   接下來,我們回到源碼當中,繼續分析main函數:

    /* 11. 解析init.rc */
    Parser& parser = Parser::GetInstance();       // 構造解析文件用的parser對象
    // 增加ServiceParser為一個section,對應name為service
    parser.AddSectionParser("service",std::make_unique<ServiceParser>());
    // 增加ActionParser為一個section,對應name為action
    parser.AddSectionParser("on", std::make_unique<ActionParser>());
    // 增加ImportParser為一個section,對應name為service
    parser.AddSectionParser("import", std::make_unique<ImportParser>());
    parser.ParseConfig("/init.rc");      // 開始實際的解析過程

  說明:

  上面在解析init.rc文件時使用了Parser類(在init目錄下的init_parser.h中定義), 初始化ServiceParser用來解析 “service”塊,ActionParser用來解析"on"塊,ImportParser用來解析“import”塊,“import”是用來引入一個init配置文件,來擴展當前配置的。

  /system/core/init/readme.txt 中對init文件中的所有關鍵字做了介紹,主要包含了Actions, Commands, Services, Options, and Imports等,可自行學習解讀。

  分析init.rc的解析過程:函數定義於system/core/init/ init_parser.cpp中

bool Parser::ParseConfig(const std::string& path) {
    if (is_dir(path.c_str())) {           // 判斷傳入參數是否為目錄地址
        return ParseConfigDir(path);      // 遞歸目錄,最終還是靠ParseConfigFile來解析實際的文件
    }
    return ParseConfigFile(path);         // 傳入傳輸為文件地址
}

  繼續分析ParseConfigFile():

bool Parser::ParseConfigFile(const std::string& path) {
    ... ...
    Timer t;
    std::string data;
    if (!read_file(path.c_str(), &data)) {       // 讀取路徑指定文件中的內容,保存為字元串形式
        return false;
}
... ...
    ParseData(path, data);        // 解析獲取的字元串
    ... ...
}

  跟蹤ParseData():

void Parser::ParseData(const std::string& filename, const std::string& data) {
    ... ...
    parse_state state;
    ... ...
    std::vector<std::string> args;

    for (;;) {
        switch (next_token(&state)) {    // next_token以行為單位分割參數傳遞過來的字元串,最先走到T_TEXT分支
        case T_EOF:
            if (section_parser) {
                section_parser->EndSection();    // 解析結束
            }
            return;
        case T_NEWLINE:
            state.line++;
            if (args.empty()) {
                break;
            }
            // 在前文創建parser時,我們為service,on,import定義了對應的parser 
            // 這裡就是根據第一個參數,判斷是否有對應的parser
            if (section_parsers_.count(args[0])) {
                if (section_parser) {
                    // 結束上一個parser的工作,將構造出的對象加入到對應的service_list與action_list中
                    section_parser->EndSection();
                }
                // 獲取參數對應的parser
                section_parser = section_parsers_[args[0]].get();
                std::string ret_err;
                // 調用實際parser的ParseSection函數
                if (!section_parser->ParseSection(args, &ret_err)) {
                    parse_error(&state, "%s\n", ret_err.c_str());
                    section_parser = nullptr;
                }
            } else if (section_parser) {
                std::string ret_err;
                // 如果第一個參數不是service,on,import
                // 則調用前一個parser的ParseLineSection函數
                // 這裡相當於解析一個參數塊的子項
                if (!section_parser->ParseLineSection(args, state.filename, 
                                                             state.line, &ret_err)) {
                    parse_error(&state, "%s\n", ret_err.c_str());
                }
            }
            args.clear();       // 清空本次解析的數據
            break;
        case T_TEXT:
            args.emplace_back(state.text);     //將本次解析的內容寫入到args中
            break;
        }
    }
}

  至此,init.rc解析完,接下來init會執行幾個重要的階段:

int main(int argc, char** argv) {
    /* 01. 創建文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工作 */•
    /* 05. 重新設置屬性 */
    /* 06. 創建epoll句柄 */
    /* 07. 裝載子進程信號處理器 */
    /* 08. 設置預設系統屬性 */
    /* 09. 啟動配置屬性的服務端 */
    /* 10. 匹配命令和函數之間的對應關係 */
/* 11. 解析init.rc*/
----------------------------------------------------------------------------
  /* 12. 向執行隊列中添加其他action */
    // 獲取ActionManager對象,需要通過am對命令執行順序進行控制
    ActionManager& am = ActionManager::GetInstance();
    // init執行命令觸發器主要分為early-init,init,late-init,boot等
    am.QueueEventTrigger("early-init");    // 添加觸發器early-init,執行on early-init內容

    // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
    am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    // ... so that we can start queuing up actions that require stuff from /dev.
    am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
    am.QueueBuiltinAction(keychord_init_action, "keychord_init");
    am.QueueBuiltinAction(console_init_action, "console_init");

    // Trigger all the boot actions to get us started.
    am.QueueEventTrigger("init");        // 添加觸發器init,執行on init內容,主要包括創建/掛在一些目錄,以及symlink等

    // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
    // wasn't ready immediately after wait_for_coldboot_done
    am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

    // Don't mount filesystems or start core system services in charger mode.
    if (bootmode == "charger") {
    am.QueueEventTrigger("charger");     // on charger階段
    } else if (strncmp(bootmode.c_str(), "ffbm", 4) == 0) {
    NOTICE("Booting into ffbm mode\n");
    am.QueueEventTrigger("ffbm");
    } else {
    am.QueueEventTrigger("late-init");          // 非充電模式添加觸發器last-init
    }

    // Run all property triggers based on current state of the properties.
    am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

  在last-init最後階段有如下代碼:

# Mount filesystems and start core system services.
on late-init
    trigger early-fs

    # Mount fstab in init.{$device}.rc by mount_all command. Optional parameter
    # '--early' can be specified to skip entries with 'latemount'.
    # /system and /vendor must be mounted by the end of the fs stage,
    # while /data is optional.
    trigger fs
    trigger post-fs

    # Load properties from /system/ + /factory after fs mount. Place
    # this in another action so that the load will be scheduled after the prior
    # issued fs triggers have completed.
    trigger load_system_props_action

    # Mount fstab in init.{$device}.rc by mount_all with '--late' parameter
    # to only mount entries with 'latemount'. This is needed if '--early' is
    # specified in the previous mount_all command on the fs stage.
    # With /system mounted and properties form /system + /factory available,
    # some services can be started.
    trigger late-fs

    # Now we can mount /data. File encryption requires keymaster to decrypt
    # /data, which in turn can only be loaded when system properties are present.
    trigger post-fs-data

    # Load persist properties and override properties (if enabled) from /data.
    trigger load_persist_props_action

    # Remove a file to wake up anything waiting for firmware.
    trigger firmware_mounts_complete

    trigger early-boot
   trigger boot

  可見出發了on early-boot和on boot兩個Action。

  我們看一下on boot:

on boot
    # basic network init
    ifup lo
    hostname localhost
    domainname localdomain
    ... ...
    class_start core

  在on boot 的最後class_start core 會啟動class為core的服務,這些服務包括ueventd、logd、healthd、adbd(disabled)、lmkd(LowMemoryKiller)、servicemanager、vold、debuggerd、surfaceflinger、bootanim(disabled)等。

  回到主題,分析trigger觸發器的代碼,QueueEventTrigger():位於system/core/init/action.cpp

void ActionManager::QueueEventTrigger(const std::string& trigger) {
    trigger_queue_.push(std::make_unique<EventTrigger>(trigger));
}

  此處QueueEventTrigger函數就是利用參數構造EventTrigger,然後加入到trigger_queue_中。後續init進程處理trigger事件時,將會觸發相應的操作。

  再看一下QueueBuiltinAction()函數:同樣位於system/core/init/action.cpp

void ActionManager::QueueBuiltinAction(BuiltinFunction func,
                                   const std::string& name) {
    // 創建action
    auto action = std::make_unique<Action>(true);
    std::vector<std::string> name_vector{name};

    // 保證唯一性
    if (!action->InitSingleTrigger(name)) {
        return;
    }

    // 創建action的cmd,指定執行函數和參數
    action->AddCommand(func, name_vector);

    trigger_queue_.push(std::make_unique<BuiltinTrigger>(action.get()));
    actions_.emplace_back(std::move(action));
}

  QueueBuiltinAction函數中構造新的action加入到actions_中,第一個參數作為新建action攜帶cmd的執行函數;第二個參數既作為action的trigger name,也作為action攜帶cmd的參數。

  接下來繼續分析main函數:

int main(int argc, char** argv) {
    /* 01. 創建文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工作 */•
    /* 05. 重新設置屬性 */
    /* 06. 創建epoll句柄 */
    /* 07. 裝載子進程信號處理器 */
    /* 08. 設置預設系統屬性 */
    /* 09. 啟動配置屬性的服務端 */
    /* 10. 匹配命令和函數之間的對應關係 */
/* 11. 解析init.rc*/
/* 12. 向執行隊列中添加其他action */
-------------------------------------------------------------------
/* 13. 處理添加到運行隊列的事件 */
while (true) {
    // 判斷是否有事件需要處理
        if (!waiting_for_exec) {
            // 依次執行每個action中攜帶command對應的執行函數
     am.ExecuteOneCommand();
        // 重啟一些掛掉的進程
            restart_processes();
        }

        // 以下決定timeout的時間,將影響while迴圈的間隔
        int timeout = -1;
        // 有進程需要重啟時,等待該進程重啟
        if (process_needs_restart) {
            timeout = (process_needs_restart - gettime()) * 1000;
            if (timeout < 0)
                timeout = 0;
        }

        // 有action待處理,不等待
        if (am.HasMoreCommands()) {
            timeout = 0;
        }

        // bootchart_sample應該是進行性能數據採樣
        bootchart_sample(&timeout);

        epoll_event ev;
        // 沒有事件到來的話,最多阻塞timeout時間
        int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));
        if (nr == -1) {
            ERROR("epoll_wait failed: %s\n", strerror(errno));
        } else if (nr == 1) {
            //有事件到來,執行對應處理函數
            //根據上文知道,epoll句柄(即epoll_fd)主要監聽子進程結束,及其它進程設置系統屬性的請求
            ((void (*)()) ev.data.ptr)();
        }
    }
    return 0;
} // end main

  看一下ExecuteOneComand()函數:同樣位於system/core/init/action.cpp

void ActionManager::ExecuteOneCommand() {
    // Loop through the trigger queue until we have an action to execute
    // 當前的可執行action隊列為空, trigger_queue_隊列不為空
    while (current_executing_actions_.empty() && !trigger_queue_.empty()) {
    // 迴圈遍歷action_隊列,包含了所有需要執行的命令,解析init.rc獲得
        for (const auto& action : actions_) {
            // 獲取隊頭的trigger, 檢查actions_列表中的action的trigger,對比是否相同
            if (trigger_queue_.front()->CheckTriggers(*action)) {
                // 將所有具有同一trigger的action加入當前可執行action隊列
                current_executing_actions_.emplace(action.get());
            }
        }
        // 將隊頭trigger出棧
        trigger_queue_.pop();
    }

    if (current_executing_actions_.empty()) {   // 當前可執行的actions隊列為空就返回
        return;
    }

    auto action = current_executing_actions_.front(); // 獲取當前可執行actions隊列的首個action

    if (current_command_ == 0) {
        std::string trigger_name = action->BuildTriggersString();
        INFO("processing action (%s)\n", trigger_name.c_str());
    }

    action->ExecuteOneCommand(current_command_);     // 執行當前的命令

    // If this was the last command in the current action, then remove
    // the action from the executing list.
    // If this action was oneshot, then also remove it from actions_.
    ++current_command_;      // 不斷疊加,將action_中的所有命令取出
    if (current_command_ == action->NumCommands()) {
        current_executing_actions_.pop();
        current_command_ = 0;
        if (action->oneshot()) {
            auto eraser = [&action] (std::unique_ptr<Action>& a) {
                return a.get() == action;
            };
            actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser));
        }
    }
}

  我們來觀察一下init.rc的開頭部分:

import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.usb.configfs.rc
import /init.${ro.zygote}.rc      // 後面我們即將重點分析zygote進程

  通過ro.zygote的屬性import對應的zygote的rc文件。

  

  我們查看init.zygote64_32.rc:

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
    class main
    socket zygote_secondary stream 660 root system
    onrestart restart zygote
    writepid /dev/cpuset/foreground/tasks

  可以看到zygote的class是main, 它是在on nonencrypted時被啟動的,如下:

on boot
    # basic network init
    ifup lo
    hostname localhost
    domainname localdomain
    ... ...
    class_start core

on nonencrypted
    # A/B update verifier that marks a successful boot.
    exec - root cache -- /system/bin/update_verifier nonencrypted
    class_start main
    class_start late_start

  至此,Init.cpp的main函數分析完畢!init進程已經啟動完成,一些重要的服務如core服務和main服務也都啟動起來,並啟動了zygote(/system/bin/app_process64)進程,zygote初始化時會創建虛擬機,啟動systemserver等。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 項目開發中在所難免的要對獲取到的數據進行模型嵌套分析,一層兩層還好,但是多了,對於一些初學者,就會很頭疼。 今天我們說一下如何利用 YYModel 來解析嵌套模型,以省市區為例: 1.先對模型嵌套分析: 假設我們最初拿到的數據是一個裝著省模型(provinceModel)的字典數組,裡面有:省名字 ...
  • 談到設置圓角頭像的問題,我想大多數人第一反應想到的是設置圖像的 layer let imageV: UIImageView = UIImageView() imageV.layer.cornerRadius = 26 imageV.layer.masksToBounds = true 這是一種方式, ...
  • 最近在Android和IOS上都需要對用戶的某些輸入進行簡單的加密,於是採用MD5加密方式。 首先將目的字元串加密一次,獲得32位字元串 然後將32位字元串拆為2段,分別加密1次 最後將加密後的2段拼接,加密100次 下麵是Android的Java部分和IOS的Objective C部分 impor ...
  • 轉載請註明出處 http://www.cnblogs.com/cnwutianhao/p/6746981.html 前幾天客戶提需求,對App增加一個功能,這個功能目前市面上已經很常見,那就是應用內切換語言。啥意思,就是 英、中、法、德、日。。。語言隨意切換。 (本案例採用Data-Bingding ...
  • 關於三個構造函數使用時機的說法 也就是說,系統預設只會調用Custom View的前兩個構造函數,至於第三個構造函數的調用,通常是我們自己在構造函數中主動調用的(例如,在第二個構造函數中調用第三個構造函數). ...
  • Activity代碼 自定義view 看下自定義view 類,主要onDraw()方法中. 繪製中分為三部分, 第一部分為上部分半透明區域 第二部分為下部分全透明區域 第三部分就是中間的progress值變化 ...
  • 一.對android:configChanges屬性,一般認為有以下幾點:1、不設置Activity的android:configChanges時,切屏會重新調用各個生命周期,切橫屏時會執行一次,切豎屏時會執行兩次2、設置Activity的android:configChanges="orienta ...
  • ex : Precondition : R_CHARGER_1 是 int type R_CHARGER_2 是 int type val 是 int type val = (((R_CHARGER_1+R_CHARGER_2) 100 val)/R_CHARGER_2)/100; 為什麼要乘100 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...