Kural kullanım alanları ve örnekleri

Aşağıdaki örnekler, optimizasyon için R8'i kullandığınız ancak saklama kurallarını oluşturmak için gelişmiş rehberliğe ihtiyaç duyduğunuz yaygın senaryolara dayanmaktadır.

Yansıma

Genel olarak, optimum performans için yansıtma kullanılması önerilmez. Ancak bazı durumlarda bu kaçınılmaz olabilir. Aşağıdaki örneklerde, yansıtmanın kullanıldığı yaygın senaryolarda saklama kurallarıyla ilgili rehberlik sağlanmaktadır.

Adına göre yüklenen sınıflarla yansıtma

Kitaplıklar, sınıf adını String olarak kullanarak sınıfları genellikle dinamik olarak yükler. Ancak R8, bu şekilde yüklenen sınıfları algılayamaz ve kullanılmadığını düşündüğü sınıfları kaldırabilir.

Örneğin, bir kitaplığınızın ve bu kitaplığı kullanan bir uygulamanızın olduğu aşağıdaki senaryoyu ele alalım. Kod, bir uygulama tarafından uygulanan StartupTask arayüzünü örnekleyen bir kitaplık yükleyiciyi gösterir.

Kitaplık kodu aşağıdaki gibidir:

// The interface for a task that runs once.
interface StartupTask {
    fun run()
}

// The library object that loads and executes the task.
object TaskRunner {
    fun execute(className: String) {
        // R8 won't retain classes specified by this string value at runtime
        val taskClass = Class.forName(className)
        val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
        task.run()
    }
}

Kitaplığı kullanan uygulamada aşağıdaki kod bulunur:

// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
    override fun run() {
        // This log will never appear if the class is removed by R8.
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is told to run the app's task by its name.
    TaskRunner.execute("com.example.app.PreCacheTask")
}

Bu senaryoda, kitaplığınızda aşağıdaki saklama kurallarını içeren bir tüketici saklama kuralları dosyası bulunmalıdır:

-keep class * implements com.example.library.StartupTask {
    <init>();
}

Bu kural olmadan R8, uygulamanın sınıfı doğrudan kullanmaması nedeniyle PreCacheTask sınıfını uygulamadan kaldırarak entegrasyonu bozar. Kural, kitaplığınızın StartupTask arayüzünü uygulayan sınıfları bulur ve bunları, bağımsız değişken içermeyen oluşturucularıyla birlikte korur. Böylece kitaplığın PreCacheTask öğesini başarıyla oluşturup yürütmesine olanak tanır.

::class.java ile düşünme egzersizi

Kitaplıklar, sınıfları doğrudan Class nesnesini uygulamaya ileterek yükleyebilir. Bu, sınıfları ada göre yüklemekten daha sağlam bir yöntemdir. Bu, R8'in algılayabileceği sınıf için güçlü bir referans oluşturur. Ancak bu, R8'in sınıfı kaldırmasını engellese de sınıfın yansıtıcı olarak örneklendiğini belirtmek ve oluşturucu gibi yansıtıcı olarak erişilen üyeleri korumak için yine de bir keep kuralı kullanmanız gerekir.

Örneğin, bir kitaplığınızın ve bu kitaplığı kullanan bir uygulamanızın olduğu aşağıdaki senaryoyu düşünün: Kitaplık yükleyici, sınıf referansını doğrudan ileterek bir StartupTask arayüzünü başlatır.

Kitaplık kodu aşağıdaki gibidir:

// The interface for a task that runs once.
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

Kitaplığı kullanan uygulamada aşağıdaki kod bulunur:

// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}

Bu senaryoda, kitaplığınızda aşağıdaki saklama kurallarını içeren bir tüketici saklama kuralları dosyası bulunmalıdır:

# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
    <init>();
}

