Skip to content

fix(android): content-based (CRC32) resource matching for AAB installs + openRawResource density fix#577

Merged
sunnylqm merged 2 commits into
masterfrom
fix/aab-resource-crc-match
Jun 9, 2026
Merged

fix(android): content-based (CRC32) resource matching for AAB installs + openRawResource density fix#577
sunnylqm merged 2 commits into
masterfrom
fix/aab-resource-crc-match

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Problem

热更新「上传 APK 基线 + 设备运行 AAB(Google Play 拆分包)」组合下,WebP 图片加载不出来(其它三种组合 OK)。

PATCH_FROM_APK 路径下,未变更资源是按 __diff.jsoncopies 里记录的路径从设备包里复制出来的(资源复制全在 BundledResourceCopier,native 侧只打 JS bundle 补丁)。但当基线是 APK、设备装的是 AAB 拆分包时,res/ drawable 路径在设备上被资源路径压缩(res/drawable-xhdpi-v4/x.webpres/xY.webp),记录的全名路径不存在 → 精确/归一化匹配落空 → resolveBundledResource 对这些 webp 兜不住 → 静默 continue → 图片缺失。assets/ 路径稳定,故只有图片受影响。

Change

1. CRC32 内容兜底层(BundledResourceCopier)
扫描 base + split APK 时顺带建 crc32 -> 条目名 索引;当 from 路径按精确/归一化都找不到时,用新清单字段 copiesCrc 提供的内容校验和按内容定位文件。CRC32 算的是解压后内容,跨 APK/AAB 打包稳定。该层排在 resolveBundledResource 之前(内容匹配比资源 ID 反查更可靠)。

2. openRawResource 密度修复
resolveBundledResource 已用 getValueForDensity 解析出正确密度的 TypedValue,但 openResolvedResourceStream 之前用单参 openRawResource(id)——会忽略请求的密度、退回设备当前密度,可能复制错变体。改为双参 openRawResource(id, typedValue)

兼容性

旧 CLI 生成的清单没有 copiesCrc → CRC 层自动空转,退回当前路径匹配行为,无回归。新增形参全部可选/向后兼容。

依赖

需配合 CLI 下发 copiesCrc:reactnativecn/react-native-update-cli#54

验证

  • 端到端回归 case 4(APK 基线 + AAB 设备):打开 UpdateContext.DEBUG,logcat 不再出现 Skipped N missing bundled resources,WebP 正常显示;回归 case 1/2/3。
  • 多 dpi 设备验证命中资源复制出的是请求密度的字节。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • More reliable update application when bundled resources differ between installations by matching resources using checksums.
    • Improved fallback selection so the correct asset variant is chosen even if the expected archive entry is missing.
    • Reduced failed or incorrect resource copies during APK-based patching, increasing update success rates.

A from-package (PATCH_FROM_APK) hot update copies unchanged resources out
of the on-device package using the path recorded in __diff.json `copies`.
When the baseline uploaded to the server was an APK but the app is
installed from an AAB (Play split APKs), res/ drawable paths are shortened
on device, so the recorded path (e.g. res/drawable-xhdpi-v4/x.webp) does
not exist verbatim and images (webp) silently fall through and go missing.

Add a CRC32 content-match tier in BundledResourceCopier: build a
crc32 -> entry index while scanning the base + split APKs, and when a
`from` path is not found by exact/normalized path, locate the file by the
content checksum supplied via the new manifest `copiesCrc` map. CRC32 is
over the uncompressed content, so it is stable across APK/AAB packaging.
This tier runs before resolveBundledResource (content match is more
reliable than the resource-id heuristic).

Also fix openResolvedResourceStream: use openRawResource(id, typedValue)
with the density-resolved TypedValue instead of openRawResource(id), which
ignored the requested density and could copy the wrong variant.

Backward/forward compatible: manifests without `copiesCrc` (older CLI)
simply skip the CRC tier and fall back to today's path-based behavior.

