钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花

news2025/1/12 16:11:41

作者:姜凡(步定)

本文为《钉钉 ANR 治理最佳实践》系列文章首篇《定位 ANR 不再雾里看花》,主要介绍了钉钉自研的 ANRCanary 通过监控主线程的执行情况,为定位 ANR 问题提供更加丰富的信息。

后续将在第二篇文章中讲述钉钉基于分析算法得出 ANR 归因,上报到 ANR 归因监控平台,帮助研发人员更快更准确的解决 ANR 问题,并总结钉钉 ANR 实战踩坑与经验总结

相信大家对 Android 的 ANR 问题并不陌生。钉钉作为一个用户数超 5 亿,服务着 2100 万家组织的产品,基本上其他 App 遇到的 ANR 问题,我们都会遇到。

和大家一样,我们最初在分析 ANR Trace 日志的时候,都会不禁怀疑上报的堆栈是否真的有问题,总有一种雾里看花的感觉。

本系列文章主要介绍钉钉在 ANR 治理过程中的思考方向,工具建设,典型问题等,希望能够通过本次分享,为有 ANR 治理诉求的团队提供一定的参考。

术语表

系统 ANR 完整流程

系统 ANR 完整流程可以分为如下三个部分:

  • 超时检测
  • ANR 信息收集
  • ANR 信息输出

对于超时检测的逻辑,业界已经有比较详细的阐述,此处不再赘述。重点聊聊检测到超时之后的处理逻辑。详细源码可以参见:ProcessRecord.java,ANR 信息收集和 ANR 信息输出两个流程图如下:

如上图所示,从系统源码中,得到的启示有:

  • ANR Trace 的堆栈抓取时机是滞后的,其堆栈不一定是 ANR 根因。
  • System Server 会对多个进程发送 SIGQUIT 信号,请求堆栈抓取的操作。
    • 收到 SIGQUIT 不代表当前进程发生了 ANR ,可能是手机里有一个其他的 App 发生了 ANR,如果不进行 ANR 的二次确认,就会导致 ANR 误报。
  • App 可以通过进程 ANR 错误状态感知发生了前台 ANR 。

刻舟求剑的 ANR Trace

  • 以广播发送导致 ANR 的过程为例,当 System Server 进程检测到广播处理超时时,会发送SIGQUIT 信号到 App 进程, App 进程收到信号之后,会将当前所有线程的执行堆栈 Dump 下来为 ANR Trace,并最终输出。
  • 然而如图所示,这个 Dump 时机是有一定的滞后性的,真正导致 ANR 的 长耗时消息3 已经执行完了。当前执行消息5 是作为替罪羊被抓到的,甚至 当前执行消息5 到底消耗了多长时间也不确定。因此 Android 系统设计提供的用来分析 ANR 问题的 ANR Trace ,其实只是刻舟求剑, 并不一定能定位到 ANR 的根因。

ANR 误报过滤

鉴于前面提到的收到 SIGQUIT 信号,并不代表当前进程发生了 ANR,需要一个二次确认逻辑,进行误报过滤。

钉钉采用的方案是:

  • 在收到 SIGQUIT 信号之后,在 20 秒内轮询进程错误状态的确认是否为前台 ANR。
  • 与此同时,因为发生后台 ANR 之后,系统会直接杀进程,而其他进程 ANR 并不会导致进程被杀,因此可以通过持久化的方案来区分。

详细流程图如下:

ANR 监控工具

工欲善其事,必先利其器。钉钉自研的 ANRCanary 监控工具,通过轮询的方式持续记录主线程最新任务的执行耗时,到发生 ANR 时,基于耗时最长的消息定位 ANR 的根因。

ANRCanary 相对于 ANR Trace,从点扩展到面,提供了主线程历史任务耗时维度的信息,解决了 ANR Trace 刻舟求剑的问题。

接下来将对上图中的关键技术方案依次进行详细说明。

历史任务监控

Android 主线程任务,可以大概划分为如下几个分类:

  • Handler 消息:最常见的基于 Handler 的主线程任务。
  • IdleHandler:消息队列进入空闲状态时执行。
  • nativePollOnce:从 Native 层触发,具体可能包括:
    • 触摸事件处理
    • 传感器事件处理

历史任务监控的目标是感知每个主线程任务的开始时间和结束时间,针对不同的主线程任务,需要采用不同的 Hook 方式。大部分的 Hook 方案,业界均有比较详细的描述,不再说明。

简单介绍一下 FakeIdle 排除法方案:

  • 基于定时的堆栈抓取能力,将堆栈始终处于 nativePollOnce 的任务,判断为 Idle 任务。
  • 那么既不是 Message 任务,也不是 IdleHandler 任务, 还不是 Idle 时间段其余任务,单独识别为 FakeIdle 任务。

