İçeriğe geç
Tüm yazılar

2026'da CameraX: bir kamera oturumu sızdırmadan Preview ve ImageAnalysis bağlamak

Android'de CameraX'e pratik bir rehber: Preview ve ImageAnalysis'i lifecycle'a bağlamak, doğru backpressure stratejisi ve rotasyonun neden olduğu çökme.

MFKAPPS 4 dk okuma

Uygulamalarımdan ikisi bir şeye kamerayı doğrultup bir saniyeden kısa sürede yanıt bekliyor: Stocky bir barkod tararken, Subly bir fatura okurken. İkisinin de arkasındaki görüntü modelleri — ML Kit’in barkod tarayıcısı ve metin tanıyıcısı — başka yazılarda ele alınıyor. Bu yazı ikisinin de altındaki katmanla ilgili: CameraX, önizlemeyi gösteren ekran hayatta kaldığı sürece açık kalması gereken, tek bir kare bile fazla değil, kısım.

CameraX’i çalıştırmak kolay, ama sinsice yanlış yapmak da öyle. Demo kod, bir önizleme ve bir analiz cihazını birkaç satırda bağlar ve ilk çalıştırmada işe yarar. Hatalar daha sonra ortaya çıkar — rotasyonda, ekranlar arasında hızlı gidiş gelişte, uygulamayı tarama ortasında duraklatan bir telefonda. İşte gerçekten doğru olması gereken şeyler.

Bir activity’e değil, bir lifecycle’a bağlamak

CameraX’i ham Camera2 yerine kullanmanın nedeni API yüzeyi değil, bindToLifecycle. Kullanım durumlarınızla birlikte ona bir LifecycleOwner verin, CameraX o owner STARTED’a ulaştığında kamerayı başlatır ve ulaşmadığında söker — yanlış gidebilecek elle onPause/onResume kamera tesisatı yok:

val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

cameraProviderFuture.addListener({
    val cameraProvider = cameraProviderFuture.get()
    cameraProvider.unbindAll()

    val preview = Preview.Builder().build().also {
        it.surfaceProvider = previewView.surfaceProvider
    }

    val analysis = ImageAnalysis.Builder()
        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
        .build()
        .also { it.setAnalyzer(analysisExecutor, ::analyzeFrame) }

    cameraProvider.bindToLifecycle(
        lifecycleOwner,
        CameraSelector.DEFAULT_BACK_CAMERA,
        preview,
        analysis,
    )
}, ContextCompat.getMainExecutor(context))

Bağlamadan önceki bu unbindAll(), savunmacı bir kalıp değil — en yaygın CameraX çökmesinin çözümü. ProcessCameraProvider.getInstance() bir ListenableFuture döndürür, bu yüzden bağlama işlemi asenkron olarak, bir listener üzerinde gerçekleşir. Eski oturum temizce serbest bırakılmadan ekran yeniden oluşursa (rotasyon, bir yapılandırma değişikliği, zaten bağlı olan bir tarama ekranına geri navigasyon), aynı kamera için birbiriyle çekişen iki kullanım durumu kümesi elde edersiniz, ya da bir kullanım durumunun farklı bir lifecycle’a zaten bağlı olduğunu söyleyen bir IllegalStateException alırsınız. Önce unbindAll() çağırmak her bağlamayı idempotent yapar: onResume’dan kaç kez geçtiğinizin hiçbir önemi kalmaz.

Önizleme ve analiz, aynı karelerin iki tüketicisidir

Preview ve ImageAnalysis bir pipeline’ın aşamaları değildir — kameranın eşzamanlı olarak beslediği iki bağımsız kullanım durumudur. Önizleme, kullanıcıya bir PreviewView üzerinde canlı bir akış gösterir; analiz cihazı, üzerinde çıkarım çalıştırmak için kare başına kendi ImageProxy’sini alır. İkisini birlikte bağlamak, “canlı tarama katmanlı kamera”yı iki kamera yerine tek bir çağrı haline getiren şeydir. Ayrıca analiz cihazının hızının ekranda kullanıcının gördüğü şeyi etkilememesi anlamına gelir — yavaş bir model önizlemeyi takılmalı yapmaz, bu da bir tarayıcının ne kadar “duyarlı” hissettirdiği için kulağa geldiğinden daha çok önemlidir.

