Guideline 2.5.4: Apple từ chối chế độ chạy nền
Bởi Mehdi Azizi
Đăng ngày , 9 phút
Apple từ chối chế độ chạy nền được khai báo nhưng không có tính năng nào dùng. Hãy xóa key hoặc cho reviewer thấy tính năng đó.
Guideline 2.5.4 giới hạn các chế độ chạy nền (background mode) trong đúng mục đích của chúng: âm thanh, vị trí, cuộc gọi VoIP, hoàn tất tác vụ, thông báo cục bộ. Bạn bị từ chối khi Info.plist khai báo một trong các chế độ này mà reviewer không tìm thấy tính năng nào cần đến nó. Nếu không có gì dùng đến key, hãy xóa nó. Nếu có, hãy chỉ cho reviewer thấy tính năng đó trong ba mươi giây.
Mình tự phát hành các app iOS của mình bằng Expo. Solys, app báo thức kèm mini-game của mình, có khai báo chế độ audio: mình đã xóa nó, rồi thêm lại vì một lý do khác. Hướng dẫn này dựa trên văn bản của Apple, các lần bị từ chối mà những dev khác đã chia sẻ công khai và các dự án của chính mình. Để có cái nhìn tổng quan về các lý do bị từ chối, hãy đọc vì sao Apple từ chối app.
Guideline 2.5.4 nói gì?
Quy định chỉ có một câu, nằm trong mục 2 "Performance", phần 2.5 "Software Requirements" của App Review Guidelines: "Multitasking apps may only use background services for their intended purposes: VoIP, audio playback, location, task completion, local notifications, etc."
Nói đơn giản: một dịch vụ chạy nền chỉ được dùng cho đúng việc mà nó sinh ra để làm. Vì thế mới có tiêu đề "Guideline 2.5.4 - Performance - Software Requirements".
Mặc định, app chuyển xuống nền sẽ bị tạm dừng. Muốn tiếp tục chạy, app phải khai báo một hoặc nhiều chế độ trong key UIBackgroundModes của Info.plist. Tài liệu của Apple về các chế độ chạy nền yêu cầu dùng chúng một cách tiết kiệm, vì pin, và ưu tiên giải pháp khác nếu có. Mỗi chế độ được khai báo là một lời hứa mà reviewer có thể kiểm tra.
Các chế độ chạy nền và mục đích của chúng
| Chế độ | Key | Mục đích |
|---|---|---|
| Audio, AirPlay, PiP | audio | Âm thanh nghe được ở nền |
| Vị trí | location | Vị trí liên tục |
| Voice over IP | voip | Cuộc gọi với CallKit |
| Background fetch | fetch | Nội dung mạng định kỳ |
| Background processing | processing | Tác vụ được lên lịch |
| Thông báo từ xa | remote-notification | Push nội dung mới |
Bảng này tóm tắt trang của Apple. Trong các lần bị từ chối được chia sẻ trên forum, audio, location và voip xuất hiện nhiều nhất.
Thông báo từ chối 2.5.4 có nghĩa là gì?
Thông báo sẽ nêu tên chế độ có vấn đề. Hai ví dụ do các dev đăng trên forum của Apple:
- Audio: "Your app still declares support for audio in the UIBackgroundModes key in your Info.plist but did not include features that require persistent audio." Phần tiếp theo nói rõ key
audiodành cho các app phát nội dung nghe được ở nền, như trình phát nhạc. Apple đưa ra hai lối thoát: thêm nội dung đó hoặc xóa key (thread trên forum). - Vị trí: trong một thread tháng 12/2024, Apple không tìm thấy tính năng nào ngoài việc theo dõi nhân viên cần đến vị trí thường trực, và viết rằng riêng mục đích này là không phù hợp.
Theo mình hiểu: reviewer không đọc code của bạn, họ tìm một tính năng. Họ phải có thể khởi động nó, chuyển app xuống nền và thấy nó vẫn tiếp tục chạy.
Trường hợp ngược lại cũng có. Trong một thread tháng 5/2026, một app đọc văn bản thành giọng nói, thực sự dùng chế độ audio, đã bị từ chối nhiều lần: reviewer không làm sao phát được âm thanh. Một dev khác viết trong đó rằng theo anh ấy, một video đính kèm đã gỡ được trường hợp của mình. Đó là lời kể, không phải quy định của Apple.
Key từ đâu ra khi bạn không hề thêm nó?
Với Expo, chính các config plugin điền vào Info.plist lúc prebuild. Vì vậy một chế độ chạy nền có thể lọt vào binary mà bạn không hề quyết định.
Plugin expo-audio với cấu hình mặc định
Tài liệu của expo-audio ghi rõ: tùy chọn enableBackgroundPlayback mặc định là true và trên iOS sẽ thêm chế độ audio. Chỉ cần khai báo plugin ở dạng ngắn, "expo-audio", là đủ để khai báo chế độ này.
Đó là điều mình phát hiện khi rà lại TradaZen trước lần nộp đầu tiên: chỉ có vài âm thanh giao diện ngắn ở foreground, nhưng plugin lại được khai báo ở dạng ngắn. Cách sửa nằm gọn trong app.json:
{
"expo": {
"plugins": [["expo-audio", { "enableBackgroundPlayback": false }]]
}
}
Các package khác thêm chế độ theo cách riêng. Tùy chọn isIosBackgroundLocationEnabled của expo-location chỉ thêm location nếu bạn bật nó. Còn expo-background-task thì thêm processing lúc prebuild ngay khi được cài.
Thư mục ios/ vẫn giữ key cũ
Cái bẫy thứ hai, mình gặp với Solys vào tháng 7/2026. Mình đã xóa audio khỏi app.json, nhưng key vẫn còn trong ios/Solys/Info.plist, được tạo từ trước. Một lần prebuild không có --clean sẽ chỉnh sửa project này thay vì tạo lại, và plugin chỉ thêm key chứ không bao giờ xóa. Một bản build local sẽ mang theo key đó. Một bản build EAS trên cloud thì bắt đầu từ một lần prebuild mới khi ios/ bị git bỏ qua, và mình rơi vào trường hợp này.
Trước mỗi lần build, mình kiểm tra những gì thực sự sẽ được gửi đi:
npx expo config --type introspect --json | grep -o '"UIBackgroundModes":\[[^]]*\]'
npx expo prebuild --clean --platform ios
Lệnh đầu liệt kê các chế độ mà prebuild sẽ ghi vào Info.plist. Lệnh thứ hai tạo lại project native cho bản build local, bằng cách xóa thư mục ios/. Với Solys, lệnh đầu hiển thị "UIBackgroundModes":["audio"], với TradaZen thì không có gì: cả hai đều đúng ý mình.
Thêm hay xóa một chế độ đều làm thay đổi Info.plist, tức là thay đổi binary: bạn cần một bản build mới, cập nhật từ xa là không đủ. Tài liệu của EAS Update xếp những thay đổi này vào loại cần binary mới, giống như với ITMS-90683.
Solys: khai báo chế độ audio, với một lý do thật
Solys là app báo thức, nhưng chế độ audio không dùng để đổ chuông: chuông báo chạy qua AlarmKit, dịch vụ báo thức của Apple có từ iOS 26 (tài liệu AlarmKit). Chính hệ thống đổ chuông, kể cả ở chế độ im lặng, chứ không phải app.
Lịch sử của key này, vì cách lập luận quan trọng hơn dòng code:
- Cuối tháng 7/2026, mình xóa nó. Không có gì phát âm thanh ở nền: hiệu ứng âm thanh của mini-game chỉ chạy ở foreground, và chuông báo không phụ thuộc vào app.
- Hai ngày sau, mình thêm lại, cùng với tính năng nghe giấc ngủ. Tùy chọn này ghi lại tiếng động ban đêm bằng micro và phân tích ngay trên iPhone. Việc ghi âm phải tiếp tục khi màn hình khóa, và tài liệu của expo-audio cho biết tùy chọn ghi âm ở nền cũng thêm chế độ
audio.
Bản chất của lý do đã thay đổi: chế độ này dùng để ghi âm, không phải để phát. Ba lớp bảo vệ, nhìn thấy được trong app:
- tính năng nghe là tùy chọn tắt mặc định, bật từ phần cài đặt hoặc từ thói quen buổi tối;
- nó có màn hình xin đồng ý riêng, như quy định 2.5.14 yêu cầu với mọi việc ghi âm, và không có gì rời khỏi điện thoại;
- micro tắt mười phút trước giờ báo thức, để việc nghe và chuông báo không bao giờ cản trở nhau.
Mình không thể đảm bảo reviewer lúc nào cũng chấp nhận khai báo này. Nhưng với từng yếu tố, mình nói được nó dùng để làm gì và xem nó ở đâu.
Những app báo thức giữ app luôn thức
Trước AlarmKit, app báo thức bên thứ ba không thể đổ chuông như báo thức của Apple. Vì vậy một số thư viện giữ cho app luôn thức: tài liệu của package Flutter alarm2 mô tả một trình phát âm thanh im lặng chạy cho đến giờ báo thức. Mà một âm thanh câm thì chẳng phát ra gì để nghe. Từ iOS 26, AlarmKit khiến đường vòng này không còn cần thiết.
Giữ hay xóa chế độ: bảng quyết định
| Tình huống của bạn | Nên làm gì |
|---|---|
| Âm thanh ngắn ở foreground | Xóa audio |
| Nhạc hoặc sách nói | Giữ, và cho thấy nó |
| Ghi âm dài | Giữ + xin đồng ý |
| Báo thức hoặc hẹn giờ | AlarmKit (iOS 26) |
| Thỉnh thoảng cần vị trí | Significant changes |
| Đồng bộ không gấp | fetch hoặc processing |
| Gọi thoại hoặc video | voip với CallKit |
Về vị trí, trang của Apple nêu dịch vụ significant-change location như một giải pháp thay cho chế độ location.
Các khoản khác của 2.5: 2.5.1 và 2.5.2
2.5.1: API công khai và framework dùng đúng chỗ
"Apps may only use public APIs and must run on the currently shipping OS." App chỉ dùng API công khai, chạy trên phiên bản hệ điều hành hiện tại và bỏ những gì đã deprecated. Giống như 2.5.4, mỗi framework dùng cho đúng mục đích của nó, có nêu trong phần mô tả: HealthKit cho sức khỏe, HomeKit cho nhà thông minh. Nếu một thư viện gọi API private, chính app của bạn sẽ lãnh lần từ chối.
2.5.2: không tải code làm thay đổi app
App phải tự đủ trong gói của nó. App không được "download, install, or execute code which introduces or changes features or functionality of the app". Chỉ các app giáo dục dạy lập trình có ngoại lệ giới hạn, với code hoàn toàn hiển thị và người dùng có thể chỉnh sửa.
Với Expo, vấn đề này liên quan đến EAS Update, mà tài liệu nhắc rằng các bản cập nhật phải tuân thủ quy định của App Store. Hãy dùng cập nhật từ xa để sửa lỗi, không bao giờ để đưa ra một tính năng mà reviewer chưa thấy. Một bản sửa liên quan đến việc duyệt phải đi qua bản build mới, như mình giải thích trong hướng dẫn về guideline 2.1.
Trả lời lần từ chối 2.5.4 thế nào?
Cuộc trao đổi diễn ra trong mục App Review của app, nơi bạn trả lời tin nhắn của App Review. Hai trường hợp:
- Chế độ không dùng vào việc gì: xóa nó, build lại, kiểm tra introspection, rồi trả lời bằng hai câu.
- Chế độ phục vụ một tính năng thật: trả lời kèm đường đi chính xác và một video ngắn, rồi đưa lại đoạn này vào ghi chú cho reviewer ở mỗi phiên bản.
Mẫu mình sẽ dùng, hãy điều chỉnh (bỏ câu về báo thức nếu app của bạn không có):
Hello,
The "audio" background mode is used only by [feature], an
optional feature that [records / plays] audio while the
device is locked. It is off by default.
How to test:
1. Open [screen] and turn on [feature].
2. Accept the microphone permission if asked.
3. Tap [button], then lock the device.
[What the reviewer will hear or see.]
Our alarm does not use this mode: it is scheduled with
AlarmKit and rings through the system.
Attached: a short screen recording of these steps.
Best regards,
Checklist 2.5.4 trước khi nộp
- Introspection của Expo chỉ hiển thị những chế độ bạn muốn, và bạn gọi tên được tính năng của từng chế độ.
- Không plugin nào được khai báo ở dạng ngắn nếu nó thêm chế độ theo mặc định, như expo-audio.
- Thư mục
ios/local không giữ key cũ nào, hoặc bạn build từ một lần prebuild sạch. - Tính năng khởi động nhanh, không cần cài đặt ẩn, và ghi chú cho reviewer nói rõ chế độ nào dùng cho việc gì và cách kiểm tra.
- Mọi việc ghi âm đều xin đồng ý rõ ràng và hiển thị một chỉ báo nhìn thấy được. Quy định về việc xin quyền có trong hướng dẫn về guideline 5.1.1.
- Với app báo thức, chuông đi qua AlarmKit, không phải âm thanh phát từ app.
Kiểm tra hồ sơ trước khi gửi
Review Kit gồm checklist trước khi gửi của mình, các mẫu ghi chú cho reviewer và các skill Claude Code rà soát dự án của bạn. Nếu cần một người xem giúp, đặt một buổi một giờ với mình: chúng ta cùng đọc thông báo của Apple và Info.plist của bạn qua video call. Toàn bộ quy trình có trên trang app bị Apple từ chối.
Bạn đang chuẩn bị phát hành lần đầu? Làm bài chẩn đoán miễn phí: nó sắp xếp lại các bước theo đúng thứ tự và gửi cho bạn checklist chống bị Apple từ chối của mình.
Không ai đảm bảo được Apple sẽ duyệt app của bạn. Nhưng một chế độ chạy nền có lý do rõ ràng thì bảo vệ được trong ba mươi giây.
Câu hỏi thường gặp
"Guideline 2.5.4 - Performance - Software Requirements" nghĩa là gì?
Đó là tiêu đề lần từ chối khi Apple cho rằng một chế độ chạy nền trong Info.plist của bạn không được dùng đúng mục đích. Thông báo nêu tên chế độ và đề nghị bạn cho thấy tính năng dùng nó hoặc xóa key.
Có cần build mới sau lần từ chối 2.5.4 không?
Nếu bạn xóa hoặc sửa một chế độ thì có: key nằm trong Info.plist, tức là trong binary. Nếu chế độ có lý do chính đáng, một câu trả lời kèm đường đi chính xác và video có thể là đủ.
Có thể giữ chế độ audio cho vài âm thanh giao diện không?
Không. Các âm thanh ngắn phát ở foreground không cần đến nó. Với Expo, hãy đặt enableBackgroundPlayback thành false trong plugin expo-audio.
App báo thức có cần chế độ audio không?
Không cần để đổ chuông, nếu app dựa vào AlarmKit, có từ iOS 26: chính hệ thống phát chuông báo. Trên Solys, chế độ audio dùng cho tính năng nghe giấc ngủ, ghi âm khi màn hình khóa.
Sao Apple nêu 2.5.4 trong khi mình không khai báo gì?
Có thể một plugin đã làm thay bạn: expo-audio thêm chế độ audio theo mặc định, và một thư mục ios/ cũ có thể giữ key đã bị xóa khỏi cấu hình. Hãy kiểm tra bằng npx expo config --type introspect.

