android6.0系統Healthd深入分析

来源:https://www.cnblogs.com/linhaostudy/archive/2019/11/26/11937467.html
-Advertisement-
Play Games

概述 Healthd是android4.4之後提出來的一種中介模型,該模型向下監聽來自底層的電池事件,向上傳遞電池數據信息給Framework層的BatteryService用以計算電池電量相關狀態信息,BatteryServcie通過傳遞來的數據來計算電池電量顯示,剩餘電量,電量級別等信息,如果收 ...


概述

Healthd是android4.4之後提出來的一種中介模型,該模型向下監聽來自底層的電池事件,向上傳遞電池數據信息給Framework層的BatteryService用以計算電池電量相關狀態信息,BatteryServcie通過傳遞來的數據來計算電池電量顯示,剩餘電量,電量級別等信息,如果收到過溫報警或者嚴重低電報警等信息,系統會直接關機,保護硬體。

主模塊處理流程

Healthd模塊代碼是在system/core/healthd/,其模塊入口在healthd的main函數,函數代碼如下:

int main(int argc, char **argv) {
 
int ch;
 
int ret;
 
klog_set_level(KLOG_LEVEL);
 
healthd_mode_ops = &android_ops;
 
 
 
if (!strcmp(basename(argv[0]), "charger")) {
 
        healthd_mode_ops = &charger_ops;
 
} else {
 
        while ((ch = getopt(argc, argv, "cr")) != -1) {
 
            switch (ch) {
 
            case 'c':
 
                healthd_mode_ops = &charger_ops;
 
                break;
 
            case 'r':
 
                healthd_mode_ops = &recovery_ops;
 
                break;
 
            case '?':
 
            default:
 
                KLOG_ERROR(LOG_TAG, "Unrecognized healthd option: %c\n",
 
                           optopt);
 
                exit(1);
 
            }
 
        }
 
    }
 
ret = healthd_init();
 
    if (ret) {
 
        KLOG_ERROR("Initialization failed, exiting\n");
 
        exit(2);
 
    }
 
 
 
    healthd_mainloop();
 
    KLOG_ERROR("Main loop terminated, exiting\n");
 
    return 3;
 
}

可以看出Main函數並不長,但是其作用確實巨大的,main函數起著一個統籌兼顧的作用,其他各個模塊函數去做一些具體相應的工作,最後彙總到main函數中被調用。

代碼中開始便是解析參數,healthd_mode_ops是一個關於充電狀態結構體變數,結構體變數里的參數是函數指針,在初始化時指向各個不同的操作函數,當開機充電時變數賦值為&android_ops,關機充電時候變數賦值為&charger_ops。

在ret = healthd_init();中進行一些初始化工作。

static int healthd_init() {
 
epollfd = epoll_create(MAX_EPOLL_EVENTS);
 
    if (epollfd == -1) {
 
       KLOG_ERROR(LOG_TAG,
 
                   "epoll_create failed; errno=%d\n",
 
                   errno);
 
       return -1;
 
   }
 
 
 
    healthd_board_init(&healthd_config);
 
    healthd_mode_ops->init(&healthd_config);
 
    wakealarm_init();
 
    uevent_init();
 
    gBatteryMonitor = new BatteryMonitor();
 
    gBatteryMonitor->init(&healthd_config);
 
    return 0;
 
}

創建一個epoll的變數將其賦值給epollfd,在healthd_board_init中未作任何事便返回了。

healthd_mode_ops->init調用有兩種情況:關機情況下調用charger_ops的init函數;開機情況下調用android_ops的init函數,這裡就開機情況來分析。android_ops的init函數指針指向healthd_mode_android_init函數

代碼如下:

