0
点赞
收藏
分享

微信扫一扫

OpenHarmony 资源调度之智能感知调度源码分析

1 简介

智能感知调度部件位于全局资源调度管控子系统中,通过帧感知调度机制,更新进程调度分组。通过获取应用的生命周期状态、应用绘帧等信息,调节内核调度参数,从而控制内核调度行为,保障系统进程调度供给。
图 1 框架图
frame_aware_fwk.png

智能感知调度部件根据执行时所属线程进行划分,可包含两大组件,即运行在App进程的绘帧信息收集组件和运行在系统服务进程的帧感知调度机制组件,每个组件分为若干模块。

  • 绘帧信息收集组件:应用绘帧感知主要负责调节内核调度的参数,进行线程负载的缩放。当用户在APP界面滑动时,识别出应用的关键线程(诸如绘帧主线程、渲染线程),感知应用绘帧子过程的执行情况,根据是否超时来判断是否调整内核调度参数,进行实时的资源供给,进行优先调度。
  • 帧感知调度机制组件:应用线程调度机制,作为应用绘帧感知实现的基础,主要负责管控线程组及线程优先级,实现应用线程的统一管理,保证整个系统性能。核心思想就是分组,即根据不同线程组提供不同的供给能力。把用户敏感度线程添加进同一个线程组,在应用启动、前后台切换等多个应用变化场景,提供资源优先供给。

2 源码分析

2.1 类关系

图 2 类图
未标题3.png
上图列出了本模块中主要类及其之间的调用、依赖关系,下面结合各流程进行介绍。

2.2 初始化

图 3 初始化时序图
res_sched_init.png

系统能力注册
在文件res_sched_service_ability.cpp中调用宏"REGISTER_SYSTEM_ABILITY_BY_ID"注册系统能力ResSchedServiceAbility
```c++
namespace OHOS {
namespace ResourceSchedule {
REGISTER_SYSTEM_ABILITY_BY_ID(ResSchedServiceAbility, RES_SCHED_SYS_ABILITY_ID, true);


**系统能力初始化**
系统能力初始化分为资源调度管理初始化、系统服务发布、cpu分组调度初始化、注册监听的SA四个子过程。

```c++
void ResSchedServiceAbility::OnStart()
{
ResSchedMgr::GetInstance().Init();
if (!service_) {
try {
service_ = new ResSchedService();
} catch(const std::bad_alloc &e) {
RESSCHED_LOGE("ResSchedServiceAbility:: new ResSchedService failed.");
}
}
if (!Publish(service_)) {
RESSCHED_LOGE("ResSchedServiceAbility:: Register service failed.");
}
CgroupSchedInit();
AddSystemAbilityListener(APP_MGR_SERVICE_ID);
AddSystemAbilityListener(WINDOW_MANAGER_SERVICE_ID);
AddSystemAbilityListener(BACKGROUND_TASK_MANAGER_SERVICE_ID);
RESSCHED_LOGI("ResSchedServiceAbility ::OnStart.");
}

资源调度管理初始化过程主要包括frame_aware和socperf两个的插件加载及函数指针获取,eventHandler对象创建。插件so库的路径配置在xml文件"res_sched_plugin_switch.xml"中。
```c++
void ResSchedMgr::Init()
{
PluginMgr::GetInstance().Init();

if (!mainHandler_) {
mainHandler_ = std::make_shared<EventHandler>(EventRunner::Create(RSS_THREAD_NAME));
}

}

void PluginMgr::Init()
{

...
//读取res_sched_plugin_switch.xml
if (!pluginSwitch_) {
pluginSwitch_ = make_unique<PluginSwitch>();

...

bool loadRet = pluginSwitch_->LoadFromConfigFile(realPath);
if (!loadRet) {
RESSCHED_LOGW("PluginMgr::Init load switch config file failed!");
}
}
//读取res_sched_config.xml
if (!configReader_) {

...

std::string realPath(tmpPath);
bool loadRet = configReader_->LoadFromCustConfigFile(realPath);
if (!loadRet) {
RESSCHED_LOGW("PluginMgr::Init load config file failed!");
}
}

StackProtect();
LoadPlugin(); //用linux系统接口dlopen加载so插件
if (!dispatcherHandler_) {
dispatcherHandler_ = std::make_shared<EventHandler>(EventRunner::Create(RUNNER_NAME));
}
RESSCHED_LOGI("PluginMgr::Init success!");

}

cpu分组调度初始化主要分为supervisor、cgroupHandler和cgroupAdjuster三个子模块的初始化。
```c++
void SchedController::Init()
{
ChronoScope cs("Init SchedController.");
// Init supervisor which contains cached data for ccgroup controller.
InitSupervisor();
// Init cgroup handler thread
InitCgroupHandler();
// Init cgroup adjuster thread
InitCgroupAdjuster();
}