Requires CLI support: reactnativecn/react-native-update-cli#54

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47a898f3-a567-49de-9d10-4109ed9f2380

📥 Commits

Reviewing files that changed from the base of the PR and between e901325 and f6849fe.

📒 Files selected for processing (1)
  • android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java

📝 Walkthrough

Walkthrough

Threads CRC32 values from the patch manifest into APK patching and extends BundledResourceCopier.copyFromResource to build a CRC32→ZipEntry index and use CRC matches (with a recorded matchedZipFile) to resolve and extract bundled resources when path lookup fails. Adds comments to openResolvedResourceStream.

Changes

CRC-based resource resolution

Layer / File(s) Summary
Manifest CRC collection and threading
android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
Adds PatchArchiveContents.copyCrcs; appendManifestEntries reads copiesCrc into copyCrcs; passes contents.copyCrcs during archive extraction and calls bundledResourceCopier.copyFromResource(copyList, contents.copyCrcs).
CRC index and CRC-based entry resolution
android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java
Adds ZipSource holder and crcByFrom parameter; builds CRC32→ZipSource index while scanning ZIPs; during copying, uses CRC match to pick the correct ZipEntry and records matchedZipFile; extraction prefers matchedZipFile when CRC-resolved. Also adds comments to openResolvedResourceStream.

Sequence Diagram