历史任务聚合

对于 ANR 来说,需要重点关注的是长耗时的任务,大部分的短耗时任务是可以忽略的。因此任务调度是可以按照一定条件进行聚合。

任务聚合的好处具体包括:

  • 减少内存操作次数:避免内存抖动和对应用性能产生影响
  • 压缩冗余数据:方便观察和分析

基于上述思路,将聚合以后的主线程历史任务记录分成如下几个类型:

  • 聚合类型:主线程连续调度多个任务,并且每一个任务耗时都很少的情况下,将这些任务耗时累加。直至这些任务累计耗时超过阈值,则汇总并记录一条聚合类型的任务记录。该类型任务通常不需要关注。
  • Huge 类型:单个任务耗时超过设定的阈值,则单独记录一条 Huge类型的任务。同时将 Huge 任务前面尚未聚合的 N 次短时间耗时任务生成一条聚合类型的任务。该类型任务需要重点关注。
  • Idle 类型:主线程进入空闲状态的时间段,自然也应该生成一条记录。该类型任务通常不需要关注。
  • Key 类型:可能会引起 ANR 的Android 四大组件的消息,需要单独记录。称之为 Key 类型的记录。
  • Freeze 类型:部分厂商手机独有的 App 退后台,进程运行被冻结,直到 App 回到前台才会继续运行,被冻结的任务时间间隔可能会很长,却不能当做 Huge 类型,需要单独记录为 Freeze 类型。

当前 Running 任务

通过 ANRCanary 的当前 Running 任务信息,可以清晰的知道当前任务到底执行了多长时间,帮忙研发人员排除干扰,快速定位。
借助这项监控,可以非常直观的看到 ANR 的 Trace 堆栈刻舟求剑的问题。

"runningTaskInfo":{
    "stackTrace":[
        "android.os.MessageQueue.nativePollOnce(Native Method)",
        "android.os.MessageQueue.next(MessageQueue.java:363)",
        "android.os.Looper.loop(Looper.java:176)",
        "android.app.ActivityThread.main(ActivityThread.java:8668)",
        "java.lang.reflect.Method.invoke(Native Method)",
        "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)",
        "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)"
    ],
    "type":"IDLE",
    "wallDuration":519
}

如上所示,基于 ANRCanary 抓取到信息,可以看到发生 ANR 时主线程当前处于 IDLE 状态,持续时间为 519 毫秒。

Pending消息列表

主线程的消息列表也是 ANRCanary 需要 Dump 的,基于 Pending 消息列表,可以感知以下几点:

  • 消息队列中的消息是否被 Block 以及被 Block 了多久:基于 Block 时长可以判断主线程的繁忙程度。
  • 判断是否存在 Barrier 消息泄露。一旦发生 Barrier 消息泄露,主线程会永久阻塞,导致永远处于 ANR 状态。
    • 关于 Barrier 消息泄露的问题,将在后续章节进行详细探讨。
  • 判断消息列表里是否存在重复消息:据此推断是否有业务逻辑异常导致重复任务,从而填满了主线程导致 ANR。

总的来说,如上图所示,ANRCanary 收集的主线程信息包括过去,现在,未来三个阶段。

主线程堆栈采样

每个主线程任务内部的业务逻辑对于研发人员来说都是黑盒。函数执行的耗时存在很多的不确定性。有可能是锁等待,跨进程通信,IO操作等各种情况都会导致任务执行耗时,因此需要堆栈信息帮忙定位到具体代码。

ANRCanary 实现的时间对齐的堆栈采样方案,主要目的包括:

  • 避免频繁添加、取消超时任务
  • 只有长耗时执行任务才会触发堆栈抓取
  • 尽可能减少堆栈抓取的次数

如上图所示,堆栈采样的时间对齐方案具体实现如下:

  • 由单独的堆栈采样线程负责堆栈抓取。
  • 基于主线程的任务监听机制,每个任务的开始和结束都会告知到堆栈采样线程。
  • 超时任务触发堆栈抓取的前提条件是:当前最新的主线程任务执行超过最低超时时间。
  • 执行完堆栈抓取后,会对超时时长进行渐进,再丢一个超时任务,直到当前任务执行完成
  • 长耗时后面的任务发现超时时长发生过渐进,就会执行一次堆栈采样线程消息队列的清理,重置超时任务。

案例分享

我们收到一例测试同学的反馈,说钉钉在长时间压测过程中,总是遇到 ANR 问题,阻塞了测试流程。

