-
一、业务背景在处理大量页面数据的场景下,用户进行快速查询操作时遇到了严重的性能问题:页面加载缓慢,长时间显示白屏,用户交互体验极差。经统计,该问题导致用户使用率降低了 60%,对业务发展产生了显著影响 二、原因分析2.1 数据加载策略失当,内存占用超限原始方案采用ForEach组件一次性加载全量数据(如数百甚至数千条记录),直接将所有数据存入内存并构建组件树:内存压力:大量数据对象(如每条记录包含文本、图片 URL、状态标识等)同时驻留内存,导致 JS 引擎内存占用激增(如 500 条记录可能占用 200MB + 内存),低端设备易触发内存溢出(OOM),引发页面崩溃或强制刷新。加载阻塞:全量数据请求通过单次接口调用完成,若数据量过大(如响应体超过 1MB),网络传输耗时增加(尤其弱网环境下),且接口处理时间延长,超过前端超时阈值(如 5 秒)后,用户看到的仍是 “白屏”。2.1 LazyForEach缓存配置僵化,预加载不足 若cachedCount(预加载缓存项数量)默认值为 1(系统默认),未根据列表项高度和屏幕尺寸动态调整:滑动时白屏:用户快速滑动列表时,视口外的列表项因未被缓存而销毁,滑动至新区域时需重新创建组件并加载资源(如图片),导致短暂白屏(尤其图片占比高的场景)。重复渲染损耗:缓存项不足时,列表项会在 “进入视口→离开视口→再次进入” 的过程中反复销毁与重建,浪费 CPU 资源,加剧滑动卡顿。三、解决思路数据按需精简与结构化传输,在现有懒加载、缓存优化及预加载机制的基础上,从数据源头减少传输与处理成本,进一步缓解前端性能压力,具体思路如下:通过后端接口优化,只传输前端当前场景下必需的数据字段,并采用结构化格式减少冗余信息,从根源上降低网络传输耗时、JS 内存占用及组件渲染压力四、解决方案4.1 列表优化列表页面结构比较复杂,最外层为Refresh组件包裹,第一层子组件为List组件,然后再里面有WaterFlow组件的嵌套,必须保证列表代码结构设计的比较好,不然会容易出现掉帧,卡顿,以及滑动过程中图片内容显示空白等问题。主要是从以下几个方面进行的优化处理:懒加载与缓存策略实现从官方文档可以了解到ForEach是从列表数据源一次性加载全量数据,且一次性并全部挂载在组件树上;LazyForEach是按需加载部分数据,只构建出一棵短小的组件树。针对数据加载和组件树构建这两个显著差异。另外我们还需要知道的是List等组件中cachedCount属性默认为1,所以当使用LazyForEach没有设置缓存项数量的时候,默认的缓存项数量实际为1懒加载代码的实现:import { ObservedArray, Observed } from '@ohos.data.observed';import { IDataSource, DataChangeListener } from '@ohos.app.ability.dataSource';const TAG = '[AdvancedLazyDataSource]';/** * 基础数据源接口实现 * 提供数据变更通知机制 */class BasicDataSource<T> implements IDataSource { private listeners: DataChangeListener[] = []; public totalCount(): number { return 0; } public getData(_index: number): T | undefined { return undefined; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 批量数据变更通知 notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }); } // 单项数据添加通知 notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } // ...}/** * 高级懒加载数据源实现 * 增加了数据分批加载、虚拟滚动支持等功能 */@Observedexport default class AdvancedLazyDataSource<T> extends BasicDataSource<T> { @State dataArray: T[] = []; private pageSize: number = 20; // 每页加载数量 private totalPages: number = 0; // 总页数 private currentPage: number = 1; // 当前页码 private isLoading: boolean = false; // 加载状态 /** * 获取数据总数 */ public totalCount(): number { return this.dataArray.length; } /** * 获取指定位置数据 */ public getData(index: number): T { return this.dataArray[index]; } /** * 添加数据到指定位置 */ public addData(index: number, data: T): void { this.dataArray.splice(index, 0, data); this.notifyDataAdd(index); } /** * 追加数据到末尾 */ public pushData(data: T): void { this.dataArray.push(data); this.notifyDataAdd(this.dataArray.length - 1); } /** * 加载更多数据 * @param page 页码 * @returns 加载结果 */ public async loadMoreData(page: number = this.currentPage + 1): Promise<boolean> { if (this.isLoading || page > this.totalPages) { return false; } this.isLoading = true; try { // 模拟网络请求加载数据 const newData = await this.fetchDataFromServer(page, this.pageSize); // 更新数据源 const startIndex = this.dataArray.length; this.dataArray.push(...newData); // 通知数据变更 for (let i = 0; i < newData.length; i++) { this.notifyDataAdd(startIndex + i); } this.currentPage = page; return true; } catch (error) { console.error(TAG, 'Load more data failed', error); return false; } finally { this.isLoading = false; } } /** * 从服务器获取数据 * @param page 页码 * @param size 每页数量 * @returns 数据数组 */ private async fetchDataFromServer(page: number, size: number): Promise<T[]> { // 实际项目中这里应该是网络请求 // 模拟延迟 await new Promise(resolve => setTimeout(resolve, 300)); // 模拟数据 return Array.from({ length: size }, (_, i) => ({ id: `${page}-${i}`, content: `Item ${page * size + i}` }) as unknown as T); } /** * 重置数据源 */ public reset(): void { this.dataArray = []; this.currentPage = 1; this.isLoading = false; this.notifyDataReload(); }}缓存策略优化LazyForEach 组件的缓存策略对性能影响显著:cachedCount 属性:指定预加载的缓存项数量,默认为 1优化策略:对于一般列表:设置 cachedCount 为 5-10,平衡首屏加载速度和滑动体验对于长列表:根据列表项高度和屏幕高度动态计算 cachedCount对于快速滑动场景:可适当增大 cachedCount,但需测试首屏加载时间4.2 动态预加载(Prefetcher 机制) HarmonyOS 提供的 Prefetcher 机制具有以下特点动态自适应:根据网络状态智能调整预加载策略按需预取:只预加载视口附近的数据和资源资源缓存:将预加载的资源缓存到本地,减少重复请求请求管理:自动管理预加载请求,避免资源浪费预加载流程:用户滑动列表 → 计算可见区域 → 触发visibleAreaChanged → Prefetcher预取可见区域附近数据 → 缓存资源 → 列表项需要时直接使用缓存资源预加载与懒加载的协同工作:懒加载:解决大数据量下的组件渲染性能问题预加载:解决资源加载延迟导致的白屏问题组合使用:实现 "即见即得" 的流畅体验预加载实现细节创建IDataSourcePrefetching接口的实现类export class CardDataSource implements IDataSourcePrefetching { private dataList: Array<V11BaseCardShowModel> = []; private listeners: DataChangeListener[] = []; private readonly requestsInFlight: HashMap<number, rcp.Request> = new HashMap(); private readonly session: rcp.Session = rcp.createSession(); private readonly cachePath = getContext().getApplicationContext().cacheDir; constructor(showModelList: Array<ShowModel>) { this.dataList = showModelList; } async prefetch(index: number): Promise<void> { const item = this.dataList[index]; if (this.requestsInFlight.hasKey(index)) { return; } if (item.cachedImage != "") { return; } const request = new rcp.Request(item.thumbnail.imageUrl, 'GET'); this.requestsInFlight.set(index, request); try { //提前拉取数据 const response = await this.session.fetch(request); if (response.statusCode !== 200 || !response.body) { Logger.error('CardDataSource response code:' + response.statusCode); return } // 将加载的数据信息存储到缓存文件中 item.cachedImage = await this.cacheImg(item.id, response.body); // 删除指定元素 this.requestsInFlight.remove(index); } catch (err) { if (err.code !== CANCEL_CODE) { //失败就直接使用url链接 item.cachedImage = item.thumbnail.imageUrl; // 移除有异常的网络请求任务 this.requestsInFlight.remove(index); } } }}创建实际使用的IDataSourcePrefetching的实现对象和BasicPrefetcher对象// 创建DataSourcePrefetching对象,具备任务预取、取消能力的数据源@State dataSource: CardDataSource = new CardDataSource(new Array<ShowModel>())private readonly prefetcher = new BasicPrefetcher(this.dataSource);数据刷新时,要立刻刷新BasicPrefetcher中DataSource // LazyForEach的数据源更新 this.dataSource.updateData(showModels) this.prefetcher.setDataSource(this.dataSource); 调用BasicPrefetcher的visibleAreaChanged方法.onScrollIndex((start: number, end: number) => { this.prefetcher.visibleAreaChanged(start, end) }) 五、方案总结本方案通过以下核心技术解决性能问题:懒加载技术:使用 LazyForEach 避免一次性加载全量数据缓存策略:优化 cachedCount 参数减少滑动延迟预加载机制:利用 Prefetcher 提前加载资源减少白屏组件优化:简化结构减少渲染开销网络优化:根据网络状态动态调整加载策略
-
前提这是小空坚持写的Android新手向系列,欢迎品尝。大佬(×)新手(√)实践过程今天我们继续学习EditText-她是程序和用户进行交互极其重要的控件。所有的App关于输入和编辑内容基本都用的这个控件来进行处理,基于该控件又封装了搜索,登录,输入限制,聊天框,内容编辑等组件。EditText继承自TextView,所以拥有TextView的“全部”功能,让我们抓紧领略下EditText的神奇吧。在布局中我们先简单看下:<EditText android:layout_width="160dp" android:layout_height="wrap_content" android:textColor="#ff0000" android:hint="芝麻粒儿提醒请输入您的内容" />细心的你运行后一定发现,浅灰色内容默认显示,当输入真正内容的时候就自动消失了。没错,那是友好型提示文本。Android发展到现在,这已经是基操了。而且也不用我们多做什么逻辑,只需要一个属性即可实现【android:hint=""】,剩下的系统自动处理,很方便。属性介绍上面也说了该控件继承自TextView,所以属性操作和TextView一致,关于TextView都有哪些属性,可以去看小空关于TextView的介绍,里面列举说明了基本所有属性。我们在此讲一些常用属性。android:textStyle=“bold” :设置文本样式,bold(粗体), italic(斜体), bold|italic(粗斜体)android:textColorHighlight="":设置选中文本的颜色android:textScaleX="1.5":设置水平方向字与字之间的间距android:textScaleY="1.5":设置垂直方向字与字之间的间距android:typeface="monospace" :设置文本的字体,monospace:等宽字体,sans:无衬线字体,serif:衬线,normal:普通字体。android:textColorHint="":设置提示文本Hint的文本颜色android:selectAllOnFocus:设置输入框获取焦点后是否选中所有文本,当为true的时候选中所有文本,默认为false。输入类型限制是很常用的了,比如仅仅显示数字,或者密码类型,也能减少bug产出。重点声明:以下属性针对Android原生系统和输入法是都支持的,针对国内系统和三方输入法有些属性是失效的。所以小空在这只列举常用的属性,小心别进坑了。android:inputType="none":EditText无限制,可以任意输入android:inputType="text":任何文本,等同于noneandroid:inputType="textMultiLine":多行输入,该属性会导致android:imeOptions属性失效android:inputType="textUri":网址类型android:inputType="textEmailAddress":电子邮件类型android:inputType="textPersonName":人名类型android:inputType="textPostalAddress":地址类型android:inputType="textPassword":密码类型android:inputType="textVisiblePassword":可见密码类型android:inputType="textWebEditText":网页变淡文本android:inputType="number":数字类型android:inputType="numberSigned": 带符号数字格式android:inputType=“numberDecimal” :带小数点的浮点格式android:inputType=“phone” :拨号键盘类型android:inputType=“datetime” :时间日期类型android:inputType=“date” :日期键盘类型android:inputType="time" :时间键盘类型android:digits="0123456789":设置只能显示哪些字符,如果内容较多不适用输入的时候控制键盘(回车键/确认键)的不同行为,该属性同上面一样,针对官方的输入法适用,但国内有很多不同的三方输入法,不一定有效,不过自定义键盘的时候会用到。android:imeOptions=“flagNoExtractUi” :使软键盘不全屏显示,只占用一部分屏幕 同时,这个属性还能控件软键盘右下角按键的显示内容,默认情况下为回车键android:imeOptions=“actionNone” :输入框右侧不带任何提示android:imeOptions=“actionGo” :右下角按键内容为’开始’android:imeOptions=“actionSearch” :右下角按键为放大镜图片,搜索android:imeOptions="actionSend":右下角按键内容为’发送’字样android:imeOptions=“actionNext” :右下角按键内容为’下一步’ 或’下一项’ 字样android:imeOptions=“actionDone” :右下角按键内容为’完成’字样小键盘控制一旦涉及输入框,那么小键盘一定少不了,在业务开发的过程中会针对小键盘隐藏显示等做出对应的措施,或者自定义键盘的业务功能都需要该技术。在AndroidMinifest.xml的【activity】中设置属性【windowSoftInputMode】即可。stateUnspecified:软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置,也就是小键盘的默认值,当输入组件EditText获取焦点的时候才会弹出stateUnchanged:当这个activity出现时,软键盘将一直保持在上一个activity里的状态,无论是隐藏还是显示,举例,该页面A是隐藏键盘,跳转页面B后,键盘也是隐藏的。stateHidden:用户选择activity时,软键盘总是被隐藏stateAlwaysHidden:当该Activity主窗口获取焦点时,软键盘也总是被隐藏的stateVisible:软键盘通常是可见的stateAlwaysVisible:用户选择activity时,软键盘总是显示的状态,不管有没有输入框。adjustUnspecified:默认设置,通常由系统自行决定是隐藏还是显示,并且会调整布局进而使输入内容始终可见adjustResize:该Activity总是调整屏幕的大小以便留出软键盘的空间adjustPan:当前窗口的内容将自动移动以便当前焦点控件不被键盘覆盖和用户能总是看到输入内容的部分。上面的属性使用过程中同样不是唯一性,可联合使用。android:windowSoftInputMode=“adjustResize|stateAlwaysHidden”光标默认光标位置是在最后显示的,这也是对用户较好的体验形式。但同时官方仍然给了我们更多的自由空间。明文密文很多App业务中都需要密码输入,有的产品会在密码旁边有个【眼睛】的图标,展示可展示密码明文或回复密文。这都是程序中用代码控制的。EditText testEditText = findViewById(R.id.testEditText);//显示明文密码testEditText.setTransformationMethod(new HideReturnsTransformationMethod());//隐藏密码 就是密文显示 都是 *** 号testEditText.setTransformationMethod(new PasswordTransformationMethod());其他作者:小空和小芝中的小空转载说明-务必注明来源:https://zhima.blog.csdn.net/https://www.zhihu.com/people/zhimalierhttps://juejin.cn/user/4265760844943479这位道友请留步☁️,我观你气度不凡,谈吐间隐隐有王者霸气,日后定有一番大作为!!!旁边有点赞收藏今日传你,点了吧,未来你成功☀️,我分文不取,若不成功⚡️,也好回来找我。
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签