注册监听的SA调用SA框架提供的接口完成监听SA的注册, 目前监听了三个SA,分别是应用管理服务(id: APP_MGR_SERVICE_ID)、窗口管理服务(id: WINDOW_MANAGER_SERVICE_ID)和后台任务管理服务(id: BACKGROUND_TASK_MANAGER_SERVICE_ID).
```c++
bool SystemAbility::AddSystemAbilityListener(int32t systemAbilityId)
{
HILOGD(TAG, "SA:%{public}d, listenerSA:%{public}d", systemAbilityId, saId
);
return LocalAbilityManager::GetInstance().AddSystemAbilityListener(systemAbilityId, saId_);
}

bool LocalAbilityManager::AddSystemAbilityListener(int32_t systemAbilityId, int32_t listenerSaId)
{
...

auto samgrProxy = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager();
if (samgrProxy == nullptr) {
HILOGE(TAG, "failed to get samgrProxy");
return false;
}

{
...

auto& listenerSaIdList = listenerMap_[systemAbilityId];
auto iter = std::find_if(listenerSaIdList.begin(), listenerSaIdList.end(), [listenerSaId](int32_t SaId) {
return SaId == listenerSaId;
});
if (iter == listenerSaIdList.end()) {
listenerSaIdList.emplace_back(listenerSaId);
}
...
}

int32_t ret = samgrProxy->SubscribeSystemAbility(systemAbilityId, GetSystemAbilityStatusChange());
if (ret) {
HILOGE(TAG, "failed to subscribe sa:%{public}d, process name:%{public}s", systemAbilityId,
Str16ToStr8(procName_).c_str());
return false;
}
return true;

}


### 2.3 绘帧信息收集流程

**图 4**
![draw_frame_sched.png](https://dl-harmonyos.51cto.com/images/202206/441eacb16847c2f991e3185bd1817324282636.png?x-oss-process=image/resize,w_820,h_1858)

绘帧信息收集主要根据场景分为输入子系统手势变化等的感知调度,ace子系统的窗口变化等的感知调度,图形子系统的渲染、动画感知调度。图4描述的是BeginListFling、BeginFlushBuild、Render三个绘帧信息资源调度时序图,其他的流程类似,不一一列举。绘帧信息收集资源调度的最终是通过内核接口ioctrl写关联线程组"/dev/sched_rtg_ctrl", 包括线程加入、移除关联线程组,设置关联线程组的属性等等。
```c++
void RSRenderThread::Render()
{
ROSEN_TRACE_BEGIN(BYTRACE_TAG_GRAPHIC_AGP, "RSRenderThread::Render");
if (RsFrameReport::GetInstance().GetEnable()) {
RsFrameReport::GetInstance().RenderStart();
}

...

rootNode->Prepare(visitor_);
rootNode->Process(visitor_);
ROSEN_TRACE_END(BYTRACE_TAG_GRAPHIC_AGP);
}

void RsFrameReport::RenderStart()
{
renderStartRunc_ = (RenderStartFunc)LoadSymbol("RenderStart");
if (renderStartRunc_ != nullptr) {
renderStartRunc_();
}

...

}

extern "C" void RenderStart()
{
FrameUiIntf::GetInstance().RenderStart();
}

void FrameUiIntf::RenderStart() const
{
if (!inited) {
return;
}
FrameMsgMgr::GetInstance().EventUpdate(FrameEvent::EVENT_RENDER_START);
}

void FrameMsgMgr::EventUpdate(FrameEvent event)
{
switch (event) {
case FrameEvent::EVENT_SET_PARAM:
SetSchedParam();
break;
default:
HandleFrameMsgKey(event);
break;
}
}

bool FrameMsgMgr::HandleFrameMsgKey(FrameEvent event)
{
std::map<FrameEvent, PHandle>::iterator iter = m_frameMsgKeyToFunc.find(event);

...

PHandle pFunction = iter->second;
(this->*pFunction)();
return true;
}

