Mobil uygulama boyutu optimizasyonu, markete ilk yükleme anından itibaren dönüşüm (conversion) oranlarını doğrudan etkileyen, ancak geliştirme süreçlerinde genellikle en son aşamaya bırakılan teknik bir borçtur. Birçok projede “çalışsın da boyutu sonra hallederiz” mantığıyla hareket edildiğini, ardından yüzlerce megabayta ulaşmış hantal paketlerle karşılaşıldığını bizzat gördüm. Kendi geliştirdiğim Android spam engelleme uygulamasında ilk derlemede onlarca megabayt olan APK boyutunu, kurduğum optimize edilmiş üretim hattıyla küçük bir kesrine düşürmeyi başardım. Bu süreçte karşılaştığım “build” patlamalarını ve uyguladığım pratik yöntemleri bu rehberde adım adım paylaşıyorum.
Google Play Store verilerine göre, 100 MB üzerindeki her 6 MB’lık artış, yükleme dönüşüm oranını yaklaşık %1 oranında düşürüyor. Kullanıcıların hücresel veri limitleri ve cihazlarındaki depolama baskısı düşünüldüğünde, paket boyutunu minimize etmek teknik bir lüks değil, doğrudan iş başarısını etkileyen bir zorunluluktur. Bu yazıda, modern mobil platformlarda paketlerin anatomisini çıkararak gereksiz yüklerden nasıl kurtulacağımızı inceleyeceğiz.
APK ve IPA Anatomisi: Dosya Boyutunu Şişiren Unsurlar
Bir optimizasyon işlemine başlamadan önce, düşmanımızı tanımamız gerekir. Derlenmiş bir Android (APK) veya iOS (IPA) paketi, aslında özel olarak sıkıştırılmış birer ZIP arşivinden ibarettir. Bu arşivi açtığınızda karşınıza çıkan dosyaların her biri, uygulamanın çalışması için gereken farklı bileşenleri temsil eder.
Aşağıdaki tabloda, optimize edilmemiş standart bir hibrit/native mobil uygulamanın paket içeriğinin tipik dağılımını ve hedeflememiz gereken optimize edilmiş oranları görebilirsiniz:
| Bileşen Adı | Görevi | Optimize Edilmemiş Oran | Hedeflenen Oran | Uygulanacak Çözüm |
|---|---|---|---|---|
classes.dex / Mach-O |
Derlenmiş kaynak kodlar | %25 - %35 | %10 - %15 | ProGuard, R8, Tree Shaking |
res / Assets |
Görseller, ikonlar, fontlar | %40 - %50 | %15 - %20 | WebP dönüşümü, SVG, Lottie |
lib (Native .so files) |
C/C++ kütüphaneleri, NDK | %20 - %30 | %5 - %10 | ABI Split, App Bundle (AAB) |
resources.arsc |
XML kaynak haritası | %5 - %10 | %2 - %3 | String ve ID optimizasyonu |
Gördüğünüz gibi, paket boyutunun neredeyse yarısını görsel kaynaklar (assets) ve native kütüphaneler oluşturuyor. Eğer uygulamanızda harici bir C++ kütüphanesi (örneğin SQLCipher veya özel bir görüntü işleme motoru) kullanıyorsanız, derleyicinin her işlemci mimarisi için (arm64-v8a, armeabi-v7a, x86_64) ayrı bir .so dosyası ürettiğini fark etmişsinizdir. Bu mimarilerin tamamını tek bir pakete koyup göndermek, kullanıcının cihazına hiç kullanmayacağı işlemci kodlarını indirtmek anlamına gelir.
Daha önce ele aldığım Docker imaj boyutu azaltma yöntemleri yazısında olduğu gibi, mobil tarafta da katmanları analiz etmek ve gereksiz bağımlılıkları ayıklamak benzer bir felsefeye dayanıyor. Şimdi bu bileşenleri tek tek nasıl eriteceğimizi pratik kod örnekleriyle ele alalım.
R8 ve ProGuard ile Kod Seviyesinde Daraltma (Shrinking)
Android ekosisteminde kod daraltma, kullanılmayan sınıfların, metotların ve niteliklerin tespit edilip silinmesi işlemidir. Google, eski ProGuard yerine artık varsayılan olarak R8 derleyicisini kullanıyor. R8 sadece kullanılmayan kodları silmekle kalmaz, aynı zamanda sınıfları optimize eder, isimlerini kısaltır (obfuscation) ve kodun tersine mühendislikle okunmasını zorlaştırır.
R8’i aktif etmek için android/app/build.gradle dosyanızda aşağıdaki yapılandırmayı uygulamamız gerekiyor. Bu ayarı açmak, classes.dex boyutunda genellikle kayda değer bir düşüş sağlar:
android {
compileSdkVersion 34
buildTypes {
release {
// Kod daraltmayı aktif et
minifyEnabled true
// Kullanılmayan kaynakları (res) temizle
shrinkResources true
// R8 kurallarını içeren dosyaları tanımla
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Ancak minifyEnabled true yapmak, çoğu zaman uygulamanın ilk derlemede patlamasına yol açar. Bunun sebebi, uygulamanızda veya kullandığınız üçüncü parti kütüphanelerde bulunan “Reflection” (yansıma) mekanizmalarıdır. Örneğin, bir JSON verisini Java/Kotlin nesnesine dönüştüren Gson veya Moshi gibi kütüphaneler, çalışma zamanında (runtime) sınıf isimlerini ararlar. R8 bu sınıfların isimlerini a, b, c şeklinde kısalttığı için kütüphane sınıfı bulamaz ve NullPointerException fırlatır.
Bu durumu çözmek için proguard-rules.pro dosyasına ilgili kütüphanelerin sınıflarını koruyacak kuralları (Keep Rules) eklemeliyiz. İşte production ortamında kullandığım kararlı bir temel kural seti:
# Gson kütüphanesi için reflection koruması
-keepattributes Signature, *Annotation*, EnclosingMethod, InnerClasses
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Kendi veri modellerinizi (Data Transfer Objects) koruyun
-keep class com.mustafaerbay.app.models.** { *; }
# Log satırlarını production build'inden tamamen temizleyin
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
}
Asset Yönetimi: WebP Dönüşümü ve Vektörel Çözümler
Mobil uygulama boyutunu şişiren en büyük günahkar her zaman görsellerdir. Tasarımcıların Figma’dan export ettiği 3x PNG dosyaları doğrudan projeye eklendiğinde, sadece bir açılış ekranı (splash screen) bile pakete 5-10 MB ek yük getirebilir.
İlk kuralımız net: Asla PNG veya JPEG formatında statik görsel kullanmayın. Tüm raster görselleri modern WebP formatına dönüştürmeliyiz. WebP, PNG’ye kıyasla kaliteden ödün vermeden ortalama %30-50 oranında daha iyi sıkıştırma sağlar. Terminal üzerinden tüm proje klasöründeki PNG dosyalarını otomatik olarak WebP’ye dönüştürmek için yazdığım şu shell betiğini CI/CD süreçlerinizde kullanabilirsiniz:
#!/bin/bash
# png_to_webp.sh
# Proje içindeki tüm PNG dosyalarını tarar ve %85 kalitede WebP'ye dönüştürür.
find ./app/src/main/res/ -name "*.png" | while read -r png_file; do
webp_file="${png_file%.png}.webp"
# cwebp aracını kullanarak dönüşümü yap
cwebp -q 85 "$png_file" -o "$webp_file"
if [ -f "$webp_file" ]; then
echo "Dönüştürüldü: $png_file -> $webp_file"
rm "$png_file" # Orijinal PNG dosyasını sil
fi
done
İkinci adım ise ikonlar için kesinlikle VectorDrawable (Android) veya PDF/SVG (iOS) kullanmaktır. Vektörel dosyalar sadece birkaç kilobayt boyutundadır ve her ekran çözünürlüğünde piksellenmeden kusursuz görünürler. Eğer uygulamanızda karmaşık animasyonlar varsa, bunları video veya GIF olarak gömmek yerine Lottie (JSON tabanlı animasyon kütüphanesi) kullanarak vektörel tabanlı animasyonlara dönüştürün. Bu sayede megabaytlarca yer kaplayan bir tanıtım videosunu, boyutu çok daha küçük bir JSON dosyasıyla ikame edebilirsiniz.
Native Kütüphaneler ve ABI Split Yapılandırması
Modern mobil uygulamalar sıklıkla native C/C++ kütüphanelerine ihtiyaç duyar. Örneğin, SQLite veritabanı şifrelemesi, imaj işleme kütüphaneleri veya performans gerektiren kriptografik işlemler native derlenmiş .so (Shared Object) dosyaları gerektirir.
Varsayılan yapılandırmada, derleyici tüm hedef mimariler için bu binary dosyalarını tek bir APK içine gömer (Universal APK). Ancak bir kullanıcının cihazı ya arm64-v8a mimarisidir ya da nadiren armeabi-v7a’dır; ikisine birden asla sahip olamaz. Bu gereksiz yükü önlemek için build.gradle dosyasında ndk.abiFilters yapılandırmasını kurmalı veya markete yükleme yaparken Android App Bundle (AAB) formatını tercih etmeliyiz.
Eğer uygulamanızı doğrudan APK olarak dağıtmanız gereken bir senaryo varsa (örneğin kurumsal içi dağıtım veya alternatif uygulama mağazaları), Gradle üzerinde “ABI Split” özelliğini aktif ederek her mimari için ayrı APK üretebilirsiniz:
android {
splits {
// ABI seviyesinde split yapılandırması
abi {
enable true
reset()
include "armeabi-v7a", "arm64-v8a"
universalApk false // Tüm mimarileri içeren tek bir universal APK üretme
}
}
}
Bu yapılandırma derleme süresini biraz uzatır çünkü Gradle arka arkaya birden fazla APK derler. Ancak günün sonunda kullanıcıya inecek olan APK boyutu neredeyse yarı yarıya düşer. Google Play Store için derleme alıyorsanız, ABI split yerine doğrudan .aab formatında çıktı almanız yeterlidir; Google Play, kullanıcının cihaz mimarisine göre dinamik olarak en uygun APK’yı üretip servis eder.
Dynamic Feature Delivery ile İsteğe Bağlı Yükleme
Uygulamanızın her özelliği, her kullanıcı için her an gerekli olmayabilir. Örneğin, bir e-ticaret uygulamasında “Fatura Görüntüleme/PDF Oluşturma” modülü veya “Müşteri Destek Canlı Sohbet” ekranı sadece kullanıcı ihtiyaç duyduğunda yüklenmelidir. Bu mimariye Dynamic Feature Delivery (Dinamik Özellik Dağıtımı) diyoruz.
Bu yaklaşım, ana uygulama paketini (base module) son derece hafif tutmamızı sağlar. Ek özellikler, kullanıcı uygulama içinde ilgili butona tıkladığında arka planda Play Core API aracılığıyla indirilir. Bir üretim ERP’sinin mobil istemcisini geliştirirken, detaylı grafiksel raporlama modülünü dinamik modül haline getirdik. Bu sayede ilk indirme boyutunu belirgin ölçüde düşürerek sahadaki operatörlerin hücresel veri üzerinden uygulamayı hızlıca güncellemesini sağladık.
Dinamik bir modül oluşturmak için projenize yeni bir “Dynamic Feature Module” eklemeli ve ana modülün build.gradle dosyasında bunu belirtmelisiniz:
// app/build.gradle
android {
...
dynamicFeatures = [':features:reporting_chart']
}
Dinamik modülün kendi build.gradle dosyasında ise teslimat tipini (delivery type) belirlemeniz gerekir:
// features/reporting_chart/build.gradle
plugins {
id 'com.android.dynamic-feature'
}
dist {
moduleName = "reporting_chart"
onDemand {
active true // Kullanıcı talep ettiğinde indirilecek
}
fusing {
include true // Dynamic delivery desteklemeyen eski cihazlar için ana pakete dahil et
}
}
Çalışma zamanında bu modülü yüklemek için Kotlin tarafında SplitInstallManager API’sini kullanırız. Bu kod bloğu, modülün indirilme durumunu dinler ve indirme tamamlandığında kullanıcıyı ilgili ekrana yönlendirir:
val splitInstallManager = SplitInstallManagerFactory.create(context)
val request = SplitInstallRequest.newBuilder()
.addModule("reporting_chart")
.build()
splitInstallManager.startInstall(request)
.addOnSuccessListener { sessionId ->
// Modül başarıyla indirildi, aktiviteyi başlatabilirsiniz
}
.addOnFailureListener { exception ->
// İndirme hatası yönetimi
}
CI/CD Pipeline Üzerinde Boyut Regresyonunu Engelleme
Boyut optimizasyonu tek seferlik bir işlem değildir. Ekibe katılan yeni bir yazılımcının projeye eklediği kontrolsüz bir kütüphane veya optimize edilmemiş bir görsel, tüm çabalarınızı bir gecede çöpe atabilir. Bu yüzden boyut takibini otomatikleştirmek ve CI/CD pipeline’ına entegre etmek hayati önem taşır.
GitHub Actions üzerinde her Pull Request açıldığında derlenen paket boyutunu ölçen ve belirlediğiniz limiti aşarsa derlemeyi hata vererek durduran (fail build) basit ama etkili bir workflow hazırladım. Bu adımı kendi projelerimde mutlaka kullanıyorum:
name: Mobile App Size Check
on:
pull_request:
branches: [ main, develop ]
jobs:
size-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build Release Bundle (AAB)
run: ./gradlew :app:bundleRelease
- name: Check Bundle Size
run: |
# Üretilen AAB dosyasının yolunu tanımla
BUNDLE_PATH="app/build/outputs/bundle/release/app-release.aab"
# Dosya boyutunu byte cinsinden al
SIZE_BYTES=$(stat -c%s "$BUNDLE_PATH")
SIZE_MB=$(echo "scale=2; $SIZE_BYTES / 1048576" | bc)
# Maksimum limit: 20 MB (20971520 Byte)
MAX_SIZE_BYTES=20971520
echo "Üretilen Paket Boyutu: $SIZE_MB MB ($SIZE_BYTES bytes)"
if [ $SIZE_BYTES -gt $MAX_SIZE_BYTES ]; then
echo "HATA: Paket boyutu limit sınırını (20 MB) aştı!"
exit 1
fi
echo "Boyut kontrolü başarılı. Limit dahilinde."
Bu kontrol mekanizması sayesinde, kod inceleme (code review) aşamasına gelmeden önce boyutu şişiren değişiklikleri otomatik olarak engellemiş olursunuz. Benzer şekilde, iOS projeleri için Fastlane entegrasyonu ile .ipa boyutunu ölçüp Slack kanalına bildirim atan otomasyonlar da kurabilirsiniz.
Sonuç: Karar Matrisi ve Süreç Takibi
Mobil uygulama boyutu optimizasyonu yaparken her zaman bir trade-off (ödünleşim) dengesi vardır. Kodları çok fazla obfuscate etmek hata loglarının (crash reports) okunmasını zorlaştırır (deobfuscation mapping dosyalarını her build için saklamanız gerekir). Dynamic Delivery kullanmak kod tabanının mimari karmaşıklığını artırır.
Benim bu konudaki net pozisyonum şudur: İlk aşamada R8/ProGuard ve WebP/Vektör dönüşümlerini tavizsiz uygulayın. Bunlar uygulamanın mimarisini bozmadan en yüksek kazancı (%40’a varan küçülme) sağlayan “düşük asılı meyvelerdir” (low-hanging fruits). Eğer hala limitlerin üzerindeyseniz, Dynamic Feature Delivery gibi daha ileri düzey ve mimari değişiklik gerektiren silahlara sarılın.
Sonraki adım olarak, uygulamanızdaki mevcut paket boyutunu analiz etmek için Android Studio içindeki Analyze APK aracını açın ve hangi klasörün en çok yer kapladığını görerek işe başlayın.