开源图表库选型:MPAndroidChart 之外还有什么
开源图表库选型:MPAndroidChart 之外还有什么
一个被过度依赖的默认选项
MPAndroidChart 在 Android 图表领域的统治地位已经持续了近十年。GitHub 上 3.6 万 star,Stack Overflow 上铺天盖地的问答,让这个项目几乎成了"Android 图表"的同义词。但这份统治力的背后,是代码库的老化、维护的停滞,以及一个越来越明显的事实:它并不是所有场景的最优解。
我去年接手一个金融数据类的项目,PM 要求实现带十字准星、多指缩放、实时刷新的 K 线图。用 MPAndroidChart 搭原型很快,但深入到细节就开始痛苦。CandleDataSet 的渲染逻辑写死在 CandleStickChartRenderer 里,想要自定义影线样式得重写整个 renderer;实时数据刷新时 notifyDataSetChanged() 的粒度太粗,高频更新下掉帧明显;最麻烦的是多 Y 轴场景,左右轴的 label 对齐问题在 issue 列表里躺了五年没人修。
这个项目最终没有用它。选型过程中我翻了不少库,有些确实能解决 MPAndroidChart 的痛点,有些则带来了新的问题。这篇文章把当时调研过的几个选项摊开来讲,包括它们的实现思路、适用边界,以及我实际踩过的坑。
原生 Canvas 派:PhilJay 的遗产与它的继承者们
MPAndroidChart 的核心架构其实相当经典:DataSet - Renderer - ViewPortHandler 三层分离,数据层和渲染层解耦,动画通过 Animator 插值驱动。这套设计在 2014 年是很先进的,但放到 2024 年来看,几个问题越来越突出。
首先是渲染管线。MPAndroidChart 的所有绘制都在主线程 Canvas 上完成,复杂图表(比如带上千个数据点的折线图)在低端机上很容易触发 GPU 渲染瓶颈。Android Studio 的 Profiler 里能看到 Chart.onDraw() 偶尔飙到 20ms 以上,而 16.67ms 的帧预算线就在那里,毫不留情。项目作者 PhilJay 在 2020 年后基本停止了维护,社区 fork 了不少分支,但没有一个形成统一的替代中心。
我调研时重点看了两个继承方向的库。一个是 HelloCharts-Android(https://github.com/lecho/HelloCharts-Android),这个库的设计更轻量,API 风格接近 MPAndroidChart 但简化了很多。它的 Preview 图表功能做得不错,适合需要缩略图导航的场景。但问题是它已经三年没有提交,且底层同样是主线程 Canvas 绘制,性能天花板和 MPAndroidChart 一样。更麻烦的是它的坐标轴系统不够灵活,我尝试实现一个非均匀分布的 X 轴(时间戳间隔不等的数据),发现它的 AxisValue 设计假设了均匀步长,改起来很别扭。
另一个方向是 WilliamChart(https://github.com/diogobernardino/WilliamChart),这个库走了极简路线,只提供折线、柱状、饼图三种基础类型,但动画效果很精致。它的 ChartAnimation 接口设计得比 MPAndroidChart 的 Animator 更直观,缓动函数可以直接插。问题是功能集太窄,稍微复杂的需求(比如组合图表、多数据集叠加)就要自己扩展。我看过它的源码,renderer 的抽象层次不够,扩展时容易破坏内部状态一致性。
这两个库让我意识到,纯 Canvas 方案在 2024 年的 Android 生态里有个根本矛盾:既要保持灵活性,又要突破性能瓶颈,而主线程绘制同时得罪了这两点。
GPU 加速派:OpenGL 与 Compose 的新战场
转向 GPU 加速的图表库是自然的思路。我测试了两个比较有代表性的实现。
SciChart(https://www.scichart.com)是商业库,但提供了功能完整的 Android 试用版。它的核心卖点是真正的 OpenGL ES 渲染,数据点数量上到十万级别仍然流畅。我跑过他们的 demo,一个包含 50 万点的实时信号波形图,在 Pixel 6 上稳定 60fps,同样的数据量 MPAndroidChart 直接 ANR。SciChart 的 API 设计很"企业级",冗长但功能齐全,多轴、游标、注释层、调色板映射都有现成实现。价格是个门槛,单平台授权年费 1499 美元起,对独立开发者不友好。另外它的 Android 版本更新频率明显低于 WPF/iOS 端,有些新功能是其他平台先上,Android 要等一到两个季度。
更贴近开源社区的是 Compose-Charts(https://github.com/bytebeats/compose-charts)。这个库完全基于 Jetpack Compose 的 Canvas API,但利用了 Compose 的重组机制做增量更新,避免了 MPAndroidChart 那种全量 invalidate() 的开销。它的折线图实现用了 PathEffect 和 drawBehind 修饰符,代码很干净,适合已经全面 Compose 化的项目。
我实际用过这个库做一个心率监测的页面,数据每秒更新一次,Compose 的细粒度重组确实比传统 View 的 invalidate 更高效。但坑在于 Compose Canvas 的坐标系和手势处理。多指缩放需要自己用 PointerInputScope 实现,而 Compose-Charts 并没有内置完整的手势识别,只提供了基础的 tap 回调。我花了两天时间写了一个基于 awaitPointerEventScope 的捏合缩放逻辑,处理了焦点缩放、边界回弹、惯性滑动,代码量比图表本身还长。另一个问题是这个库的维护状态,作者 bytebeats 的提交频率不高,去年有个 PR 修复了 Y 轴 label 重叠的问题,拖了四个月才合并。
Compose 生态里还有一个更底层的选项:Vico(https://github.com/patrykandpatrick/vico)。这个项目由 patrykandpatrick 维护,设计目标是成为"Compose 原生的 MPAndroidChart 替代品"。它的架构很有意思,核心模块 vico-core 是纯 Kotlin 多平台(依赖 Compose Multiplatform 的 ComposeCanvas),Android 端通过 vico-compose 接入。这意味着理论上它能跑在 Desktop 和 iOS 上,但我只测试过 Android 端。
Vico 的 API 设计明显吸收了 MPAndroidChart 的经验教训。ChartEntryModel 是不可变数据类,图表状态通过 ChartEntryModelProducer 管理,更新时自动触发重组。它的 AxisValueFormatter 和 AxisLabel 分离得很干净,我实现那个非均匀时间轴的需求时,只需要自定义 formatter 而不碰渲染层。性能方面,Vico 没有走 OpenGL 路线,而是依赖 Compose 的图层合成优化,在中等数据量(几千个点)下表现不错,但上到万级别就开始吃力。我提过一个 issue 问大数据量的优化方向,维护者回复说正在考虑基于 android.graphics.Path 的批量绘制优化,但还没有具体时间表。
Vico 的当前版本是 2.0.0-alpha,稳定版是 1.14.0。alpha 版的 API 变动比较激进,我项目启动时用的是 1.9.0,中途升级到 1.12.0 时发现 ChartStyle 的构造方式变了,改了一下午。这是个需要权衡的点:它的设计在进化,但进化过程中有迁移成本。
跨平台派:Flutter 生态的溢出与原生端的尴尬
有些团队会考虑把图表逻辑放在 Flutter 层统一实现,但原生 Android 项目偶尔也需要集成一些跨平台方案。我调研过两个相关选项。
fl_chart(https://github.com/imaNNeo/fl_chart)是 Flutter 社区最流行的图表库,star 数已经超过 MPAndroidChart。如果项目本身有 Flutter 模块,用它做图表再嵌入 Android 是可行的。但原生项目为了图表单独引入 Flutter Engine 的代价太大,APK 体积增加 10MB 以上,启动内存占用也多出几十兆。我做过一个技术验证,纯原生项目集成 Flutter 图表模块,冷启动时间从 800ms 涨到 1.4s,被架构组直接否决。
另一个方向是 Kandy(https://github.com/Kotlin/kandy),JetBrains 官方推出的 Kotlin 数据可视化库。它基于 Kotlin Notebook 和 Compose 桌面端设计,目标是成为 Kotlin 生态的 ggplot2。但当前版本(0.6.0)对 Android 的支持很有限,官方文档的 Android 示例几乎空白。我尝试把它集成到一个 Android 项目里,依赖解析就卡了半小时——它底层拉了不少 Kotlin DataFrame 和 Compose Desktop 的传递依赖,和 Android 的 Compose BOM 版本冲突。这是个典型的"技术愿景很好,工程落地不成熟"的案例,建议至少等 1.0 之后再评估。
WebView 派:Highcharts 与 ECharts 的嵌入策略
有些复杂图表需求,原生库确实覆盖不全,比如金融领域的多指标技术分析图(MACD、KDJ、BOLL 叠加),或者需要特定交互模式(画线工具、形态识别)。这时候 WebView 嵌入 H5 图表是个务实选项。
ECharts(https://echarts.apache.org)通过 WebView 嵌入 Android 的方案我做过两次。一次是用腾讯 X5 内核,一次是系统 WebView。X5 的兼容性更好,但包体积增加 30MB 且初始化慢;系统 WebView 轻量,但不同厂商的 Chromium 版本差异会导致渲染不一致。ECharts 的 dataZoom 组件在移动端的手势适配需要额外调参,zoomLock 和 preventDefaultMouseMove 的组合我试了四个版本才找到不跟页面滚动冲突的配置。
性能方面,WebView 图表的瓶颈在 JSBridge 通信。我测过实时推送场景,每秒 10 次数据更新通过 evaluateJavascript 注入,在小米 10 上 CPU 占用 15% 左右,同等数据量的原生方案(SciChart)不到 5%。另一个坑是内存泄漏,WebView 的销毁时机和 Activity 生命周期不同步,如果图表页面是 Fragment 嵌套,很容易留下僵尸 WebView 实例。我最后的方案是自定义一个 LifecycleAwareWebView,在 onStop 时主动调用 loadUrl("about:blank") 释放资源,但这属于防御性编程,不是根治。
Highcharts(https://www.highcharts.com)也有类似的嵌入方案,但它对商业使用的授权更严格,个人开源免费,商业产品年费 160 美元起。它的 Stock 模块对金融图表的支持比 ECharts 更专业,但体积也更大,minified 后的 highstock.js 接近 300KB,加上导出模块能上 500KB。对 APK 体积敏感的项目需要斟酌。
一个不太常见的选择:自研渲染器
回到我开头提到的 K 线项目,最终的方案是自研。不是从零写,而是基于 Android-GraphView(https://github.com/jjoe64/GraphView)的核心做改造。这个库本身已经停止维护,但它的 GraphView 自定义 View 结构很简单,没有 MPAndroidChart 那种厚重的抽象层。
我的改造方向是三层替换:底层用 SurfaceView + 独立渲染线程替代主线程 Canvas;中间层保留 GraphView 的坐标映射逻辑但改成双缓冲;上层用 Kotlin Flow 驱动数据更新,替代原来的 OnDataChangedListener。SurfaceView 的渲染线程通过 Choreographer 同步,避免和主线程 UI 抢资源。实测下来,5000 个 K 线点的缩放流畅度接近 SciChart 的 80%,但代码完全可控。
这个方案的代价是开发周期。MPAndroidChart 搭一个能看的原型要半天,自研方案花了三周才稳定。维护成本也高,后续每个新图表类型都要写对应的 renderer。我现在的判断是:数据点少于 1000、交互简单的场景,用 Vico 或 Compose-Charts 足够;数据量大、交互复杂、且团队有图形开发能力的,自研或 SciChart 更合理;WebView 方案适合快速验证或强依赖 Web 生态的特定图表类型。
选型决策的几个具体维度
抛开技术细节,实际选型时我通常会问这几个问题,顺序有先后。
第一个是 数据更新频率。静态图表和动态图表对架构的要求完全不同。MPAndroidChart 的设计假设了相对静态的数据,它的动画系统是"数据变更 → 插值动画 → 新状态",而不是"持续流式更新"。我看过一个量化交易团队的实现,他们用 MPAndroidChart 做实时盘口,每秒更新 20 次,最后不得不把 notifyDataSetChanged() 改成脏标记批量刷新,代码里到处是 workaround。如果项目有实时需求,优先考虑 Vico 的 Flow 驱动或 SciChart 的 OpenGL 管线。
第二个是 交互复杂度。单指滑动、双指缩放、长按十字准星、多数据集联动高亮——每加一种交互,自定义成本就指数上升。MPAndroidChart 的 OnChartGestureListener 把几种手势揉在一起,回调参数的设计也不够精细,比如双指缩放的焦点坐标需要自己去 MotionEvent 里算。Vico 的 ChartScrollState 和 ChartZoomState 用 State 对象暴露,和 Compose 的手势系统衔接更自然。如果是重度交互的图表,Compose 系的库有明显优势。
第三个是 团队技术栈。全面 Compose 化的项目,引入 MPAndroidChart 意味着混用 View 系统,互操作有摩擦成本。我测过 AndroidView 包裹 MPAndroidChart 在 Compose 里的性能,重组时 AndroidView 的更新触发传统 invalidate,和周围 Compose 元素的合成不在一个管线,偶尔有画面撕裂。反过来,传统 View 项目为了图表引入 Compose 更不合理,初始化开销和依赖膨胀都不值得。
第四个是 长期维护预期。开源库的维护状态很难预测,但有几个信号可以参考:issue 的响应速度、PR 的合并周期、作者的其他项目活跃度。MPAndroidChart 的 issue 区有 1800 多个 open issue,最近一年几乎没有作者回复。Vico 的维护者 patrykandpatrick 响应很快,但他们是两个人兼职维护,长期投入有限。SciChart 是商业公司,支持有保障,但价格过滤掉了小团队。这些风险需要在项目初期就评估,而不是等踩坑后再迁移。
一些具体的版本号和迁移经验
最后列几个我实际验证过的版本组合,供参考。
MPAndroidChart 的 3.1.0 是最后一个稳定版,3.1.1 之后作者只合并了少量 PR,没有发新版本。如果必须用,建议锁死 3.1.0,3.1.1 引入了一个 BarChart 的渲染回归,group bar 的间距计算有误,issue #7692 里有详细复现。
Vico 的 1.12.0 到 1.14.0 之间,ChartStyle 的默认构造方式从 ChartStyle.fromChartStyle 改成了直接 data class 构造,迁移时注意 axisLabelRotationDegrees 参数的位置变动。2.0.0-alpha 引入了 CartesianChartHost 替代原来的 Chart composable,API 变化更大,不建议生产环境使用。
Compose-Charts 的 0.4.0 版本有个坑:Y 轴的 maxValue 和 minValue 必须是 Float,但业务数据经常是 Double,精度转换时如果数据范围很大(比如股价 300000.00 级别),会丢失小数部分。我提的 issue #47 里给了 workaround,用 remember 缓存缩放后的值,但官方还没修复。
SciChart 的 Android SDK 最新是 4.4.0,但文档站点(https://www.scichart.com/documentation/android)的示例代码有些是 4.2 版本的,SciChartBuilder.init() 的调用方式在 4.3 后变了,直接 copy 文档代码会编译报错。他们的技术支持回复很快,邮件一般当天回,这是商业库的优势。
自研方案里,SurfaceView 的渲染线程有个容易忽视的坑:如果图表在 RecyclerView 里作为 item 使用,SurfaceView 的 z-order 会和列表滚动冲突,画面可能穿透到其他 item 上。解决方式是用 TextureView 替代,但 TextureView 的帧率比 SurfaceView 低 10-15%,需要权衡。我最后的方案是列表里用 TextureView,详情页全屏用 SurfaceView,根据场景切换。
没有银弹,只有权衡
MPAndroidChart 的统治地位是历史形成的,不是技术最优解的必然结果。2014 年它填补了 Android 图表库的空白,2024 年的生态已经多元得多。Compose 的崛起、实时数据需求的增加、GPU 性能的释放,都在推动新的解决方案。
我的实际选择是:新项目用 Vico(如果全面 Compose)或自研(如果有特殊性能/交互需求);遗留项目维护 MPAndroidChart 的,评估迁移成本后再决定;金融类重度场景,SciChart 的授权费通常比自研人力成本低。
选型没有标准答案,但有一个原则:不要让"大家都在用"成为唯一的决策依据。去跑一下 Profiler,看一下源码,提一个 issue 测试维护者的响应,这些动作花不了多少时间,但能避免项目后期的大量返工。