void FrameMsgMgr::FrameMapKeyToFunc()
{
...

m_frameMsgKeyToFunc[FrameEvent::EVENT_RENDER_START] = &FrameMsgMgr::RenderStart;
m_frameMsgKeyToFunc[FrameEvent::EVENT_SEND_COMMANDS_START] = &FrameMsgMgr::SendCommandsStart;
m_frameMsgKeyToFunc[FrameEvent::EVENT_END_FRAME] = &FrameMsgMgr::HandleEndFrame;
}

void FrameMsgMgr::RenderStart()
{
FrameSceneSched *scene = GetSceneHandler();

...

scene->RenderStart();
}

void RmeCoreSched::RenderStart()
{
RmeTraceBegin(("FrameS-SetMargin:" + to_string(m_rtg) + " margin:" + to_string(MARGIN_THREE)).c_str());
SetMargin(m_rtg, MARGIN_THREE);
RmeTraceEnd();
}

int SetMargin(int grpId, int stateParam)
{
...

int fd = BasicOpenRtgNode();
if (fd < 0) {
return fd;
}
ret = ioctl(fd, CMD_ID_SET_MARGIN, &state_data);

...

}

2.4 帧感知调度流程

下图是窗口焦点变化的资源调度时序图,其他的资源调度流程与其类似,不一一列举。
图 5

win_chang_sched.png

  • 被监听的SA的相关属性或状态变化,调用订阅的对象接口进行响应。
    ```c++
    void WindowStateObserver::OnFocused(const sptr<FocusChangeInfo>& focusChangeInfo)
    {
    auto cgHander = SchedController::GetInstance().GetCgroupEventHandler();
    if (cgHander && focusChangeInfo) {
    auto windowId = focusChangeInfo->windowId_;
    auto token = reinterpret_cast<uint64t>(focusChangeInfo->abilityToken.GetRefPtr());
    auto windowType = focusChangeInfo->windowType;
    auto displayId = focusChangeInfo->displayId
    ;
    auto pid = focusChangeInfo->pid;
    auto uid = focusChangeInfo->uid
    ;

    cgHander->PostTask([cgHander, windowId, token, windowType, displayId, pid, uid] {
    cgHander->HandleFocusedWindow(windowId, token, windowType, displayId, pid, uid);
    });

    }
    }

  • 该对象通过eventHandler机制在event runner线程中调整进程组, 通过进程间通讯IPC通知资源调度服务进程。
    cgroup处理窗口焦点的eventHandler回调函数
    ```c++
    void CgroupEventHandler::HandleFocusedWindow(uint32_t windowId, uint64_t abilityToken,
    WindowType windowType, uint64_t displayId, int32_t pid, int32_t uid)
    {
    ...

    supervisor_->focusedApp_ = app;
    SchedController::GetInstance().AdjustAllProcessGroup(*(app.get()), AdjustSource::ADJS_FOCUSED_WINDOW);

    }
    payload["bundleName"] = app->name_;
    ResSchedUtils::GetInstance().ReportDataInProcess(ResType::RES_TYPE_WINDOW_FOCUS, 0, payload);
    }

调整进程组
```c++
void SchedController::AdjustAllProcessGroup(Application &app, AdjustSource source)
{
...

cgAdjuster_->AdjustAllProcessGroup(app, source);

}

void CgroupAdjuster::AdjustProcessGroup(Application &app, ProcessRecord &pr, AdjustSource source)
{
CGS_LOGI("%{public}s for %{public}d, source : %{public}d", func, pr.GetPid(), source);
ComputeProcessGroup(app, pr, source);
ApplyProcessGroup(app, pr);
}

void CgroupAdjuster::ComputeProcessGroup(Application &app, ProcessRecord &pr, AdjustSource source)
{
SchedPolicy group = SchedPolicy::SP_DEFAULT;

...

if (group == SchedPolicy::SP_BACKGROUND && pr.runningContinuousTask_) {
group = SchedPolicy::SP_FOREGROUND; // move background key task to fg
}

pr.setSchedGroup_ = group;
}

}

void CgroupAdjuster::ApplyProcessGroup(Application &app, ProcessRecord &pr)
{
ChronoScope cs("ApplyProcessGroup");
if (pr.curSchedGroup != pr.setSchedGroup) {
pidt pid = pr.GetPid();
int ret = CgroupSetting::SetThreadGroupSchedPolicy(pid, (int)pr.setSchedGroup
);
if (ret != 0) {
CGSLOGE("%{public}s set %{public}d to group %{public}d failed, ret=%{public}d!",
func, pid, pr.setSchedGroup
, ret);
return;
}

    ...

}

}

通过进程间通讯IPC通知资源调度服务进程
```c++
void ResSchedClient::ReportDataInProcess(uint32_t resType, int64_t value, const Json::Value& payload)
{
RESSCHED_LOGI("ResSchedClient::ReportDataInProcess receive resType = %{public}u, value = %{public}lld.",
resType, value);
ResSchedMgr::GetInstance().ReportData(resType, value, payload);
}

