鸿蒙OS开发实例:【埋点探究】

news/2024/7/27 8:54:10/文章来源:https://blog.csdn.net/m0_70748845/article/details/137238400

背景

大多数软件产品上线前,都会采用有规则的日志来对软件进行相关数据的采集,这个过程称为:埋点,采集的数据主要用于产品分析。

埋点技术已在PC端, 移动端非常成熟,并且有大批量以此为生的公司。

本篇将探究一下HarmonyOS中的埋点,目标是统计用户浏览页面轨迹

准备

  1. 了解移动端的埋点技术方案
  2. 了解HarmonyOS页面生命周期

搜狗高速浏览器截图20240326151450.png

声明周期

先回顾一下有关页面显示的生命周期

UIAbility

在HarmonyOS中这个算是一个页面容器,因为它仅仅加载带@Entry装饰的自定义组件,几乎所有和业务相关的逻辑都是从自定义组件中触发。

这个容器共有四个状态,创建(Create),回到前台(Foreground), 回到后台(Background), 销毁(Destrory)

状态CreateForegroundBackgroundDestroy
API接口onCreateonForeground()onBackground()onDestroy()

被@Entry修饰的自定义组件

在HarmonyOS中的业务页面,实际上指的就是这个。这个对移动端的Web开发人员 ,React Native开发人员,Flutter开发人员比容易接受。

注意:这种自定义组件的生命周期,容易产生混淆

被 @Entry 修饰

总共有三个生命周期接口

  1. [onPageShow],页面每次显示时触发一次
  2. [onPageHide],页面每次隐藏时触发一次
  3. [onBackPress],当用户点击返回按钮时触发

被 @Component 修饰

  1. [aboutToAppear],组件即将出现时回调该接口
  2. [aboutToDisappear],自定义组件析构销毁之前执行

预研小结

  • 对于UIAbility的生命周期监测,可以监听事件‘[abilityLifecycle]‘事件,进而实现应用全局监测
  • 对于@Entry修饰的组件生命周期监测,目前还没有可统一监听的事件,只能手动在相应的方法中添加埋点

本篇探究的对象就是针对@Entry修饰的组件,实现生命周期的统一监听

探究

1)注解/装饰器方案

HarmonyOS 应用研发语言ArkTS,是基于TypeScript扩展而来,因此,理论上是可以自定义装饰器来完成对函数执行时的统计。

[TypeScript装饰器]可以了解一下:《鸿蒙NEXT星河版开发学习文档》

准备代码

定义一个统计方法
export function Harvey(params?: string) {return function(target:any, methodName:any, desc:any){console.log(params);console.log(JSON.stringify(target));console.log(JSON.stringify(methodName));console.log(JSON.stringify(desc));}
}
布局测试页面
......
//引入自定义方法装饰器文件
import { Harvey } from './HarveyEventTrack';@Entry
@Component
struct RadomIndex {@Harvey('注解-aboutToAppear')aboutToAppear(){console.log('方法内-aboutToAppear')}@Harvey('注解-aboutToDisappear')aboutToDisappear(){console.log('方法内-aboutToDisappear')}@Harvey('注解-onPageShow')onPageShow(){console.log('方法内-onPageShow')}@Harvey('注解-onPageHide')onPageHide(){console.log('方法内-onPageHide')}@Harvey('注解-onBackPress')onBackPress(){console.log('方法内-onBackPress')}@Harvey('注解-build')build() {......}}

运行效果

日志分析

  1. 所有的生命周期上的装饰器方法全部跑了一遍,即 "注解-" 开头的日志
  2. 生命周期API最后运行,即 “方法内-” 开头的日志

    

结论

自定义装饰器没法满足统一埋点需求的

2)TypeScript AST

结论

这种方案暂时没有尝试成功

相关链接

3) 脚本硬插入代码

这个方案比较原始,属于最笨的方法。

  • 适用编译场景: 打包机编译
  • 原因:编译前会直接修改源文件

大概流程如下

最终效果

2xw.png

尝试

创建埋点文件
  1. 在项目项目根目录下创建一个“Project”的文件夹
  2. Project文件夹下创建埋点文件
