HarmonyOS 鸿蒙Next ViewModel中使用Monitor监控属性,退出页面后不被释放,依然能响应数据变化

HarmonyOS 鸿蒙Next ViewModel中使用Monitor监控属性,退出页面后不被释放,依然能响应数据变化

Monitor 问题

场景说明

APP中有对专家进行关注的需求,所有会在不同的页面对专家进行关注,然后结果同步其他页面。这是一个简单的事件分发模型,现在的做法是:

  1. 创建一个全局单例的关注管理类FollowCommunityUserManager,实例为 followCommunityUserManager,在进行关注的时候调用followCommunityUserManager.follow方法进行关注。
export class FollowCommunityUserEvent extends BaseEvent {
  userId: string
  hasFollowed: boolean

  constructor(userId: string, hasFollowed: boolean) {
    super()
    this.userId = userId
    this.hasFollowed = hasFollowed
  }
}

[@ObservedV2](/user/ObservedV2)
export class FollowCommunityUserManager {
  @Trace followsFlow?: FollowCommunityUserEvent

  async follow(userId?: string, hasFollowed?: boolean) {
    HCLog.e("MonitorQuestion", `followCommunityUserManager.follow`)
    this.followsFlow = new FollowCommunityUserEvent(userId, !hasFollowed)
    ToastUtil.showToast(hasFollowed ? "取消关注成功" : "关注成功")
  }
}

export let followCommunityUserManager =  new FollowCommunityUserManager()
  1. 在需要关注的页面中用Monitor对followManager.followsFlow进行监听,触发逻辑处理。
[@ObservedV2](/user/ObservedV2)
export class CommunityDetailsPageVM {
  @Trace pageState: CommunityDetailsPageState = new CommunityDetailsPageState()
  @Trace followManager = followCommunityUserManager
  timestamp: number = Date.now()

  [@Monitor](/user/Monitor)('followManager.followsFlow')
  onFollowChange(monitor: IMonitor) {
    let path = "followManager.followsFlow"
    HCLog.e("MonitorQuestion", `CommunityDetailsPageVM ${this.timestamp} ${path} changed from ${monitor.value(path)?.before} to ${JSON.stringify(monitor.value(path)?.now)}`)
    
    // 处理关注事件
  }
}
@Entry
@ComponentV2
export struct CommunityDetailsPage {
  @Local mVM: CommunityDetailsPageVM = new CommunityDetailsPageVM()
  @Local pageState: CommunityDetailsPageState = this.mVM.pageState
  @Local followManager: FollowCommunityUserManager = followCommunityUserManager
  timestamp: number = Date.now()

  [@Monitor](/user/Monitor)('followManager.followsFlow')
  onFollowChange(monitor: IMonitor) {
    let path = "followManager.followsFlow"
    HCLog.e("MonitorQuestion", `CommunityDetailsPage ${this.timestamp} ${path} changed from ${monitor.value(path)?.before} to ${JSON.stringify(monitor.value(path)?.now)}`)

    // 处理关注事件
  }
  
  aboutToAppear(): void {
    HCLog.e("MonitorQuestion", `CommunityDetailsPage ${this.timestamp} aboutToAppear`)
  }

  aboutToDisappear(): void {
    HCLog.e("MonitorQuestion", `CommunityDetailsPage ${this.timestamp} aboutToDisappear`)
  }
  
  build() {
    NavDestination() {
    }
  }
}

问题

  1. 当进入页面->关注->离开页面,重复执行三次后,会出现如下日志:
02-21 11:58:49.468   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110329467 aboutToAppear
02-21 11:58:51.855   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, followCommunityUserManager.follow
02-21 11:58:51.856   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110329467 followManager.followsFlow changed from undefined to {"userId":206541,"hasFollowed":true}
02-21 11:58:51.856   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPageVM 1740110329467 followManager.followsFlow changed from undefined to {"userId":206541,"hasFollowed":true}
02-21 11:58:54.876   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110329467 aboutToDisappear
02-21 11:58:56.581   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110336581 aboutToAppear
02-21 11:58:58.412   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, followCommunityUserManager.follow
02-21 11:58:58.413   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPageVM 1740110329467 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":false}
02-21 11:58:58.414   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110336581 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":false}
02-21 11:58:58.414   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPageVM 1740110336581 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":false}
02-21 11:59:03.491   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110336581 aboutToDisappear
02-21 11:59:05.045   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110345045 aboutToAppear
02-21 11:59:07.160   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, followCommunityUserManager.follow
02-21 11:59:07.161   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPageVM 1740110329467 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":true}
02-21 11:59:07.161   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPageVM 1740110336581 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":true}
02-21 11:59:07.161   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110345045 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":true}
02-21 11:59:07.162   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPageVM 1740110345044 followManager.followsFlow changed from [object Object] to {"userId":206541,"hasFollowed":true}
02-21 11:59:09.317   28364-28364   A0FFFF/com.net...ttery/lottery  com.netease.lottery   E     MonitorQuestion, CommunityDetailsPage 1740110345045 aboutToDisappear

从日志中发现,ViewModel在页面退出后没有被释放,依然能监听到事件,可能会导致内存的泄漏,请问为什么会出现这种情况?目前没有找到类似生命周期管理的文档说明,假设没有Monitor的情况,ViewModel是否会被释放。

从业务分离的调度说,此处监听放到ViewModel中更加合理,但从目前情况来看不得不放Page中。

理论上Page能做退出后监听不在被响应,他的内部属性ViewModel应该也应该被释放,在《@Monitor装饰器:状态变量修改监听》文档中看到Monitor是可以像我这样在@ObservedV2中使用的。

  1. 关于单例模式的创建

目前使用export let followCommunityUserManager = new FollowCommunityUserManager()方式创建,需要同时对外暴露FollowCommunityUserManager和followCommunityUserManager 因为@Local followManager: FollowCommunityUserManager = followCommunityUserManager必须写明类型。这样在使用中有被错误使用的可能。

参考TS的单例写法export default new FollowCommunityUserManager(),此时暴露FollowCommunityUserManager即是单例,FollowCommunityUserManager类型不对外暴露,但目前看无法实现,包括AppStorageV2也有类似问题。

  1. 这样的事件分发是否有问题或隐患?

这是一次简单的事件分发,后续我们会接入websocket,然后全局实时同步上百场赛事的数据,在多页面同步,这样的设计是否会有并发上的问题? 参考Android有Flow做数据队列的缓冲,ArkTS中没有找到类似的功能,像以上这种设计是否可以满足需求?


更多关于HarmonyOS 鸿蒙Next ViewModel中使用Monitor监控属性,退出页面后不被释放,依然能响应数据变化的实战教程也可以访问 https://www.itying.com/category-93-b0.html

3 回复

问题1,View(UI)和State是分离的,并且可以多个页面使用一份数据,所以页面的显示与隐藏与监控对象是分开的;

问题2,单例可以通过对外暴露类和获取实例化的方法实现的

问题3,关于您所说的隐患问题请放心,鸿蒙支持的更复杂的场景如银行、证券、期货类交易已有很多落地实现;

更多关于HarmonyOS 鸿蒙Next ViewModel中使用Monitor监控属性,退出页面后不被释放,依然能响应数据变化的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


问题1,View(UI)和State是分离的,这个逻辑可以理解,但那如何做到相关联呢?关联或者分离应该可以由开发者控制才对。当页面创建,我创建并绑定VIewModel,进行了监控,然后页面销毁,我该如何销毁当前页面的VIewModel吗?有示例代码可可以参考吗?

在我的示例中,是要在aboutToDisappear中手动销毁mVM吗?鸿蒙中怎么销毁?

问题2,是这样处理吗?

export class FollowCommunityUserManager {
  private constructor() {
  }
  static instance = new FollowCommunityUserManager()
}

在HarmonyOS鸿蒙Next中,ViewModel的生命周期通常与页面的生命周期绑定。当页面退出时,ViewModel应被释放以回收资源。如果在ViewModel中使用Monitor监控属性,且退出页面后ViewModel依然存在并能响应数据变化,可能是由于ViewModel未被正确释放。

ViewModel的生命周期管理依赖于ViewModelStore,当页面销毁时,ViewModelStore会调用ViewModel的onCleared()方法进行清理。如果ViewModel未被释放,可能是以下原因之一:

  1. ViewModelStore未正确清理:可能是页面销毁时,ViewModelStore未正确调用onCleared()方法,导致ViewModel未被释放。

  2. 强引用持有ViewModel:可能存在其他对象对ViewModel的强引用,导致ViewModel无法被垃圾回收机制回收。

  3. Monitor未正确释放:Monitor监控属性可能未在onCleared()方法中正确释放,导致ViewModel继续持有资源。

确保在页面销毁时,ViewModel能够被正确释放,可以在onCleared()方法中手动释放Monitor监控属性,并检查是否有其他对象持有ViewModel的引用。

如果问题依然存在,建议检查页面生命周期管理逻辑,确保ViewModel在页面退出时被正确清理。

回到顶部