void ResSchedServiceProxy::ReportData(uint32_t resType, int64_t value, const Json::Value& payload)
{
...

error = Remote()->SendRequest(IResSchedService::REPORT_DATA, data, reply, option);
if (error != NO_ERROR) {
RESSCHED_LOGE("Send request error: %{public}d", error);
return;
}
RESSCHED_LOGD("ResSchedServiceProxy::ReportData success.");
}

int ResSchedServiceStub::OnRemoteRequest(uint32_t code, MessageParcel &data,
MessageParcel &reply, MessageOption &option)

{
auto uid = IPCSkeleton::GetCallingUid();
RESSCHED_LOGD("ResSchedServiceStub::OnRemoteRequest, code = %{public}u, flags = %{public}d,"
" uid = %{public}d", code, option.GetFlags(), uid);

auto itFunc = funcMap_.find(code);
if (itFunc != funcMap_.end()) {
auto requestFunc = itFunc->second;
if (requestFunc) {
return requestFunc(data, reply);
}
}
return IPCObjectStub::OnRemoteRequest(code, data, reply, option);
}
  • 资源调度服务进程收到信息后,根据资源调度类型分发任务给frame_aware和socperf两个插件分别进行处理。
    ```c++
    int32_t ResSchedServiceStub::ReportDataInner(MessageParcel& data, [[maybe_unused]] MessageParcel& reply)
    {
    ...

    ReportData(type, value, StringToJson(payload));
    return ERR_OK;
    }

void ResSchedService::ReportData(uint32_t resType, int64_t value, const Json::Value& payload)
{
RESSCHED_LOGI("ResSchedService::ReportData from ipc receive data resType = %{public}d, value = %{public}lld.",
resType, value);
ResSchedMgr::GetInstance().ReportData(resType, value, payload);
}

void ResSchedMgr::ReportData(uint32_t resType, int64_t value, const Json::Value& payload)
{
...

mainHandler_->PostTask([resType, value, payload] {
PluginMgr::GetInstance().DispatchResource(std::make_shared<ResData>(resType, value, payload));
});

}

void PluginMgr::DispatchResource(const std::shared_ptr<ResData>& resData)
{
...

auto iter = resTypeLibMap_.find(resData->resType);
if (iter == resTypeLibMap_.end()) {
RESSCHED_LOGW("PluginMgr::DispatchResource resType no lib register!");
return;
}

...

for (const auto& libPath : iter->second) {
dispatcherHandler_->PostTask(
[libPath, resData, this] { deliverResourceToPlugin(libPath, resData); });
}
}

}

void PluginMgr::deliverResourceToPlugin(const std::string& pluginLib, const std::shared_ptr<ResData>& resData)
{
std::lockguard<std::mutex> autoLock(pluginMutex);
auto itMap = pluginLibMap.find(pluginLib);
if (itMap == pluginLibMap
.end()) {
RESSCHED_LOGE("PluginMgr::deliverResourceToPlugin no plugin %{public}s !", pluginLib.cstr());
return;
}
OnDispatchResourceFunc fun = itMap->second.onDispatchResourceFunc
;
if (!fun) {
RESSCHED_LOGE("PluginMgr::deliverResourceToPlugin no DispatchResourceFun !");
return;
}

auto beginTime = Clock::now();
// if a exception happen, will goto else
if (!sigsetjmp(env, 1)) {
fun(resData);
} else {
return;
}

...

}

- frame_aware插件主要负责关联线程组的操作,ioctrl操作的文件是"/dev/sched_rtg_ctrl"
```c++
void FrameAwarePlugin::DispatchResource(const std::shared_ptr<ResData>& data)
{
...

switch (data->resType) {
case RES_TYPE_APP_STATE_CHANGE:
{
int uid = payload["uid"].asInt();
RESSCHED_LOGD("FrameAwarePlugin::[DispatchResource]:app state! uid:%{public}d", uid);
}
break;
case RES_TYPE_PROCESS_STATE_CHANGE:
{
...

RME::FrameMsgIntf::GetInstance().ReportProcessInfo(pid, tid, state);
RESSCHED_LOGD("FrameAwarePlugin::[DispatchResource]:process info! resType: %{public}u.", data->resType);
}
break;
case RES_TYPE_WINDOW_FOCUS:
{
pid = payload["pid"].asInt();
RME::FrameMsgIntf::GetInstance().ReportWindowFocus(pid, data->value);
RESSCHED_LOGD("FrameAwarePlugin::[DispatchResource]:window focus! resType: %{public}u.", data->resType);
}
break;
default:
RESSCHED_LOGI("FrameAwarePlugin::[DispatchResource]:unknow msg, resType: %{public}u.", data->resType);
break;
}
}