Bu kurallar, bu tür yansıtmayla mükemmel şekilde çalışacak şekilde tasarlanmıştır. Bu sayede, kodun doğru şekilde çalıştığından emin olurken maksimum optimizasyon sağlanır. Kurallar, uygulama hiçbir zaman kullanmıyorsa R8'in sınıf adını karartmasına ve StartupTask sınıfının uygulamasını küçültmesine veya kaldırmasına olanak tanır. Ancak, örnekte kullanılan PrecacheTask gibi uygulamalarda, kitaplığınızın çağırması gereken varsayılan oluşturucuyu (<init>()) korurlar.

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: Bu kural, StartupTask arayüzünüzü uygulayan tüm sınıfları hedefler.
    • -keep class * implements com.example.library.StartupTask: Bu, arayüzünüzü uygulayan tüm sınıfları (*) korur.
    • ,allowobfuscation: Bu, R8'e sınıfı korumasına rağmen yeniden adlandırabileceği veya karartabileceği talimatını verir. Kitaplığınız sınıfın adına bağlı olmadığından ve Class nesnesini doğrudan aldığından bu işlem güvenlidir.
    • ,allowshrinking: Bu değiştirici, R8'e sınıf kullanılmıyorsa kaldırılabileceğini bildirir. Bu, R8'in StartupTask öğesinin TaskRunner.execute() öğesine hiçbir zaman aktarılmayan bir uygulamasını güvenli bir şekilde silmesine yardımcı olur. Kısacası bu kural, bir uygulama StartupTask uygulayan bir sınıf kullanıyorsa R8'in bu sınıfı koruyacağı anlamına gelir. R8, sınıfın boyutunu küçültmek için sınıfı yeniden adlandırabilir ve uygulama kullanmıyorsa silebilir.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: Bu kural, ilk kuralda tanımlanan sınıfların belirli üyelerini (bu örnekte oluşturucu) hedefler.
    • -keepclassmembers class * implements com.example.library.StartupTask: Bu, StartupTask arayüzünü uygulayan sınıfın belirli üyelerini (yöntemler, alanlar) korur ancak yalnızca uygulanan sınıfın kendisi korunuyorsa.
    • { <init>(); }: Bu, üye seçicidir. <init>, Java bayt kodundaki bir oluşturucunun özel dahili adıdır. Bu bölüm, özellikle varsayılan, bağımsız değişken içermeyen oluşturucuyu hedefler.
    • Bu kural çok önemlidir. Çünkü kodunuz, varsayılan oluşturucuyu yansıtıcı olarak çağıran herhangi bir bağımsız değişken olmadan getDeclaredConstructor().newInstance() çağrısı yapıyor. Bu kural olmadan R8, new PreCacheTask() öğesini doğrudan çağıran bir kod olmadığını görür, oluşturucunun kullanılmadığını varsayar ve oluşturucuyu kaldırır. Bu durum, uygulamanızın çalışma zamanında InstantiationException ile kilitlenmesine neden olur.

Yöntem ek açıklamasına dayalı yansıtma

Kitaplıklar genellikle geliştiricilerin yöntemleri veya alanları etiketlemek için kullandığı ek açıklamaları tanımlar. Kitaplık daha sonra çalışma zamanında bu açıklama eklenmiş üyeleri bulmak için yansıtmayı kullanır. Örneğin, @OnLifecycleEvent ek açıklaması, çalışma zamanında gerekli yöntemleri bulmak için kullanılır.

Örneğin, bir kitaplığınızın ve kitaplığı kullanan bir uygulamanızın olduğu aşağıdaki senaryoyu ele alalım. Bu örnekte, @OnEvent ile açıklama eklenmiş yöntemleri bulan ve çağıran bir etkinlik veri yolu gösterilmektedir.

Kitaplık kodu aşağıdaki gibidir:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent

class EventBus {
    fun dispatch(listener: Any) {
        // Find all methods annotated with @OnEvent and invoke them
        listener::class.java.declaredMethods.forEach { method ->
            if (method.isAnnotationPresent(OnEvent::class.java)) {
                try {
                    method.invoke(listener)
                } catch (e: Exception) { /* ... */ }
            }
        }
    }
}

Kitaplığı kullanan uygulamada aşağıdaki kod bulunur:

class MyEventListener {
    @OnEvent
    fun onSomethingHappened() {
        // This method will be removed by R8 without a keep rule
        Log.d(TAG, "Event received!")
    }
}

fun onCreate() {
    // Instantiate the listener and the event bus
    val listener = MyEventListener()
    val eventBus = EventBus()

    // Dispatch the listener to the event bus
    eventBus.dispatch(listener)
}

Kitaplıkta, ek açıklamalarını kullanan tüm yöntemleri otomatik olarak koruyan bir tüketici koruma kuralları dosyası bulunmalıdır:

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: Bu kural, çalışma zamanında okunması amaçlanan ek açıklamaları korur.
  • -keep @interface com.example.library.OnEvent: Bu kural, OnEvent açıklama sınıfının kendisini korur.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: Bu kural, yalnızca sınıf kullanılıyorsa ve bu üyeleri içeriyorsa sınıfı ve belirli üyeleri korur.
    • -keepclassmembers: Bu kural, yalnızca sınıf kullanılıyorsa ve bu üyeleri içeriyorsa sınıfı ve belirli üyeleri korur.
    • class *: Kural tüm sınıflar için geçerlidir.
    • @com.example.library.OnEvent <methods>;: Bu, @com.example.library.OnEvent ile açıklama eklenmiş bir veya daha fazla yöntemi (<methods>) olan tüm sınıfları ve açıklama eklenmiş yöntemlerin kendilerini korur.