void healthd_mode_android_init(struct healthd_config* /*config*/) {
    ProcessState::self()->setThreadPoolMaxThreadCount(0);//線程池裡最大線程數
    IPCThreadState::self()->disableBackgroundScheduling(true);//禁用後臺調度
    IPCThreadState::self()->setupPolling(&gBinderFd);//
 
    if (gBinderFd >= 0) {
        if (healthd_register_event(gBinderFd, binder_event))
            KLOG_ERROR(LOG_TAG,
                       "Register for binder events failed\n");
    }
 
    gBatteryPropertiesRegistrar = new BatteryPropertiesRegistrar();
    gBatteryPropertiesRegistrar->publish();
}

再來看看wakealarm_init函數:

static void wakealarm_init(void) {
    wakealarm_fd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK);
    if (wakealarm_fd == -1) {
        KLOG_ERROR(LOG_TAG, "wakealarm_init: timerfd_create failed\n");
        return;
    }
 
    if (healthd_register_event(wakealarm_fd, wakealarm_event))
        KLOG_ERROR(LOG_TAG,
                   "Registration of wakealarm event failed\n");
 
    wakealarm_set_interval(healthd_config.periodic_chores_interval_fast);
}

首先創建一個wakealarm_fd的定時器與之對應的文件描述符,healthd_register_event將wakealarm事件註冊到wakealarm_fd文件節點用以監聽wakealarm事件,wakealarm_set_interval設置alarm喚醒的間隔

再看看uevent_init函數:

static void uevent_init(void) {
    uevent_fd = uevent_open_socket(64*1024, true);
 
    if (uevent_fd < 0) {
        KLOG_ERROR(LOG_TAG, "uevent_init: uevent_open_socket failed\n");
        return;
    }
 
    fcntl(uevent_fd, F_SETFL, O_NONBLOCK);
    if (healthd_register_event(uevent_fd, uevent_event))
        KLOG_ERROR(LOG_TAG,
                   "register for uevent events failed\n");
}

創建並打開一個64k的socket文件描述符uevent_fd,設置文件狀態標誌為非阻塞模,將uevent事件註冊到uevent_fd文件節點用以監聽uevent事件。

 

我們可以看到android利用epoll監聽了三個文件節點的改變事件,分別是:通過gBinderfd監聽線程Binder通信事件;通過wakealarm_fd監聽wakealarm事件;通過uevent_fd監聽wakealarm事件。至於如何監聽後面做詳細分析

 

在healthd_init中最後創建BatteryMonitor的對象,並將其初始化。BatteryMonitor主要接受healthd傳來的數據,做電池狀態的計算並更新。

 

我們可以看到在BatterMonitor中的init函數中有以下語句:

DIR* dir = opendir(POWER_SUPPLY_SYSFS_PATH);
 
struct dirent* entry;
 
。。。。。。。
 
    while ((entry = readdir(dir))) {
 
        const char* name = entry->d_name;
 
。。。。。。
 
}

POWER_SUPPLY_SYSFS_PATH定義為"/sys/class/power_supply",在init函數中打開系統該文件夾,然後一一讀取該文件夾下的文件內容,在while迴圈中判斷該文件夾下各個文件節點的內容,並將其初始化給相關的參數.

 

 

 

至此,healthd_init函數就分析完了,其主要工作就是:創建了三個文件節點用來監聽相應的三種事件改變;創建BatteryMonitor對象,並通過讀取/sys/class/power_supply將其初始化。

 

Healthd_init走完之後,接著就是調用healthd_mainloop函數,該函數維持了一個死迴圈,代碼如下:

static void healthd_mainloop(void) {
 
while (1) {
 
        struct epoll_event events[eventct];
 
        int nevents;
 
        int timeout = awake_poll_interval;
 
        int mode_timeout;
 
        mode_timeout = healthd_mode_ops->preparetowait();
 
        if (timeout < 0 || (mode_timeout > 0 && mode_timeout < timeout))
 
            timeout = mode_timeout;
 
        nevents = epoll_wait(epollfd, events, eventct, timeout);
 
        if (nevents == -1) {
 
            if (errno == EINTR)
 
                continue;
 
            KLOG_ERROR(LOG_TAG, "healthd_mainloop: epoll_wait failed\n");
 
            break;
 
        }
 
        for (int n = 0; n < nevents; ++n) {
 
            if (events[n].data.ptr)
 
                (*(void (*)(int))events[n].data.ptr)(events[n].events);
 
        }
 
        if (!nevents)
 
            periodic_chores();
 
        healthd_mode_ops->heartbeat();
 
    }
 
    return;
 
}

