1 关键字
Launcher;Storage
2 问题描述
问题现象:安装应用到达 20 个以上后,重启设备,Launcher 页面没有任何应用图标
运行环境:硬件 dayu200,软件:3.1release
测试步骤:
- 使用 hdc 将测试安装包安装到设备上,安装数量在 20 个以上,尽可能稍多一些
- 重启设备
- Launcher 桌面显示异常,不显示任何应用图标
3 问题原因
3.1 正常机制
- Launcher 的桌面应用布局信息是通过 Storage 存储在文件中。
- Launcher 在系统安装应用后会更新桌面布局配置,调用 putSync 将当前布局写入内存中,然后通过 flushSync 持久化到文件中。
- 桌面布局配置持久化后,设备重启,Launcher 页面正常加载。
3.2 异常机制
- 当桌面应用安装过多,桌面布局配置信息超过 8192 字节时,底层通过定长数组接收参数,导致数组越界未定义,从而无法正确获取桌面布局配置信息,属性为空,从而将内存中属性更新为空了。
- 持久化过程中再将内存中的属性更新到文件中,从而导致文件信息也被更新为空。
- 重启设备,重新加载文件,桌面布局配置为空,从而无法显示任何应用图标。
Storage 官方文档说明:key 的最大长度限制,需小于 80 字节。value 的最大长度限制,需小于 8192 字节。
4 解决方案
- 应用层面,可通过对超长字段进行拆分存储,规避该问题
- 存储方式,可通过使用关系型数据库 @ohos.data.rdb 的 rdbStore 或分布式数据管理 @ohos.data.distributedData 的 kvStore 来进行数据存储
- 底层实现,系统目前文件存储的最大长度限制为 8192 字节,可通过修改底层存储逻辑,修改限定长度或自动扩容实现
5 定位过程
该问题为底层实现与特殊场景不兼容导致的运行异常:
-
安装应用 20+ 复现相关问题。
-
关注系统桌面布局配置 LauncherPreference 文件中的 DesktopApplicationInfo 信息更新情况。
文件路径:/data/app/el2/100/database/com.ohos.launcher/pad/LauncherPreference
文件内容:<?xml version="1.0" encoding="UTF-8"?> <preferences version="1.0"> <string key="DesktopApplicationInfo">[{"appName":"$string:MainAbility_label","isSystemApp":true,"isUninstallAble":false,"appIconId":16777219,"appLabelId":16777217,"bundleName":"com.example.AircraftWar","abilityName":"com.example.AircraftWar.MainAbility","type":0,"area":[1,1]},{"appName":"$string:MainAbility_label","isSystemApp":true,"isUninstallAble":false,"appIconId":16777219,"appLabelId":16777217,"bundleName":"com.example.Billiards","abilityName":"com.example.Billiards.MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":16777229,"appLabelId":16777219,"bundleName":"com.example.Browser","abilityName":"MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":false,"isUninstallAble":true,"appIconId":16777219,"appLabelId":16777216,"bundleName":"com.example.baseanimation","abilityName":"com.example.baseanimation.MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":16777218,"appLabelId":16777216,"bundleName":"com.example.distributedcalc","abilityName":"com.example.distributedcalc.default","type":0,"area":[1,1]},{"appName":"$string:entry_MainAbility","isSystemApp":true,"isUninstallAble":false,"appIconId":16777218,"appLabelId":16777217,"bundleName":"com.example.shopping","abilityName":"com.example.shopping.MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":false,"isUninstallAble":true,"appIconId":16777217,"appLabelId":16777219,"bundleName":"com.huawei.himovie","abilityName":"MainAbility","type":0,"area":[1,1]},{"appName":"$string:entry_Music","isSystemApp":true,"isUninstallAble":false,"appIconId":16777311,"appLabelId":16777219,"bundleName":"com.huawei.himusicdemo","abilityName":"com.huawei.himusicdemo.Music","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":150995030,"appLabelId":150994944,"bundleName":"com.ohos.camera","abilityName":"com.ohos.camera.MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":16777431,"appLabelId":16777225,"bundleName":"com.ohos.contacts","abilityName":"com.ohos.contacts.MainAbility","type":0,"area":[1,1]},{"appName":"$string:messages","isSystemApp":true,"isUninstallAble":false,"appIconId":16777565,"appLabelId":16777321,"bundleName":"com.ohos.mms","abilityName":"com.ohos.mms.MainAbility","type":0,"area":[1,1]},{"appName":"$string:entry_MainAbility","isSystemApp":true,"isUninstallAble":false,"appIconId":134217900,"appLabelId":134217748,"bundleName":"com.ohos.note","abilityName":"com.ohos.note.MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":16777834,"appLabelId":16777216,"bundleName":"com.ohos.photos","abilityName":"com.ohos.photos.MainAbility","type":0,"area":[1,1]},{"appName":"$string:entry_MainAbility","isSystemApp":true,"isUninstallAble":false,"appIconId":50332153,"appLabelId":50331728,"bundleName":"com.ohos.settings","abilityName":"com.ohos.settings.MainAbility","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":16777218,"appLabelId":16777216,"bundleName":"ohos.samples.airquality","abilityName":"ohos.samples.airquality.default","type":0,"area":[1,1]},{"appName":"$string:app_name","isSystemApp":true,"isUninstallAble":false,"appIconId":16777218,"appLabelId":16777216,"bundleName":"ohos.samples.clock","abilityName":"ohos.samples.clock.default","type":0,"area":[1,1]},{"appName":"$string:entry_MainAbility","isSystemApp":true,"isUninstallAble":false,"appIconId":16777219,"appLabelId":16777217,"bundleName":"ohos.samples.webdemo","abilityName":"ohos.samples.webdemo.MainAbility","type":0,"area":[1,1]}]</string> <string key="DesktopModeConfig">{"appStartPageType":"Grid","gridConfig":0,"deviceType":"pad"}</string> <string key="GridLayoutInfo">{"layoutDescription":{"pageCount":1,"row":5,"column":11},"layoutInfo":[{"bundleName":"com.example.AircraftWar","type":0,"area":[1,1],"page":0,"column":0,"row":0},{"bundleName":"com.example.Billiards","type":0,"area":[1,1],"page":0,"column":1,"row":0},{"bundleName":"com.example.Browser","type":0,"area":[1,1],"page":0,"column":2,"row":0},{"bundleName":"com.example.baseanimation","type":0,"area":[1,1],"page":0,"column":3,"row":0},{"bundleName":"com.example.distributedcalc","type":0,"area":[1,1],"page":0,"column":4,"row":0},{"bundleName":"com.example.shopping","type":0,"area":[1,1],"page":0,"column":5,"row":0},{"bundleName":"com.huawei.himovie","type":0,"area":[1,1],"page":0,"column":6,"row":0},{"bundleName":"com.huawei.himusicdemo","type":0,"area":[1,1],"page":0,"column":7,"row":0},{"bundleName":"com.ohos.camera","type":0,"area":[1,1],"page":0,"column":8,"row":0},{"bundleName":"com.ohos.contacts","type":0,"area":[1,1],"page":0,"column":9,"row":0},{"bundleName":"com.ohos.mms","type":0,"area":[1,1],"page":0,"column":10,"row":0},{"bundleName":"com.ohos.note","type":0,"area":[1,1],"page":0,"column":0,"row":1},{"bundleName":"com.ohos.photos","type":0,"area":[1,1],"page":0,"column":1,"row":1},{"bundleName":"com.ohos.settings","type":0,"area":[1,1],"page":0,"column":2,"row":1},{"bundleName":"ohos.samples.airquality","type":0,"area":[1,1],"page":0,"column":3,"row":1},{"bundleName":"ohos.samples.clock","type":0,"area":[1,1],"page":0,"column":4,"row":1},{"bundleName":"ohos.samples.webdemo","type":0,"area":[1,1],"page":0,"column":5,"row":1}]}</string> <string key="SmartDockLayoutInfo">[{"itemType":2,"editable":false,"bundleName":"com.ohos.launcher","abilityName":"com.ohos.launcher.appcenter.MainAbility","appIconId":184549428,"appLabelId":184549400,"appName":"全部应用"},{"itemType":2,"editable":false,"bundleName":"com.ohos.launcher","abilityName":"com.ohos.launcher.recents.MainAbility","appIconId":184549429,"appLabelId":184549401,"appName":"最近任务"},{"itemType":0,"editable":true,"appName":"图库","bundleName":"com.ohos.photos","abilityName":"com.ohos.photos.MainAbility","appIconId":16777834,"appLabelId":16777216},{"itemType":0,"editable":false,"appName":"设置","bundleName":"com.ohos.settings","abilityName":"com.ohos.settings.MainAbility","appIconId":50332153,"appLabelId":50331728}]</string> </preferences>
-
关注属性写入内存 putSync 与内存信息持久化到文件 flushSync 调用前后的属性变量。
-
通过日志定位到问题原因:putSync 调用导致的异常。
-
分析 api 底层实现:底层实现调用 napi_storage.cpp 的 StorageProxy::SetValueSync。
-
SetValueSync 具体逻辑:声明定长 char 数组用来接收 key(MAX_KEY_LENGTH = 80),获取 value 类型,当类型为 string 时,声明定长 char 数组用来接收 value(MAX_VALUE_LENGTH = 8 * 1024),然后调用 preferences_impl.cpp 的 PutString。
napi_value StorageProxy::SetValueSync(napi_env env, napi_callback_info info) { napi_value thiz = nullptr; size_t argc = 2; napi_value args[2] = { 0 }; LOG_DEBUG("SETVALUE"); NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, &thiz, nullptr)); NAPI_ASSERT(env, argc == 2, "Wrong number of arguments"); // get value type napi_valuetype valueType = napi_undefined; NAPI_CALL(env, napi_typeof(env, args[0], &valueType)); NAPI_ASSERT(env, valueType == napi_string, "type mismatch for key"); // get input key char key[MAX_KEY_LENGTH] = { 0 }; size_t out = 0; NAPI_CALL(env, napi_get_value_string_utf8(env, args[0], key, MAX_KEY_LENGTH, &out)); StorageProxy *obj = nullptr; NAPI_CALL(env, napi_unwrap(env, thiz, reinterpret_cast<void **>(&obj))); NAPI_ASSERT(env, (obj != nullptr && obj->value_ != nullptr), "unwrap null native pointer"); NAPI_CALL(env, napi_typeof(env, args[1], &valueType)); if (valueType == napi_number) { double value = 0.0; NAPI_CALL(env, napi_get_value_double(env, args[1], &value)); int result = obj->value_->PutDouble(key, (double)value); NAPI_ASSERT(env, result == E_OK, "call PutDouble failed"); } else if (valueType == napi_string) { char *value = new char[MAX_VALUE_LENGTH]; napi_status status = napi_get_value_string_utf8(env, args[1], value, MAX_VALUE_LENGTH, &out); if (status != napi_ok) { LOG_DEBUG("napi_get_value_string_utf8 failed"); LOG_DEBUG(value); } else { // get value int result = obj->value_->PutString(key, value); } delete[] value; NAPI_ASSERT(env, result == E_OK, "call PutString failed"); } else if (valueType == napi_boolean) { bool value = false; NAPI_CALL(env, napi_get_value_bool(env, args[1], &value)); // get value int result = obj->value_->PutBool(key, value); NAPI_ASSERT(env, result == E_OK, "call PutBool failed"); } else { NAPI_ASSERT(env, false, "Wrong second parameter type"); } return nullptr; }
注:当属性长度超长后会导致数组越界未定义,从而无法正常获取属性值,导致属性值为空,从而导致内存中存放属性有误
-
PutString 具体逻辑:调用 CheckStringValue 进行 value 值校验(校验长度),校验通过调用 PutPreferencesValue。
int PreferencesImpl::PutString(const std::string &key, const std::string &value) { int errCode = CheckKey(key); if (errCode != E_OK) { return errCode; } errCode = CheckStringValue(value); if (errCode != E_OK) { return errCode; } PutPreferencesValue(key, PreferencesValue(value)); return E_OK; }
int PreferencesImpl::CheckKey(const std::string &key) { if (key.empty()) { LOG_ERROR("The key string is null or empty."); return E_KEY_EMPTY; } if (Preferences::MAX_KEY_LENGTH < key.length()) { LOG_ERROR("The key string length should shorter than 80."); return E_KEY_EXCEED_MAX_LENGTH; } return E_OK; }
int PreferencesImpl::CheckStringValue(const std::string &value) { if (Preferences::MAX_VALUE_LENGTH < value.length()) { LOG_ERROR("The value string length should shorter than 8 * 1024."); return E_VALUE_EXCEED_MAX_LENGTH; } return E_OK; }
-
PutPreferencesValue 具体逻辑:AwaitLoadFile 获取资源锁,调用 insert_or_assign 存放属性到内存 map 中,调用 push_back 记录已修改属性的 key 值到内存 map 中。
void PreferencesImpl::PutPreferencesValue(const std::string &key, const PreferencesValue &value) { AwaitLoadFile(); std::lock_guard<std::mutex> lock(mutex_); auto iter = map_.find(key); if (iter != map_.end()) { PreferencesValue &val = iter->second; if (val == value) { return; } } map_.insert_or_assign(key, value); modifiedKeys_.push_back(key); }
-
属性写入内存后,再由 flushSync 来进行数据的持久化操作,将内存数据写入到文件中。
6 知识分享
- OpenHarmony 文件存储 @ohos.data.storage 目前的长度限制:key 的最大长度限制,需小于 80 字节,value 的最大长度限制,需小于 8192 字节。
- 数据库存储目前逻辑上是没有长度限制的,长度限制需参考数据库属性配置,sqlite 中 text 存储可变长度的非 Unicode 数据,最大长度为 2^31-1(2,147,483,647)个字符。
最后
小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。
为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:
鸿蒙(HarmonyOS NEXT)最新学习路线
该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案
路线图适合人群:
IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术
2.视频学习资料+学习PDF文档
HarmonyOS Next 最新全套视频教程
纯血版鸿蒙全套学习资料(面试、文档、全套视频等)
总结
参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线