Sınıf notlarına dayalı yansıtma

Kitaplıklar, belirli bir ek açıklamaya sahip sınıfları taramak için yansıtmayı kullanabilir. Bu durumda, görev çalıştırıcı sınıf, yansıtma kullanarak ReflectiveExecutor ile açıklama eklenmiş tüm sınıfları bulur ve execute yöntemini yürütür.

Örneğin, bir kitaplığınızın ve bu kitaplığı kullanan bir uygulamanızın olduğu aşağıdaki senaryoyu düşünün.

Kitaplıkta aşağıdaki kod bulunur:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor

class TaskRunner {
    fun process(task: Any) {
        val taskClass = task::class.java
        if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
            val methodToCall = taskClass.getMethod("execute")
            methodToCall.invoke(task)
        }
    }
}

Kitaplığı kullanan uygulamada aşağıdaki kod bulunur:

// In consumer app

@ReflectiveExecutor
class ImportantBackgroundTask {
    fun execute() {
        // This class will be removed by R8 without a keep rule
        Log.e("ImportantBackgroundTask", "Executing the important background task...")
    }
}

// Usage of ImportantBackgroundTask

fun onCreate(){
    val task = ImportantBackgroundTask()
    val runner = TaskRunner()
    runner.process(task)
}

Kitaplık, belirli sınıfları almak için yansıtma özelliğini yansıtıcı bir şekilde kullandığından kitaplık, aşağıdaki koruma kurallarını içeren bir tüketici koruma kuralları dosyası içermelidir:

# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations

# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor

# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
   public void execute();
}

Bu yapılandırma, R8'e tam olarak neyin korunacağını söylediği için son derece etkilidir.

İsteğe bağlı bağımlılıkları desteklemek için yansıtma

Yansıtmanın yaygın bir kullanım alanı, temel bir kitaplık ile isteğe bağlı bir eklenti kitaplığı arasında yumuşak bir bağımlılık oluşturmaktır. Temel kitaplık, eklentinin uygulamaya dahil olup olmadığını kontrol edebilir ve dahilse ek özellikleri etkinleştirebilir. Bu sayede, temel kitaplığın bu modüllere doğrudan bağımlı olmasını zorunlu kılmadan eklenti modüllerini gönderebilirsiniz.

Temel kitaplık, belirli bir sınıfı adına göre aramak için yansıtma (Class.forName) kullanır. Sınıf bulunursa özellik etkinleştirilir. Aksi takdirde, işlem sorunsuz bir şekilde başarısız olur.

Örneğin, video analizlerini etkinleştirmek için isteğe bağlı bir VideoEventTracker sınıfını kontrol eden bir temel AnalyticsManager sınıfının bulunduğu aşağıdaki kodu ele alalım.

Çekirdek kitaplıkta aşağıdaki kod bulunur:

object AnalyticsManager {
    private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"

    fun initialize() {
        try {
            // Attempt to load the optional module's class using reflection
            Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
            Log.d(TAG, "Video tracking enabled.")
        } catch (e: ClassNotFoundException) {
            Log.d(TAG,"Video tracking module not found. Skipping.")
        } catch (e: Exception) {
            Log.e(TAG, e.printStackTrace())
        }
    }
}

İsteğe bağlı video kitaplığında aşağıdaki kod bulunur:

package com.example.analytics.video

class VideoEventTracker {
    // This constructor must be kept for the reflection call to succeed.
    init { /* ... */ }
}

İsteğe bağlı kitaplığın geliştiricisi, gerekli tüketici saklama kuralını sağlamaktan sorumludur. Bu koruma kuralı, isteğe bağlı kitaplığı kullanan tüm uygulamaların, temel kitaplığın bulması gereken kodu korumasını sağlar.

# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

Bu kural olmadan R8, bu modüldeki hiçbir şey doğrudan kullanmadığı için VideoEventTracker öğesini isteğe bağlı kitaplıktan kaldırır. Keep kuralı, sınıfı ve oluşturucusunu korur. Böylece temel kitaplık, sınıfı başarıyla örnekleyebilir.

Özel üyelere erişmek için yansıtma

Bir kitaplığın herkese açık API'sinin parçası olmayan özel veya korumalı koda erişmek için yansıtma kullanmak önemli sorunlara yol açabilir. Bu tür kodlar, bildirimde bulunulmaksızın değiştirilebilir. Bu durum, uygulamanızda beklenmedik davranışlara veya kilitlenmelere yol açabilir.

Herkese açık olmayan API'ler için yansıtmayı kullandığınızda aşağıdaki sorunlarla karşılaşabilirsiniz:

  • Engellenen güncellemeler: Özel veya korumalı koddaki değişiklikler, daha yüksek kitaplık sürümlerine güncelleme yapmanızı engelleyebilir.
  • Kaçırılan avantajlar: Yeni işlevleri, önemli kilitlenme düzeltmelerini veya temel güvenlik güncellemelerini kaçırabilirsiniz.

R8 optimizasyonları ve yansıtma

Bir kitaplığın özel veya korumalı koduna yansıtmanız gerekiyorsa R8'in optimizasyonlarına çok dikkat edin. Bu üyelere doğrudan referans verilmiyorsa R8, bunların kullanılmadığını varsayabilir ve ardından bunları kaldırabilir veya yeniden adlandırabilir. Bu durum, çalışma zamanı çökmelerine ve genellikle NoSuchMethodException veya NoSuchFieldException gibi yanıltıcı hata mesajlarına yol açabilir.

Örneğin, bir kitaplık sınıfından özel bir alana nasıl erişebileceğinizi gösteren aşağıdaki senaryoyu inceleyin.

Sahibi olmadığınız bir kitaplıkta şu kod var:

class LibraryClass {
    private val secretMessage = "R8 will remove me"
}

Uygulamanızda aşağıdaki kod var:

fun accessSecretMessage(instance: LibraryClass) {
    // Use Java reflection from Kotlin to access the private field
    val secretField = instance::class.java.getDeclaredField("secretMessage")
    secretField.isAccessible = true
    // This will crash at runtime with R8 enabled
    val message = secretField.get(instance) as String
}

R8'in özel alanı kaldırmasını önlemek için uygulamanıza -keep kuralı ekleyin:

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: Bu, yalnızca sınıfın kendisi saklandığında sınıfın belirli üyelerini saklar.
  • class com.example.LibraryClass: Bu, alanı içeren tam sınıfı hedefler.
  • private java.lang.String secretMessage;: Bu, belirli özel alanı adıyla ve türüyle tanımlar.

Java Native Interface (JNI)

R8'in optimizasyonları, yerel (C/C++ kodu) öğelerden Java veya Kotlin'e yapılan yukarı çağrılarla çalışırken sorunlara neden olabilir. Ters durum da geçerlidir. Java veya Kotlin'den yerel koda yapılan aşağı yönlü çağrılarda sorunlar olabilir. Ancak aşağı yönlü çağrıların çalışmaya devam etmesi için varsayılan dosya proguard-android-optimize.txt aşağıdaki kuralı içerir. Bu kural, yerel yöntemlerin kırpılmasını önler.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

Java Native Interface (JNI) aracılığıyla yerel kodla etkileşim

Uygulamanız, yerel (C/C++) koddan Java veya Kotlin'e upcall yapmak için JNI kullandığında R8, yerel kodunuzdan hangi yöntemlerin çağrıldığını göremez. Uygulamanızda bu yöntemlere doğrudan referans verilmiyorsa R8, bu yöntemlerin kullanılmadığını yanlışlıkla varsayar ve bunları kaldırarak uygulamanızın kilitlenmesine neden olur.

Aşağıdaki örnekte, yerel bir kitaplıktan çağrılması amaçlanan bir yönteme sahip Kotlin sınıfı gösterilmektedir. Yerel kitaplık, bir uygulama türünü başlatır ve yerel koddan Kotlin koduna veri aktarır.

package com.example.models

// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
    /**
     *   This method is called from the native side.
     *   R8 will remove it if it's not kept.
     */
    fun onNativeEvent(data: NativeData) {
        Log.d(TAG, "Received event from native code: $data")
    }
    // Use 'external' to declare a native method
    external fun startNativeProcess()

    companion object {
        init {
            // Load the native library
            System.loadLibrary("my-native-lib")
        }
    }
}

Bu durumda, uygulama türünün optimize edilmesini önlemek için R8'i bilgilendirmeniz gerekir. Ayrıca, yerel koddan çağrılan yöntemler imzalarında parametre veya dönüş türü olarak kendi sınıflarınızı kullanıyorsa bu sınıfların yeniden adlandırılmadığını da doğrulamanız gerekir.

