Báo cáo kỹ thuật · AdMob
Đánh giá flow quảng cáo & rủi ro crash
Trace toàn bộ luồng ads first-run: Splash → Language → Intro → Welcome → Main. Soi ảnh hưởng match_rate, show_rate, hiệu năng và crash. Chưa động code — tài liệu này là spec để bắt tay sửa.
Luồng ads first-run
tracedBảng ưu tiên
13 findings| Ưu tiên | ID | Vấn đề | Loại | File |
|---|---|---|---|---|
| P0 | C1 | ad.show() không guard isFinishing/isDestroyed | Crash | Inter/Reward/AppOpenAdsManager |
| P0 | C2 | onAdFailedToShow quên onLoadingEnd → hard-lock | Bug | InterstitialAdsManager.kt:119 |
| P0 | M1 | Không có UMP / consent (GDPR) | match_rate | toàn app |
| P1 | S1 | Inter timeout 25s bỏ fill trễ, không cache | show_rate | InterstitialAdsManager.kt |
| P1 | P2 | getAdConfig() re-parse JSON mỗi call | perf | AdRemoteConfig.kt:59 |
| P2 | S2 | Preload native cho page user không tới | show_rate | Splash / Intro |
| P2 | P1 | Splash busy-wait Remote Config ≤9s | perf | SplashScreenActivity.kt:71 |
| P2 | P3 | offscreenPageLimit = size-1 | perf | IntroScreenActivity.kt:187 |
| P2 | P4 | Init audio-hw trên main thread | perf/ANR | MainActivity.kt:669 |
| P2 | C3 | Không nav guard / click không debounce | Bug | Splash/Intro/Welcome |
| P2 | C4 | Splash không set isShowOpen=false | Bug | SplashScreenActivity |
| P2 | C5 | Native cache không evict | RAM | NativeAdsManager |
| P2 | C6 | AOA fetch trước khi config ready | Logic | MyApplication.kt:47 |
P0 — Fix ngay
Crash & Compliancead.show()CrashVấn đề
Load ad async (timeout ≤25s). User rời màn khi đang chờ → WeakReference<Activity> chưa null nhưng Activity đã chết → ad.show() ném BadTokenException / IllegalStateException.
Vị trí
InterstitialAdsManager.kt:129 RewardAdsManager.kt:145 AppOpenAdManager.kt:78
Repo đã có helper Core/utils/Utils.kt:27
// onAdLoaded val activity = activityRef.get() if (activity == null || activity.isFinishing || activity.isDestroyed) { onLoadingEnd?.invoke() onActionPerform.invoke() // vẫn nav, không show return } ad.show(activity)
onLoadingEnd khi show failHard-lockVấn đềLoadingInterAdsDialog là setCancelable(false). Inter load OK nhưng show fail (ad fullscreen khác đang hiện / activity state đổi) → callback fail quên tắt dialog → dialog modal kẹt vĩnh viễn → user phải kill app.
Vị trí
InterstitialAdsManager.kt:119-126
override fun onAdFailedToShowFullScreenContent(adError: AdError) { AdMobAdsManager.isShowingInterRewardAds = false onLoadingEnd?.invoke() // ← THÊM DÒNG NÀY onActionPerform.invoke() }
Vấn đề
Zero ref UserMessagingPlatform / ConsentInformation toàn repo. Request native + inter tại splash/language không có consent signal → fill rớt mạnh vùng EEA/UK (eCPM cao nhất). Rủi ro match_rate lớn nhất + rủi ro policy.
val params = ConsentRequestParameters.Builder().build() consentInfo = UserMessagingPlatform.getConsentInformation(this) consentInfo.requestConsentInfoUpdate(this, params, { UserMessagingPlatform.loadAndShowConsentFormIfRequired(this) { err -> if (consentInfo.canRequestAds()) initializeAdsAndPreload() } }, { /* error → fallback theo policy */ })
P1 — Tăng doanh thu
show_rate & perf rẻVấn đềloadAndShowAds fresh load mỗi lần; withTimeout(25_000) cancel coroutine — fill về sau 25s bị drop, không cache lại → mất luôn. show_rate loss trực tiếp.
Vị trí InterstitialAdsManager.kt:31 (timeout) · :147
Hướng fix
• Tách load() giữ InterstitialAd trong field + show() riêng.
• Preload INTER_SPLASH sớm ở splash; INTER_WELCOME ở Intro; preload next sau onAdDismissed.
• Giảm timeout 25s → 8-10s (đa số fill về <3s).
• Fill trễ vẫn nằm cache → dùng lần show sau, không rớt.
getAdConfig()perfVấn đềgetAdConfig() tạo new Gson() + parse full JSON mỗi call, main thread, không cache. bindNativeAd gọi ~5-6 lần/native. Cả intro build, mỗi menu click, buildTrainingListWithAds → jank lúc transition.
Vị trí AdRemoteConfig.kt:59-73 AdConfig.kt:24-32
@Volatile private var cachedConfig: AdConfig? = null fun getAdConfig(): AdConfig = cachedConfig ?: AdConfig.fromJson(remoteConfig.getString("ad_config")) .also { cachedConfig = it } // invalidate khi fetchAndActivate() success: cachedConfig = null
P2 — Cải thiện thêm
perf · RAM · logicBỏ preload up-front NATIVE_OB2/OB23_FULL nếu tỉ lệ thoát onboarding cao. Mở rộng pattern preload-lăn-dần đã có (IntroScreenActivity.kt:65-82). Đo show_rate từng placement trên AdMob console; placement preload nhiều mà show thấp → giảm preload.
SplashScreenActivity.kt:71-83 loop 30×300ms = ≤9s. Offline = treo 9s. Fix: bake default ad_config (enabled:true) dùng ngay, fetch config async không chặn nav, hard cap splash ~3s.
offscreenPageLimitperfIntroScreenActivity.kt:187: size-1 tạo hết mọi slide + native fragment cùng lúc → jank + RAM. Về default (1); native chỉ load khi page gần tới.
MainActivity.kt:669-683: MediaPlayer.setDataSource + Equalizer init audio-hw trên main thread trong initData (sync trong onCreate). Đẩy sang IO / lazy khi tab Whistle được chọn.
Splash navigateNextScreen(), Intro startActivity(), Welcome tvGetStarted: thêm cờ hasNavigated + đổi sang setOnSafeClick (repo có SafeOnClickListener / ViewExtension.kt:76). Chặn double-tap launch 2 Activity.
Set AdMobAdsManager.isShowOpen = false khi vào splash, = true khi rời. Tránh AOA fullscreen đè splash khi background→resume trong lúc chờ config.
NativeAdsView.onDetachedFromWindow gọi release() cho intro fragment + Main training native. Hiện chỉ Language + TrainingItemAdapter release → cache phình dần suốt process.
MyApplication.kt:47: fetchAd() chạy trong MobileAds.initialize callback trước khi Remote Config activate → getAdConfig() trả empty(enabled=false) → AOA không preload lần đầu. Move vào callback sau khi AdRemoteConfig activate xong.