Healthd_mainloop中維持了一個死迴圈,死迴圈中變數nevents 表示從epollfd中輪循中監聽得到的事件數目,這裡介紹一下輪詢機制中重要函數epoll_waite().

 

 

epoll_wait運行的道理是:等侍註冊在epfd上的socket fd的事務的產生,若是產生則將產生的sokct fd和事務類型放入到events數組中。且timeout如果為-1則為阻塞式,timeowout為0則表示非阻塞式。可以看到代碼中timeout為-1,故為阻塞式輪詢,當epollfd上有事件發生,則會走到下麵的處理邏輯。事件處理主要在for迴圈中:

 

在periodic_chores()中調用到healthd_battery_update()更新電池狀態。

void healthd_battery_update(void) {
 
   int new_wake_interval = gBatteryMonitor->update() ?
 
       healthd_config.periodic_chores_interval_fast :
 
           healthd_config.periodic_chores_interval_slow;
 
 
 
    if (new_wake_interval != wakealarm_wake_interval)
 
            wakealarm_set_interval(new_wake_interval);
 
    if (healthd_config.periodic_chores_interval_fast == -1)
 
        awake_poll_interval = -1;
 
    Else
 
        awake_poll_interval = new_wake_interval == healthd_config.periodic_chores_interval_fast ?
 
                -1 : healthd_config.periodic_chores_interval_fast * 1000;
 
}

可以看出該函數並不長,new_wake_interval表示新的wakealarm喚醒間隔,通過調用BatteryMonitor的update函數(後面詳細分析如何更新),其返回值為是否處於充電狀態,當處於充電狀態,則喚醒間隔為healthd_config.periodic_chores_interval_fast(短間隔),當不再充電狀態時喚醒間隔為healthd_config.periodic_chores_interval_slow(長間隔)

 

 

當新的間隔變數new_wake_interval與舊的變數wakealarm_wake_interval不一樣,則將新的喚醒間隔設置成wakealarm的喚醒間隔;

awake_poll_internal作為下一次epoll_waite的timeout參數,在這裡將其更新,在充電狀態下awake_poll_internal為-1,沒有充電的狀態下awake_poll_internal為60000ms

healthd主流程都是在main函數中處理,至此main已經分析完成,其簡要流程圖如下

image

Healthd處理邏輯

初始化處理

前面將healthd模塊中main函數分析完了,其主要工作流程有個大概的瞭解,但是其詳細處理邏輯並未做分析,在此之後,對Healthd的初始化,事件處理,狀態更新將做一個詳細的分析。

前面已經說過在healthd_init中創建了三個文件節點gBinderfd,uevent_fd,wakealarm_fd,並用以註冊監聽三種事件,註冊監聽都是通過healthd_register_event函數實現的。

healthd_register_event(gBinderFd, binder_event);

healthd_register_event(wakealarm_fd, wakealarm_event);

healthd_register_event(uevent_fd, uevent_event);

其healthd_register_event實現代碼如下:

int healthd_register_event(int fd, void (*handler)(uint32_t)) {
 
struct epoll_event ev;
 
    ev.events = EPOLLIN | EPOLLWAKEUP;
 
    ev.data.ptr = (void *)handler;
 
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
 
        KLOG_ERROR(LOG_TAG,
 
                   "epoll_ctl failed; errno=%d\n", errno);
 
        return -1;
 
    }
 
    eventct++;
 
    return 0;
 
}

