Skip to content

feat: 新增 Android/iOS 零依赖 APNG 适配器参考实现 - #1627

Open
lingzeanlan wants to merge 3 commits into
Tencent-TDS:mainfrom
lingzeanlan:apng-lite-adapters
Open

lingzeanlan wants to merge 3 commits into
Tencent-TDS:mainfrom
lingzeanlan:apng-lite-adapters

Conversation

@lingzeanlan

Copy link
Copy Markdown

背景

APNG 组件在 Android/iOS 是宿主扩展,需要业务自行实现适配器。目前 demo 工程的两个参考实现均依赖第三方解码库:

  • Android:KRAPNGViewAdapter 依赖 APNG4Androidcom.github.penfeizhou.android.animation:apng
  • iOS:KRAPNGViewHandler 依赖 SDWebImage(SDAnimatedImageView

对于不希望引入这类第三方库的业务(包体积/合规审计/依赖收敛场景),缺少一个可直接使用的零依赖选择。

改动

新增两个零第三方依赖的 APNG 适配器参考实现(单文件、可直接拷贝使用):

  • AndroidKRAPNGViewAdapterLite.kt——手工解析 APNG 块结构(IHDR/acTL/fcTL/IDAT/fdAT),逐帧重建标准 PNG 交 BitmapFactory 解码,Handler 按帧延时切帧;
  • iOSKRAPNGViewHandlerLite.h/.m——同一解析思路,帧数据交 UIImage 解码,dispatch_after + frameIndex 守卫调度;CRC32 自实现,不引入 libz;
  • 文档docs/API/components/apng.md 新增「零第三方依赖参考实现」一节,说明注册方式与适用边界。

解析失败或不含动画帧时降级为静态图显示,不崩溃。

适用边界(已在代码注释与文档中标注)

仅支持「全帧」APNG(每帧尺寸 = 画布尺寸、offset = 0、blend/dispose = 0)。常见导出工具默认产出全帧,覆盖图标/加载动效等大多数场景;需要局部帧/帧合成的复杂 APNG 仍建议使用第三方解码库适配器。

测试

在业务项目(Kuikly 2.23.2)真机验证:Android/iOS 双端 APNG 正常解码播放、播完定格末帧、repeatCount 语义生效;解析失败路径降级正常。

说明

  • 不动现有 demo 适配器与构建配置,纯新增文件;iOS 侧特意不做 +load 自动注册(demo 中 KRAPNGViewHandler 已在 +load 注册,两个 +load 并存时生效顺序不确定),由业务显式调用 +registerToKuikly

@tencent-adm

tencent-adm commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@KuiklyAI KuiklyAI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 KuiklyAI Code Review

基本信息

  • PR:#1627 · feat: 新增 Android/iOS 零依赖 APNG 适配器参考实现
  • 作者:lingzeshanhai main ← apng-lite-adapters

变更概述

  • 干了什么:新增 Android(KRAPNGViewAdapterLite.kt)与 iOS(KRAPNGViewHandlerLite.h/.m)两个零第三方依赖的 APNG 宿主适配器,手工解析 APNG 块结构、逐帧重建标准 PNG 交系统解码器播放,并在 docs/API/components/apng.md 补充文档。纯新增文件,不动现有 demo 适配器与构建。
  • 解决什么:对齐作者声明意图——为不希望引入 APNG4Android / SDWebImage 的业务提供可拷贝的零依赖参考实现。
  • 方案 & 合理性:落在宿主扩展层(androidApp/iosApp 的 demo 适配器目录与 render-expand 目录),符合「各 core-render-* 只依赖平台原生 API、demo 只依赖 core」的边界;iOS 侧不做 +load 自动注册以避免与已有 KRAPNGViewHandler+load 顺序不确定,由业务显式注册,合理。适用边界(仅全帧、解析失败降级静态图)在代码注释与文档中均有标注,清晰。

需要关注的问题

  1. Android 侧 repeatCount=0 的语义与 Kuikly 契约不一致:契约约定 0=无限循环,当前实现 0 时回退到文件 num_plays,当文件 num_plays!=0 时会变成有限播放,与 iOS 侧(0=无限)和现有 KRAPNGViewAdapter 行为都不同。详见 inline。
  2. Android 与 iOS 对「单帧跨多个 IDAT/fdAT」的处理不一致:iOS 会累积后 flush,Android 只取第一个 chunk,会得到残缺帧。两端同为参考实现,建议对齐。详见 inline。
  3. iOS 侧每轮播完都会回调 apngImageView:playEndLoop:,会触发框架 animationEnd 事件;而现有 handler 只在停止时回调一次。无限循环场景下会持续触发「播放结束」事件,与事件语义不符。详见 inline。

文档同步

docs/API/components/apng.md 新增「零第三方依赖参考实现」一节,注册方式与适用边界与代码一致,链接指向正确文件,无影响 ✅。

Comment thread iosApp/iosApp/KuiklyRenderExpand/Handlers/KRAPNGViewHandlerLite.m Outdated

@KuiklyAI KuiklyAI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 KuiklyAI Code Review

这一笔提交(style: translate comments to English)只把注释从中译英,没有动任何逻辑代码——逐行核对过,所有改动行的 +/- 只是行内注释文案替换,解析、调度、播放计数逻辑均无变化。

因此上一轮提的三点仍然 open:

  1. Android repeatCount=0 回退到文件 num_plays 的语义,与 DSL(0=无限)、现有 KRAPNGViewAdapter、iOS Lite 三方都不一致;
  2. Android 解析端首帧遇到第一个 IDAT 就 flush 并置 pendingFirstFrame=false,单帧拆成多个 IDAT 的文件会丢数据(iOS 侧用 pendingIdat 累积是对的,建议两端对齐);
  3. iOS p_advanceFromIndex 每轮播完都回调 playEndLoop:,而 KRAPNGView.m 里这个回调直接触发 animationEnd 事件——无限循环场景每轮都会误触发一次「动画结束」,建议只在真正播完或 stopAPNGAnimating 时回调。

这轮没有新增 inline 问题。

…late multi-chunk frame data, fire playEnd only on true completion or explicit stop
@lingzeanlan

Copy link
Copy Markdown
Author

感谢 review,三点已全部修改(commit 547396f):

  1. Android repeatCount 语义:已对齐 DSL 契约与现有适配器——0 即无限循环,不再回退文件 num_playsnumPlays 字段已移除)。
  2. Android 多 IDAT/fdAT 累积:解析改为与 iOS 侧一致的「pending 累积 + fcTL/IEND 时统一 flush」,单帧数据拆多个 chunk 的文件现在两端解析结果一致。
  3. iOS playEndLoop 回调时机:不再每轮回调——只在真正播完(playedLoops >= maxLoops)或显式 stopAPNGAnimating(且此前处于播放中)时回调一次,与 KRAPNGViewHandler 行为对齐。