基于 BugReport 里的 ANR Trace 信息显示,是传感器事件处理耗时的问题。

"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0 obj=0x749019e8 self=0x743c014c00
  | sysTid=26378 nice=-10 cgrp=default sched=0/0 handle=0x74c1b47548
  | state=R schedstat=( 12722053200 4296751760 139559 ) utm=949 stm=323 core=2 HZ=100
  | stack=0x7febba5000-0x7febba7000 stackSize=8MB
  | held mutexes= "mutator lock"(shared held)
  at java.io.CharArrayWriter.<init>(CharArrayWriter.java:67)
  at java.io.CharArrayWriter.<init>(CharArrayWriter.java:58)
  at java.net.URLEncoder.encode(URLEncoder.java:206)
  at xxx.b(SourceFile:???)
  at xxx.build(SourceFile:???)
  at xxx.onEvent(SourceFile:???)
  at xxx.onEvent(SourceFile:???)
  at xxx.handleSensorEvent(SourceFile:???)
  - locked <0x00052b82> (a sco)
  at android.hardware.SystemSensorManager$SensorEventQueue.dispatchSensorEvent(SystemSensorManager.java:833)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:326)
  at android.os.Looper.loop(Looper.java:160)
  at android.app.ActivityThread.main(ActivityThread.java:6718)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

可是基于 钉钉接入的 CrashSDK 里的 ANR Trace 信息显示,是硬件渲染的问题。

"main" prio=10 tid=1 Native
  | group="" sCount=0 dsCount=0 flags=0 obj=0x749019e8 self=0x7602814c00
  | sysTid=25052 nice=-10 cgrp=default sched=0/0 handle=0x7688258548
  | state=? schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
  | stack=0x7fe795e000-0x7fe7960000 stackSize=8MB
  | held mutexes=
  at android.view.ThreadedRenderer.nSyncAndDrawFrame(Native method)
  at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:823)
  at android.view.ViewRootImpl.draw(ViewRootImpl.java:3321)
  at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3125)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2484)
  at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1466)
  at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7196)
  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
  at android.view.Choreographer.doCallbacks(Choreographer.java:761)
  at android.view.Choreographer.doFrame(Choreographer.java:696)
  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
  at android.os.Handler.handleCallback(Handler.java:873)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:193)
  at android.app.ActivityThread.main(ActivityThread.java:6718)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

两个结论僵持不下,最后再来看看 ANRCanary 提供的信息

  "cpuDuration": 9,
    "messageStr": ">>>>> Dispatching to Handler (android.view.Choreographer$FrameHandler) {3b01fdc} android.view.Choreographer$FrameDisplayEventReceiver@bdac8e5: 0",
    "threadStackList": [
        ...
        {
            "stackTrace":[
                "android.view.ThreadedRenderer.nSyncAndDrawFrame(Native Method)",
                "android.view.ThreadedRenderer.draw(ThreadedRenderer.java:823)",
                "android.view.ViewRootImpl.draw(ViewRootImpl.java:3321)",
                "android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3125)",
                "android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2484)",
                "android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1466)",
                "android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7196)",
                "android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)",
                "android.view.Choreographer.doCallbacks(Choreographer.java:761)",
                "android.view.Choreographer.doFrame(Choreographer.java:696)",
                "android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)",
                "android.os.Handler.handleCallback(Handler.java:873)",
                "android.os.Handler.dispatchMessage(Handler.java:99)",
                "android.os.Looper.loop(Looper.java:193)",
                "android.app.ActivityThread.main(ActivityThread.java:6718)",
                "java.lang.reflect.Method.invoke(Native Method)",
                "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)",
                "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)"
            ],
            "state":"RUNNABLE",
            "wallTime":65347
        }
    ],
    "type": "HUGE",
    "wallDuration": 68497
}

ANRCanary的信息显示钉钉在硬件渲染阶段耗费了 68 秒的时间。

{
    "curThreadStack":{
        "stackTrace":[
            "android.os.MessageQueue.enqueueMessage(MessageQueue.java:569)",
            "- locked <192655128> (a android.os.MessageQueue)",
            "android.os.Handler.enqueueMessage(Handler.java:745)",
            "android.os.Handler.sendMessageAtTime(Handler.java:697)",
            "android.os.Handler.postAtTime(Handler.java:445)",
            "xxx.send(SourceFile:???)",
            "xxx.handleSensorEvent(SourceFile:???)",
            "- locked <189021104> (a xxx)",
            "android.hardware.SystemSensorManager$SensorEventQueue.dispatchSensorEvent(SystemSensorManager.java:833)",
            "android.os.MessageQueue.nativePollOnce(Native Method)",
            "android.os.MessageQueue.next(MessageQueue.java:326)",
            "android.os.Looper.loop(Looper.java:160)",
            "android.app.ActivityThread.main(ActivityThread.java:6718)",
            "java.lang.reflect.Method.invoke(Native Method)",
            "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)",
            "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)"
        ],
        "state":"RUNNABLE",
        "wallTime":12
    },
    "messageStr": "",
    "type": "LOOPER",
    "wallDuration": 12
}