函數將相應的文件節點事件賦值為函數的第二個形參,也就是說相應的gBinderfd的事件處理函數為binder_event函數,同理wakealarm_fd,ueven_fd的事件事件處理分別為wakealarm_event,uevent_event函數。然後將其三個文件節點加入到epollfd中。

事件獲取與處理

Healthd中維持了一個阻塞式的死迴圈healthd_mainloop,在該函數中提供阻塞式的監聽已發送的事件函數epoll_wait(),healthd_mainloop中有如下代碼

nevents = epoll_wait(epollfd, events, eventct, timeout);
 
 
 
for (int n = 0; n < nevents; ++n) {
 
if (events[n].data.ptr)
 
        (*(void (*)(int))events[n].data.ptr)(events[n].events);
 
}

當epoll_waite接受到gBinderfd,wakealarm_fd,uevent_fd其中的事件,便會將監聽到的事件加入到event數組中。在for迴圈中做處理,for迴圈中代碼看起來非常難懂,其實if判斷的便是event有沒有相應的處理函數,在前面註冊事件時候已經提到,三種句柄上的事件都有對應的處理函數,也就是當收到gBinderfd上的事件,便用binder_event函數處理,當收到uevent_fd上的事件便用uevent_event處理,當收到wakealarm_fd上的事件便用wakealarm_event處理。

這裡以較為重要的uevent_event事件處理為例:

#define UEVENT_MSG_LEN 2048
 
static void uevent_event(uint32_t /*epevents*/) {
 
char msg[UEVENT_MSG_LEN+2];
 
    char *cp;
 
    int n;
 
 
 
    n = uevent_kernel_multicast_recv(uevent_fd, msg, UEVENT_MSG_LEN);
 
    if (n <= 0)
 
        return;
 
    if (n >= UEVENT_MSG_LEN)   /* overflow -- discard */
 
        return;
 
 
 
    msg[n] = '\0';
 
    msg[n+1] = '\0';
 
    cp = msg;
 
 
 
    while (*cp) {
 
        if (!strcmp(cp, "SUBSYSTEM=" POWER_SUPPLY_SUBSYSTEM)) {
 
            healthd_battery_update();
 
            break;
 
        }
 
 
 
        /* advance to after the next \0 */
 
        while (*cp++)
 
            ;
 
    }
 
}

處理函數首先從uevent_fd 獲取事件數目,然後迴圈判斷是否是來自與power_supply目錄下的事件,如果是,則調用到healthd_battery_update中去更新電池狀態。

更新電池狀態

當收到事件,做一些判斷工作便需要更新電池狀態,其更新函數為healthd.cpp下的healthd_battery_update函數,但是主要更新並不在heathd中完成的,而是在BatteryMonitor中的update函數,其代碼較多,分段分析:

props.chargerAcOnline = false;
 
props.chargerUsbOnline = false;
 
props.chargerWirelessOnline = false;
 
props.batteryStatus = BATTERY_STATUS_UNKNOWN;
 
props.batteryHealth = BATTERY_HEALTH_UNKNOWN;
 
 
 
if (!mHealthdConfig->batteryPresentPath.isEmpty())
 
props.batteryPresent = getBooleanField(mHealthdConfig->batteryPresentPath);
 
else
 
    props.batteryPresent = mBatteryDevicePresent;
 
 
 
props.batteryLevel = mBatteryFixedCapacity ?
 
    mBatteryFixedCapacity :
 
    getIntField(mHealthdConfig->batteryCapacityPath);
 
props.batteryVoltage = getIntField(mHealthdConfig->batteryVoltagePath) / 1000;
 
 
 
props.batteryTemperature = mBatteryFixedTemperature ?
 
    mBatteryFixedTemperature :
 
    getIntField(mHealthdConfig->batteryTemperaturePath);
 
const int SIZE = 128;
 
char buf[SIZE];
 
String8 btech;
 
 
 