@KuiklyAI KuiklyAI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 KuiklyAI Code Review

增量盘点(commit 547396f

上一轮提的三条都改到位了,已逐条回复「已解决」:

  1. Android repeatCount=0 语义 — 移除了 numPlays 字段和 acTL 解析分支,0 不再回退到文件 num_plays,注释与停止条件都改为 0 = loop forever (DSL contract),与 IAPNGView.setRepeatCount 契约(「默认 0 为无限次」)和现有 KRAPNGViewAdaptersetLoopLimit(0) = 无限)对齐。✅
  2. Android 多块帧数据丢失 — 用 pendingIdat: ByteArrayOutputStream 累积同一帧的多个连续 IDAT/fdATflushFrame() 在下一个 fcTLIEND 时统一 flush,与 iOS 侧 pendingIdat + flushFrame 写法一致,两端不会再对同一文件产生不同的解析结果。✅
  3. iOS 每轮回调 playEndLoop — 抽出 p_notifyPlayEnd,只在自然播完(playedLoops >= maxLoops)和显式 stopAPNGAnimatingwasAnimating 守卫,避免 dealloc 误触发)两种情况各回调一次,与 KRAPNGViewHandler.mstopAPNGAnimating 回调一次的语义一致,框架侧 animationEnd 不再被无限循环每轮触发。✅

本次新改动评估

通读了 HEAD~1..HEAD 的全部 hunk(Android adapter + iOS handler 各一处),未发现引入新的功能或兼容性风险,也没有遗留的对 numPlays/pendingFirstFrame 的悬空引用。代码可直接合入。

@lingzeanlan

Copy link
Copy Markdown
Author

三条 review 意见均已修复并 resolve(commit 547396f)。CLA 已签署,mergeable 状态正常。麻烦各位 code owner 抽空 review,感谢!@elixxli @iPel @luoyibu @ruifanyuan @zealotchen0 @zenipchen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants