• Android7关机充电流程


    2021-09-03:Android7关机充电流程

    背景

    为了修改关机充电中的显示效果,因此学习一下Android 7关机充电的流程是怎么样的。

    以msm8909为例。

    [   94.741021] charger: [94654] animation starting
    [   94.744542] charger: animation starting cur_frame =0
    

    参考:

    动画的初始化

    main

    // system/core/healthd/healthd.cpp
    
    struct healthd_mode_ops *healthd_mode_ops;
    
    static struct healthd_mode_ops charger_ops = {
        .init = healthd_mode_charger_init,
        .preparetowait = healthd_mode_charger_preparetowait,
        .heartbeat = healthd_mode_charger_heartbeat,
        .battery_update = healthd_mode_charger_battery_update,
    };
    
    // main函数根据参数赋值不同ops给healthd_mode_ops
    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;
                }
            }
        }
    	// 初始化、显示以及循环
        ret = healthd_init();
    
        periodic_chores();
        healthd_mode_ops->heartbeat();
    
        healthd_mainloop();
        KLOG_ERROR("Main loop terminated, exiting
    ");
        return 3;
    }
    

    healthd_init

    // system/core/healthd/healthd.cpp
    static struct healthd_config healthd_config = {
        .periodic_chores_interval_fast = DEFAULT_PERIODIC_CHORES_INTERVAL_FAST,
        .periodic_chores_interval_slow = DEFAULT_PERIODIC_CHORES_INTERVAL_SLOW,
        .batteryStatusPath = String8(String8::kEmptyString),
        .batteryHealthPath = String8(String8::kEmptyString),
        .batteryPresentPath = String8(String8::kEmptyString),
        .batteryCapacityPath = String8(String8::kEmptyString),
        .batteryVoltagePath = String8(String8::kEmptyString),
        .batteryTemperaturePath = String8(String8::kEmptyString),
        .batteryTechnologyPath = String8(String8::kEmptyString),
        .batteryCurrentNowPath = String8(String8::kEmptyString),
        .batteryCurrentAvgPath = String8(String8::kEmptyString),
        .batteryChargeCounterPath = String8(String8::kEmptyString),
        .batteryFullChargePath = String8(String8::kEmptyString),
        .batteryCycleCountPath = String8(String8::kEmptyString),
        .energyCounter = NULL,
        .boot_min_cap = 0,
        .screen_on = NULL,
    };
    
    static int healthd_init() {
        // 创建epoll
        epollfd = epoll_create(MAX_EPOLL_EVENTS);
        if (epollfd == -1) {
            KLOG_ERROR(LOG_TAG,
                       "epoll_create failed; errno=%d
    ",
                       errno);
            return -1;
        }
    
        healthd_board_init(&healthd_config);
        // 注册监听的句柄
        healthd_mode_ops->init(&healthd_config);
        wakealarm_init();
        // 添加其他句柄(socket)
        uevent_init();
        gBatteryMonitor = new BatteryMonitor();
        gBatteryMonitor->init(&healthd_config);
        return 0;
    }
    

    healthd_mode_charger_init

    // system/core/healthd/healthd_mode_charger.cpp
    void healthd_mode_charger_init(struct healthd_config* config)
    {
        int ret;
        // // charger的状态是charger_state
        struct charger *charger = &charger_state;
        int i;
        int epollfd;
    
        // 初始化kernel log
        dump_last_kmsg();
    
        LOGW("--------------- STARTING CHARGER MODE ---------------
    ");
    
        healthd_board_mode_charger_init();
    	// 注册事件处理函数,包括监听输入,电池唤醒
        ret = ev_init(input_callback, charger);
        if (!ret) {
            epollfd = ev_get_epollfd();
            healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);
        }
    
        // 解析animation.txt文件的内容,初始化animation结构体,下面会详细分析
        // (大家可以先阅读这个函数的内容是什么,然后再返回这里往下看)
        struct animation* anim = init_animation();
        charger->batt_anim = anim;
    
    	// 看函数名及参数可知,用于创建电池状态出错的界面
        ret = res_create_display_surface(anim->fail_file.c_str(), &charger->surf_unknown);
        if (ret < 0) {
            LOGE("Cannot load custom battery_fail image. Reverting to built in.
    ");
            ret = res_create_display_surface("charger/battery_fail", &charger->surf_unknown);
            if (ret < 0) {
                LOGE("Cannot load built in battery_fail image
    ");
                charger->surf_unknown = NULL;
            }
        }
    #ifdef CHARGER_USER_ANIMATION
            GRSurface* scale_frames[USER_IMAGE_NUM];
    
            for(int i = 0; i<USER_IMAGE_NUM; i++){
                ret = res_create_display_surface(anim->user_animation_file[i].c_str(), &scale_frames[i]);
                    if (ret < 0) {
                        LOGE("Cannot load custom %s image. Reverting to built in.
    ",anim->user_animation_file[i].c_str());
                    }else{
                        anim->frames[i].surface = scale_frames[i];
                        LOGW("file is:[%s],anim->frames[%d].surface = charger->surf_unknown;
    ",
                            anim->user_animation_file[i].c_str(),i);
                    }
            }
    #else
        GRSurface** scale_frames;
        int scale_count;
        int scale_fps;  // Not in use (charger/battery_scale doesn't have FPS text
                        // chunk). We are using hard-coded frame.disp_time instead.
        // 从函数名可知,从png文件中创建多帧动画界面
        // 这个函数会根据png图片头部信息,将一张png图切成多个frame
        // 因为之前UI部门给的图片都是动画切出来的多张图片,所以这个函数很少用到
        ret = res_create_multi_display_surface(anim->animation_file.c_str(),
            &scale_count, &scale_fps, &scale_frames);
        if (ret < 0) {
            LOGE("Cannot load battery_scale image
    ");
            anim->num_frames = 0;
            anim->num_cycles = 1;
        } else if (scale_count != anim->num_frames) {
            LOGE("battery_scale image has unexpected frame count (%d, expected %d)
    ",
                 scale_count, anim->num_frames);
            anim->num_frames = 0;
            anim->num_cycles = 1;
        } else {
            for (i = 0; i < anim->num_frames; i++) {
                anim->frames[i].surface = scale_frames[i];
            }
        }
    #endif
        ev_sync_key_state(set_key_callback, charger);
    
        charger->next_screen_transition = -1;
        charger->next_key_check = -1;
        charger->next_pwr_check = -1;
        healthd_config = config;
        charger->boot_min_cap = config->boot_min_cap;
    }
    

    创建epoll的fd,创建关机闹钟的处理线程(创建一个新的epoll,并让关机闹钟处理事件epoll_wait在上面,如果关机闹钟事件触发,那么直接以rtc的开机原因热重启)

    init_animation

    // system/core/healthd/healthd_mode_charger.cpp
    animation* init_animation()
    {
        bool parse_success;
    
        std::string content;
        // 读取animation.txt文件信息,animation_desc_path定义如下
        // static constexpr const char* animation_desc_path="/res/values/charger/animation.txt";
        if (base::ReadFileToString(animation_desc_path, &content)) {
            // 解析读取到的animation文件的内容,该函数在下面分析
            // 大家也可以往下拉看怎么分析的,再回来这里往下看
            parse_success = parse_animation_desc(content, &battery_animation);
        } else {
            LOGW("Could not open animation description at %s
    ", animation_desc_path);
            parse_success = false;
        }
    
        if (!parse_success) {
            // 解析失败,使用默认的animation动画
            LOGW("Could not parse animation description. Using default animation.
    ");
            battery_animation = BASE_ANIMATION;
    #ifdef CHARGER_USER_ANIMATION
            battery_animation.user_animation_file[0].assign("charger/battery00");
            battery_animation.user_animation_file[1].assign("charger/battery01");
            //battery_animation.user_animation_file[2].assign("charger/battery02");
            //battery_animation.user_animation_file[3].assign("charger/battery03");
            //battery_animation.user_animation_file[4].assign("charger/battery04");
            //battery_animation.user_animation_file[5].assign("charger/battery05");
            battery_animation.frames = user_animation_frames;
            battery_animation.num_frames = ARRAY_SIZE(user_animation_frames);
    #else
            battery_animation.animation_file.assign("charger/battery_scale");
            battery_animation.frames = default_animation_frames;
            battery_animation.num_frames = ARRAY_SIZE(default_animation_frames);
    #endif
        }
        if (battery_animation.fail_file.empty()) {
            // 未定义电池信息未知动画,就采用默认的电池信息未知动画
            battery_animation.fail_file.assign("charger/battery_fail");
        }
    
        // 输出解析到的内容
        LOGV("Animation Description:
    ");
        LOGV("  animation: %d %d '%s' (%d)
    ",
             battery_animation.num_cycles, battery_animation.first_frame_repeats,
             battery_animation.animation_file.c_str(), battery_animation.num_frames);
        LOGV("  fail_file: '%s'
    ", battery_animation.fail_file.c_str());
        LOGV("  clock: %d %d %d %d %d %d '%s'
    ",
             battery_animation.text_clock.pos_x, battery_animation.text_clock.pos_y,
             battery_animation.text_clock.color_r, battery_animation.text_clock.color_g,
             battery_animation.text_clock.color_b, battery_animation.text_clock.color_a,
             battery_animation.text_clock.font_file.c_str());
        LOGV("  percent: %d %d %d %d %d %d '%s'
    ",
             battery_animation.text_percent.pos_x, battery_animation.text_percent.pos_y,
             battery_animation.text_percent.color_r, battery_animation.text_percent.color_g,
             battery_animation.text_percent.color_b, battery_animation.text_percent.color_a,
             battery_animation.text_percent.font_file.c_str());
    
        // 输出每帧动画的信息,显示时间,最低显示电量和最高显示电量
        for (int i = 0; i < battery_animation.num_frames; i++) {
            LOGV("  frame %.2d: %d %d %d
    ", i, battery_animation.frames[i].disp_time,
                 battery_animation.frames[i].min_level, battery_animation.frames[i].max_level);
        }
    
        return &battery_animation;
    }
    

    parse_animation_desc

    // system/core/healthd/AnimationParser.cpp
    bool parse_animation_desc(const std::string& content, animation* anim) {
        // 定义animation.txt文件可以解析的字段
        static constexpr const char* animation_prefix = "animation: ";
        static constexpr const char* fail_prefix = "fail: ";
        static constexpr const char* clock_prefix = "clock_display: ";
        static constexpr const char* percent_prefix = "percent_display: ";
        static constexpr const char* frame_prefix = "frame: ";
    
        // 把帧动画 放入vector(动态数组)
        std::vector<animation::frame> frames;
    
    	// for循环逐行解析
        // 以 animation: 3 2 charger/boot_charger_02 为例
        for (const auto& line : base::Split(content, "
    ")) {
            animation::frame frame;
            const char* rest;
    
            // 跳过空行和'#'开头的行
            if (can_ignore_line(line.c_str())) {
                continue;
                // remove_prefix会根据第二个参数是否符合,上面的例子刚好符合这一项
            	// 符合对应xxx_prefix后,会将"animation: "之后的内容,即"3 2 charger/boot_charger_02"
            	// 返回给rest,remove_prefix函数下面也会分析
            } else if (remove_prefix(line, animation_prefix, &rest)) {
                int start = 0, end = 0;
                // sscanf将3赋给anim->num_cycles,其余类似
                // 需要说明的是%n用于获取sscanf到%n未知读取的字符数,然后赋给start或者end
                // 这里"%n%*s%n"就是将"charger/boot_charger_02"字符串在line中的
                // 起始结束位置赋给start、end
                if (sscanf(rest, "%d %d %n%*s%n", &anim->num_cycles, &anim->first_frame_repeats,
                        &start, &end) != 2 ||
                    end == 0) {
                    LOGE("Bad animation format: %s
    ", line.c_str());
                    return false;
                } else {
                    // 如果上面解析成功,就将"charger/boot_charger_02"赋给animation_file
                    anim->animation_file.assign(&rest[start], end - start);
                }
                // 下面的内容类似
            } else if (remove_prefix(line, fail_prefix, &rest)) {
                anim->fail_file.assign(rest);
            } else if (remove_prefix(line, clock_prefix, &rest)) {
                if (!parse_text_field(rest, &anim->text_clock)) {
                    LOGE("Bad clock_display format: %s
    ", line.c_str());
                    return false;
                }
            } else if (remove_prefix(line, percent_prefix, &rest)) {
                if (!parse_text_field(rest, &anim->text_percent)) {
                    LOGE("Bad percent_display format: %s
    ", line.c_str());
                    return false;
                }
            } else if (sscanf(line.c_str(), " frame: %d %d %d",
                    &frame.disp_time, &frame.min_level, &frame.max_level) == 3) {
                frames.push_back(std::move(frame));
            } else {
                LOGE("Malformed animation description line: %s
    ", line.c_str());
                return false;
            }
        }
    
        if (anim->animation_file.empty() || frames.empty()) {
            LOGE("Bad animation description. Provide the 'animation: ' line and at least one 'frame: ' "
                 "line.
    ");
            return false;
        }
    
        anim->num_frames = frames.size();
        anim->frames = new animation::frame[frames.size()];
        std::copy(frames.begin(), frames.end(), anim->frames);
    
        return true;
    }
    

    因为公司使用的是将gif动画切出来的一张张独立的图,不像默认动画battery_scale.png图那样,一张图包含所有的充电动画surface信息,所以这里我们可以修改这个函数:

    • 在animation结构体里面的frame结构体中添加一个string类型的字段frame_file,用来记录对应的frame文件名
    • 修改frame: 这个prefix的解析内容,添加frame文件的文件名字段解析,例如:frame: 50 16 20 charger/charging_animation_09,将最后的charger/charging_animation_09内容记录到新添加的frame_file字段中
    • 根据frame_file指定的文件名,通过res_create_display_surface函数来为每一帧动画图创建对应的frame,因为animation的frames指针就是用来记录动画内容的,所以将创建的frame记录到frames中即可。
    • 需要注意注释掉healthd_mode_charger_init函数中通过charger/scale创建muti frame的代码
    remove_prefix
    bool remove_prefix(const std::string& line, const char* prefix, const char** rest) {
        const char* str = line.c_str();
        int start;
        char c;
     
        // 经过解析后format为" animation: %n%c"
        std::string format = base::StringPrintf(" %s%%n%%c", prefix);
        // sscanf解析后,start为"animation: 3 2 charger/boot_charger_02"字符串中3的所在位置
        if (sscanf(str, format.c_str(), &start, &c) != 1) {
            return false;
        }
     
        // rest为"3 2 charger/boot_charger_02"字符串
        *rest = &str[start];
        return true;
    }
    
    open_png

    到这里,动画的初始化过程就基本讲完了,不过还有一个需要注意的地方就是,在调用res_create_display_surface的时候会调用open_png函数来打开png图片,在这个函数里面会判断png图片是否符合要求

    会检查位深

    if (bit_depth == 8 && *channels == 3 && color_type == PNG_COLOR_TYPE_RGB) {
            // 8-bit RGB images: great, nothing to do.
        } else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_GRAY) {
            // 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
            png_set_expand_gray_1_2_4_to_8(*png_ptr);
        } else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_PALETTE) {
            // paletted images: expand to 8-bit RGB.  Note that we DON'T
            // currently expand the tRNS chunk (if any) to an alpha
            // channel, because minui doesn't support alpha channels in
            // general.
            png_set_palette_to_rgb(*png_ptr);
            *channels = 3;
        } else {
            fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d
    ",
                    bit_depth, *channels, color_type);
            result = -7;
            goto exit;
        }
    

    从上面一段代码可以看出,animation动画图支持3通道的RGB图,4通道的RGBA类型的图是支持不了的,这里的A指的是Alpha通道;另外就是单通道灰阶图也是支持的。

    与动画有关的事件注册与处理

    healthd_register_event

    事件通过这个接口注册的,底层是epoll实现的。

    // system/core/healthd/healthd.cpp
    
    // int healthd_register_event(int fd, void (*handler)(uint32_t), EventWakeup wakeup = EVENT_NO_WAKEUP_FD);
    int healthd_register_event(int fd, void (*handler)(uint32_t), EventWakeup wakeup) {
        struct epoll_event ev;
    
        ev.events = EPOLLIN;
    
        if (wakeup == EVENT_WAKEUP_FD)
            ev.events |= 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
    ", errno);
            return -1;
        }
    
        eventct++;
        return 0;
    }
    

    注册的事件如下

    healthd_mode_android.cpp:55:        if (healthd_register_event(gBinderFd, binder_event))
    healthd.cpp:262:    if (healthd_register_event(uevent_fd, uevent_event, EVENT_WAKEUP_FD))
    healthd.cpp:285:    if (healthd_register_event(wakealarm_fd, wakealarm_event, EVENT_WAKEUP_FD))
    healthd_mode_charger.cpp:956:        healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);
    

    定时事件:wakealarm_event

    if (healthd_register_event(wakealarm_fd, wakealarm_event, EVENT_WAKEUP_FD))
    

    设置定时刷新电池电量的闹钟(默认10 min刷新一次)

    static void wakealarm_event(uint32_t /*epevents*/) {
        unsigned long long wakeups;
    
        if (read(wakealarm_fd, &wakeups, sizeof(wakeups)) == -1) {
            KLOG_ERROR(LOG_TAG, "wakealarm_event: read wakealarm fd failed
    ");
            return;
        }
    
        periodic_chores();
    }
    

    IPC通信:binder_event

    和安卓系统各个部分进行相互通知,这里不展开了。

    #define LOG_TAG "healthd-android"
    
    #include <healthd/healthd.h>
    #include "BatteryPropertiesRegistrar.h"
    
    #include <binder/IPCThreadState.h>
    #include <binder/ProcessState.h>
    #include <cutils/klog.h>
    #include <sys/epoll.h>
    
    using namespace android;
    
    static int gBinderFd;
    static sp<BatteryPropertiesRegistrar> gBatteryPropertiesRegistrar;
    
    void healthd_mode_android_battery_update(
        struct android::BatteryProperties *props) {
        if (gBatteryPropertiesRegistrar != NULL)
            gBatteryPropertiesRegistrar->notifyListeners(*props);
    
        return;
    }
    
    int healthd_mode_android_preparetowait(void) {
        IPCThreadState::self()->flushCommands();
        return -1;
    }
    
    static void binder_event(uint32_t /*epevents*/) {
        IPCThreadState::self()->handlePolledCommands();
    }
    
    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
    ");
        }
    
        gBatteryPropertiesRegistrar = new BatteryPropertiesRegistrar();
        gBatteryPropertiesRegistrar->publish(gBatteryPropertiesRegistrar);
    }
    

    内核事件:uevent_event

    监听来自内核的供电子系统(POWER_SUPPLY)的事件

    #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] = '';
        msg[n+1] = '';
        cp = msg;
    
        while (*cp) {
            if (!strcmp(cp, "SUBSYSTEM=" POWER_SUPPLY_SUBSYSTEM)) {
                healthd_battery_update();
                break;
            }
    
            /* advance to after the next  */
            while (*cp++)
                ;
        }
    }
    

    唤醒处理:charger_event_handler

    由于是EVENT_WAKEUP_FD的事件,代表了能够唤醒系统的输入源(键(例如音量键,电源键));

    向下分发了ev,处理用户的输入。

    static void charger_event_handler(uint32_t /*epevents*/)
    {
        int ret;
    
        ret = ev_wait(-1);
        if (!ret)
            ev_dispatch();
    }
    

    输入事件处理:input_callback

    例如音量键,电源键。

    static int input_callback(int fd, unsigned int epevents, void *data)
    {
        struct charger *charger = (struct charger *)data;
        struct input_event ev;
        int ret;
    
        ret = ev_get_input(fd, epevents, &ev);
        if (ret)
            return -1;
        update_input_state(charger, &ev);
        return 0;
    }
    

    input_callback是这样子执行的。

    注册、分发:

    void healthd_mode_charger_init(struct healthd_config* config)
    {
        struct charger *charger = &charger_state;
        // ...
        // 创建了一个epoll
        ret = ev_init(input_callback, charger);
        if (!ret) {
            epollfd = ev_get_epollfd(); // 返回这个epoll的fd
            // 将这个epoll的fd放到了healthd里面的healthd_mainloop中的epoll中统一进行监听
            healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);
        }
    }
    
    static void charger_event_handler(uint32_t /*epevents*/)
    {
        int ret;
    
        ret = ev_wait(-1);
        if (!ret)
            ev_dispatch();
    }
    
    // vendor/qcom/proprietary/fastmmi/libmmi/events.cpp
    void ev_dispatch(void)
    {
        int n;
        int ret;
    
        for (n = 0; n < npolledevents; n++) {
            struct fd_info *fdi = (fd_info *)(polledevents[n].data.ptr);
            ev_callback cb = fdi->cb;
            if (cb)
                cb(fdi->fd, polledevents[n].events, fdi->data);
        }
    }
    

    因此input_callback就通过这样子的方式执行了:

    charger_event_handler
    	->ev_dispatch
    		->input_callback
    			->update_input_state
    				->set_key_callback
    
    update_input_state

    获取key的输入事件,对其他类型的输入事件不作处理。

    static void update_input_state(struct charger *charger,
                                   struct input_event *ev)
    {
        if (ev->type != EV_KEY)
            return;
        set_key_callback(ev->code, ev->value, charger);
    }
    
    set_key_callback
    static int set_key_callback(int code, int value, void *data)
    {
        struct charger *charger = (struct charger *)data;
        int64_t now = curr_time_ms();
        int down = !!value;
    
        if (code > KEY_MAX)
            return -1;
    
        /* ignore events that don't modify our state */
        if (charger->keys[code].down == down)
            return 0;
    
        /* only record the down even timestamp, as the amount
         * of time the key spent not being pressed is not useful */
        if (down)
            charger->keys[code].timestamp = now;
        charger->keys[code].down = down;
        charger->keys[code].pending = true;
        if (down) {
            LOGV("[%" PRId64 "] key[%d] down
    ", now, code);
        } else {
            int64_t duration = now - charger->keys[code].timestamp;
            int64_t secs = duration / 1000;
            int64_t msecs = duration - secs * 1000;
            LOGV("[%" PRId64 "] key[%d] up (was down for %" PRId64 ".%" PRId64 "sec)
    ",
                 now, code, secs, msecs);
        }
    
        return 0;
    }
    

    将按键的code码和按下和抬起的状态,pending状态存入charger->keys中。

    由于按键唤醒了healthd进程,处理完按键事件后,会再次调用healthd_mode_charger_heartbeat之后再进入poll_wait状态

    healthd_mode_charger_heartbeat

    还记得吗,在动画的循环中会执行这个函数进行处理:

    void healthd_mode_charger_heartbeat()
    {
        struct charger *charger = &charger_state;
        int64_t now = curr_time_ms();
    	// 处理接收进来的输入事件
        handle_input_state(charger, now);
        handle_power_supply_state(charger, now);
    
        /* do screen update last in case any of the above want to start
         * screen transitions (animations, etc)
         */
        update_screen_state(charger, now);
    }
    
    handle_input_state
    static void handle_input_state(struct charger *charger, int64_t now)
    {
        // 处理按键,关键函数
        process_key(charger, KEY_POWER, now);
        process_key(charger, KEY_BACK, now);
    
        if (charger->next_key_check != -1 && now > charger->next_key_check)
            charger->next_key_check = -1;
    }
    

    调用process_key时,传入了KEY_POWERKEY_BACK,代表需要处理的值,这个函数很重要,重点关注。

    process_key

    针对KEY_POWER的处理是这样子的:

    • 1、如果上次电源键按下设置的2s超时时间到了,且当前的按键依然是按下状态,那么判定用户是希望重启系统而不是点亮屏幕继续保持关机充电。
    • 2、如果在上次按下电源键的2s时间内有抬起的动作,那么判定用户的意图只是希望看下当前充电的状态(电量)
    • 3、如果用户是在屏幕亮起的状态下按了电源键,我们认为用户是想要灭屏

    针对KEY_BACK的处理与KEY_POWER类似,只不过少了针对第一点(用户希望重启系统而不是点亮屏幕)的重启系统判断

    • 1、如果在上次按下电源键的2s时间内有抬起的动作,那么判定用户的意图只是希望看下当前充电的状态(电量)
    • 2、如果用户是在屏幕亮起的状态下按了电源键,我们认为用户是想要灭屏
    static void process_key(struct charger *charger, int code, int64_t now)
    {
        struct animation *batt_anim = charger->batt_anim;
        struct key_state *key = &charger->keys[code];
    
        if (code == KEY_POWER) {
            if (key->down) {
                int64_t reboot_timeout = key->timestamp + POWER_ON_KEY_TIME;
    
                /*make sure backlight turn off before fb ready for display*
                 *when press the power key */
                if (charger->charger_power_key == false) {
                    healthd_board_mode_charger_set_backlight(false);
                    gr_fb_blank(true);
                    charger->charger_power_key = true;
                }
    
                // 如果上次电源键按下设置的2s超时时间到了,且当前的按键依然是按下状态,
                // 那么我们判定用户是希望重启系统而不是点亮屏幕继续保持关机充电。
                if (now >= reboot_timeout) {
                    /* We do not currently support booting from charger mode on
                       all devices. Check the property and continue booting or reboot
                       accordingly. */
                    if (property_get_bool("ro.enable_boot_charger_mode", false)) {
                        LOGW("[%" PRId64 "] booting from charger mode
    ", now);
                        property_set("sys.boot_from_charger_mode", "1");
                    } else {
                        if (charger->batt_anim->cur_level >= charger->boot_min_cap) {
                            LOGW("[%" PRId64 "] rebooting
    ", now);
                            android_reboot(ANDROID_RB_RESTART, 0, 0);
                        } else {
                            LOGV("[%" PRId64 "] ignore power-button press, battery level "
                                 "less than minimum
    ", now);
                        }
                    }
                } else {
                    /* if the key is pressed but timeout hasn't expired,
                     * make sure we wake up at the right-ish time to check
                     */
                    // 第一次按下按键会先进入这儿
                    // 意义:更新poll_wait的超时时间为2s
                    set_next_key_check(charger, key, POWER_ON_KEY_TIME);
                }
            } else {
                if (key->pending) {
                    /* If key is pressed when the animation is not running, kick
                     * the animation and quite suspend; If key is pressed when
                     * the animation is running, turn off the animation and request
                     * suspend.
                     */
                    if (!batt_anim->run) {
                        // 如果在上次按下电源键的2s时间内有抬起的动作,
                        // 那么判定用户的意图只是希望看下当前充电的状态(电量)
                        kick_animation(batt_anim);
                        request_suspend(false);
                    } else {
                        // 如果用户是在屏幕亮起的状态下按了电源键,我们认为用户是想要灭屏
                        reset_animation(batt_anim);
                        charger->next_screen_transition = -1;
                        healthd_board_mode_charger_set_backlight(false);
                        gr_fb_blank(true);
                        if (charger->charger_connected)
                            request_suspend(true);
                    }
                }
            }
        }
        else if(code == KEY_BACK)
        {
            if (key->down) {
                /*make sure backlight turn off before fb ready for display*
                        *when press the power key */
                if (charger->charger_power_key == false) {
                    healthd_board_mode_charger_set_backlight(false);
                    gr_fb_blank(true);
                    healthd_board_mode_charger_set_backlight(true);
                    charger->charger_power_key = true;
                }
    
            } else {
                if (key->pending) {
                    /* If key is pressed when the animation is not running, kick
                            * the animation and quite suspend; If key is pressed when
                            * the animation is running, turn off the animation and request
                            * suspend.
                            */
                    if (!batt_anim->run) {
                        kick_animation(batt_anim);
                        request_suspend(false);
                    } else {
                        reset_animation(batt_anim);
                        charger->next_screen_transition = -1;
                        healthd_board_mode_charger_set_backlight(false);
                        gr_fb_blank(true);
                        if (charger->charger_connected)
                            request_suspend(true);
                    }
                }
            }
    
        }
    
        key->pending = false;
    }
    

    只有在按键抬起的时候,batt_anim->run的状态才会发生改变,kick_animation会将batt_anim->run置为true,从而使能动画显示。

    之后在处理完本次按键事件后,会通过调用healthd_mode_ops->heartbeat()(即healthd_mode_charger_heartbeat)开启动画的显示,随后更新超时时间,进入三轮动画的显示流程。

    内核主动通过uevent上报事件

    uevent_event
    	->healthd_battery_update
    		->BatteryMonitor->update()
    			->healthd_mode_charger_battery_update()
    

    healthd_mode_charger_battery_update函数很简单,只是简单的将从内核获取的prop信息传递给batt_prop

    随后代码流程会回到healthd_mainloop中:

    healthd_mainloop:
    	healthd_mode_ops->heartbeat();
    	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);
        ...
    

    显然之后还会CALL到heartbeat,之后再更新epoll的超时时间heartbeat在关机充电的时候指向healthd_mode_charger_heartbeat

    动画的循环

    main:
        periodic_chores();
        healthd_mode_ops->heartbeat();
    

    healthd_mainloop

    在主循环中,会定期去处理

    // system/core/healthd/healthd.cpp
    static void healthd_mainloop(void) {
        int nevents = 0;
        while (1) {
            struct epoll_event events[eventct];
            int timeout = awake_poll_interval;
            int mode_timeout;
    
            /* Don't wait for first timer timeout to run periodic chores */
            if (!nevents)
                periodic_chores();
    
            // 执行 healthd_mode_charger_heartbeat
            healthd_mode_ops->heartbeat();
    
            // 执行 healthd_mode_charger_preparetowait
            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
    ");
                break;
            }
    
            // 执行通过healthd_register_event注册过的事件
            for (int n = 0; n < nevents; ++n) {
                if (events[n].data.ptr)
                    (*(void (*)(int))events[n].data.ptr)(events[n].events);
            }
        }
    
        return;
    }
    

    periodic_chores

    static void periodic_chores() {
        healthd_battery_update();
    }
    

    直接调用了healthd_battery_update,那么继续看下去

    healthd_battery_update

    调用

    void healthd_battery_update(void) {
        // Fast wake interval when on charger (watch for overheat);
        // slow wake interval when on battery (watch for drained battery).
    
        // 执行了BatteryMonitor::update
       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);
    
        // During awake periods poll at fast rate.  If wake alarm is set at fast
        // rate then just use the alarm; if wake alarm is set at slow rate then
        // poll at fast rate while awake and let alarm wake up at slow rate when
        // asleep.
    
        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;
    }
    
    插入USB进行关机充电会直接显示三轮动画

    gBatteryMonitor->update()实际上会执行BatteryMonitor::update

    从而导致healthd_mode_charger_battery_update的执行

    healthd_mode_charger_battery_update
    void healthd_mode_charger_battery_update(
        struct android::BatteryProperties *props)
    {
        struct charger *charger = &charger_state;
    
        charger->charger_connected =
            props->chargerAcOnline || props->chargerUsbOnline ||
            props->chargerWirelessOnline;
    
    	//第一次进入have_battery_state为false,之后一直为true
        if (!charger->have_battery_state) {
            charger->have_battery_state = true;
            charger->next_screen_transition = curr_time_ms() - 1;
            reset_animation(charger->batt_anim);
            kick_animation(charger->batt_anim);
        }
        batt_prop = props;
    }
    

    通过kick_animation使能动画显示anim->run = true;之后再进入healthd_mode_ops->heartbeat();从而进入动画的显示流程。

    static void kick_animation(struct animation *anim)
    {
        anim->run = true;
    }
    

    healthd_mode_charger_heartbeat

    void healthd_mode_charger_heartbeat()
    {
        struct charger *charger = &charger_state;
        int64_t now = curr_time_ms();
    	// 处理接收进来的输入事件
        handle_input_state(charger, now);
        handle_power_supply_state(charger, now);
    
        /* do screen update last in case any of the above want to start
         * screen transitions (animations, etc)
         */
        // 刷新屏幕显示
        update_screen_state(charger, now);
    }
    

    如果是uevent事件,那么按键处理相关是不会触发的,同时动画相关的屏幕显示其实也不会触发。
    原因在于batt_anim->run,并没有被置1。那么这里还有一种情况就是在三轮动画的显示流程中,如果有来自于内核的uevent电源事件到来怎么处理。

    healthd_mode_charger_preparetowait

    int healthd_mode_charger_preparetowait(void)
    {
        struct charger *charger = &charger_state;
        int64_t now = curr_time_ms();
        int64_t next_event = INT64_MAX;
        int64_t timeout;
    
        LOGV("[%" PRId64 "] next screen: %" PRId64 " next key: %" PRId64 " next pwr: %" PRId64 "
    ", now,
             charger->next_screen_transition, charger->next_key_check,
             charger->next_pwr_check);
    
        if (charger->next_screen_transition != -1)
            next_event = charger->next_screen_transition;
        if (charger->next_key_check != -1 && charger->next_key_check < next_event)
            next_event = charger->next_key_check;
        if (charger->next_pwr_check != -1 && charger->next_pwr_check < next_event)
            next_event = charger->next_pwr_check;
    
        if (next_event != -1 && next_event != INT64_MAX)
            timeout = max(0, next_event - now);
        else
            timeout = -1;
    
       return (int)timeout;
    }
    

    动画显示过程

    update_screen_state

    动画显示无非就是判断当前电池的状态,然后选择到对应的frame,再通过healthd_draw显示到屏幕上面。

    对于动画,每轮显示会进入update_screen_state多次,与下面的2个变量有关;

    • batt_anim->num_cycles :充电完整动画重复显示的次数
    • batt_anim->num_frames :每次充电完整动画需要分解为几次显示

    其中每显示一帧,都会进入update_screen_state一次,最后灭屏进入suspend,细分的话有2种场景:

    1、按键唤醒或者插入充电器的时候,当前电量没到最后一个frame的范围,那么连续显示当前电量的frame到电量满的frame动画,并重复三次,其中每轮动画中,当前电池电量对应的frame显示1.5秒,除此之外的每个frame默认显示时间为750ms。

    2、按键唤醒或者插入充电器的时候,当前电量已经达到最后一个frame的范围,那么显示充电满的frame,同样连续显示3次,每次1.5s。

    // system/core/healthd/healthd_mode_charger.cpp
    static void update_screen_state(struct charger *charger, int64_t now)
    {
        struct animation *batt_anim = charger->batt_anim;
        int disp_time;
    
    	/*
    		1. batt_anim->run如果设置为false,则意味着不显示动画,就此返回
    		2. 如果当前时间小于下一次动画的触发时间,就此返回
    	*/
        /*  如果是uevent事件,那么按键处理相关是不会触发的,同时动画相关的屏幕显示其实也不会触发。
            原因在于batt_anim->run,并没有被置1。
            那么这里还有一种情况:
            	在三轮动画的显示流程中,如果有来自于内核的uevent电源事件到来时。
            	由于当前时间小于下一次的frame显示时间而返回
        */
        if (!batt_anim->run || now < charger->next_screen_transition) return;
    
        // 如下一段代码主要是为了点亮屏幕,如果无法点亮屏幕就退出
        if (!minui_inited) {
            if (healthd_config && healthd_config->screen_on) {
                if (!healthd_config->screen_on(batt_prop)) {
                    LOGV("[%" PRId64 "] leave screen off
    ", now);
                    batt_anim->run = false;
                    charger->next_screen_transition = -1;
                    if (charger->charger_connected)
                        request_suspend(true);
                    return;
                }
            }
    		//初始化显示
            gr_init();
            //获取字符显示的长和宽
            gr_font_size(gr_sys_font(), &char_width, &char_height);
            init_status_display(batt_anim);
    
            #ifndef CHARGER_DISABLE_INIT_BLANK
            healthd_board_mode_charger_set_backlight(false);
            gr_fb_blank(true);
            #endif
            minui_inited = true;
        }
    
        /* animation is over, blank screen and leave */
        // 如果本轮显示的次数达到了num_cycles(默认3次,4.5秒),则清屏,关背光,进入suspend
        if (batt_anim->num_cycles > 0 && batt_anim->cur_cycle == batt_anim->num_cycles) {
            reset_animation(batt_anim);
            charger->next_screen_transition = -1;
            charger->charger_power_key = false;
            healthd_board_mode_charger_set_backlight(false);
            gr_fb_blank(true);
            LOGV("[%" PRId64 "] animation done
    ", now);
            if (charger->charger_connected)
                request_suspend(true);
            return;
        }
    
        // 获取该帧显示的时间,第一次进入,batt_anim->cur_frame是为0的
        // 取出第0帧的图像需要显示的时间(750ms)
        disp_time = batt_anim->frames[batt_anim->cur_frame].disp_time;
    
        if (batt_anim->cur_cycle == 0 && batt_anim->cur_frame == 0)
            redraw_screen(charger);
    
        /* animation starting, set up the animation */
        // 动画开始,根据读到的电池百分比,选择当前应该显示的图片,并重新计算距离下次显示需要等待的最小时间间隔
        if (batt_anim->cur_frame == 0) {
            LOGV("[%" PRId64 "] animation starting
    ", now);
            if (batt_prop) {
                // // 从battery service中获取当前点亮和电池状态
                batt_anim->cur_level = batt_prop->batteryLevel;
                batt_anim->cur_status = batt_prop->batteryStatus;
                if (batt_prop->batteryLevel >= 0 && batt_anim->num_frames != 0) {
                    /* find first frame given current battery level */
                    // 找到第一张符合对应电量要求的帧
                    for (int i = 0; i < batt_anim->num_frames; i++) {
                        if (batt_anim->cur_level >= batt_anim->frames[i].min_level &&
                            batt_anim->cur_level <= batt_anim->frames[i].max_level) {
                            batt_anim->cur_frame = i;
                            break;
                        }
                    }
    
                    LOGE("animation starting cur_frame =%d 
    ", batt_anim->cur_frame);
    
                    // repeat the first frame first_frame_repeats times
                    // 动画第一帧可以设置重复显示次数,所以需要更新第一帧显示的时间
                    disp_time = batt_anim->frames[batt_anim->cur_frame].disp_time *
                        batt_anim->first_frame_repeats;
                }
            }
        }
    
        /* unblank the screen on first cycle */
        // 如果是本轮显示的第一次,则显示屏幕内容点亮背光灯
        if (batt_anim->cur_cycle == 0) {
            gr_fb_blank(false);
            gr_flip();
            healthd_board_mode_charger_set_backlight(true);
        }
    
        /* draw the new frame (@ cur_frame) */
        //开始显示动画
        redraw_screen(charger);
    
        /* if we don't have anim frames, we only have one image, so just bump
         * the cycle counter and exit
         */
        // 如果没有动画资源,则自增圈数,更新距离下次显示的最小时间间隔,然后就此返回
        if (batt_anim->num_frames == 0 || batt_anim->cur_level < 0) {
            LOGW("[%" PRId64 "] animation missing or unknown battery status
    ", now);
            charger->next_screen_transition = now + BATTERY_UNKNOWN_TIME;
            batt_anim->cur_cycle++;
            return;
        }
    
        /* schedule next screen transition */
        // 计算下一张动画的切换时间
        charger->next_screen_transition = now + disp_time;
    
        /* advance frame cntr to the next valid frame only if we are charging
         * if necessary, advance cycle cntr, and reset frame cntr
         */
        // 下面的逻辑是:只要充电的电量没有达到最后一个frame的阶段,那么直接取出下一次显示需要的frame用于显示。
        // 如果还插着充电器
        if (charger->charger_connected) {
    
            LOGE("update screen state cur cycle=%d 
    ", batt_anim->cur_cycle);
            LOGE("update screen state cur_frame=%d 
    ", batt_anim->cur_frame);
            LOGE("update screen state cur_level=%d 
    ", batt_anim->cur_level);
            
            // 记录下一帧动画的索引
            batt_anim->cur_cycle++;
    
    
            /* don't reset the cycle counter, since we use that as a signal
                 * in a test above to check if animation is over*/
            // 如果当前的电量已经没有落在下一帧中,那么需要调整下一帧的动画到合理的位置
            while (batt_anim->cur_frame < batt_anim->num_frames &&
                   (batt_anim->cur_level < batt_anim->frames[batt_anim->cur_frame].min_level ||
                    batt_anim->cur_level > batt_anim->frames[batt_anim->cur_frame].max_level)) {
                batt_anim->cur_frame++;
            }
            // 如果当前已经是最后一帧,那么将下一帧重置为0
            if (batt_anim->cur_frame >= batt_anim->num_frames) {
                batt_anim->cur_cycle++;
                batt_anim->cur_frame = 0;
    
            }
    
    
        } else {
            /* Stop animating if we're not charging.
             * If we stop it immediately instead of going through this loop, then
             * the animation would stop somewhere in the middle.
             */
            // 拔出了充电器,重置变量
            batt_anim->cur_frame = 0;
            batt_anim->cur_cycle++;
        }
    }
    

    redraw_screen

    // system/core/healthd/healthd_mode_charger.cpp
    static void redraw_screen(struct charger *charger)
    {
        struct animation *batt_anim = charger->batt_anim;
    
        clear_screen();
    
        /* try to display *something* */
        // 首先判断电池状态和动画内容是否有问题
        if (batt_anim->cur_level < 0 || batt_anim->num_frames == 0)
            draw_unknown(charger); // 有问题则显示battery_fail动画
        else
            draw_battery(charger); // 否则,显示电池充电动画
        // 刷新屏幕
        gr_flip();
    }
    
    static void draw_battery(const struct charger* charger)
    {
        const struct animation& anim = *charger->batt_anim;
        const struct animation::frame& frame = anim.frames[anim.cur_frame];
    
        if (anim.num_frames != 0) {
            // 如函数名,将动画显示到屏幕中间
            draw_surface_centered(frame.surface);
            LOGV("drawing frame #%d min_cap=%d time=%d
    ",
                 anim.cur_frame, frame.min_level,
                 frame.disp_time);
        }
        // 如果有设置显示时钟和电池百分百,则同时描绘
        draw_clock(anim);
        draw_percent(anim);
    }
    

    所以,动画的连续显示也显得比较简单。如果想要做修改,理清显示流程即可。

    另外,看上面的流程可以知道,如果需要显示电量百分比,可以提供对应的font字体文件,当然更简单的就是提供110和百分号的png图,然后按照上面解析动画的流程一样,将百分比对应的图片也加载进来,最后显示即可。

    附录:关机充电时,按下按键的有关log

    [ 1191.430816] qpnp_kpdpwr_irq into!
    [ 1191.434579] PMIC input: code=116, sts=0x0
    [ 1191.440183] healthd: void healthd_mainloop():epoll_wait is up
    [ 1191.447273] charger: int input_callback(int, unsigned int, void*) into!
    [ 1191.455508] healthd: void healthd_mainloop() ->heartbeat()
    [ 1191.462325] charger: void healthd_mode_charger_heartbeat() into!
    [ 1191.469792] charger: void process_key(charger*, int, int64_t) into! code=116
    [ 1191.478251] charger: void process_key(charger*, int, int64_t) into! key->down=0
    [ 1191.487067] charger: void process_key(charger*, int, int64_t) key->pending=1
    [ 1191.495594] charger: void process_key(charger*, int, int64_t) !batt_anim->run
    [ 1191.504184] charger: void update_screen_state(charger*, int64_t) into!
    [ 1191.512167] charger: void update_screen_state(charger*, int64_t) into!  gr_fb_blank(false)
    
    do_fb_ioctl ->fb_blank->fb_notifier_call_chain ->fb_notifier_callback
    

    附录:ev_init、ev_get_input有关的库

    在高通fastmmi中用到的一个用来处理输入事件的库events,本质是也是一个epoll_ctl

    int ev_init(ev_callback input_cb, void *data)
    {
        DIR *dir;
        struct dirent *de;
        int fd;
        struct epoll_event ev;
        bool epollctlfail = false;
    
        epollfd = epoll_create(MAX_DEVICES + MAX_MISC_FDS);
        if (epollfd == -1)
            return -1;
    
        dir = opendir("/dev/input");
        if(dir != 0) {
            while((de = readdir(dir))) {
                unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)];
    
    //            fprintf(stderr,"/dev/input/%s
    ", de->d_name);
                if(strncmp(de->d_name,"event",5)) continue;
                fd = openat(dirfd(dir), de->d_name, O_RDONLY);
                if(fd < 0) continue;
    
                /* read the evbits of the input device */
                if (ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) < 0) {
                    close(fd);
                    continue;
                }
    
                /* TODO: add ability to specify event masks. For now, just assume
                 * that only EV_KEY and EV_REL event types are ever needed. */
                if(!test_bit(EV_KEY, ev_bits) && !test_bit(EV_REL, ev_bits) && !test_bit(EV_SW, ev_bits)) {
                    close(fd);
                    continue;
                } else if(test_bit(EV_ABS, ev_bits) && test_bit(EV_REL, ev_bits)) {
                    close(fd);
                    continue;
                }
    
                ev.events = EPOLLIN | EPOLLWAKEUP;
                ev.data.ptr = (void *)&ev_fdinfo[ev_count];
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) {
                    close(fd);
                    epollctlfail = true;
                    continue;
                }
    
                ev_fdinfo[ev_count].fd = fd;
                ev_fdinfo[ev_count].cb = input_cb;
                ev_fdinfo[ev_count].data = data;
                ev_count++;
                ev_dev_count++;
                if(ev_dev_count == MAX_DEVICES) break;
            }
        }
        if (epollctlfail && !ev_count) {
            close(epollfd);
            epollfd = -1;
            return -1;
        }
    
        return 0;
    }
    
    如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。
    若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
    博客地址:https://www.cnblogs.com/schips/
  • 相关阅读:
    数据结构小总结(成都磨子桥技工学校数据结构前12题)
    Scrum 冲刺博客第二篇
    Scrum 冲刺博客第一篇
    centos部署keepalived服务
    第四周作业
    Svelte 中怎样做双向数据绑定
    Svelte 中多层组件事件转发
    Svelte 中的事件修饰符
    怎样在 Svelte 中设置自定义事件
    怎样使用 Svelte 中的异步块
  • 原文地址:https://www.cnblogs.com/schips/p/15224691.html
Copyright © 2020-2023  润新知