void FrameMsgIntf::ReportWindowFocus(const int pid, const int isFocus)
{
...

threadHandler_->PostTask([pid, isFocus] {
IntelliSenseServer::GetInstance().ReportWindowFocus(pid, isFocus);
});
}

void IntelliSenseServer::ReportWindowFocus(const int pid, int isFocus)
{
if (!m_switch) {
return;
}
RME_FUNCTION_TRACE();
int rtGrp = AppInfoMgr::GetInstance().GetAppRtgrp(pid);
switch (isFocus) {
case static_cast<int>(WindowState::FOCUS_YES): // isFocus: 0
{
rtGrp = RtgMsgMgr::GetInstance().OnForeground("", pid);
AppInfoMgr::GetInstance().OnForegroundChanged(pid, "", rtGrp);
RME_LOGI("[ReportWindowFocus]: Focus yes!rtGrp: %{public}d", rtGrp);
}
break;
case static_cast<int>(WindowState::FOCUS_NO): // isFocus: 1
{
RtgMsgMgr::GetInstance().OnBackground("", pid, rtGrp);
AppInfoMgr::GetInstance().OnBackgroundChanged(pid, "");
RME_LOGI("[ReportWindowFocus]: Focus No!rtGrp: %{public}d", rtGrp);
}
break;
default:
RME_LOGI("[ReportWindowFocus]:unknown msg!");
break;
}
AppInfoMgr::GetInstance().OnWindowFocus(pid, isFocus);
RtgMsgMgr::GetInstance().FocusChanged(pid, isFocus);
}

int RtgMsgMgr::OnForeground(const std::string appName, const int pid)
{
// for multiwindow
RmeTraceBegin(("FrameC-createRtgGrp-pid"+ to_string(pid)).c_str());
int rtGrp = CreateNewRtgGrp(PRIO_TYPE, RT_NUM);
if (rtGrp <= 0) {
RME_LOGE("[OnForeground]: createNewRtgGroup failed! rtGrp:%{public}d, pid: %{public}d", rtGrp, pid);
return rtGrp;
}
RmeTraceEnd();
RmeTraceBegin(("FrameC-addThread-pid:" + to_string(pid), +" rtgrp:" + to_string(rtGrp)).c_str());
int ret = AddThreadToRtg(pid, rtGrp, PID_PRIO_TYPE); // add ui thread
if (ret != 0) {
RME_LOGE("[OnFore]:add thread fail! pid:%{public}d,rtg:%{public}d!ret:%{publid}d", pid, rtGrp, ret);
}
RmeTraceEnd();
return rtGrp;
}

