作者:張守忠
1 簡介
智能感覺排程部件位于全局資源排程管控子系統中,通過幀感覺排程機制,更新程序排程分組。通過擷取應用的生命周期狀态、應用繪幀等資訊,調節核心排程參數,進而控制核心排程行為,保障系統程序排程供給。
圖 1 架構圖
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI5cTOfhGLwIDOfdHLlpXazVmcvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CN5AjMwczYmRzMxITNxgDZzMTNlJzYjdTYyEjNyUDM2Q2NkZ2Mm9CX2AjMyAjMvw1cldWYtl2Lc12bj5yb0NWM14ycvlnbv1mchhWLsR2Lc9CX6MHc0RHaiojIsJye.png)
智能感覺排程部件根據執行時所屬線程進行劃分,可包含兩大元件,即運作在App程序的繪幀資訊收集元件和運作在系統服務程序的幀感覺排程機制元件,每個元件分為若幹子產品。
- 繪幀資訊收集元件:應用繪幀感覺主要負責調節核心排程的參數,進行線程負載的縮放。當使用者在APP界面滑動時,識别出應用的關鍵線程(諸如繪幀主線程、渲染線程),感覺應用繪幀子過程的執行情況,根據是否逾時來判斷是否調整核心排程參數,進行實時的資源供給,進行優先排程。
- 幀感覺排程機制元件:應用線程排程機制,作為應用繪幀感覺實作的基礎,主要負責管控線程組及線程優先級,實作應用線程的統一管理,保證整個系統性能。核心思想就是分組,即根據不同線程組提供不同的供給能力。把使用者敏感度線程添加進同一個線程組,在應用啟動、前背景切換等多個應用變化場景,提供資源優先供給。
2 源碼分析
2.1 類關系
圖 2 類圖
上圖列出了本子產品中主要類及其之間的調用、依賴關系,下面結合各流程進行介紹。
2.2 初始化
圖 3 初始化時序圖
系統能力注冊
在檔案res_sched_service_ability.cpp中調用宏"REGISTER_SYSTEM_ABILITY_BY_ID"注冊系統能力ResSchedServiceAbility
namespace OHOS {
namespace ResourceSchedule {
REGISTER_SYSTEM_ABILITY_BY_ID(ResSchedServiceAbility, RES_SCHED_SYS_ABILITY_ID, true);
系統能力初始化
系統能力初始化分為資源排程管理初始化、系統服務釋出、cpu分組排程初始化、注冊監聽的SA四個子過程。
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"中。
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三個子子產品的初始化。
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).
bool SystemAbility::AddSystemAbilityListener(int32_t 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
繪幀資訊收集主要根據場景分為輸入子系統手勢變化等的感覺排程,ace子系統的視窗變化等的感覺排程,圖形子系統的渲染、動畫感覺排程。圖4描述的是BeginListFling、BeginFlushBuild、Render三個繪幀資訊資源排程時序圖,其他的流程類似,不一一列舉。繪幀資訊收集資源排程的最終是通過核心接口ioctrl寫關聯線程組"/dev/sched_rtg_ctrl", 包括線程加入、移除關聯線程組,設定關聯線程組的屬性等等。
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
- 被監聽的SA的相關屬性或狀态變化,調用訂閱的對象接口進行響應。
void WindowStateObserver::OnFocused(const sptr<FocusChangeInfo>& focusChangeInfo)
{
auto cgHander = SchedController::GetInstance().GetCgroupEventHandler();
if (cgHander && focusChangeInfo) {
auto windowId = focusChangeInfo->windowId_;
auto token = reinterpret_cast<uint64_t>(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回調函數
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);
}
調整程序組
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_) {
pid_t pid = pr.GetPid();
int ret = CgroupSetting::SetThreadGroupSchedPolicy(pid, (int)pr.setSchedGroup_);
if (ret != 0) {
CGS_LOGE("%{public}s set %{public}d to group %{public}d failed, ret=%{public}d!",
__func__, pid, pr.setSchedGroup_, ret);
return;
}
...
}
}
通過程序間通訊IPC通知資源排程服務程序
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兩個插件分别進行處理。
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::lock_guard<std::mutex> autoLock(pluginMutex_);
auto itMap = pluginLibMap_.find(pluginLib);
if (itMap == pluginLibMap_.end()) {
RESSCHED_LOGE("PluginMgr::deliverResourceToPlugin no plugin %{public}s !", pluginLib.c_str());
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"
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, &grp_data);
close(fd);
return ret;
}
- socperf主要負責cpu頻率的修改,操作檔案是"/dev/cpuctl"和"/dev/cpuset"
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;
}
...
}
}
總結
本文主要介紹了智能感覺排程子產品的主要類關系、初始化流程、繪幀資訊排程流程和幀感覺排程流程并貼出相關主要代碼,為開發人員維護和擴充功能提供參考。
更多原創内容請關注:深開鴻技術團隊
入門到精通、技巧到案例,系統化分享HarmonyOS開發技術,歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生态。
附件連結:文中圖檔