import common from '@ohos.app.ability.common';export default class PageLifecycle{public static record(uiContext: common.UIAbilityContext, fileName: string,  funName: string){console.log('埋点:' + uiContext.abilityInfo.bundleName + ' -> ' + uiContext.abilityInfo.moduleName + ' -> '+uiContext.abilityInfo.name + ' -> ' + fileName + ' ' +'-> ' +funName)}}
插入时机
  • entry 模块中的** hvigorfile.ts**

    注意: hvigorfile.ts 文件中提示文件不能修改,暂时不用去关心它

脚本代码
import * as fs from 'fs';
import * as path from 'path';const INSERT_FUNCTION: string[] = ['aboutToAppear','aboutToDisappear','onPageShow','onPageHide','onBackPress',
]const PAGELIFECYCLE_NAME = 'PageLifecycle.ets'//开始复制埋点文件
copyConfigFile(process.cwd() + `/Project/${PAGELIFECYCLE_NAME}`, __dirname + `/src/main/ets/${PAGELIFECYCLE_NAME}`)//遍历所有带@Entry装饰器的自定义组件
findAllPagesFiles(__dirname + '/src/main/ets/', __dirname + '/src/main/ets/', PAGELIFECYCLE_NAME);/*** 文件遍历方法* @param filePath 需要遍历的文件路径*/
function findAllPagesFiles(codeRootPath: string, filePath: string, configFileName: string) {// 根据文件路径读取文件,返回一个文件列表fs.readdir(filePath, (err, files) => {if (err) {console.error(err);return;}// 遍历读取到的文件列表files.forEach(filename => {// path.join得到当前文件的绝对路径const filepath: string = path.join(filePath, filename);// 根据文件路径获取文件信息fs.stat(filepath, (error, stats) => {if (error) {console.warn('获取文件stats失败');return;}const isFile = stats.isFile();const isDir = stats.isDirectory();if (isFile) {let checkPages: boolean = falselet config: string = fs.readFileSync(__dirname + '/src/main/resources/base/profile/main_pages.json','utf8');let temps = JSON.parse(config)temps.src.forEach( (value) => {if(filepath.endsWith(value+'.ets') || filepath.endsWith(value+'.ts')){checkPages = truereturn}})if(!checkPages){return}fs.readFile(filepath, 'utf-8', (err, data) => {if (err) throw err;let content = (data as string)content = formatCode(content)//开始计算相对路径let tempFilePath: string = filepath.substring(codeRootPath.length+1)let slashCount: number = 0for(let char of tempFilePath){if(char == '/'){slashCount++}}//导入PageLife.ts文件if(configFileName.indexOf('.') != -1){configFileName = configFileName.substring(0, configFileName.indexOf('.'))}let importPath: string = 'import ' + configFileName + ' from ''for(let k = 0; k < slashCount; k++){importPath += '../'}importPath += configFileName + '''content = insertImport(content, importPath)//导入@ohos.app.ability.commoncontent = insertImport(content, "import common from '@ohos.app.ability.common'", '@ohos.app.ability.common')content = insertVariable(content, "private  autoContext = getContext(this) as common.UIAbilityContext")INSERT_FUNCTION.forEach( value => {content = insertTargetFunction(content, value, `PageLifecycle.record(this.autoContext, '${filename}', '${value}')`)})fs.writeFile(filepath, content, (err) => {if (err) throw err;});});}if (isDir) {findAllPagesFiles(codeRootPath, filepath, configFileName);}});});});
}/*** 复制埋点入口文件至目标地址** @param originFile* @param targetFilePath*/
function copyConfigFile(originFile: string, targetFilePath: string){let config = fs.readFileSync(originFile,'utf8');console.log(config)fs.writeFileSync(targetFilePath, config)
}/*** 格式化代码,用于删除所有注释* @param inputContent* @returns*/
function formatCode(inputContent: string): string{inputContent = deleteMulComments(inputContent)inputContent = deleteSingleComments(inputContent)return inputContent
}/*** 删除多行注释* @param inputContent* @returns*/
function deleteMulComments(inputContent: string): string{//删除注释let mulLinesStart = -1let mulLinesEnd = -1mulLinesStart = inputContent.indexOf('/*')if(mulLinesStart != -1){mulLinesEnd = inputContent.indexOf('*/', mulLinesStart)if(mulLinesEnd != -1){inputContent = inputContent.substring(0, mulLinesStart) + inputContent.substring(mulLinesEnd+'*/'.length)return deleteMulComments(inputContent)}}return inputContent
}/*** 删除单行注释* @param inputContent* @returns*/
function deleteSingleComments(inputContent: string): string{//删除注释let mulLinesStart = -1let mulLinesEnd = -1let splitContent = inputContent.split(/\r?\n/)inputContent = ''splitContent.forEach( value => {// console.log('输入 >> ' + value)let tempvalue = value.trim()//第一种注释, 单行后边没有跟注释// m = 6if(tempvalue.indexOf('//') == -1){if(tempvalue.length != 0){inputContent = inputContent + value + '\n'}//第二种注释,一整行都为注释内容//这是一个演示注释} else if(tempvalue.startsWith('//')){// inputContent = inputContent + '\n'} else {//第三种注释// m = 'h//' + "//ell" + `o` //https://www.baidu.comlet lineContentIndex = -1let next: number = 0let label: string[] = []label.push(''')label.push("`")label.push(""")let shunxu: number[] = []while (true) {for(let k = 0; k < label.length; k++){let a = tempvalue.indexOf(label[k], next)let b = tempvalue.indexOf(label[k], a+1)if(a != -1 && b != -1){shunxu.push(a)}}//第四种注释// m = 2 //这是一个演示注释if(shunxu.length == 0){if(tempvalue.indexOf('//', next) != -1){inputContent = inputContent +  value.substring(0, value.indexOf('//', next)) + '\n'} else {inputContent = inputContent +  value.substring(0) + '\n'}break} else {//获取最先出现的let position = Math.min(...shunxu);let currentChar = tempvalue.charAt(position)let s = tempvalue.indexOf(currentChar, next)let e = tempvalue.indexOf(currentChar, s+1)if(s != -1 && e != -1 ){next = e + 1}while (shunxu.length != 0){shunxu.pop()}}}}})while (splitContent.length != 0){splitContent.pop()}splitContent = nullreturn inputContent
}function insertImport(inputContent: string, insertContent: string, keyContent?: string): string{let insertContentIndex: number = inputContent.indexOf(insertContent)if(keyContent){insertContentIndex = inputContent.indexOf(keyContent)}if(insertContentIndex == -1){inputContent = insertContent + '\n' + inputContent}return inputContent
}function insertVariable(inputContent: string, insertContent: string): string{if(inputContent.indexOf(insertContent) == -1){let tempIndex = inputContent.indexOf('@Entry')tempIndex = inputContent.indexOf('{', tempIndex)inputContent = inputContent.substring(0, tempIndex+1) + '\n'  + insertContent + '\n' + inputContent.substring(tempIndex+1)}return inputContent
}function insertTargetFunction(inputContent: string, funName: string, insertContent: string): string{let funNameIndex: number = inputContent.indexOf(funName)if(funNameIndex != -1){let funStartLabelIndex: number = inputContent.indexOf('{', funNameIndex)let funEndLabelIndex: number = findBrace(inputContent, funStartLabelIndex).endIndexif(funEndLabelIndex != -1){let funContent: string = inputContent.substring(funStartLabelIndex, funEndLabelIndex)let insertContentIndex: number = funContent.indexOf(insertContent)if(insertContentIndex == -1){inputContent = inputContent.substring(0, funStartLabelIndex+1)+ '\n'+ insertContent+ '\n'+ inputContent.substring(funStartLabelIndex+1)}}} else {let findEntryIndex = inputContent.indexOf('@Entry')findEntryIndex = inputContent.indexOf('{', findEntryIndex)let codeEndIndex = findBrace(inputContent, findEntryIndex).endIndexif(codeEndIndex != -1){inputContent = inputContent.substring(0, codeEndIndex)+ '\n'+ funName +'(){'+ '\n'+ insertContent+ '\n'+ '}'+ '\n'+ inputContent.substring(codeEndIndex)} else {throw Error('解析错误')}}return inputContent
}function findBrace(inputContent: string, currentIndex: number): BraceIndex{let computer: BraceIndex = new BraceIndex()computer.startIndex = currentIndexlet count: number = 0if(currentIndex != -1){count++currentIndex++}let tempChar: string = ''while(count != 0){tempChar = inputContent.charAt(currentIndex)if(tempChar == '}'){count--} else if(tempChar == '{'){count++}if(count == 0){computer.endIndex = currentIndexbreak}currentIndex++}return computer}class BraceIndex{public startIndex: number = 0public endIndex: number = 0
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1033454.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【目标检测】YOLOv5 网络结构,bottleneckCSP 与 C3 模块图解

文章目录 Focus 模块Csp 模块BottleneckCspC3CSP1_X 与 CSP_2XYOLOv4 的 CSP_X SSP 与 SSPF YOLOv5 作为 YOLO 家族的第五个版本&#xff0c;本身也演进了几个子版本&#xff0c;现在网上的资料都没有标注具体是哪个子版本的&#xff0c;导致不同文章之间各种混乱&#xff0c;像…

Java学习之类和对象、内存底层

目录 表格结构和类结构 表格的动作和类的方法 与面向过程的区别 具体实现 对象和类的详解 类的定义 属性&#xff08;field 成员变量&#xff09; 方法 示例--编写简单的学生类 简单内存分析(理解面向对象) 构造方法(构造器 constructor) 声明格式&#xff1a; 四…

前端缓存揭秘:5年前端技术的必备知识点

背景 本篇文章将详细讲述何为前端缓存&#xff0c;这也是前端工程师必备的知识点。 首先&#xff0c;我们不能少了干这事的目的&#xff0c;前端缓存有什么用&#xff1f; 缓存作用&#xff1a;加快资源获取速度&#xff0c;提高用户体验感&#xff0c;缓解服务端压力 我们带…

iOS移动应用实时查看运行日志的最佳实践

目录 一、设备连接 二、使用克魔助手查看日志 三、过滤我们自己App的日志 &#x1f4dd; 摘要&#xff1a; 本文介绍了如何在iOS iPhone设备上实时查看输出在console控制台的日志。通过克魔助手工具&#xff0c;我们可以连接手机并方便地筛选我们自己App的日志。 &#x1f4…

pytorch yolov5+Deepsort实现目标检测和跟踪+单目测距

最近一直在整理单目测距的内容&#xff0c;想着检测单目测距都写完了&#xff0c;顺手也写个检测跟踪单目测距&#xff0c;算是总结下这部分内容吧&#xff0c;如果有错误&#xff0c;还请不吝赐教&#xff01;&#xff01; 参考文献: YOLOv5DeepSort实现目标跟踪 pytorch yolo…

微信过期图片恢复,这样做真的超简单(4个方法)

如今&#xff0c;微信已经成为我们日常生活中不可或缺的一部分。我们通过微信与朋友分享生活的点滴&#xff0c;其中不乏许多珍贵的图片。然而&#xff0c;随着时间的推移&#xff0c;一些图片可能会因为过期而被自动清理&#xff0c;这让我们感到遗憾和失落。幸运的是&#xf…

华为流量整形配置

组网需求 如图1所示&#xff0c;企业网内部LAN侧的语音、视频和数据业务通过Switch连接到RouterA的Eth2/0/0上&#xff0c;并通过RouterA的GE3/0/0连接到WAN侧网络。 不同业务的报文在LAN侧使用802.1p优先级进行标识&#xff0c;在RouterA上根据报文的802.1p优先级入队列&…

【软件安装】(十五)Ubuntu22.04+Anaconda安装labelimg

一个愿意伫立在巨人肩膀上的农民...... LabelImg是一款开源的图片标注工具&#xff0c;使用Python编写&#xff0c;基于PyQt5框架。它提供了一个直观的图形用户界面&#xff0c;方便用户对图片进行标注&#xff0c;并生成标注结果。LabelImg支持多种常见的标注格式&#xff0c;…

el-select的错误提示不生效、el-select验证失灵、el-select的blur规则失灵

发现问题 在使用el-select进行表单验证的时候&#xff0c;发现点击下拉列表没选的情况下&#xff0c;他不会提示没有选择选项的信息&#xff0c;我设置了rule如下 <!--el-select--><el-form-item label"等级" prop"level"><el-select v-m…

Elementor Pro最新学习版:强大的WordPress页面构建器插件

产品用途 Elementor Pro的核心功能包括拖放编辑器、前端编辑器、实时预览、允许导入和导出模板、支持35预建模板、多种营销工具和插件支持、多种排版选项、能够放置内联元素、Font Awesome图标支持、允许构建移动响应页面、登陆页面构建器、弹出窗口生成器、对评级系统的架构标…

LINUX笔记温习

目录 DAY1 DAY2 day3&#xff1a; day4 day5 day6 day7 day8 day9 day10 day11 day12 day13 day14 day15 20day DAY1 1、多层级文件夹创建要带-p&#xff1b; 2、创建多文件&#xff0c;要先到该目录下才能创建(第一个目录必须存在才能有效建立)&#xff1b; D…

建立统一的机台数据传输管控通道,发挥数据的价值

机台数据传输通常指的是在工业自动化和制造过程中&#xff0c;将机器设备&#xff08;机台&#xff09;产生的数据通过网络或其他通信手段传输到监控系统、数据库或远程控制中心的过程。这一过程对于实现智能制造、提高生产效率和质量控制至关重要。比如传统制造业&#xff0c;…

探讨在大数据体系中API的通信机制与工作原理

** 引言 关联阅读博客文章&#xff1a;深入解析大数据体系中的ETL工作原理及常见组件 关联阅读博客文章&#xff1a;深入理解HDFS工作原理&#xff1a;大数据存储和容错性机制解析 ** 在当今数字化时代&#xff0c;数据已经成为企业发展和决策的核心。随着数据规模的不断增长…

【4月2日更新】低至50元/年 京东云 阿里云 腾讯云服务器价格对比表 幻兽帕鲁 雾锁王国 我的世界 饥荒 通用

更新日期&#xff1a;4月2日 本文纯原创&#xff0c;侵权必究 【云服务器推荐】价格对比&#xff01;阿里云 京东云 腾讯云 选购指南视频截图 《最新对比表》已更新在文章头部—腾讯云文档&#xff0c;文章具有时效性&#xff0c;请以腾讯文档为准&#xff01; 【腾讯文档实…

谷粒商城——通过接口幂等性防止重复提交订单

如果用户向后端服务提交多次相同订单的提交服务&#xff0c;那么后端应该只生成一条订单记录。 有一些操作天然是幂等的&#xff0c;如查询操作和删除操作等。 幂等性实现 1.token机制&#xff08;仅这个方法适用于订单的重复提交&#xff09; 后端先生成1个令牌将其记录在R…

Docker Compose环境的安装通过docker compose完成python程序的运行

目录 Docker Compose环境的安装 通过docker compose完成python程序的运行 Docker Compose环境的安装 ##### 方法一&#xff1a;直接下载编译好的二进制文件 注意&#xff1a;只有linux平台上在安装docker时没有安装docker-compose&#xff0c;windows、macos安装docker时自…

Verilog语法之always语句学习

always语法是Verilog_HDL中最常用的一种语法。 always过程语句和语句块组成的&#xff0c;语法格式如下所示。 always(敏感信号1 or 敏感信号2.....) always实现组合逻辑和时序逻辑。用always实现组合逻辑要将所有的敏感信号加入敏感列表中&#xff1b;用always实现时序逻辑时…

Kafka入门到实战-第二弹

Kafka入门到实战 Kafka快速开始官网地址Kafka概述Kafka术语Kafka初体验更新计划 Kafka快速开始 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流…

香港服务器怎么看是CN2 GT线路还是CN2 GIA线路?

不知道有没有小伙伴们注意过&#xff0c;很多人在租用香港服务器的时候都习惯性选择 CN2 线路&#xff1f;仿佛香港服务器是否采用 CN2 线路成为个人企业选择香港服务器的一个标准。其实&#xff0c;香港服务器有CN2、优化直连(163)、BGP多线(包含了国际和国内线路)&#xff0c…

基于java的智能停车场管理系统

开发语言&#xff1a;Java 框架&#xff1a;ssm 技术&#xff1a;JSP JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclip…