鸿蒙性能优化
一、什么是性能问题和性能优化?
性能问题:应用在运行期间遇到的卡顿、白屏、动画不流畅、掉帧等影响用户使用体验的问题。
性能优化:针对出现的情况进行定位和解决。
二、为什么要做性能给你优化?
- 提升用户体验,增加用户粘性 一个具有商业价值的App不是它的界面有多精美而是用户日活有多少
- 应用上架通过率、市场评分以及业务转化率。直接影响App曝光度和排行,与用户下载使用量挂钩
三、如何做性能优化?
- 检测、分析应用的性能指标、通过调试代码将指标优化到推荐区间
1. 检测工具
Code Linter 静态扫描工具
①检测代码、尤其是新增的代码 并得到解决方案
②根据提示或者官方文档修改代码,修改后再次检查 直至修复完成
如果项目中有具体的规则可以在项目根目录下的code-linter.json5中配置(规划文件名单来确定范围、规则集以及额外规则集等)
代码检测规则见 文档中心
AppAnalyzer 动态运行工具
动态检查相当于对项目以及所选模块进行一个“体检” 包含兼容性、性能以及最佳实践方案的测试。
动态检查需要真机 体检方案需要Python环境
工具会自动帮我们去检查,然后输出结果。结果中包含本次检查分数和优化方案。
修改后再次检查直至100分。
2. 分析工具
得到应用和设备的运行状态 根据运行状态发现问题 然后去制定解决方案
ArkUlInspector 界面状态分析工具
它可以用来观察当前组件的属性以及当前页面的组件结构,类似网页的F12中的元素检查器。
可以帮助我们直观地看到所有组件,找出哪些不合理的布局。
Profiler 性能实时监测工具
同样需要真机,监测设备、应用和进程等 这个工具非常丰富,不仅可以检测系统事件应用运行情况,还可以看到时间泳道中的各项精细数据,对比前后变化。还支持录制整个模版,抓取一系列的数据进行对比。
三个重要参数:
Launch 关注启动耗时
Frame 关注丢帧率和丢帧次数
Allocation 关注内存占用情况
3. 性能优化的方向
- 启动时长的优化
启动时长是应用被拉起至应用显示页面的时间。
页面时长是页面被拉起至显示页面的执行时间。
1)减少耗时操作(冷启动回调中减少日志打印打点)
2)资源提前加载
1.网络请求优化(避免在宏任务微任务中执行网络请求)
2.Web组件加载性能优化
(利用生命周期钩子函数 预解析、预链接、预下载)JS解析JS执行绘制都需要时间
3.资源预下载(提前将大文件通过多线程到沙箱,直接读文件,避免每次都)
4.延迟加载(使用import lazy懒加载方式导入模块)
5.动态加载(在导入模式前添加await关键字修饰 只有真正调用时才去加载模块)
解释:动态加载是异步的,只有调用所依赖的模块时才会解析执行,支持运行时拼接,也就是说可以写在代码中。而延迟加载是同步的,冷启动仍然会解析遍历(只是建立依赖关系,不会立刻初始化里面代码),因为代码是从上往下执行的,所以必须在顶级导入,不支持运行拼接。
6.使用并发能力或者异步
- 如果是网络请求这种耗时任务,同时调用多个接口,多个接口之间没有关联的,串行请求会逐个等待逐个执行,如果将请求改为并发,并加到任务队列中,这样会一次性执行完然后返回,相当于是并行请求。
- 异步Promise.allSettled()也可以实现这样的效果
渲染丢帧的优化
表现:屏幕没有按照120帧/秒进行渲染内容,应该呈现的帧未按时呈现。
- 合理地控制显示隐藏 当有一个状态控制组件显隐,如果通过if/else控制组件创建与销毁,对性能非常不友好,因此可以通过.visibility这个属性控制显示隐藏而不是创建销毁,这样避免了CPU频繁绘制的压力
- 组件复用(使用@Reusable标记自定义组件来实现组件复用)其原理和visibility属性原理是一样的,如果自定义组件被@Reusable装饰,那么它就不会真正被销毁,而是放入缓存中复用。
- 懒加载(子组件迭代)使用ForEach对数据源全量渲染,大量数据会造成列表卡顿。使用LazyForEach替代ForEach,它只会创建页面可视区域的这一部分组件。
- 分帧渲染(displaySync.DisplaySync)这个在项目中用的并不多,它是创建一个displaySync对象,通过这个对象来订阅监听每一帧变化,这样的好处是只渲染需要的部分或者说是看到的部分,需要反复渲染的不应该被重复销毁创建而是复用。
- 布局节点减少
- 如果组件不需要依赖 @Component的生命周期 我觉得可以通过自定义构建函数builder代替自定义组件。
- 使用合理的容器组件,Flex和Grid组件是测量、布局绘制耗时比较大的组件。非必须场景可以避免使用它们。
原因是:Flex和Grid需要根据子组件数量、尺寸约束、排列规则动态计算每个子节点的位置和大小,因此会增加 Measure测量和 Layout布局阶段的开销。
- 精简节点数、减少嵌套层数。
如果是嵌套层级较深的,尽可能使用相对布局来进行扁平化处理。
- 组件绘制优化
- 避免在组件初始化时 aboutToAppear中做耗时任务 避免阻塞UI。
- 按需注册组件属性:通过attributeModifier属性,手动维护一个继承并实现AttributeModifier接口的属性管理类,通过拿到当前组件的实例对象来动态注册属性。
- 当容器限定其为百分比或者没有设置宽高,它的整体绘制、测量、布局都是需要比较高的时长,设置宽高后就能够减小绘制的负担,这样耗时就能大大减小。
// 导入 AttributeModifier 接口
import { AttributeModifier } from '@ohos.arkui.modifier';
// 1. 自定义属性管理类,实现 AttributeModifier 接口
// 泛型 <TextAttribute> 表示这个类是专门用来管理 Text 组件属性的
class MyTextModifier implements AttributeModifier<TextAttribute> {
// 存放要动态修改的属性值
backgroundColor: ResourceColor = Color.Transparent;
fontSize: number = 14;
// 2. 核心方法:应用普通属性
// 参数 instance 就是当前组件的属性实例对象,可以用它来注册属性
applyNormalAttribute(instance: TextAttribute): void {
// 拿到组件实例后,像在链式调用里一样直接设置属性
// 这里相当于动态地给组件加上了 .backgroundColor(...).fontSize(...)
instance.backgroundColor(this.backgroundColor);
instance.fontSize(this.fontSize);
}
}
// 页面入口
@Entry
@Component
struct AttributeModifierDemo {
// 创建属性管理类的实例,用 @State 修饰以便修改后刷新界面
@State modifier: MyTextModifier = new MyTextModifier();
build() {
Column({ space: 20 }) {
// 3. 将属性管理类绑定到组件上
// 只要 modifier 里的属性值发生变化,applyNormalAttribute 就会被自动调用
Text('动态属性示例')
.attributeModifier(this.modifier) // 关键:绑定自定义属性管理对象
.width('80%')
.height(60)
.textAlign(TextAlign.Center)
// 按钮:点击时更换背景色和字号
Button('更换样式')
.onClick(() => {
// 修改 modifier 里的属性值,触发界面更新
this.modifier.backgroundColor = Color.Orange;
this.modifier.fontSize = 24;
})
// 按钮:恢复默认样式
Button('恢复默认')
.onClick(() => {
this.modifier.backgroundColor = Color.Transparent;
this.modifier.fontSize = 14;
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
- 实现
AttributeModifier<TextAttribute>接口
我们创建的MyTextModifier类负责管理Text组件的属性,泛型决定了它能操作哪种组件。 applyNormalAttribute(instance)方法
每当界面刷新(且 modifier 发生变化)时,系统会调用这个方法,并把当前Text组件的属性对象传进来。我们通过instance动态调用.backgroundColor()、.fontSize()等方法,相当于在组件上重新注册了这些属性。- 绑定到组件
.attributeModifier(this.modifier)将管理类实例和组件关联起来,后续只要改变this.modifier中的属性值,组件样式就会自动更新,无需手动重新创建组件或写一堆if/else。 - 按需修改
在按钮的onClick中直接修改modifier的字段,框架检测到变化后会自动重新执行applyNormalAttribute,从而实现“动态注册属性”的效果。
通过这种方式,我们可以将组件的样式逻辑全部集中到一个类中,让代码更干净、更易维护,尤其适合需要频繁切换主题、动态调整样式的场景。
- 状态刷新控制
- 避免不必要的状态变量使用,不一定所有的变量都需要响应式。比如不需要更新UI的数据无需状态修饰。
- 如果只是某一个组件使用的状态,应定义在组件内部,避免定义外层组件,导致状态变化整页刷新。
- 如果是多层组件嵌套,需要传递应该选择合适的装饰器做状态管理,而不是属性透传。
- 通过@Observed和@Track精细化拆分复杂的状态进行管理。
- 通过@Watch精准监听订阅精细控制组件刷新。减少状态变化而影响的范围。
- 动画控制
- 使用系统提供的animation属性动画来替代自己使用定时器实现的动画。
- 使用图形变换属性实现组件变化(rotate、translate、scale、transform)
- 合理使用animateTo闭包动画,相同动画参数的动画放入一个animateTo去执行。
- 使用renderGroup缓存动效:可以将组件动画效果缓存起来,但是组件必须是静态组件,子组件内不能有动画效果。
- 感知流畅优化
- 视觉感知优化:进入应用时添加浮层 放置动效、骨架屏 缓解用户等待时间
- 转场动效感知:一镜到底
- 合理使用动画时长感知应用流程:进度条 圆环 需要显示当前进度 填充等待时常
4. 实际项目优化方案
- Web组件加载网页的白屏优化
之所Web渲染的页面会在返回后再次打开时白屏,是因为WebView组件的核心机制是按需创建 离开释放。Web组件很重,里面承载了浏览器渲染的一些列的接口以及生命周期。如果每个页面都保存就会有很大的内存开销。因此每次进入Web加载的页面都会重新创建组件包括控制器一系列挂载操作。
所有操作都需要时间,所以每次用户看到都有一个白屏的阶段。
- 首先在整个项目的入口,也就是根节点index的aboutToAppear中通过WebviewController的initialize方法来初始化这个webView的引擎。接着在调用WebviewController中的prepareForPageLoad()方法来开启预解析,只需要传入需要提前解析的url地址,开启预链接以及配置sockets的数量即可。
- 预渲染主要是通过创建一个WebView的管理器,将Web组件先封装在builder中,然后手动维护一个用于动态创建节点的NodeController()和一个FrameNode节点容器,再把Web节点挂到FrameNode下面,接着初始化Web,在里面将封装的builder创建挂载到节点上。此时我会将url地址和创建的NodeController对象对应存储到字典中,作为缓存的NodeMap。最后就可以在Web组件的onAppear中调用prefetchPage预下载了。
- 这样测试后发现网页基本可以秒开。但是有个坑点,如果返回再次进入就空白了。其实是因为先创建好要渲染的节点,再解析页面之后,这个节点中有内容,如果内容没有发生变化,默认就不会再次渲染了。所以我记得当时解决办法是先判断,看看节点有没有内容,如果有就调用clear先清空再去预加载。
/**
* ============================================================================
* 文件说明:Web 预加载秒开方案
* ============================================================================
* 核心思路:利用 NodeController + BuilderNode + NodeContainer 实现 Web 组件的
* 离屏预渲染,用户点击前已完成页面加载,达到"秒开"效果。
*
* 整体架构:
* 1. WebData —— Web 组件入参数据封装(url + controller)
* 2. webBuilder —— @Builder 函数,声明 Web 组件的 UI 结构
* 3. WebNodeController —— 继承 NodeController,负责通过 BuilderNode 创建节点
* 4. WebPreloadNode —— 封装预加载节点(含 controller + nodeController)
* 5. WebPreloadManager —— 单例管理器,统一管理预加载节点的创建与复用
* 6. Index —— 入口页面,包含首页列表与 Web 显示两个视图
*
* 关键点:
* - 隐藏容器:NodeContainer 宽高为 0 且 visibility=Hidden,保持 Web 节点不被销毁
* - 节点借用:在隐藏容器与可见容器之间切换 NodeController 引用,避免重复创建
* - 防白屏:返回时清空 + 重新预加载,避免内容未变化导致不渲染
* ============================================================================
*/
// 导入所需模块
import { webview } from '@kit.ArkWeb';
import { NodeController, FrameNode, UIContext, BuilderNode } from '@kit.ArkUI';
// ============================================================================
// 0. Web 组件的入参封装类
// ============================================================================
/**
* Web 组件的入参数据封装。
* 将 WebviewController 和目标 URL 打包传递给 Builder,避免直接传递多个参数。
*/
class WebData {
/** WebviewController 实例,用于控制 Web 组件的加载、刷新等操作 */
controller: webview.WebviewController;
/** 要加载的目标网页地址 */
url: string;
/**
* 构造函数
* @param controller - Web 控制器实例
* @param url - 目标网页 URL
*/
constructor(controller: webview.WebviewController, url: string) {
this.controller = controller;
this.url = url;
}
}
// ============================================================================
// 1. 定义 Web 组件的 Builder(用于动态创建节点)
// ============================================================================
/**
* Web 组件的 @Builder 函数。
* 该函数会被 wrapBuilder 包装后传给 BuilderNode,用于创建离屏预渲染节点
* 以及最终显示的节点。
*
* @param data - 包含 url 和 controller 的 WebData 对象
*/
@Builder
function webBuilder(data: WebData) {
// src 指定初始加载地址,controller 用于后续控制
Web({ src: data.url, controller: data.controller })
.domStorageAccess(true) // 开启 DOM Storage 访问能力
.width('100%')
.height('100%')
}
// ============================================================================
// 2. 自定义 NodeController,负责创建 Web 的 FrameNode
// ============================================================================
/**
* 自定义 NodeController,继承自系统 NodeController。
* 职责:通过 BuilderNode + UIContext 创建实际的 Web FrameNode 节点,
* 供 NodeContainer 组件渲染显示。
*/
class WebNodeController extends NodeController {
/** 包装后的 @Builder 函数引用,用于 BuilderNode.build() 调用 */
private builder: WrappedBuilder<[WebData]>;
/** BuilderNode 实例(懒加载),持有实际创建的 FrameNode */
private builderNode: BuilderNode<[WebData]> | null = null;
/** Web 控制器实例,传递给 @Builder 中的 Web 组件 */
private controller: webview.WebviewController;
/** 目标网页 URL,传递给 @Builder 中的 Web 组件 */
private url: string;
/**
* 构造函数
* @param controller - Web 控制器实例
* @param url - 目标网页 URL
*/
constructor(controller: webview.WebviewController, url: string) {
super();
this.controller = controller;
this.url = url;
// 将全局 @Builder 函数包装为 WrappedBuilder,供 BuilderNode 使用
this.builder = wrapBuilder(webBuilder);
}
/**
* 核心方法(由 NodeContainer 触发):使用 UIContext 创建实际的 FrameNode 节点。
* 采用懒加载策略:首次调用时创建 BuilderNode 并构建内容,后续直接返回已创建的节点。
*
* @param uiContext - 当前 UI 上下文,BuilderNode 需要它来创建节点
* @returns 创建的 FrameNode,失败返回 null
*/
makeNode(uiContext: UIContext): FrameNode | null {
// 懒加载:仅在首次调用时创建 BuilderNode
if (this.builderNode === null) {
this.builderNode = new BuilderNode(uiContext);
// 使用 builder 和 WebData 参数构建 Web 组件内容
this.builderNode.build(this.builder, new WebData(this.controller, this.url));
}
// 返回已创建的 FrameNode
if (this.builderNode) {
return this.builderNode.getFrameNode();
}
return null;
}
}
// ============================================================================
// 3. 预加载节点封装
// ============================================================================
/**
* 预加载节点封装类。
* 将 WebNodeController 和 WebviewController 打包在一起,
* 同时提供"清空 + 重新预加载"能力以解决返回白屏问题。
*/
class WebPreloadNode {
/** 节点控制器,传递给 NodeContainer 用于渲染 Web 内容 */
public nodeController: WebNodeController;
/** Web 控制器,直接操作 Web 组件的加载、预取等行为 */
public webController: webview.WebviewController;
/** 目标网页 URL */
public url: string;
/**
* 构造函数:根据 URL 创建 WebviewController 和 WebNodeController
* @param url - 目标网页 URL
*/
constructor(url: string) {
this.url = url;
this.webController = new webview.WebviewController();
this.nodeController = new WebNodeController(this.webController, url);
}
/**
* 关键方法:解决"返回再进入页面时白屏"的问题。
*
* 白屏原因:Web 内容未变化时,NodeContainer 可能不会重新触发渲染。
*
* 解决方案:
* 1. 先加载 about:blank 清空当前内容
* 2. 调用 prefetchPage 预取目标页面(利用已初始化的 Web 引擎)
* 3. 再加载真实 URL,触发重新渲染
*/
clearAndPreload(): void {
try {
// 第一步:清空当前内容,避免因内容未变化导致 NodeContainer 不重新渲染
this.webController.loadUrl('about:blank');
// 第二步:预取目标页面,利用已初始化的 Web 引擎提前下载资源
this.webController.prefetchPage(this.url);
// 第三步:加载真实页面,触发完整渲染
this.webController.loadUrl(this.url);
} catch (err) {
console.error(`clearAndPreload error: ${JSON.stringify(err)}`);
}
}
}
// ============================================================================
// 4. 预渲染管理器(单例)
// ============================================================================
/**
* 预渲染管理器(单例模式)。
* 职责:
* - 统一管理预加载节点的创建、复用与获取
* - 提供 doPreload 方法在隐藏容器挂载后执行真正的预加载
*
* 单例设计确保整个应用生命周期内只有一个管理器实例,
* 避免重复创建多个预渲染节点浪费资源。
*/
class WebPreloadManager {
/** 单例实例 */
private static instance: WebPreloadManager;
/** 当前持有的预加载节点(URL 相同时复用) */
private preloadNode: WebPreloadNode | null = null;
/**
* 获取单例实例(懒加载)
* @returns WebPreloadManager 单例
*/
static getInstance(): WebPreloadManager {
if (!WebPreloadManager.instance) {
WebPreloadManager.instance = new WebPreloadManager();
}
return WebPreloadManager.instance;
}
/**
* 创建预加载节点。如果已存在节点且 URL 相同则直接复用,避免重复创建。
* @param url - 目标网页 URL
* @returns 预加载节点实例
*/
createPreloadNode(url: string): WebPreloadNode {
// 不存在节点,或 URL 变化时,创建新节点
if (!this.preloadNode || this.preloadNode.url !== url) {
this.preloadNode = new WebPreloadNode(url);
}
return this.preloadNode;
}
/**
* 获取当前预加载节点
* @returns 预加载节点,未创建时返回 null
*/
getPreloadNode(): WebPreloadNode | null {
return this.preloadNode;
}
/**
* 在隐藏容器挂载后调用,执行真正的预加载。
* 此方法触发 prefetchPage(预取页面资源)和 loadUrl(加载页面),
* 让 Web 引擎在用户点击前就完成页面下载与渲染。
*
* @param node - 要执行预加载的节点
*/
doPreload(node: WebPreloadNode): void {
try {
// 预取页面资源,提前下载 HTML/JS/CSS 等
node.webController.prefetchPage(node.url);
// 加载页面,触发渲染(在隐藏容器中完成,用户不可见)
node.webController.loadUrl(node.url);
} catch (err) {
console.error(`doPreload error: ${JSON.stringify(err)}`);
}
}
}
// ============================================================================
// 5. 入口页面
// ============================================================================
/**
* 入口页面组件。
*
* 页面结构:
* - 首页视图(showWeb=false):标题 + "打开网页"按钮
* - Web视图(showWeb=true) :返回按钮 + NodeContainer 显示 Web 内容
* - 隐藏容器(始终在布局中):宽高为 0 且不可见,保持 Web 节点活跃
*
* 核心交互流程:
* 1. aboutToAppear:初始化引擎 → 预连接 → 创建预渲染节点放入隐藏容器
* 2. onPageShow:隐藏容器挂载后执行预加载(提前下载页面)
* 3. 点击"打开网页":清空+重新预加载 → 节点从隐藏容器借到可见容器
* 4. 点击"返回":节点从可见容器还给隐藏容器(不被销毁)
*/
@Entry
@Component
struct Index {
/** 是否显示 Web 视图(false=首页列表,true=Web 内容) */
@State showWeb: boolean = false;
/** 当前可见区域的节点控制器(用于 Web 显示界面的 NodeContainer) */
@State visibleController: WebNodeController | null = null;
/** 隐藏容器中的节点控制器(保持 Web 节点活跃,不被销毁) */
@State hiddenController: WebNodeController | null = null;
/** 预加载节点引用(包含 controller + nodeController) */
private preloadNode: WebPreloadNode | null = null;
/** 预渲染管理器单例 */
private webManager: WebPreloadManager = WebPreloadManager.getInstance();
/**
* 组件生命周期:aboutToAppear
* 在组件创建后、build() 执行前调用,用于初始化数据。
*
* 执行步骤:
* 1. 初始化 Web 引擎(应用生命周期内只需一次)
* 2. 预连接目标 URL(DNS 解析 + TCP/TLS 握手)
* 3. 创建预渲染节点并放入隐藏容器
*/
aboutToAppear(): void {
// 步骤1:初始化浏览器引擎(整个应用生命周期只需调用一次)
webview.WebviewController.initializeWebEngine();
// 步骤2:预解析目标 URL,提前进行 DNS 解析、TCP/TLS 握手等
try {
// 参数:url, 是否预连接, socket 数量
webview.WebviewController.prepareForPageLoad('https://www.suning.com', true, 3);
} catch (err) {
console.error(`prepareForPageLoad error: ${JSON.stringify(err)}`);
}
// 步骤3:创建预渲染节点并暂存到隐藏容器
this.preloadNode = this.webManager.createPreloadNode('https://www.suning.com');
this.hiddenController = this.preloadNode.nodeController;
}
/**
* 组件生命周期:onPageShow
* 页面显示时调用。此时隐藏容器中的 Web 节点已挂载到布局中,
* 可以安全地执行预加载(prefetchPage + loadUrl)。
*/
onPageShow(): void {
// 隐藏容器中的 Web 节点已挂载,执行预下载
if (this.preloadNode) {
this.webManager.doPreload(this.preloadNode);
}
}
/**
* 构建 UI 布局。
*
* 使用 Stack 层叠布局,包含三个互斥/共存的部分:
* - if (!showWeb) : 首页列表视图(按钮触发跳转)
* - else : Web 显示视图(按钮触发返回)
* - 隐藏容器 : 始终存在于布局中,宽高为 0 且不可见
*/
build() {
Stack() {
// ========== 首页列表界面 ==========
if (!this.showWeb) {
Column({ space: 20 }) {
Text('首页')
.fontSize(30)
.fontWeight(FontWeight.Bold)
Button('打开网页(秒开)')
.onClick(() => {
// 获取预加载节点
let node = this.webManager.getPreloadNode();
if (node) {
// 核心:清空 + 重新预加载,解决返回白屏问题
node.clearAndPreload();
// 将节点从隐藏容器"借"到可见容器
this.visibleController = node.nodeController;
// 隐藏容器置空(同一节点不能同时被两个 NodeContainer 引用)
this.hiddenController = null;
// 切换到 Web 显示视图
this.showWeb = true;
}
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// ========== Web 显示界面 ==========
else {
Column() {
Button('返回')
.onClick(() => {
// 将节点"还"给隐藏容器,保证其不被销毁
this.hiddenController = this.visibleController;
// 可见容器置空
this.visibleController = null;
// 切换回首页列表视图
this.showWeb = false;
})
// 可见的 Web 容器:渲染预加载好的 Web 内容
if (this.visibleController) {
NodeContainer(this.visibleController)
.width('100%')
.height('90%')
}
}
.width('100%')
.height('100%')
}
// ========== 隐藏容器 ==========
// 一直存在于布局中,但宽高为 0 且不可见。
// 作用:保持 Web 节点活跃,避免被系统销毁,实现秒开。
if (this.hiddenController) {
NodeContainer(this.hiddenController)
.width(0)
.height(0)
.visibility(Visibility.Hidden)
}
}
}
}
- 渲染丢帧优化方案
- 创建一个displaySync的实例对象 displaySync.create()
- 通过setExpectedFrameRateRange设置可变帧率。在这里可以设置预计值、最小值和最大值(一般最小值给60,因为大于60肉眼无法分辨出来)。
- 通过实例对象中的on方法监听帧率的变化,第一个参数是'frame'类型,第二个参数是一个回调函数,可以在回调函数中监听第一帧、第二帧、更多帧,但是这个计数count不会自增,所以每次监听后需要+1来到下一帧。
- 如果要取消监听,通过实例对象的stop和off方法,传入'frame'即可
- 准备工作做好后,就是实际使用了,这里会根据业务需求,需要多次测试,这里也是最麻烦的。
因为在业务中,接口返回的数据一般是一个数组,而不是一条数据。所以我们需要根据具体业务情况进行规划渲染和拆分。以及拆分一些静态内容也需要把这个帧率计算排列的位置空出来,确保下一帧可以按照我们期待的业务需求去渲染。这个过程是非常复杂的,需要精细化拆分业务,包括这一帧渲染内容、下一帧渲染内容。需要多次反复测试,利用Profiler分析每一次变化,观察泳道的丢帧情况。例如,第一帧丢帧就需要把第一帧拆分更加精细,因为涉及到冷启动除了渲染页面还有很多事情要做,这些都是造成丢帧的元素。
长列表优化方案:将ForEach替换为LazyForEach的话,首先要准备一个数据源类,需要这个类继承并实现(implements) IDataSource接口。然后需要实现四个方法:totalCount、getData、registerDataChangeListener和unregisterDataChangeListener。要实现这些方法,还需要准备数据数组和监听器数组。以及自己操作数据源数组的方法 如pushData。在页面中,首先声明一个当前数据源类实例化出来的数据源对象。LazyForEach组件接收三个参数,第一个是数据源对象,传入即可,第二个是子组件迭代函数,第三个是键值生成函数。这些和ForEach使用方式一样。只是操作数组的方法有一点不同,需要通过调用数据源类的方法去实现。
创建display实例:displayAbi = displaySync.create()
设置可变帧率:displayAbi.setExpectFrameRareRange()
监听帧变化:displayAbi.on()
开启监听:displayAbi.start()
更新帧:停止和取消监听: displayAbi.stop()
5. 内存优化方向及内存泄漏
目的:降低应用运行时内存占用,避免内存泄漏、溢出等问题
内存泄漏:本质是内存只占不还,由于对象已不再使用,但因被强引用而无法被GC回收,内存占用呈现持续上升趋势。
由于订阅监听没有取消,页面组件及上下文被全局对象持有无法回收。
定时器没有在页面销毁时清除,导致回调函数持有页面this引用。
长生命周期对象AppStorage、全局单例持有了短生命周期对象或组件的闭包或回调。
Native层申请了C++侧内存或创建了napi_ref跨作用域延长和管理 JavaScript 对象的生命周期,但没有手动释放。
内存溢出:本质是内存不够用了,应用占用的内存超过了系统分配给该应用/进程的上限,直接会导致应用crash崩溃退出。
一次性解码并加载多张超高分辨率图片到内存中,导致进程内存限制被越界。
在for循环或频繁触发的UI更新中大量创建临时大数组、缓冲区,冲出ArrayBuffer
用户反复打开关闭有内存泄漏的页面,内存逐步累积,最终触顶内存溢出阈值
内存优化方向:
- 避免内存泄漏:避免声明变量始终处于可访达的状态,影响垃圾回收机制回收它们。
- 使用LRUcache缓存,它可以设置最大缓存数量,当超过这个数量后,会根据最近最少使用原则去覆盖之前存储的内容,内存上限是固定的。
- 合理使用图片尺寸。
- 预置图片加载优化:"compression"开启纹理压缩,配置纹理超压缩。内存占用下降 图片渲染时间增加。
- 资源释放优化,在使用涉及到媒体功能的录音机、播音播放器等组件,在使用完毕后要及时停止和释放资源。
- 在UIAbility生命周期回调onMemoryLevel监听内存等级,对不重要的资源进行内存释放、停止非核心任务以及释放不影响程序运行的内存。
使用LRU来缓存对象,可以避免重新计算、重新加载以及重新创建对象。
核心思想是 当存储进来后,如果缓存容量不足时,优先覆盖掉最长时间没有被访问的数据
底层逻辑:HashMap+双向链表
HashMap负责快速查找,双向链表负责排序
时间复杂度:是用来衡量一个算法随着输入规模增长,执行时间增长速度的指标。它不是计算具体执行多少秒,而是关注:当数据量越来越大时,算法性能会怎么变化。
O(1)常数时间、O(n)线性增长、 O(n²)平方复杂度、O(log n)对数复杂度
附:
高性能编程常见问题:
- 尽量使用const声明常量
- 减少使用嵌套 export */import *的方式
- 减少全局变量存储,使用局部变量提升属性访问速度
- 防止传感器滥用
- 减少不必要的后台任务,防止抢占资源
- 防止算法时间复杂度失控
- 清理冗余文件和代码
- 使用HSP代替HAR
- 依赖管理优化,删除无用依赖
更多推荐


所有评论(0)