Uygulamanıza aşağıdaki saklama kurallarını ekleyin:

-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
    public void onNativeEvent(com.example.model.NativeData);
}

-keep class NativeData{
        <init>(java.lang.Integer, java.lang.String);
}

Bu koruma kuralları, R8'in onNativeEvent yöntemini ve en önemlisi parametre türünü kaldırmasını veya yeniden adlandırmasını engeller.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: Bu, bir sınıfın belirli üyelerini yalnızca sınıf önce Kotlin veya Java kodunda oluşturulmuşsa korur. R8'e uygulamanın sınıfı kullandığını ve sınıfın belirli üyelerini koruması gerektiğini bildirir.
    • -keepclassmembers: Bu, yalnızca sınıf önce Kotlin veya Java kodunda oluşturulmuşsa sınıfın belirli üyelerini korur. R8'e uygulamanın sınıfı kullandığını ve sınıfın belirli üyelerini koruması gerektiğini bildirir.
    • class com.example.JniBridge: Bu, alanı içeren tam sınıfı hedefler.
    • includedescriptorclasses: Bu değiştirici, yöntemin imzasında veya tanımlayıcısında bulunan tüm sınıfları da korur. Bu durumda, parametre olarak kullanılan com.example.models.NativeData sınıfının R8 tarafından yeniden adlandırılması veya kaldırılması engellenir. NativeData, a.a olarak yeniden adlandırılırsa yöntem imzası artık yerel kodun beklediğiyle eşleşmez ve kilitlenmeye neden olur.
    • public void onNativeEvent(com.example.models.NativeData);: Bu, korunacak yöntemin tam Java imzasını belirtir.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: includedescriptorclasses, NativeData sınıfının kendisi korunurken NativeData içindeki ve doğrudan yerel JNI kodunuzdan erişilen tüm üyeler (alanlar veya yöntemler) için kendi keep kuralları gerekir.
    • -keep class NativeData: Bu, NativeData adlı sınıfı hedefler ve blok, NativeData sınıfında hangi üyelerin tutulacağını belirtir.
    • <init>(java.lang.Integer, java.lang.String): Bu, oluşturucunun imzasıdır. Bu işlev, iki parametre alan oluşturucuyu benzersiz şekilde tanımlar: Birincisi Integer, ikincisi ise String.

Dolaylı platform çağrıları

Parcelable uygulamasıyla veri aktarma

Android çerçevesi, Parcelablenesnelerinizin örneklerini oluşturmak için yansıtma kullanır. Modern Kotlin geliştirmede, kotlin-parcelize eklentisini kullanmanız gerekir. Bu eklenti, CREATOR alanı ve çerçevenin ihtiyaç duyduğu yöntemler de dahil olmak üzere gerekli Parcelable uygulamasını otomatik olarak oluşturur.

Örneğin, kotlin-parcelize eklentisinin Parcelable sınıfı oluşturmak için kullanıldığı aşağıdaki örneği inceleyin:

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
    val name: String,
    val age: Int
) : Parcelable

Bu senaryoda önerilen bir saklama kuralı yoktur. kotlin-parcelize Gradle eklentisi, @Parcelize ile ek açıklama eklediğiniz sınıflar için gerekli tutma kurallarını otomatik olarak oluşturur. Bu kitaplık, oluşturulan CREATOR ve oluşturucuların Android çerçevesinin yansıtma çağrıları için korunmasını sağlayarak karmaşık işlemleri sizin yerinize yönetir.

@Parcelize kullanmadan Kotlin'de Parcelable sınıfını manuel olarak yazarsanız CREATOR alanını ve Parcel kabul eden oluşturucuyu korumak sizin sorumluluğunuzdadır. Bunu yapmayı unutursanız sistem nesnenizi seri durumdan çıkarma işlemi yapmaya çalıştığında uygulamanız kilitlenir. @Parcelize kullanmak standart ve daha güvenli bir uygulamadır.

kotlin-parcelize eklentisini kullanırken aşağıdakilere dikkat edin:

  • Eklenti, derleme sırasında otomatik olarak CREATOR alanları oluşturur.
  • proguard-android-optimize.txt dosyası, bu alanların düzgün işlevsellik için korunması amacıyla gerekli keep kuralları içerir.
  • Uygulama geliştiriciler, özellikle özel uygulamalar veya üçüncü taraf bağımlılıkları için gerekli tüm keep kuralların mevcut olduğunu doğrulamalıdır.