sequenceDiagram
  participant DownloadTask
  participant PatchArchiveContents
  participant BundledResourceCopier
  participant SafeZipFile

  DownloadTask->>PatchArchiveContents: parse __diff.json -> populate copyCrcs
  DownloadTask->>BundledResourceCopier: copyFromResource(copyList, copyCrcs)
  BundledResourceCopier->>SafeZipFile: scan archives -> read ZipEntry CRCs
  BundledResourceCopier->>BundledResourceCopier: build crc32 -> ZipSource index
  BundledResourceCopier->>SafeZipFile: extract using matchedZipFile or entryToZipFileMap
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through zips with careful paws,
Matched bytes and paths without a pause.
CRC trails led me true,
Picked the right archive, stitched anew —
A nimble patch, no broken laws.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(android): content-based (CRC32) resource matching for AAB installs + openRawResource density fix' accurately summarizes the two main technical changes in this PR: CRC32-based resource matching for AAB compatibility and a density-aware openRawResource fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/aab-resource-crc-match

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java`:
- Around line 42-45: The CRC index currently records only entry names which can
mismatch when multiple APKs expose the same name; update the CRC map used in
copyFromResource to store the matched ZipEntry and its corresponding SafeZipFile
(or a small holder object) instead of just the entry name, populate that map
wherever crcToEntryName is set (the places noted around lines ~50, ~68, ~107,
~135) and change the lookup paths in copyFromResource to retrieve and use the
stored ZipEntry+SafeZipFile directly when performing the CRC fallback unzip/copy
so you always read from the exact archive that matched the CRC.
- Around line 29-34: The code currently calls
resources.openRawResource(resourceId, typedValue) which re-resolves and
overwrites the density-resolved TypedValue; instead use the already-resolved
TypedValue to open the asset directly (e.g., use
resources.getAssets().openNonAsset(typedValue.assetCookie,
typedValue.string.toString()) or openNonAssetFd(...) as appropriate) so the
density-selected asset is preserved—replace calls to openRawResource(resourceId,
typedValue) in the BundledResourceCopier (look for usages in
ResolvedResourceSource and the methods that open streams/copy resources) with
direct asset opening using typedValue.assetCookie and typedValue.string
(guarding for null string and falling back if necessary), and apply the same
change to the other occurrences called out in the review.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b0257c3-dfc4-454b-95b8-b42e0fb87290

📥 Commits

Reviewing files that changed from the base of the PR and between edc40be and e901325.

📒 Files selected for processing (2)
  • android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java

Comment on lines +29 to +34
final TypedValue typedValue;

ResolvedResourceSource(int resourceId, String assetPath) {
ResolvedResourceSource(int resourceId, String assetPath, TypedValue typedValue) {
this.resourceId = resourceId;
this.assetPath = assetPath;
this.typedValue = typedValue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the referenced lines to ensure the final-state snippet matches
FILE="android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

nl -ba "$FILE" | sed -n '1,120p'
echo "----"
nl -ba "$FILE" | sed -n '120,220p'
echo "----"
nl -ba "$FILE" | sed -n '220,340p'

Repository: reactnativecn/react-native-update

Length of output: 121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find occurrences of openRawResource(resourceId, typedValue) to confirm other line ranges
rg -n "openRawResource\s*\(\s*source\.resourceId\s*,\s*source\.typedValue\s*\)" -S android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java
rg -n "openRawResource\s*\(" -S android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java

Repository: reactnativecn/react-native-update

Length of output: 361


🌐 Web query:

AOSP ResourcesImpl openRawResource(int id, TypedValue value) calls getValue(id, value, true) and overwrites TypedValue

💡 Result:

In AOSP, ResourcesImpl.openRawResource(int id, TypedValue value) does call getValue(id, value, true) to fill the provided TypedValue with the resolved raw asset information (including overwriting its fields). This happens because openRawResource directly passes the same TypedValue instance as the outValue parameter to getValue, which writes into that outValue. Specifically, ResourcesImpl.openRawResource calls: - getValue(id, value, true); and then uses value.assetCookie and value.string to open the asset stream [1]. And ResourcesImpl.getValue is implemented as: - void getValue(... TypedValue outValue, boolean resolveRefs) { boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);... } [1]. Because outValue is used as the target object for mAssets.getResourceValue(..., outValue,...) and openRawResource passes the same TypedValue object, the TypedValue passed to openRawResource is overwritten (filled/resolved) by getValue before being used [1].

Citations:


🌐 Web query:

ResourcesImpl openRawResource TypedValue assetCookie open stream from resolved asset cookie path openNonAsset usage

💡 Result:

In the Android framework, the assetCookie is an integer identifier used by the AssetManager to represent a specific APK or asset package where a resource originates [1][2]. When opening resources, this cookie allows the system to correctly locate and load the file from the appropriate package [2]. The interaction between ResourcesImpl, TypedValue, and AssetManager works as follows: 1. TypedValue and assetCookie: When you request a resource (such as via Resources.openRawResource), the system first resolves the resource ID into a TypedValue object [3][4]. If the resource is a file-based resource (like a layout, drawable, or raw file), the TypedValue is populated with its string path and the assetCookie, which indicates which specific APK/resource package contains the file [1]. 2. ResourcesImpl.openRawResource: The ResourcesImpl class uses the resolved TypedValue.assetCookie to call mAssets.openNonAsset or mAssets.openNonAssetFd [3][5]. The assetCookie serves as an index into the AssetManager's list of loaded ApkAssets (which start at index 1) [2]. 3. AssetManager.openNonAsset: This is a lower-level API that opens a non-asset file (i.e., any file within an APK that is not in the dedicated 'assets' folder) [6][7]. It takes the assetCookie and the file path to stream or mmap the resource data [2][7]. Because this is an internal/low-level API, it is generally not intended for direct use by applications [6][7]. In summary, the assetCookie is essential for internal resource resolution, ensuring that the system retrieves the correct file from the correct APK package when multiple packages are present [2]. Direct usage of openNonAsset is typically restricted and discouraged for standard application development, as developers should rely on the higher-level Resources API [2][6].

Citations:


openRawResource(resourceId, typedValue) re-resolves and overwrites the density-resolved TypedValue, so the density fix is a no-op.

AOSP’s ResourcesImpl.openRawResource(int, TypedValue) calls getValue(id, value, true), which writes into the same TypedValue instance using the current configuration before opening the stream. That overwrites the TypedValue previously populated by getValueForDensity(...), so the requested density variant isn’t preserved. (android.googlesource.com)

Use the already-resolved typedValue (e.g., assetCookie + path) to open the stream instead of passing it back through openRawResource(...).

<details>
<summary>Suggested direction</summary>

+import android.content.res.AssetManager;
@@
-            return context.getResources().openRawResource(source.resourceId, source.typedValue);
+            return context.getAssets().openNonAsset(
+                source.typedValue.assetCookie,
+                source.typedValue.string.toString(),
+                AssetManager.ACCESS_STREAMING
+            );

</details>

Also applies to: 274-275, 279-282

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java`
around lines 29 - 34, The code currently calls
resources.openRawResource(resourceId, typedValue) which re-resolves and
overwrites the density-resolved TypedValue; instead use the already-resolved
TypedValue to open the asset directly (e.g., use
resources.getAssets().openNonAsset(typedValue.assetCookie,
typedValue.string.toString()) or openNonAssetFd(...) as appropriate) so the
density-selected asset is preserved—replace calls to openRawResource(resourceId,
typedValue) in the BundledResourceCopier (look for usages in
ResolvedResourceSource and the methods that open streams/copy resources) with
direct asset opening using typedValue.assetCookie and typedValue.string
(guarding for null string and falling back if necessary), and apply the same
change to the other occurrences called out in the review.

