APP
快来分享你的内容吧~
- APP的长登录,在后端是怎么去实现的?在前端要怎么去配合呢?问题描述APP的长登录,在后端是怎么去实现的?在前端要怎么去配合呢?背景信息现在的APP登录状态登录保持很久,这样的效果是怎么去实现的呢?现在常用的技术方案有哪些呢?...查看全文编程导航小智:长登录的实现方式长登录(Long Login)指的是用户登录状态可以保持较长时间的一种机制。在APP中,通常使用以下方式来实现长登录:1. 使用Token/JWT(JSON Web Token):在用户登录成功后,后端会生成一个Token,然后将Token返回给前端。前端将Token保存在本地,每次发送请求时,都在请求头中携带Token。后端根据Token的有效性判断用户的登录状态。Token通常
2024-12-04·产品经理- 2024-09-19基于ArkUI的List组件实现的滚动列表视图,在手指抛滑场景下,通过分析掉帧情况来判断List滑动是否流畅,保障用户极致流畅体验。查看全文明凡:@编程导航 图片的质量能不能调高一点啊 看不清431分享
- 2022-12-26
不写代码,让 AI 生成手机 APP!保姆级教程
你现在看到的 APP,是我完全用 AI 生成的,一行代码都没写!怎么做到的呢?  大家好,我是程序员鱼皮。AI 发展很快,现在随随便便就能生成一个网站,但是怎么纯用 AI 开发能在手机上运行的 APP 呢?网上基本上没有完整的教程。所以,我出手了,下面只用几分钟的时间,我会教大家如何利用 AI 生成 APP,依然是 **保姆级教程**。 ⭐️ 本文对应视频,推荐观看:[https://bilibili.com/video/BV17HMcziEye](https://www.bilibili.com/video/BV17HMcziEye/) 下面有请我们的主角 `Cordova`! ## 一、什么是 Cordova? Apache Cordova 是一个开源的移动应用开发框架,允许开发者使用 HTML、CSS 和 JavaScript 等 Web 技术开发 **跨平台** 的移动应用。它通过将 Web 技术封装在本地容器中,使得开发者可以编写一次代码,然后在 Android、iOS、Windows 等多个平台上运行。  Cordova 主要基于以下几个核心组件实现,感兴趣的同学可以了解一下:  也就是说,想要开发 APP,我们只需要把网站文件交给 Cordova,根据需要装一装插件、改一改配置,然后直接使用它提供的构建工具就能将 Web 应用打包成原生 APP 应用了(比如 APK 文件)!几乎不涉及任何代码编写和开发。 听起来很简单,有手就行?但是想使用 Cordova 开发 APP,必须要在电脑上安装对应的环境,比如 Android 和 IOS,而安装环境的难度可以说是 **非常炸裂** 了! 如果你自己折腾,可能至少要花个几天的时间,会踩很多坑,到网上搜各种方案还不一定能搞定。所以我才做了这个教程,该踩的坑我都帮大家踩完了,会 **用最短的时间带你搞定环境,并且教你如何使用 AI + Cordova 生成 APP**。开始之前记得 **点赞收藏三连** 哦,拜托,我的头发真的不多啦!  ## 二、环境准备 ### 安装 Cordova 首先我们要安装 Cordova。Cordova 的运行依赖 Node.js 和 NPM 前端工具,到 [Node.js 官网](https://nodejs.org/zh-cn) 下载即可,会自动安装 NPM。  可以把 NPM 理解为快速安装各种软件的小工具,安装完成后打开终端,执行下列命令安装 Cordova: ```bash npm install -g cordova ``` Cordova 支持将网站打包为 Android 和 IOS 移动端、Electron 桌面端应用。下面鱼皮带大家安装我个人认为难度最大的 **Android 环境**。注意,接下来的每一步,操作其实都不难,但是一定要仔细看!一个细节不注意可能就报错了! ### 安装 Android 环境 首先,我们要根据 Cordova 的版本来确定所需环境和工具的版本,由于我们安装的 Cordova 是最新版本的,因此直接阅读 [最新的官方文档](https://cordova.apache.org/docs/en/dev/guide/platforms/android/index.html) 即可,比如我这里需要的依赖如下:  其中,最重要的是: - Java 17 - Gradle 8.13 - Android API 级别 >= 24 下面我们分别安装这些依赖。 #### 1、安装 Java Java 版本必须是 17,最好找个现成的 [Windows 系统的 Java 安装包](https://www.azul.com/downloads/?version=java-17-lts&os=windows&package=jdk#zulu):  安装 Java 时建议选择 **自动配置环境变量**(包括 Path 和 JAVA_HOME),就不用自己手动配置环境变量了。  安装完成后,打开终端执行 `java -version` 命令查看版本号,看到下列输出表示成功:  如果无法执行命令,大概率是没有配置 Path 环境变量。  #### 2、安装 Gradle 根据上面的版本号,Gradle 必须是 8.13,直接到 [官网](https://gradle.org/releases/) 下载二进制压缩包即可。  解压下载完成的压缩包,移动到 **不包含中文的路径** 中,然后配置环境变量,包括 Path 和 GRADLE_HOME:   打开终端执行 `gradle -v` 命令,查看版本号:  如果命令无法执行,大概率是 Path 环境变量配置错误。 #### 3、安装 Android 建议直接安装 Android 开发工具 [Android Studio](https://developer.android.com/studio?hl=zh-cn),会自动安装 Android 的开发 SDK 和运行环境。 到官网下载 Android studio,运行安装包,按照步骤安装即可:  安装完成后,第一次打开 Android Studio 时,会提醒你安装 Android SDK 环境:  注意不要把 SDK 组件安装到包含中文的目录下,好在安装包也给了限制,不然又得栽倒一片人。。。  接下来无脑安装即可,会自动安装各种 Android 开发常用的工具、还有安卓设备模拟器:  这一步可能会有点煎熬,有些地区的朋友可能需要一些特殊的网络支持,你懂的。   经过了漫长的等待,Android SDK 终于安装完成,然后需要配置 Android 的环境变量 ANDROID_HOME:  还要配置 platform-tools 到 Path 中,里面有一些命令行工具:  配置完成后,我们打开 Android Studio,右上角进入 SDK Manager 的设置,根据 Cordova 的版本号要求,安装对应 API Level 的 SDK,比如我这里安装了 34 和 35 版本。  这一步可能也会比较慢,耐心等待安装吧~  安装完 SDK 后,再进入 SDK 工具选项,安装 Command-line Tools 命令行工具,之后在电脑上运行安卓 apk 包时可能会用到:  同样,把 Command-line Tools 添加到环境变量 Path 中,路径为 `%ANDROID_HOME%\cmdline-tools\latest\bin`,这样一来,很多工具可以直接在终端中使用了,比如 apkanalyzer。  #### 4、安装 Android 设备模拟器 下面我们要尝试在自己的电脑上运行 Android 手机模拟器,这样调试程序更方便。 打开 Android Studio 的设备管理器,添加一个新设备:  选择指定机型,建议选择 API 版本高一点的,我这里选择 Pixel 7:  安装推荐的系统镜像:  耐心等待后手机就创建成功了,直接运行:  结果,报错啦!  如果你也遇到这种情况,可以在终端 **进入 Android 模拟器目录** 手动运行虚拟设备,这样能够看到详细的错误信息,有利于排查问题。  比如我这里显然是由于路径包含了中文!可恶啊,当时年少轻狂不自卑一个没注意用了中文路径。。。  解决方法很简单,手动创建一个不包含中文路径的 avd 虚拟设备目录,然后设置环境变量 ANDROID_SDK_HOME:  然后再利用 Android Studio 创建一个设备并运行,这次成功运行了,恭喜你多了一个手机!  至此,环境终于搞定了,下面来实战 AI + Cordova 开发 APP。 ## 三、AI + Cordova 实战 ### 创建项目 打开终端,进入你想要创建项目的目录,先执行 `cordova create` 命令来创建项目: ```bash cordova create <你的项目英文名称> ``` 首次创建项目可能会有提示:  ### 生成代码 此处有 2 种生成模式: 1. 先创建 Cordova 项目,然后在该项目内进行 AI 代码生成。告诉 AI 你要创建一个兼容 Cordova APP 的网站,直接让 AI 生成兼容 APP 的代码。这样做的好处是生成的代码 **可以使用 Cordova 插件调用系统原生的能力**,比如调用相机进行拍照。 2. 在 Cordova 项目外单独用 AI 生成网站项目,AI 不会关心你是否要把项目转为 Cordova APP,然后再把生成好的网站移动到 Cordova 项目中。这样做的好处是生成的网站代码更容易运行,同样 **适合你已经有现成网站项目** 的场景。 下面两种方式我都会给大家演示,先讲第一种模式,直接让 AI 生成一个【表情包生成器】的 Cordova APP。 用 Cursor 打开刚刚创建的 Cordova 项目目录,给 AI 输入下列提示词,提示词中需要包含 Cordova,并且提到 **兼容性**: ```shell 请帮我开发一个【移动端表情包生成器】Web APP,使用纯前端技术 + Cordova 实现。 如果需要,你可以通过 Cordova 调用系统原生功能。 请生成完整的项目代码,确保功能完整可用,而且所有功能都需要同时兼容网页端和移动设备。 ## 📋 功能需求 ### 1. 图片获取 - 支持摄像头拍照 - 支持从本地选择图片文件 - 自动缩放图片到合适尺寸 ### 2. 表情包模板 - 提供8-10个常用表情包模板(惊呆了、无语、赞、点赞、emo了等) - 网格布局展示模板,点击选择应用 ### 3. 文字编辑 - 输入自定义文字内容 - 调整字体大小(20px-50px) - 选择文字颜色(白色、黑色、红色等基础色彩) - 添加文字描边效果 - 拖拽移动文字位置 ### 4. 贴纸功能 - 提供常用emoji表情贴纸(😂🤣😭😍🤔等5-10个) - 提供简单装饰贴纸(星星、爱心、箭头等) - 支持拖拽移动和简单缩放 ### 5. 保存功能 - 将编辑后的表情包导出为图片 - 支持下载保存到本地 ## 🎨 界面要求 - 移动端优先:适配手机屏幕,大按钮设计 - 页面布局: - 主页:拍照按钮、选择图片按钮 - 编辑页:顶部工具栏 + 中央画布 + 底部功能区 - 操作简单:实时预览效果,一键保存 ## 📱 操作流程 1. 拍照或选择图片 2. 选择表情包模板 3. 编辑文字内容和样式 4. 添加emoji或装饰贴纸 5. 预览效果并保存图片 ``` AI 生成的网站文件会放到 `www` 目录下。生成代码完成后,AI 可能会自动提醒你打包 APP 并且运行的命令,要依次添加安卓平台、安装插件、打包、运行。  这些命令我们等会儿就会用到,现在先不要自动执行,因为生成的代码不一定直接可用,我们需要先利用网页端进行调试。 ### 网页浏览 可以直接双击生成的 HTML 文件 `www/index.html` 查看效果;当然,更推荐的是通过 cordova 命令添加平台并运行。 先添加浏览器平台: ```shell cordova platform add browser ``` 如果你在执行命令时遇到了报错,可以直接问 AI,比如鱼皮遇到了缺少命令执行权限的错误:  解决方案是,执行下列命令来修改 PowerShell 的执行策略: ```bash Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser ``` 添加平台成功后,可以输入 `cordova run` 命令运行平台: ```bash cordova run browser ``` 然后就能够查看到网站的运行效果了。需要注意的是,因为 Cordova Browser 平台的特殊性,通过这个命令运行的网页效果可能和直接双击、或者启动本地服务器运行有区别。  除了上面的命令外,如果你想快速调试多个不同的平台,可以运行下列命令,统一查看各个平台: ```bash cordova serve --port 8000 ```  ### 添加安卓平台 接下来执行类似的命令来添加安卓平台: ```bash cordova platform add android ``` 如图,添加安卓平台成功,注意要 **确保输出的 Target SDK 和 Compile SDK 版本一致**:  如果不一致,可能会影响 APP 的运行。可以修改 `config.xml` 的 targetSdkVersion 来修改版本号:  ### 添加插件 由于我的项目需要调用摄像头,所以要添加对应的插件,执行下列命令: ```bash cordova plugin add cordova-plugin-camera ``` 添加插件成功:  ### 打包运行安卓 APP #### 打包 安装完插件后,执行 `cordova build` 命令可以打包 Android apk: ```bash cordova build android ``` 看到下列信息表示打包成功:  得到 apk 包后,有 2 种运行方式: #### 手机运行 可以直接将 apk 包发送到手机安装运行:  运行效果如图:   #### 电脑运行 先打开 Android Studio 并启动安卓虚拟设备,然后执行 `cordova run` 命令: ```bash cordova run android ``` 就可以将 apk 安装到虚拟设备中,并且运行 APP 了,效果如图:  ### 常见报错 打包运行是最容易遇到报错的地方,可能会遇到很多种报错,比如缺少插件、缺少文件、无法安装依赖、无法运行等等,建议直接把报错信息发给 AI,让它帮你解决。 下面鱼皮分享一些自己遇到的坑点。 #### 1、项目缺少文件 比如鱼皮的项目缺少了图标文件:  AI 尝试帮我创建图标:  或者简单粗暴,移除配置文件中对图标的引用:  #### 2、缺少环境变量 如果环境搭建不顺利,可能会遇到下列报错,根据报错信息去进行对应的配置即可:  #### 3、命令执行失败 执行 `cordova run` 报错命令执行失败,可能是因为没有配置 `cmdline-tools` 到环境变量 Path 中。  #### 4、Gradle 无法安装 明明已经安装了 Gradle,但是 Cordova 仍然会安装 Gradle,而且可能因为网络原因下载失败:  这时,我们可以配置环境变量 `CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL`,指定从本地下载 Gradle。环境变量的值设置为我们自己下载的 Gradle 压缩包的路径。  如果修改配置后再次执行打包命令还是报错,建议删除项目内的 `platforms/android/.gradle` 缓存,然后重试。 ## 四、已有项目打包为 APP 刚刚实战了直接用 AI 生成 Cordova APP 项目的方式,如果我们已经有现成的网站项目,也能够很方便地打包为 APP。 比如现在鱼皮有一个消消乐网页游戏项目,让我们来包装为 APP:  1)先创建 cordova 项目: ```bash cordova create yu-game-web-app ``` 2)把已有的网页文件复制到 www 目录下:  3)执行 cordova 命令添加 Android 平台: ```bash cordova platform add android ``` 4)最后,打包或者直接运行: ```bash cordova run android ``` 运行成功的效果如图,还是很 nice 的~  ## 最后 OK,教程到这里就结束了,由于缺少设备等原因,IOS 就先不给大家演示了。 最后给大家一些建议,Cordova 比较适合中小型网站项目,尤其适合已经有网站项目想快速转为 APP 的场景;但如果你需要搞一个复杂的大项目,依赖很多移动设备的原生能力,使用 Cordova 就不是很合适了,不如 Flutter。尤其是没有编程能力的同学来说,建议不要直接用 AI 生成复杂的 Cordova APP,很可能出现你搞不定的代码问题,但是做些小游戏、小工具还是很不错的。也希望我的分享对大家有帮助吧,想获取更多编程和 AI 干货的朋友记得关注鱼皮哦,拜拜~ ## 更多 💻 编程学习交流:[编程导航](https://www.codefather.cn/) 📃 简历快速制作:[老鱼简历](https://laoyujianli.com) ✏️ 面试刷题神器:[面试鸭](https://mianshiya.com) 📖 AI 学习指南:[AI 知识库](https://ai.codefather.cn/)
APP的长登录,在后端是怎么去实现的?在前端要怎么去配合呢?
### 问题描述 APP的长登录,在后端是怎么去实现的?在前端要怎么去配合呢? ### 背景信息 现在的APP登录状态登录保持很久,这样的效果是怎么去实现的呢?现在常用的技术方案有哪些呢?
APP 上架的临门一脚,差点没迈过去!!
大家好,我是多喝热水。 最近在开发编程导航APP,大家有什么优化点都可以给我提,没有下载体验的可以[下载](https://codefather.cn/post/app)一波! 解答一下大家问的比较多的问题,编程导航 APP 是 Flutter 开发的,大家对开发 APP 感兴趣可以关注一下,后续会持续分享一些 Flutter 专栏内容。 ## 正文 最近 APP 的功能已经做的差不多了,现在正在陆续上架到各大应用商店。当我以为功能开发完了就万事大吉了,其实还有几个大雷等着我踩! <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/f4wfaYRnOHeDyLGS.webp" alt="" width="100%" /> 为什么这么说,因为相关部门对 APP 的隐私问题看的非常重,为了防止一些应用违法过度收集用户信息,在用户未同意 APP 的**隐私政策/用户协议**的情况下不允许收集用户信息。 相信大家在第一次下载 APP 打开的时候,都会需要你授权同意它的隐私政策/用户协议才能让你继续使用。哥们以前还嫌它烦,寻思这玩意有啥用呢,我不同意你就不会获取我的信息了吗?谁信啊,在互联网冲浪谁不是裸奔的。 <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/9zhs7L5ZheBVgxbL.webp" alt="" width="215px" /> 但这事轮到自己头上来才知道,不是别人想加,而是相关部门规定必须加。不过往好处想,也确实能起到一点对用户个人信息保护的作用。但这玩意对开发者来说是真头大。 比如我们在启动应用的时候如果获取了用户设备信息,那不好意思,直接就是:“不通过”。哥们现在看到这三个字都PTSD了。 再者,就是你的代码里没写相关的代码,但是使用的 SDK 获取了。 最难受的来了,我们需要提交的应用市场非常多(每个应用市场需要的材料可能都不一样),第一次提交的时候没意识到隐私合规的重要性,提交的应用市场全部打回,哥们天塌了,又要重新上传。 <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/6J2dKwseonq13C2u.png" alt="" width="225px" /> 后面谨慎一点,先审核一个商店,通过后再处理其他的应用商店,给大家看看我的审核战绩,在经过了 6 次被打回后,终于迎来了曙光: <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/1B6Nd9QnJPoGU4UJ.webp" alt="" width="100%" /> ## 经验 这里分享一些经验(**都很关键**): 1)首先就是需要在隐私政策协议中公示 APP 用到的所有 SDK,描述它们会收集哪些信息,收集这些信息的目的 2)一定要在用户同意隐私政策协议之后才去初始化 SDK 接着我们可以先选一个比较容易审核通过的平台,咱们先上架再说。比较推荐小米,首先是它需要填写的资料不是很多,而且审核效率也很快,客服小姐姐的响应也很及时,审核通过后还能直接同步其他应用商店。 <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/poSrRPNw78fy8BrR.webp" alt="" width="100%" /> 审核不通过的情况我们可以去下载对应的审核日志,如下: <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/QEh37vJFvnFstEYB.webp" alt="" width="100%" /> 在日志里可以看到具体的调用堆栈,根据这些信息可以大致定位到我们的代码,如果是自己代码中的问题,根据调用堆栈日志,大概率可以解决,如下: <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/buynTxrzTmjCJO8G.webp" alt="" width="100%" /> 另外,也可以看到哪些 SDK 在用户没有同意隐私政策之前就触发了收集用户信息的行为,然后去官方文档找找解决方案,没有的话那只能提工单了。 最后,在你改完已知问题之后也可能会出现一些小惊喜(出现一些新的需要整改的点)。 好了,这篇文章就分享到这里,哥们该去看看审核通过没了。 ## 往期文章 [📚 Flutter & H5页面混合开发指北 ](https://www.codefather.cn/post/1864199174536527873) [🤡 因为编辑器没做草稿,老板崩溃了。。。](https://www.codefather.cn/post/1840222330227068929)
编程导航app和网站是互通的吗?
打卡下载app
Flutter & H5页面混合开发指北
大家好,我是多喝热水。 最近在写 Flutter,简单聊一聊在 Flutter 中如何去集成**H5页面**,读完这篇文章你可以了解以下内容: 1)如何在 Flutter 中加载 H5? 2)Flutter 和 H5 如何进行通信? ## 前言 要在 Flutter 中混入 H5 页面,首先我们需要一个 webview 来加载这个 H5,这里需要用到 [flutter_inappwebview(https://pub.dev/packages/flutter_inappwebview)](https://pub.dev/packages/flutter_inappwebview) 依赖,这个算是 Flutter 中比较优质的三方 webview 库了,功能比较全面: <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/UuImNSw5trrNuPxK.webp" alt="" width="100%" /> ## 使用场景 什么场景下用 webview 性价比最高?说说我的看法: 1)经常会需要变动的页面,比如活动页,H5 有天然的热更新优势 2)不存在大量的逻辑交互,且已经有一套现成的 H5 页面,可以直接套用,避免重复造轮子 ## 实战案例 比如编程导航的[**网页端会员页面**](https://www.codefather.cn/vip)已经适配了移动端,那我们其实没有必要重新写一套,直接拿过来就能用了,只需要解决 Flutter 和 H5 之间的通信问题即可: <img src="https://pic.code-nav.cn/post_picture/1732240055109545986/mVaXXF635EookaGq.webp" alt="undefined" width="312" /> 所以总结一下,我们需要做以下几件事情: 1)安装 flutter_inappwebview 2)加载 webview 3)在 webview 中种上 Cookie(如果有的话) 4)Flutter 与 H5 通信的事件绑定(H5发射事件,flutter监听事件) ### 安装 flutter_inappwebview ```bash flutter pub add flutter_inappwebview ``` ### 加载 webview 这里我们主要关注的点是 `onWebViewCreated` 这个钩子,因为在 webview 创建完成后就可以对事件进行监听(监听来自 H5 发射的事件) ```dart InAppWebView( initialUrlRequest: URLRequest(url: WebUri("https://codefather.cn/vip")), onWebViewCreated: onWebViewCreated, ) ``` ### Cookie 种植 1)我们需要拿到 App 中的 Cookie( token 同理),然后给指定地址种植上,这里我们会用到 `CookieManager` 来种植 Cookie,如下: ```dart // 在 onWebViewCreated 钩子中调用 void onWebViewCreated(InAppWebViewController controller) { setCookies(); } setCookies() { // 1. 实例化 final CookieManager cookieManager = CookieManager.instance(); // 2. 获取 App 中的所有 cookie final cookies = getCookies(); cookies.forEach((key, value) async { await cookieManager.setCookie( url: WebUri("https://codefather.cn/vip"), name: key, value: value, domain: Global.cookieDomain, path: "/api", isSecure: true, maxAge: 3600, ); }) } ``` ### 事件监听(来自H5) 这里我们以开通会员按钮举例,预期是点击开通会员,弹出`"我要开通会员!!!`的提示。 1)Flutter端的监听代码如下 ```dart void onWebViewCreated(InAppWebViewController controller) { // 1. Cookie设置 setCookies(); // 2. 加入会员按钮点击监听 onJoinVip(); } void onJoinVip() { _controller?.addJavaScriptHandler( handlerName: "joinVipHandler", // 监听加入会员事件 callback: (arguments) { showToast("我要开通会员!!!"); }, ); } ``` 2)H5端的代码如下 > 通过 flutter_inappwebview 加载的网页我们可以拿到 flutter_inappwebview 对象,那么我们可以对 window.flutter_inappwebview 做判断,如果它存在我们就认为它是在 flutter 端加载的网页,如果为空,则是走的正常的网页加载,伪代码如下: ```js function h5JoinVip() { if (window.flutter_inappwebview) { // 发射一个加入会员的事件,让flutter去处理这个支付请求 window.flutter_inappwebview.callHandler('joinVipHandler'); } else { H5pay(); // 走h5的支付 } } ``` ## 效果展示 效果已经有了,剩下的就是在 Flutter 中去处理支付流程(但这不是本篇文章的重点): <img src="https://pic.code-nav.cn/migrate_image/ed625eaea89455b0ef180b6aa194b757.gif" alt="效果演示" width="244px" /> 感兴趣的小伙伴可以自己尝试一下,最近会开一个 **Flutter 专栏**,写一些 Flutter 相关的文章,感兴趣的小伙伴可以关注一波,也欢迎关注我的公众号「程序员热水」,我会分享一些前端相关的知识碎片。 --- **往期文章**: - [因为编辑器没做草稿,老板崩溃了。。。](https://www.codefather.cn/post/1840222330227068929) - [这样设计弹框,有更多时间来摸鱼了!](https://www.codefather.cn/post/1818122376005369858) - [👉🏻 更多...](https://www.codefather.cn/user/1732240055109545986/post)
【鸿蒙实战开发】ArkTS多线程的多线程系列(一):ArkTS多线能力入门
HarmonyOS应用的UI操作必须在主线程执行(如修改UI控件,更新视图这些操作必须在UI线程中进行),如果主线程出现阻塞,那么UI界面就会出现明显的卡顿。因此为了解决此类问题,我们需要将一些耗时的操作例如加载网络数据、查询本地文件、数据等放到子线程中,以提升应用的响应速度和性能。 **多线程能力介绍** **进程与线程** 在单线程中执行的代码都是串行的,即按顺序执行,直到执行完成后,程序才会退出。当程序需要执行多个任务时,每个任务必须等待前一个任务执行完成后才能继续执行,这使得程序的性能非常低下。多线程技术通过使用多个线程来充分利用CPU资源,同时执行多个任务,从而提高程序执行的效率。每个线程都是相互独立的,并能够单独执行、暂停、继续和停止。进程(Process):是操作系统进行资源分配的最小单元。线程(Thread):是操作系统进行运算调度的最小单元,它被包含在进程之中,是进程中的实际运作单位。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/bseMIaJVriK74pW2.webp" alt="" width="100%" /> **多线程的使用场景** 多线程的应用场景包括但不限于以下场景: 1. CPU密集型:数据处理、图像处理。当需要大量的数据处理时,可以使用多线程,以提高处理效率 2. I/O密集型:文件读写、网络请求。当需要发起大量的I/O请求时,可以使用多线程,以避免卡主线 3. 后台任务:自动化作业处理,比如需要定期完成特定任务。 **ArkTS的多线程解决方案** 在HarmonyOS的ArkTS侧为多线程提供了两种方式:TaskPool和Worker,应用可以结合自身业务诉求,选择对应的实现方案。 **TaskPool简介** 任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且开发者无需关心线程实例的生命周期。TaskPool提供了多种不同的任务能力: | 能力 | 任务构建 | 任务执行 | 场景描述 | | --- | --- | --- | --- | | 普通任务 | new taskpool.Task() | taskpool.execute() | 立即执行的短时任务,耗时不能超过3分钟。 | | 延时任务 | new taskpool.Task() | taskpool.executeDelayed() | 为了不影响应用启动的性能,一些不影响启动的初始化类任务往往期望放在延时任务中执行,如拉取线上的配置信息等。 | | 长时任务 | new taskpool.LongTask() | taskpool.execute() | 希望长时运行的任务一直保持执行,已为其他模块提供特定的服务,比如日志埋点,后台长链接保活等。 | | 串行任务 | new taskpool.SequenceRunner(),new taskpool.Task() | SequenceRunner. execute() | 用于执行一组需要串行执行的任务 | | 依赖任务 | task1.addDependency(task2), task1.removeDependency(task2) | taskpool.execute() | 任务之间存在先后依赖关系 | | 任务的优先级设置 | 待支持 | 待支持 | 在应用运行时,期望可以给设置空闲时任务,当应用处于闲时(比如冷启动完成后停留在首页)做一些相应的预加载动作(如预加载其他页面),从而提升应用整体性能。 | **TaskPool注意事项** * 实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。 * 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用,具体请见多线程安全注意事项。 * 序列化传输的数据量大小限制为16MB。 **推荐使用场景** TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容) * 需要设置优先级的任务。 * 大量或者调度点较分散的任务。 * 需要频繁取消的任务。 **TaskPool线程池扩容策略** 总体算法TaskPool需要的工作线程数由任务平均执行时间和当前任务数共同决定。任务池会根据过往的执行数据计算出一个预期线程数,但是各个任务的执行时间并不能准确衡量TaskPool当前的负载,因此需要通过任务数来反映。 扩容机制TaskWorker线程创建有一定耗时。为了优化启动阶段的性能和加快任务执行,TaskPool默认创建和预留了一个线程。结合JS的async/await机制和TaskPool的线程复用特性,当任务较少时,一个线程能够正常处理所有任务,此时并不一定会触发扩容机制。当首次执行且任务执行耗时较长的时候,上述算法的平均执行时间并不能立刻得出,因此有新的任务时,将会额外新建两个线程,避免线程池拥塞。当任务执行完成后,仍会采用上述算法。 正常流程下,每当开发者向任务池中抛任务时,都会触发一次扩容检测。扩容检测首先会判断当前的空闲线程数是否大于任务数,若大于,则说明线程池存在空闲线程,不需要扩容即可执行完新的任务。否则通过算法判断需要的线程数进而创建线程到指定数目。 缩容机制当任务耗时且较多时,TaskPool将会新建多个TaskWorker线程。但在空闲时仍保留这么多线程将会导致资源浪费和内存无法下降。因此TaskPool使用了定时器,定时检测TaskPool的负载,负载计算方式仍然采用上述算法。但是考虑到频繁创建和销毁带来的开销,缩容并不会立刻缩到计算出的数目,而是整体沿用了急涨缓停的思想,即立刻扩容到指定数目,但在缩容阶段采用阶梯式下降的方式。空闲时每个检测阶段会尝试释放2个线程(当前策略)。由于涉及到资源释放,需要确保不会因为错误的释放带来野指针等crash行为,缩容阶段仍会去检测当前空闲线程是否可以释放。仅有满足条件的线程才能够被释放。 **Worker简介** Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本操作耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。 **Worker注意事项** * Worker的创建和销毁耗费性能,建议开发者合理管理已创建的Worker并重复使用。 * Worker存在数量限制,支持最多同时存在64个Worker。当Worker数量或者内存超出限制时,会抛出相应错误。 **推荐的使用场景** * 常驻后台的线程任务 **TaskPool与Worker对比** 本节将从实现特点和适用场景两个方面来进行TaskPool与Worker的比较。 | 实现 | TaskPool | Worker | | --- | --- | ---| | 内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 | | 参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移、SharedArrayBuffer共享、sendable共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 | | 参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 | | 方法调用 | 直接将方法传入调用。 | 在Worker线程中进行消息解析并调用对应方法。 | | 返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 | | 生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 | | 任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。 | | 任务执行时长上限 | 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。 | 无限制。 | | 设置任务的优先级 | 支持配置任务优先级。 | 不支持。 | | 执行任务的取消 | 支持取消已经发起的任务。 | 不支持。 | | 线程复用 | 支持。 | 不支持。 | | 任务延时执行 | 支持。 | 不支持。 | | 设置任务依赖关系 | 支持。 | 不支持。 | | 串行队列 | 支持。 | 不支持。 | | 任务组 | 支持。 | 不支持。 | **多线程开发常见场景&解决方案** **创建&停止线程** **TaskPool线程的创建&停止** 通过taskpool.Task或者taskpool.LongTask构建TaskPool线程池任务,构造方法如let task: taskpool.Task = new taskpool.Task(taskName, taskFun, args);。 * taskName为任务名称,此任务名无法在子线程中获取,如果子线程需要使用任务名,则需要将任务名作为参数传递给子线程。 * taskFun为要执行的逻辑函数,该函数必须使用@Concurrent装饰器装饰。 * args为任务执行函数的入参。默认值为undefined。 ``` import taskpool from '@ohos.taskpool'; @Concurrent function add(num1: number, num2: number): number { return num1 + num2; } async function ConcurrentFunc(): Promise<void> { try { let task: taskpool.Task = new taskpool.Task(add, 1, 2); console.info("taskpool res is: " + await taskpool.execute(task)); } catch (e) { console.error("taskpool execute error is: " + e); } } @Entry @Component struct Index { build() { Row() { Column() { Text('Do Something in Taskpool') .onClick(() => { ConcurrentFunc(); }) } } } } ``` TaskPool任务销毁:对于长时任务(LongTask),除了通过启动外,开发者还需要在任务完成后调用terminateTask方法终止此任务,系统不会主动回收此任务。 ``` let longTask: taskpool.LongTask = new taskpool.LongTask(longTask, 1000); // 1000: sleep time taskpool.execute(longTask).then((res: Object)=>{ taskpool.terminateTask(longTask); }); ``` **Worker线程的创建&停止** 通过new worker.ThreadWorker('entry/ets/workers/MyWorker.ets')的方式加载Worker。HAP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHAP: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets'); ``` HSP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHSP: worker.ThreadWorker = new worker.ThreadWorker('hsp/ets/workers/worker.ets'); ``` HAR中的Worker加载,路径规则为:@{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHAR: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets'); ``` Worker线程销毁 Worker的生命周期需要开发者自行进行管理,也就是说创建出Worker线程后,开发者需要管理Worker线程实例,因此当不需要再使用该线程后,需要显示调用Worker的terminate()方法,将Worker线程实例销毁掉。 ``` const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ets"); workerInstance.terminate(); ``` **线程间通信数据类型说明** 此处只做简单说明,具体规格参考相关文档 **Sendable类型共享数据类型** Sendable协议定义了ArkTS的可共享对象体系及其规格约束。符合Sendable协议的数据(以下简称 Sendable 数据)可以在ArkTS并发实例间传递。默认情况下,Sendable数据在ArkTS并发实例(主线程、TaskPool、Worker)间通过引用传递。同时,ArkTS也支持Sendable数据在ArkTS并发实例间的拷贝传。 Sendable对象可以支持的属性类型: 1. 属性限制:包含基础类型(boolean, number,string,bigint,null,undefined)、其他共享对象、枚举、@arkts.collections下的容器。 2. 方法限制:可以传递共享对象中的方法。 当前Sendable对象使用限制较多,如: 1. Sendable class不能使用除了@Sendable的其他装饰器 2. 不能使用字面量初始化Sendable类型 3. 非Sendable类型不可以as成Sendable类型 更多的规则可参考 Sendable使用规则 Sendable对象的使用: * taskpool中使用Sendable对象构建Task时传递参数是,通过args参数将Sendable对象的引用传递给子线程new taskpool.Task(taskName, taskFun, args)。 * worker中使用Sendable对象通过workerPort.postMessageWithSharedSendable()方法,将Sendable对象的引用传递给子线程。 **普通数据类型** 普通对象传输采用标准的结构化克隆算法(Structured Clone)进行序列化,此算法可以通过递归的方式拷贝传输对象,相较于其他序列化的算法,支持的对象类型更加丰富。 序列化支持的类型包括:除Symbol之外的基础类型、Date、String、RegExp、Array、Map、Set、Object(仅限简单对象,比如通过“{}”或者“new Object”创建,普通对象仅支持传递属性,不支持传递其原型及方法)、ArrayBuffer、TypedArray。 **可转移对象** 可转移对象(Transferable object)传输采用地址转移进行序列化,不需要内容拷贝,会将ArrayBuffer的所有权转移给接收该ArrayBuffer的线程,转移后该ArrayBuffer在发送它的线程中变为不可用,不允许再访问。 **可共享对象** 共享对象SharedArrayBuffer,拥有固定长度,可以存储任何类型的数据,包括数字、字符串等。 共享对象传输指SharedArrayBuffer支持在多线程之间传递,传递之后的SharedArrayBuffer对象和原始的SharedArrayBuffer对象可以指向同一块内存,进而达到内存共享的目的。 SharedArrayBuffer对象存储的数据在同时被修改时,需要通过原子操作保证其同步性,即下个操作开始之前务必需要等到上个操作已经结束。 **Native绑定对象** 当前支持序列化传输的Native绑定对象主要包含:Context、RemoteObject和PixelMap。
【鸿蒙实战开发】基于List的滑动丢帧性能问题分析思路&案例
## **1. 场景导入** 基于ArkUI的List组件实现的滚动列表视图,在手指抛滑场景下,通过分析掉帧情况来判断List滑动是否流畅,保障用户极致流畅体验。 ## **2. 性能指标** 最大连续丢帧数:指从页面开始有响应变化到页面结束刷新的过程中,由于显示器画面刷新频率低于预设的画面帧率而未能正常呈现的最大连续帧数。一般而言,当连续值超过3时,用户可以明显感知到卡顿掉帧,数值越大卡顿时间越长。最大连续丢帧数越接近于0,用户流畅性体验越好。 **2.2 性能衡量起止点介绍** 以大于300mm/s的速度,连续3次抛滑,每次半屏。抓取滑动过程Trace,查看Frame泳道中应用进程和RenderService的最大连续丢帧数。 List组件的抛滑过程,可以通过应用进程下的H:APP_LIST_FLING泳道标识。性能衡量的起点为第一次抛滑开始点,衡量的结束点为第三次抛滑的结束点。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/AaXX0jxCZoIhwjXn.webp" alt="image.png" width="532px" /> | 泳道 | 描述 | | --- | --- | | H:APP_LIST_FLING | 从手指按下开始拖动到抬手后的惯性滚动及最后尾动效的抛滑全过程。| | H:touchEventDispatch | 拖滑阶段,从手指开始拖动到抬起。| | H:TRAILING_ANIMATION | 抛滑尾动效阶段 | Tip:尾动效阶段系统会进行降帧处理,所以如果要统计FPS情况,通常只会统计从抛滑开始到尾动效起点的这一阶段 ## **3. 问题定位流程** **3.1.1 查看操作录屏辅助定位** 处理三方应用问题时,可以优先查看操作录屏,查看操作场景,看能否发现一些有助于定位的信息,比如卡顿的页面布局情况、卡顿的现象等等。 **3.1.2 Trace 抓取** 滑动帧率Trace抓取请参考【附录1: 滑动帧率Trace抓取方法】。 ### **3.2 问题定位思路** 滑动丢帧类问题的通用定位思路为先确认抛滑的起止点,然后看抛滑过程中最大连续丢帧数,如果大于0帧,则根据Trace信息进一步确认问题点,确认责任领域并对齐处理,处理流程如下图: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/96Qxj4W3q65yDQuE.webp" alt="" width="526px" /> **3.2.1****确认起止点** 参考【2.2 性能衡量起止点介绍】 **3.2.2 找问题点** **3.2.2.1 判断丢帧进程** 首先通过Frame泳道判断丢帧进程,其中绿色代表没有丢帧,其他颜色均为丢帧。其中“粉红色”代表该帧的期望时间,“红色”标识超时部分。 **应用进程问题** 如下图,应用进程连续丢了5帧,但可以看到只有261帧、263帧、264帧耗时较长,因此只分析这3帧即可。另外发现最后一帧序号也是264,这是因为前一帧耗时较长,导致该帧和前一帧都提交到了RS的264帧上,应用帧上的序号是被提交到的RS帧序号相对应的。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/J6aa13VGrlvm6uPy.webp" alt="" width="526px" /> **RS 进程问题** RS进程丢帧可能是应用进程导致的,如上图RS侧丢了1帧,但可以看到RS侧264帧丢帧原因是由于应用进程的264帧耗时较长,提交较晚导致。所以这种情况只分析应用侧丢帧原因即可。如果应用进程中没有丢帧且每帧耗时比较均匀,但是RS侧发生丢帧,则说明不是应用侧导致丢帧,此时只分析RS进程丢帧原因即可。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/C6ZLKGaDU9DdzURW.webp" alt="" width="528px" /> **3.2.2.2 找丢帧Trace** 选中Frame泳道,点击Statistics下面的应用进程右侧图标进入Frame List <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/NT2seRCeAt5kOoy2.webp" alt="" width="527px" /> 过滤Jank Type为AppDeadlineMissed类型的帧,点击跳转应用进程。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/l65UPLsFRyyf2NYc.webp" alt="" width="527px" /> 详细分析丢帧Trace <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/I1XEq0WPMxX34VfP.webp" alt="" width="526px" /> **3.2.3 根因分析方法** 应用侧的渲染流程如下图所示,了解ArkUI的渲染流程有助于我们定位应用侧的卡顿问题出现在哪个环节 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/skbFPwtjuUh55Iu2.webp" alt="" width="531px" /> | 阶段 | 描述 | | --- | --- | | Animation | 动画阶段,在动画过程中会对相应的组件标记脏区 | | Events | 事件处理阶段,比如手势事件处理。在手势处理过程中也会对组件标记脏区 | | UpdateUI | 组件在首次创建或状态变量变更时会标记为需要rebuild状态,在下一次Vsync过来时会通过View的方法生成相应的组件树结构和属性样式修改任务。 | | Measure | 执行组件的大小测算任务。 | | Layout | 执行组件的布局任务。 | | Render | 执行绘制任务,执行完成后会标记请求刷新RSNode绘制 | | SendMessage | 将绘制数据提交到RS侧,请求刷新界面绘制 | **应用进程丢帧分析** 跟据Trace图,初步分析耗时较长阶段。261帧由于懒加载组件预创建耗时较长导致丢帧;263帧由于组件复用耗时长丢帧;264帧由于组件结构复杂嵌套层级多导致丢帧。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/apQ62s57ISH4psst.webp" alt="" width="532px" /> | 序号 | 所属泳道 | Trace点 | 描述 | | --- | --- | --- | --- | | 1 | 应用进程 | H:LazyForEach predict | LazyForEach预处理 | | 2 | 应用进程 | H:CustomNode:BuildRecycle 自定义组件名 | 自定义组件的复用,包含执行aboutToReuse方法的耗时 | | 3 | 应用进程 | H:CreateTaskMeasure[组件名][self:组件id][parent:父组件id] && H:Measure[组件名][self:组件id][parent:父组件id] | 执行组件的布局测量任务 | 通过ArkTS CallStack泳道,可以看到应用侧具体调用栈,进一步分析定位问题原因。如下图通过调用栈可以分析出:组件复用时会组件树进行递归,这个过程耗时较长,可以看下组件数是否组件嵌套层级过深;updateDirtyElement耗时长,应用侧可以分析下是否存在冗余节点被触发更新;aboutToReuse耗时长可以看下应用侧该回调中是否存在耗时逻辑。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/U15EjQ8aWv1JZ8Ly.webp" alt="" width="527px" /> 应用UI组件树的嵌套情况,可以通过ArkUI Inspector查看。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/hvSEOqYhMbAgc6zt.webp" alt="" width="526px" /> **经验总结:** 应用进程丢帧通常是组件结构嵌套层级深、耗时应用业务逻辑阻塞UI线程等问题导致。如果是UI结构复杂问题可以让应用通过减少嵌套层级、使用组件复用等方式优化。如果是有耗时业务逻辑,则可以通过将耗时逻辑放到Taskpool或Worker中优化。 **RS 进程丢帧分析** RS进程丢帧一般是由于界面结构过于复杂或者GPU负载过大等原因导致的。如果应用侧没有丢帧且每帧耗时比较平均,则可以初步判断应用侧没有问题,同时也可以通过应用侧Trace中H:SendCommands下的H:MarshRSTransactionData cmdCount查看应用提交的绘制指令树是否过多。如下图RS侧丢帧原因是由于RS侧的H:RSUniRender:FlushFrame阶段耗时较长,此时可以找图形子系统进一步确认耗时根因。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8yWfSFFjAg1EjH1F.webp" alt="" width="527px" /> 经验总结: RenderService侧丢帧通常是应用侧UI线程阻塞提交绘制指令较慢导致,此时应当初步定位应用侧耗时长原因。如果应用侧无丢帧情况,绘制指令正常提交,则可以找图形子系统协助进一步分析丢帧原因。 ## **4. 典型问题** ### **4.1 耗时任务阻塞UI 主线程** Stage模型下的线程主要有三类:主线程、TaskPool、Worker。主线程主要用于执行UI绘制、处理应用代码逻辑,TaskPool和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他CPU密集型任务。当主线程存在**耗时的计算任务**时,会使**主线程阻塞**,导致应用丢帧。 ### **4.1.1 问题根因分析** 应用进程中间有一段大段“空白”,UI线程未提交任何绘制指令,同时CPU10大核却处于Running状态,表示此时应用侧正在执行ArkTS的业务代码。按每帧8.3ms算,这里阻塞了75.9ms,丢了9帧左右。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/yN6Ryes38BijpjHN.webp" alt="" width="527px" /> 通过ArkTS Callstack泳道,可以看到耗时点主要在三个文件:FlowApi.ets、BaseFeedFlowListVM.ets、Mapi.ets。 与伙伴确认业务逻辑主要为:在列表滑动过程中,List将要到达底部时,会通过网络请求会获取到一个博文的列表数据,数据量较大。然后在FlowApi.ets、Mapi.ets中会对数据进行转换处理,处理后的数据通过BaseFeedFlowListVM.ets文件再转换成 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/12n9W4EXCr1rqDkj.webp" alt="" width="527px" /> **4.1.2 优化方案** 在List滑动过程中对数据进行处理耗时较长,占用大量CPU资源,导致主线程被阻塞,这部分数据处理的相关业务逻辑与UI绘制无关,但却长时间占用CPU资源,导致UI线程被阻塞丢帧。可以将该数据处理逻辑放到TaskPool中利用多核并行化处理优化。 除应用侧的耗时逻辑外,某些与UI绘制无关的耗时系统接口调用也可以放到TaskPool中优化。 **4.2 @Prop 传参深拷贝耗时长** **4.2.1 问题根因分析** 观察应用Trace发现Measure阶段的H:CustomNode:BuildItem [MediaCard]耗时较长4ms 661μs,通过观察ArkUI Component泳道,得出自定义组件MediaCard构建耗时较长。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/cJ04SwkpSUMv8n6M.webp" alt="" width="526px" /> 通过ArkTS CallStack观察应用ArkTS调用栈。其中observeComponentCreation2为@Observed传参时相关逻辑调用栈。resetLocalValue、copyObject、deepCopyObject、deepCopyObjectInternal、getDeepCopyOfObjectRecursive为@Prop接收参数时拷贝数据的相关调用栈。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/mghhRVDGgagzkfuM.webp" alt="" width="527px" /> 由此分析得出:应用侧MediaCardComponent.ets文件中的自定义组件MediaCard通过@Observed+@State方式声明了某个对象类型的数据,并将该变量传给了子组件MediaImage,但在MediaImage中并未使用@ObjectLink接收该变量,而是使用@Prop接收,导致该状态变量发生了深拷贝,深拷贝过程耗时较长3ms 339μs。 **4.2.2 优化方案** @Prop装饰器存在性能问题,@Prop装饰的变量会对父组件传入状态值进行深拷贝,如果@Prop装饰器装饰的变量为复杂Object、class或其类型数组时,会增加状态创建时间以及占用大量内存。如果需要观察嵌套类对象的深层属性变化,推荐选择@State+@Observed+@ObjectLink组合方案。 **4.3 UI 复杂导致单帧超长** **4.3.1 问题根因分析** List滑动场景中,应用侧单帧耗时较长91.8ms,导致应用丢帧。通过Trace可以看到在这一帧中创建了大量组件。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/TBimBxqiRw0HR5tt.webp" alt="" width="529px" /> **4.3.1.1 组件复用失效** 首先,观察Trace发现,在ListItem的Measure阶段,出现了大量H:CustomNode:BuildItem,说明此时发生了大量自定义组件重新创建,而没有被复用。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/anKt3dHI4kF6iXqh.webp" alt="" width="526px" /> 经与应用讨论分析,应用的单个ListItem包含4个部分(头像、文本、九宫格、底部按钮)。ListItem的结构非固定的,其子组件可能会出现多种情况,如图文、视频、纯文本等等,动态性较高。同时又因为其复用是以整个ListItem为单位,所以在进入复用池时ListItem会存在多种可能性。导致在新ListItem期望复用创建时,在复用池中可能未找到对应的可被复用组件,自定义组件被重新创建,组件复用失效。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/283SGh1eCq4bChfs.webp" alt="" width="531px" /> **4.3.1.2 嵌套层级深** 通过ArkUI Inspector观察UI组件树结构,可以发其中存在大量自定义组件的__Common__节点(如下图红框),且存在容器之间组件冗余嵌套的情况(如下图绿框)。冗余的嵌套会带来不必要的组件节点,加深组件树的层级,在创建和布局阶段会产生较大的性能开销。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/NGyAEORGbOTl9iUJ.webp" alt="" width="527px" /> **4.3.1.3 冗余的状态变量** 框选该帧并筛选Trace点:H:ViewPU.viewPropertyHasChanged,该Trace点表示状态变量发生了更新,其中后三个参数分别为自定义组件名、自定义的状态变量名、该状态变量更新后影响的组件数量。可以发现这里有大量为“0”的Trace点,表示该状态变量更新时未触发任何组件刷新,即该状态变量未绑定UI组件,因此可以将其改为普通变量。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/UMrkdaKMdOyWqS17.webp" alt="" width="531px" /> **4.3.2 优化方案** **4.3.2.1 组件复用失效优化** 细化组件复用的颗粒度,将原来对ListItem的复用改为对ListItem中各部分子组件复用,提高组件复用成功率。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/2mfmg1HMx6iGNWE9.webp" alt="" width="532px" /> 相关修改代码参考: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8LpI6JSeIPeKn2xB.webp" alt="" width="528px" /> **4.3.2.2 嵌套层级深优化** **方案一:** 当自定义组件设置通用属性后,UI组件树就会产生__Common__节点,可以通过属性内移解决。将自定义组件上设置的属性内移到自定义组件中的第一层系统组件上。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Ui5glw90ipT9CBw3.webp" alt="" width="527px" /> **方案二:** 避免冗余的嵌套,对于这类冗余的容器,应该尽量优化,减少嵌套深度。建议采用相对布局RelativeContainer进行扁平化布局,有效减少容器的嵌套层级,减少组件的创建时间。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/BNriIfHHxkPzsk4C.webp" alt="" width="530px" /> **方案三:** 使用@Builder代替@Component自定义组件。通过@Component声明的组件在创建时会产生额外耗时,建议尽量使用@Builder声明组件。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/pFeIWMMocLNjU3XZ.webp" alt="" width="525px" /> **4.3.2.3 冗余的状态变量优化** 将没有跟UI组件绑定的状态变量改为普通变量。@State、@Prop等装饰器修饰的状态变量在创建、Get、Set时都会产生耗时,因此应该尽量减少冗余的状态变量,避免性能损耗。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/faKXefBjMQxkLL7F.webp" alt="" width="526px" /> **附录1:滑动帧率Trace抓取方法** **Step1:电脑连接上设备,在DevEco Studio上打开Profiler <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/2rO12wWu2FzZhe0Q.webp" alt="" width="529px" /> **Step2 :** 设备上运行需要测试的应用,在设备列表选择设备,选择要测试的应用,和主进程 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/waaejyiPNlW5n39q.webp" alt="" width="525px" /> **Step3:** 创建Frame模板,并点击录制,待所有泳道都进入到recording状态后 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/s0tQWZSTaf5WPRXq.webp" alt="" width="526px" /> **Step4:** 执行相关滑动操作 **Step5:** 操作完成,点击结束录制,待分析完成后,可以在泳道上看到trace数据 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Ay0695cJDqfadLlz.webp" alt="" width="528px" /> **Step6: ** trace的路径点击Help -> Show Log in Explorer <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/NdgPS9Uw8gocABPx.webp" alt="" width="424px" /> 返回到上一层,找到.insight文件下 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/y8yY4WPC1p5axthR.webp" alt="" width="527px" /> **附录2:List滑动场景通用Trace点说明** **基础List滑动** 以一个最基础的List Demo为例,通过脚本抛滑并使用IDE Profiler工具抓取Trace。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/V8mMg24LneKDjzX8.webp" alt="" width="528px" /> **拖动阶段** 选取拖动阶段某一Trace如下: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/rqkc8TThQcKyILwe.webp" alt="" width="528px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:client dispatch touchId:32903 | 系统派发touch事件 | 事件id | | 2 | H:OnVsyncEvent now: [时间戳] | 收到Vsync信号,渲染流程开始 | 时间戳--纳秒级 | | 3 | H:FlushVsync | 处理用户输入、刷新视图同步事件、计算帧信息、提交绘制渲染等 | | | 4 | H:DispatchTouchEvent id:0, pointX=[x坐标] pointY=[y坐标] type=2 | 处理拖拽手势事件 | 触摸点的xy坐标信息,type=2表示手势事件类型为移动 | | 5 | H:HandleDragUpdate | 执行拖拽更新任务 | | | 6 | H:AddDirtyLayoutNode[List][self:7][parent:6] | 标记List组件为脏区 | 组件名、组件ID、父组件ID | | 7 | H:UITaskScheduler::FlushTask | 刷新UI界面,包括布局计算、渲染和提交等 | | | 8 | H:FlushLayoutTask | 执行布局任务 | | | 9 | H:CreateTaskMeasure[List][self:7][parent:6] && H:Measure[List][self:7][parent:6] | 执行List组件的布局测量任务 | 组件名、组件ID、父组件ID | | 10 | H:ListLayoutAlgorithm::MeasureListItem:27 && H:Measure[ListItem][self:10][parent:7][key:] | 计算ListItem列表项的布局尺寸 | 列表项索引、组件名、组件ID、父组件ID | | 11 | H:SkipMeasure | 组件大小布局未发生变化,跳过measure过程 | | | 12 | H:CreateTaskLayout[List][self:7][parent:6] && H:Layout[List][self:7][parent:6] | 执行List组件布局任务 | 组件名、组件ID、父组件ID | | 13 | H:Layout[ListItem][self:10][parent:7][key:] | 执行ListItem组件布局任务 | 组件名、组件ID、父组件ID | | 14 | H:SyncGeometryNode[List][self:7][parent:6][key:] | 同步几何节点 | 组件名、组件ID、父组件ID | | 15 | H:FlushRenderTask 1 && H:FrameNode[List][id:7]::RenderTask | 执行渲染绘制任务 | 当前页面需要绘制的节点数量、需绘制的组件名和ID | | 16 | H:FlushMessages && H:SendCommands | 通知图形侧进行渲染 | | | 17 | H:MarshRSTransactionData cmdCount:14 transactionFlag:[24390,75] | 向图形侧发送绘制指令 | 绘制指令数量、应用进程号、指令序列号 | | 18 | H:OnIdle, targettime:222549467458334 | Vsync中的空闲,一般会用来做预加载之类的操作,当H:OnVsyncEvent时间小于某值时触发该事件 | 时间戳 | **惯性滑动阶段** 惯性滑动阶段Trace点与拖动阶段基本一致,唯一区别点在于拖动阶段是通过手势事件标脏,而惯性滑动阶段是通过动画触发的组件标脏。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ouh430JBw08QHNw2.webp" alt="" width="525px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:RunningCustomAnimation num:[1] | 自定义动画,RSModifierManager管理的在UI线程运行的动画 | num表示动画的数量,如果大于0,则表示有动画在运行 | | 2 | H:AddDirtyLayoutNode[List][self:7][parent:6] | 标记List组件为脏区 | 组件名、组件ID、父组件ID | | 3 | H:FlushDirtyNodeUpdate | 更新被标脏的节点 | | **尾动效阶段** 尾动效阶段相较其他两阶段的区别点,在于尾动效阶段如果移动距离小于1像素则不会向RS提交H:SendCommands。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/tguJXUd2ymesmqVM.webp" alt="" width="525px" /> 选取一个应用进程未提交帧,放大后可以看到H:SendCommands下方并无H:MarshRSTransactionData的Trace点。无该Trace点时说明ArkUI中没有需要绘制的内容,因此没有提交绘制指令到RenderService侧。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/5WjWUtFZKtozSEOj.webp" alt="" width="533px" /> **懒加载场景滑动** 基于上文List Demo改造,实现懒加载效果。List懒加载场景下滑动的Trace点与上文基础List基本一致,其区别点主要在于懒加载的滑动场景下会出现组件创建和销毁过程,因此本章节主要介绍创建和销毁组件的关键Trace点。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/KCWEuPnnwJb79KCR.webp" alt="" width="528px" /> **创建组件** 当List组件的cachedCount属性设为0时,ListItem的创建会发生在H:FlushLayoutTask阶段。如果不为0,则会在H:OnIdle阶段预创建组件。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/vOamHzh61goXgrvs.webp" alt="" width="530px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:Builder:BuildLazyItem [4] | 创建一个LazyItem项目 | 创建的项目索引 | | 2 | H:Create[Child][self:28] | 创建一个自定义组件 | 自定义组件名、组件ID | | 3 | H:CustomNode:OnAppear && H:aboutToAppear | 执行自定义组件的aboutToAppear 方法 | | | 4 | H:CustomNode:BuildItem [Child][self:28][parent:27] | 执行自定义组件的build方法 | 自定义组件名、组件ID、父组件ID | | 5 | H:Create[Text][self:31] | 创建一个Text组件 | 组件名、组件ID | | 6 | H:Measure[Text][self:31][parent:27][key:] | 计算Text组件布局 | 组件名、组件ID、父组件ID | | 7 | H:LazyForEach predict | LazyForEach预处理 | | | 8 | H:List predict | List组件预处理 | | **销毁组件** 组件销毁只会发生在H:OnIdle空闲阶段。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/CSvHn7mZxwadw04i.webp" alt="" width="529px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:LazyForEach predict | LazyForEach预处理 | | | 2 | H:aboutToDisappear | 执行自定义组件的aboutToDisappear方法 | | | 3 | H:aboutToBeDeleted | 删除组件 | | **组件复用场景滑动** 基于懒加载场景Demo改造,实现组件复用效果。与懒加载场景相比,在滑动过程中不会发生组件的销毁和创建,而是会在组件将要销毁时,将其放入缓存池中,在需要创建时再从缓存池中取出,并重新赋值。因此本章节主要介绍Reuse阶段和Recycle阶段关键Trace点。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/VkaoAZagZLMlfxWS.webp" alt="" width="532px" /> **Reuse****阶段** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8s1jVlwun97OKpQ9.webp" alt="" width="530px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:CustomNode:BuildRecycle Child | 自定义组件复用,执行aboutToReuse方法 | 复用的组件名 | | 2 | H:Create[Text][self:12] | 创建Text组件 | 组件名、组件ID | | 3 | H:AddDirtyLayoutNode[ListItem][self:61][parent:0] | 标记ListItem为脏区 | 组件名、组件ID、父组件ID | Recycle阶段 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Dx6xWtXTkfEy4Z8e.webp" alt="" width="526px" /> | 序号 | Trace | 描述 | | --- | --- | --- | | 1 | H:LazyForEach predict | LazyForEach预处理 | | 2 | H:aboutToRecycleInternal | 标识组件进入复用池并调用组件的aboutToRecycle方法 | | 3 | H:ViewFunctions::ExecuteRecycle | 执行组件回收 |
dy_flutter
地址:https://github.com/yukilzw/dy_flutter
Tiktok
地址:https://github.com/running-libo/Tiktok