Backpressure stratejisi küçük bir ayar değildir

ImageAnalysis, daha yeni CameraX sürümlerinde varsayılan olarak STRATEGY_KEEP_ONLY_LATEST kullanır, ama açıkça ayarlamaya değer, çünkü alternatif — STRATEGY_BLOCK_PRODUCER — analiz cihazınız yetişemediğinde kareleri kuyruğa alır, ve bir barkod ya da OCR modeli saniyede 30 kareli bir kameraya nadiren yetişir. KEEP_ONLY_LATEST, analiz cihazı hâlâ meşgulken en yeni olan dışındaki her kareyi düşürür; bu tam olarak istediğiniz davranıştır: kullanıcı canlı bir önizleme görür ve modelin bakmakta serbest olduğu hangi kareyse ondan bir sonuç alır, pipeline’ın geride kalıp bir saniye önceki kareleri işlemesi yerine.

Her ImageProxy’yi kapatın, yoksa akış sessizce durur

Bu, testte fark edilmesi en zor hatadır çünkü çökmez — sadece durur. ImageAnalysis, KEEP_ONLY_LATEST altında bile, önceki ImageProxy kapatılana kadar analiz cihazınıza yeni bir kare teslim etmez. Null bir görüntüde erken bir return, finally’nizden önce fırlatılan bir exception, ya da asenkron çalışıp proxy’yi yalnızca callback’inde kapatan bir sorgu — bunlardan herhangi biri bir ImageProxy’yi sonsuza dek açık bırakabilir ve o kareden itibaren analiz cihazı sessizce susar. Hata yok, log yok, sadece taramayı bırakmış bir tarayıcı:

fun analyzeFrame(image: ImageProxy) {
    try {
        val media = image.image ?: return
        val input = InputImage.fromMediaImage(media, image.imageInfo.rotationDegrees)
        scanner.process(input).addOnSuccessListener { /* handle result */ }
    } finally {
        image.close()
    }
}

Kapatma işlemini mutlu yolun sonuna değil, bir finally’ye koyun. Tüm kurulumda atlaması kolay ve sonradan hata ayıklaması pahalı olan tek satır bu.

İzni bağlamadan önce kontrol edin, callback içinde değil

CameraX kamera izninizi sizin için istemez, ve izinsiz bağlarsanız zarifçe başarısız olmaz — bindToLifecycle, CAMERA henüz verilmemişse ekranı çökertecek bir SecurityException fırlatır. Çözüm sıralamadır: ContextCompat.checkSelfPermission’ı kontrol edin ve ActivityResultContracts.RequestPermission’ı ProcessCameraProvider listener’ı çalışmadan önce geçin, içinde değil. Yalnızca kullanıcı bir kez reddettikten sonra çalışan ya da asenkron provider future’ıyla yarışan bir izin kontrolü, yalnızca yeni bir kurulumda tekrarlanan bir çökme raporunun nasıl oluştuğudur.

Sonuç

CameraX, ham Camera2’ye karşı yerini tam olarak bir şekilde kazanır: lifecycle’ın sahibi olmasına gerçekten izin verdiğiniz sürece “kamera lifecycle yönetimi”ni çözülmüş bir problem haline getirir. Bir LifecycleOwner üzerinden bağlayın, rotasyon ve navigasyonun bir kullanım durumunu asla çift bağlayamaması için her bağlamadan önce unbindAll() çağırın, varsayılana güvenmek yerine backpressure stratejisini açıkça ayarlayın ve her ImageProxy’yi bir finally içinde kapatın. Bu dört şeyi doğru yapın, kamera sıkıcı hale gelir — bir kullanıcının telefonunu bir barkoda ya da faturaya her doğrultuşunda çalışması gereken bir özellik için, tam olarak olmasını istediğiniz şey budur.