int AddThreadToRtg(int tid, int grpId, int prioType)
{
...

ret = ioctl(fd, CMD_ID_SET_RTG,
close(fd);
return ret;
}
  • socperf主要负责cpu频率的修改,操作文件是"/dev/cpuctl"和"/dev/cpuset"
    ```c++
    void SocPerfPlugin::DispatchResource(const std::shared_ptr<ResData>& data)
    {
    RESSCHED_LOGI("SocPerfPlugin::DispatchResource resType=%{public}u, value=%{public}lld",
    data->resType, data->value);
    auto funcIter = functionMap.find(data->resType);
    if (funcIter != functionMap.end()) {
    auto function = funcIter->second;
    if (function) {
    function(data);
    }
    }
    }

void SocPerfPlugin::HandleWindowFocus(const std::shared_ptr<ResData>& data)
{
if (data->value == WINDOW_FOCUSED) {
RESSCHED_LOGI("SocPerfPlugin: socperf->WINDOW_SWITCH");
OHOS::SOCPERF::SocPerfClient::GetInstance().PerfRequest(PERF_REQUEST_CMD_ID_WINDOW_SWITCH_FIRST, "");
OHOS::SOCPERF::SocPerfClient::GetInstance().PerfRequest(PERF_REQUEST_CMD_ID_WINDOW_SWITCH_SECOND, "");
}
}

void SocPerfClient::PerfRequest(int cmdId, const std::string& msg)
{
if (!CheckClientValid()) {
return;
}
std::string newMsg = AddPidAndTidInfo(msg);
client->PerfRequest(cmdId, newMsg);
}

void SocPerfProxy::PerfRequest(int cmdId, const std::string& msg)
{
...

Remote()->SendRequest(TRANS_IPC_ID_PERF_REQUEST, data, reply, option);

}

int SocPerfStub::OnRemoteRequest(uint32_t code, MessageParcel &data,
MessageParcel &reply, MessageOption &option)
{
auto remoteDescriptor = data.ReadInterfaceToken();
if (GetDescriptor() != remoteDescriptor) {
return ERR_INVALID_STATE;
}
switch (code) {
case TRANS_IPC_ID_PERF_REQUEST: {
int cmdId = data.ReadInt32();
std::string msg = data.ReadString();
PerfRequest(cmdId, msg);
return 0;
}
case TRANS_IPC_ID_PERF_REQUEST_EX: {
int cmdId = data.ReadInt32();
bool onOffTag = data.ReadBool();
std::string msg = data.ReadString();
PerfRequestEx(cmdId, onOffTag, msg);
return 0;
}

    ...

}

void SocPerf::PerfRequest(int cmdId, const std::string& msg)
{
...

if (perfActionInfo.find(cmdId) == perfActionInfo.end()
|| perfActionInfo[cmdId]->duration == 0) {
SOC_PERF_LOGE("Invalid PerfRequest cmdId[%{public}d]", cmdId);
return;
}
SOC_PERF_LOGI("PerfRequest cmdId[%{public}d]msg[%{public}s]", cmdId, msg.c_str());
DoFreqAction(perfActionInfo[cmdId], EVENT_INVALID, ACTION_TYPE_PERF);

}

void SocPerf::DoFreqAction(std::shared_ptr<Action> action, int onOff, int actionType)
{
for (int i = 0; i < (int)action->variable.size(); i += RES_ID_AND_VALUE_PAIR) {
auto resAction = std::make_shared<ResAction>(action->variable[i + 1], action->duration, actionType, onOff);
auto event = AppExecFwk::InnerEvent::Get(INNER_EVENT_ID_DO_FREQ_ACTION, resAction, action->variable[i]);
handlers[action->variable[i] / RES_ID_NUMS_PER_TYPE - 1]->SendEvent(event);
}
}

void SocPerfHandler::ProcessEvent(const AppExecFwk::InnerEvent::Pointer &event)
{
switch (event->GetInnerEventId()) {
...

    case INNER_EVENT_ID_DO_FREQ_ACTION: {
int resId = event->GetParam();
if (!IsValidResId(resId)) {
return;
}
std::shared_ptr<ResAction> resAction = event->GetSharedObject<ResAction>();
UpdateResActionList(resId, resAction, false);
break;
}
...

}

}



## 总结

本文主要介绍了智能感知调度模块的主要类关系、初始化流程、绘帧信息调度流程和帧感知调度流程并贴出相关主要代码,为开发人员维护和扩展功能提供参考。

## 更多原创内容请关注:[深开鸿技术团队](https://harmonyos.51cto.com/person/posts/15292440)
入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

附件链接:[文中图片](https://ost.51cto.com/resource/2054)

[想了解更多关于开源的内容,请访问:](https://ost.51cto.com/#bkwz)

[51CTO 开源基础软件社区](https://ost.51cto.com#bkwz)

https://ost.51cto.com/#bkwz
举报

相关推荐

0 条评论