而一开始 BugReport 指出的传感器事件处理,作为当前 Running 任务,只耗费了 12 毫秒。

最终基于 ANRCanary 给出的排查方向,开发同学定位到阻塞的原因是因为测试机系统硬件渲染底层有一个锁等待导致的问题。

后续

本篇文章介绍了钉钉自研的 ANRCanary 通过监控主线程的执行情况,为定位 ANR 问题提供更加丰富的信息。不过 ANRCanary 日志信息比较多,希望每个研发人员都能从中分析出导致 ANR 的原因是比较困难的。

接下来将在下篇文章中讲述钉钉基于分析算法得出 ANR 归因,并上报到 ANR 归因监控平台,帮助研发人员更快更准确的解决 ANR 问题。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【TuyaOS开发之旅】BK7231N GPIO的简单使用

接口讲解 GPIO初始化 /*** brief gpio 初始化* * param[in] pin_id: 需要初始化的GPIO编号&#xff0c; 对应TUYA_GPIO_NUM_E枚举* param[in] cfg: gpio 配置** return OPRT_OK on success. Others on error, please refer to tuya_error_code.h*/ OPERATE_RET tkl_gpio_ini…

基于SpringBoot工程开发Docker化微服务

目录 1. 微服务容器化治理的优缺点 1.1 微服务容器化的优点 1.2 微服务容器化的缺点 2. 微服务的两种模式 2.1 Microservice SDK 2.2 ServiceMesh 3. 微服务容器化治理的推荐模式 4.Windows下开发容器化微服务&#xff08;非K8S&#xff09; 4.1 开发环境 4.2 代码框架…

全网最新、最详细的使用burpsuite验证码识别绕过爆破教程(2023最新)

1、前沿 最近一直在研究绕过验证码进行爆破的方法&#xff0c;在这里对自己这段时间以来的收获进行一下分享。在这里要分享的绕过验证码爆破的方法一共有2个&#xff0c;分为免费版本&#xff08;如果验证码比较奇怪可能会有识别错误的情况&#xff09;和付费版本&#xff08;…

【Qt】QtCreator远程部署、调试程序

1、添加远程设备 1)QtCreator 工具–> 选项 --> 设备 --> 添加 2)设备设置向导选择–> Generic Linux Device --> 开启向导 3)填写“标识配置的名称”(随便写)、设备IP、用户名 --> 下一步 4)选择配对秘密文件,第一次配对,可以不填写,点击“下一…

嵌入式:ARM嵌入式系统开发流程概述

文章目录嵌入式开发的具体过程开发流程图嵌入式软件开发环境交叉开发环境远程调试结构图嵌入式应用软件开发的基本流程软件模拟环境目标板与评估板嵌入式软件开发的可移植性和可重用性嵌入式开发的具体过程 系统定义与需求分析阶段方案设计阶段详细设计阶段软硬件集成测试阶段…

Tomcat架构分析—— Engine

文章目录一、Tomcat的核心模块&#xff08;核心组件&#xff09;二、Engine 组件1.核心类与依赖图2.核心类源码分析构造函数&#xff1a;初始化方法 init&#xff1a;启动方法 start&#xff1a;3.Engine的启动过程总结一、Tomcat的核心模块&#xff08;核心组件&#xff09; …

机器学习之支持向量机(手推公式版)

文章目录前言1. 间隔与支持向量2. 函数方程描述3. 参数求解3.1 拉格朗日乘数3.2 拉格朗日对偶函数前言 支持向量机(Support(Support(Support VectorVectorVector Machine,SVM)Machine,SVM)Machine,SVM)源于统计学习理论&#xff0c;是一种二分类模型&#xff0c;是机器学习中获…

mysql查询当天,近一周,近一个月,近一年的数据

1.mysql查询当天的数据 select * from table where to_days(时间字段) to_days(now()); 2.mysql查询昨天的数据 select * from table where to_days(now( ) ) - to_days( 时间字段名) < 1 3.mysql查询近一周的数据 SELECT * FROM table WHERE date(时间字段) > D…

MySQL表的创建修改删除