if (readFromFile(mHealthdConfig->batteryStatusPath, buf, SIZE) > 0)
 
    props.batteryStatus = getBatteryStatus(buf);
 
 
 
if (readFromFile(mHealthdConfig->batteryHealthPath, buf, SIZE) > 0)
 
    props.batteryHealth = getBatteryHealth(buf);
 
 
 
if (readFromFile(mHealthdConfig->batteryTechnologyPath, buf, SIZE) > 0)
 
    props.batteryTechnology = String8(buf);

在init函數中將healthd_config 對象傳入,並且將裡面的成員的一些地址信息去初始化保存起來。主要是保存一些地址信息,以及充電方式。在BatteryMonitor初始化中,heathd_config傳入init函數中,賦值為mHealthdConfig,上面一段主要是讀取/sys/class/power_supply下的文件節點信息初更新電池數據屬性值,

path.appendFormat("%s/%s/online", POWER_SUPPLY_SYSFS_PATH,
 
                  mChargerNames[i].string());
 
 
 
if (readFromFile(path, buf, SIZE) > 0) {
 
if (buf[0] != '0') {
 
        path.clear();
 
        path.appendFormat("%s/%s/type", POWER_SUPPLY_SYSFS_PATH,
 
                          mChargerNames[i].string());
 
        switch(readPowerSupplyType(path)) {
 
        case ANDROID_POWER_SUPPLY_TYPE_AC:
 
            props.chargerAcOnline = true;
 
            break;
 
        case ANDROID_POWER_SUPPLY_TYPE_USB:
 
            props.chargerUsbOnline = true;
 
            break;
 
        case ANDROID_POWER_SUPPLY_TYPE_WIRELESS:
 
            props.chargerWirelessOnline = true;
 
            break;
 
        default:
 
            KLOG_WARNING(LOG_TAG, "%s: Unknown power supply type\n",
 
                         mChargerNames[i].string());
 
        }
 
    }
 
}

將電池當前的電量級別,電壓,溫度,健康狀況,電池狀態以及充放電倍率存入dmesgline變數中,在後面會將電池充電類型,電池使用時間都以字元串存入dmesgline變數中,然後:

KLOG_WARNING(LOG_TAG, "%s\n", dmesgline);//向log記錄電池當前各種狀態信息

}

healthd_mode_ops->battery_update(&props);//更新電池

return props.chargerAcOnline | props.chargerUsbOnline |

props.chargerWirelessOnline;//返回是否在充電狀態

整個update函數做完更新數據,記錄數據到log之後,然後調用到BatteryPropertiesRegistrar的update函數繼續更新電池狀態,最後返回值為是否處於充電狀態。

 

BatteryPropertiesRegistrar的update函數未作任何操作調用Healthd_mode_android.cpp中的healthd_mode_android_battery_update函數,我們可以看看該函數

void healthd_mode_android_battery_update(
struct android::BatteryProperties *props) {
 
    if (gBatteryPropertiesRegistrar != NULL)
 
        gBatteryPropertiesRegistrar->notifyListeners(*props);
 
 
 
    return;
 
}

這裡這裡直接調用到BatteryPropertiesRegistrar的notifyListeners去通知props改變了,props是什麼呢?props是定義的一個BatteryProperties屬性集,裡面的成員變數包含了所有的電池狀態信息,在update開始便通過讀取各個文件節點的實時數據更新電池屬性props,更新完成後通過BatteryPropertiesRegistrar通知其屬性監聽者去更新狀態,但是誰是監聽呢?

我們可以看到framework層中的BatteryService.java的onStart函數中有如下代碼:

public void onStart() {
 
IBinder b = ServiceManager.getService("batteryproperties");
 
    final IBatteryPropertiesRegistrar batteryPropertiesRegistrar =
 
            IBatteryPropertiesRegistrar.Stub.asInterface(b);
 
    try {
 
        batteryPropertiesRegistrar.registerListener(new BatteryListener());
 
    } catch (RemoteException e) {
 
        // Should never happen.
 
    }

我們在初始化的時候已經提到過,當healthd初始化時候會創建BatteryPropertiesRegistrar的對象並將其publish註冊到系統服務中,註冊服務的語句如下:

 

 

 defaultServiceManager()->addService(String16("batteryproperties"), this);

 

所以BatteryService在這裡獲取該服務,並以此註冊其監聽器為BatteryListener(),該監聽器監聽到BatteryProperties改變便會調用到BatteryService的update函數,去做電池電量相關計算以及顯示。

至此更新操作基本分析完成,其簡要流程如下圖所示
image

總結

Healthd是framework層傳遞來自底層電池事件信息並調用相關模塊更新電池狀態的一個中間層,其向下監聽來自底層PMU驅動上報的uevent電池事件,向上調用BatteryService去計算電池,電量,使用等相關信息,它通過一個阻塞式的死迴圈不斷監聽底層三個文件節點上的事件信息,當監聽到事件便調用到BatteryMonitor執行更新操作,通過BatteryService.java中註冊監聽電池屬性改變的函數,當電池屬性信息發生改變,即回調到BatteryService中做更新操作,更新完成一次電池事件的上報到更新整個流程就完成;總之Healthd是連接Battery模塊framework中java層與HAL層交互的主要通道。


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

-Advertisement-
Play Games
更多相關文章
  • 本文介紹了C#中時間和時間戳的轉換方法,以及DateTimeOffset的簡單使用。 ...
  • PDF是當今最流行的文檔格式之一,各種應用程式將其用作最終輸出。由於支持多種數據類型和可移植性,因此它是創建和共用內容的首選格式。作為對開發文檔管理應用程式感興趣的.NET應用程式開發人員,可能希望嵌入處理功能,以讀取PDF文檔並將其轉換為其他文件格式,例如HTML。 Aspose.PDF for ...
  • Aspose.Cells for .NET是Excel電子錶格編程API,可加快電子錶格管理和處理任務,同時支持構建具有生成,修改,轉換,呈現和列印電子錶格功能的跨平臺應用程式。 將Excel電子錶格轉換為圖像格式始終是熱門話題。有時,您聲稱此過程花費的時間太長。其他人則抱怨該過程卡在了較大的文件上 ...
  • object m = Type.Missing; const int MENU_ITEM_TYPE = 1; const int NEW_MENU = 18; CommandBarControl oNewMenu = ExcelGlobals.Application.CommandBars["Wor ...
  • 1. 前言 之前用PointLight做了一個番茄鐘,效果還不錯,具體可見這篇文章: "[UWP]使用PointLight並實現動畫效果" 後來試玩了Win2D,這次就用Win2D實現文字的鏤空效果,配合PointLight做一個內斂不張揚的番茄鐘。 實現鏤空文字的核心思想是使用CanvasGeom ...
  • 前言 Saga單詞翻譯過來是指尤指古代挪威或冰島講述冒險經歷和英雄業績的長篇故事,對,這裡強調長篇故事。許多系統都存在長時間運行的業務流程,NServiceBus使用基於事件驅動的體繫結構將容錯性和可伸縮性融入這些業務處理過程中。 當然一個單一介面調用則算不上一個長時間運行的業務場景,那麼如果在給定 ...
  • 知識需要不斷積累、總結和沉澱,思考和寫作是成長的催化劑 梯子 一、任務Task1、啟動任務2、阻塞延續3、任務層次結構4、枚舉參數5、任務取消6、任務結果7、異常二、並行Parallel1、Parallel.For()、Parallel.ForEach()2、Parallel.For3、Parall ...
  • 背景: 因伺服器部署了flask項目,安裝了python3,故重啟寶塔面板報錯 1 [Traceback (most recent call last): 2 File "/root/anaconda3/lib/python3.7/site-packages/gunicorn/util.py", l ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...