…ct copy

Two follow-ups from review on the resource copier:

1. CRC index stored only the entry name, so resolving it back through
   availableEntries could yield a different-content entry when two APKs
   (base + split) expose the same name with different bytes. Store the
   matched ZipEntry together with its SafeZipFile (ZipSource) and copy from
   that archive directly.

2. openRawResource(id, typedValue) is a no-op for density: AOSP's
   ResourcesImpl re-runs getValue(id, value, true) and overwrites the passed
   TypedValue at the current configuration before opening the stream. Instead
   of going through openRawResource (or the hidden openNonAsset API), take the
   density-correct file path resolved by getValueForDensity and copy that
   exact entry from the already-open archives — public API, correct variant.
   openRawResource(id) remains only as a defensive fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sunnylqm

sunnylqm commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — both findings are valid and addressed in f6849fe:

1. CRC index stored only the entry name (could copy wrong bytes on cross-APK name collision).
The CRC index now stores the matched ZipEntry together with its SafeZipFile (ZipSource), and the fallback copies from that exact archive instead of re-resolving the name through availableEntries/entryToZipFileMap.

2. openRawResource(id, typedValue) is a no-op for density.
Confirmed against AOSP — ResourcesImpl.openRawResource(int, TypedValue) calls getValue(id, value, true) first and overwrites the passed TypedValue at the current configuration. Rather than use the hidden openNonAsset API, I take the density-correct path that getValueForDensity already resolved (assetPath) and copy that exact entry from the already-open archives — public API, correct variant, and it reuses the existing zip index. openRawResource(id) is kept only as a defensive fallback for the (practically unreachable) case where that path isn't a zip entry in any loaded APK.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@sunnylqm: Both fixes look excellent — thanks for the thorough investigation and clear explanations.

  1. CRC index (ZipSource binding): Storing the ZipEntry + SafeZipFile together eliminates the cross-APK name collision risk entirely — no re-resolution through availableEntries/entryToZipFileMap, the exact bytes are pinned at index-build time. Clean fix.

  2. Density-correct resource copy: The AOSP confirmation seals it — relying on openRawResource(int, TypedValue) for density selection was never going to work since the framework immediately overwrites the value. Using the assetPath already resolved by getValueForDensity to pull directly from the open zip archives is both more correct and more efficient, and the openRawResource(id) defensive fallback for non-zip paths is a sensible safety net.

Great work on the deep dive! 🐇✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@sunnylqm sunnylqm merged commit 767841c into master Jun 9, 2026
3 of 6 checks passed
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.

1 participant