目录 1、表的创建 2、查看表结构 3、表的修改 4、表的删除 1、表的创建 CREATE TABLE table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储引擎&#xff1b;说明&#xff1a; field 表示列名 datatype 表…

计算机系统基础实验 - 定点数加减法的机器级表示

实验序号&#xff1a;2 实验名称&#xff1a;定点数加减法的机器级表示 适用专业&#xff1a;软件工程 学 时 数&#xff1a;2学时 一、实验目的 1、掌握定点数加法的机器级表示。 2、掌握定点数减法的机器级表示。 3、掌握EFLAGS中4个牵涉到计算的标志位的计算方法。 4、掌握…

python实现动态柱状图

目录 一.基础柱状图 反转x轴&#xff0c;y轴&#xff0c;设置数值标签在右侧 小结 二.基础时间线柱状图 三.GDP动态柱状图绘制 1.了解列表的sort方法并配合lambda匿名函数完成列表排序 2.完成图表所需数据 3.完成GDP动态图表绘制 添加主题类型 设置动态标题 四.完整代码…

5.6 try语句块和异常处理

文章目录throw表达式(异常检测)try语句块&#xff08;异常处理&#xff09;编写处理代码函数在寻找处理代码的过程中退出标准异常异常是指存在于运行时的反常行为&#xff0c;这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。当程序的某部…

Android Studio实现一个旅游课题手机app

文章目录&#xff1a; 目录 一、课题介绍 二、软件的运行环境 三、软件运行截图 四、软件项目总结 一、课题介绍 本次课题是实现了一个外出旅游的app&#xff0c;通过app可以显示景点的信息&#xff0c;以及根据地区查询&#xff0c;具体功能如下&#xff1a; 客户端 1.用…

【算法】面试题 - 数组(附讲解视频)

目录标题原地修改数组&#xff08;快慢指针&#xff09;26. 删除有序数组中的重复项扩展&#xff1a;83. 删除排序链表中的重复元素27. 移除元素283. 移动零左右指针167. 两数之和15. 三数之和[一个方法团灭 NSUM 问题](https://blog.csdn.net/yzx3105/article/details/1284606…

JavaWeb学生系统+教师系统+管理员系统

目录&#xff1a;一、前言&#xff1a;一、用到的技术&#xff1a;1.前端&#xff1a;HTMLCssJavaScriptAjaxJQueryBootStrap2.后端&#xff1a;ServletJSPSpringMVCJPA二、系统实现的效果&#xff1a;1.登录登出功能&#xff1a;(1)不同用户可以跳转到不同的系统页面。(2)设有…

window 和虚拟机ubuntu通讯的网络设置 本地连接桥接和NAT

工作需要&#xff0c;最近在linux下开发&#xff0c;需要将windows里的文件传至虚拟机里以及下位机树莓派中&#xff0c;三者需要实现互传。 windows连接树莓派时是采用网口建立本地连接的&#xff0c;而当不需要连接树莓派时&#xff0c;windows和虚拟机不能通过有线本地连接…

09、SpringCloud 系列:Nacos - 配置文件中心

SpringCloud 系列列表&#xff1a; 文章名文章地址01、Eureka - 集群、服务发现https://blog.csdn.net/qq_46023503/article/details/12831902302、Ribbon - 负载均衡https://blog.csdn.net/qq_46023503/article/details/12833228803、OpenFeign - 远程调用https://blog.csdn.…

Python接口测试实战1(下)- 接口测试工具的使用

本节内容 抓包工具的使用Postman的使用 抓包工具的使用 抓包工具简介 Chrome/Firefox 开发者工具: 浏览器内置&#xff0c;方便易用Fiddler/Charles: 基于代理的抓包&#xff0c;功能强大&#xff0c;可以手机抓包&#xff0c;模拟弱网&#xff0c;拦截请求&#xff0c;定制…

xpdf在windows下的编译记录

目录 1、下载源码 ​编辑 2、准备工作 3、编译freetype 3.1 打开vs工程 3.2 生成之后查看 4、编译zlib 5、编译libpng 6、编译lcms 7、编译xpdf 8、存在问题 1、下载源码 Xpdf官网下载&#xff1a;Download Xpdf and XpdfReader 2、准备工作 3、编译freetype 3.1 打…

财务数字化转型怎么转?从哪几个方面出发

财务的数字化转型如何进行&#xff1f;许多企业在推动各大业务部门进行数字化转型时&#xff0c;往往会忽略财务部门。然而&#xff0c;作为掌握公司核心资源与数据和推动企业数字化建设的部门&#xff0c;财务也应成为企业数字化转型的重要突破口。 这篇就用几个案例详细拆解…