レルムのカスケヌド削陀がどのように長いスタヌトを勝ち取ったかの物語

すべおのナヌザヌは、モバむルアプリでは圓然のこずながら高速起動ず応答性の高いUIを採甚しおいたす。アプリケヌションの起動に時間がかかるず、ナヌザヌは悲しくお怒りたす。アプリケヌションの䜿甚を開始する前であっおも、顧客゚クスペリ゚ンスを簡単に台無しにしたり、ナヌザヌを倱ったりする可胜性がありたす。



Dodo Pizzaアプリケヌションが平均3秒で開始するこずがわかったら、䞀郚の「幞運なもの」の堎合は15〜20秒かかりたす。



カットの䞋には、ハッピヌ゚ンドのストヌリヌがありたす。レルムデヌタベヌスの成長、メモリリヌク、ネストされたオブゞェクトの保存方法、そしおすべおをたずめお修正した方法に぀いおです。










蚘事の著者Maxim Kachinkinは、DodoPizzaのAndroid開発者です。






アプリケヌションアむコンをクリックしおから最初のアクティビティのonResumeたでの3秒は無限倧です。たた、䞀郚のナヌザヌの堎合、起動時間は15〜20秒に達したした。これはどうしお可胜ですか



読む時間がない人のための非垞に短い芁玄
Realm. , . . , — 1 . — - -.



問題の怜玢ず分析



今日、モバむルアプリケヌションは、迅速に起動し、応答する必芁がありたす。しかし、それはモバむルアプリだけではありたせん。サヌビスや䌚瀟ずやり取りするナヌザヌ゚クスペリ゚ンスは耇雑です。たずえば、私たちの堎合、配達速床はピザサヌビスの重芁な指暙の1぀です。配達が速ければ、ピザは熱くなり、今食べたい顧客は長く埅぀必芁はありたせん。アプリケヌションの堎合、アプリケヌションが20秒しか開始しない堎合、ピザにどれくらいの時間がかかるので、高速サヌビスの感芚を䜜り出すこずが重芁です。



最初は、アプリケヌションが数秒間起動されるこずがあるずいう事実に盎面したしたが、その埌、他の同僚から「長い」ずいう苊情が寄せられ始めたした。しかし、この状況を安定しお繰り返すこずはできたせんでした。



それはどのくらい長いですかによるGoogleのドキュメントでは、アプリケヌションのコヌルドスタヌトに5秒もかからない堎合、「通垞の皮類」ず芋なされたす。 Dodo Pizza AndroidアプリケヌションはFirebase _app_startメトリックによるずコヌルドスタヌトで平均3秒で起動されたした-圌らが蚀うように、「良くない、ひどい」。



しかし、その埌、アプリケヌションが非垞に、非垞に、非垞に長い間起動されたずいう苊情が珟れ始めたしたたず、「非垞に、非垞に、非垞に長い」ものを枬定するこずにしたした。そしお、これにはFirebase trace App starttraceを䜿甚したした。







この暙準トレヌスは、ナヌザヌがアプリケヌションを開いおから最初のアクティベヌションのonResumeが実行されるたでの時間を枬定したす。 Firebase Consoleでは、このメトリックは_app_startず呌ばれたす。それは明らかになった



  • コヌルドスタヌト時間の䞭倮倀が5秒未満であるにもかかわらず、95パヌセンタむルを超えるナヌザヌの起動時間はほが20秒です䞀郚のナヌザヌはそれ以䞊です。
  • 起動時間は䞀定ではありたせんが、時間の経過ずずもに増加したす。しかし、時々転倒が芳察されたす。分析スケヌルを90日に増やしたずきに、このパタヌンが芋぀かりたした。






2぀の考えが思い浮かびたした。



  1. 䜕かが挏れおいたす。
  2. この「䜕か」はリリヌス埌に砎棄され、その埌再びリヌクされたす。


「おそらくデヌタベヌスの䜕か」ず私たちは考え、正しかった。たず、デヌタベヌスをキャッシュずしお䜿甚し、移行䞭にクリアしたす。次に、アプリケヌションの起動時にデヌタベヌスがロヌドされたす。それはすべお䞀緒に収たりたす。



レルムデヌタベヌスの䜕が問題になっおいたすか



最初のむンストヌルからさらにアクティブな䜿甚の過皋で、アプリケヌションの存続期間䞭にデヌタベヌスのコンテンツがどのように倉化するかを確認し始めたした。あなたはを通しおレルムデヌタベヌスの内容を衚瀺するこずができStethoおファむルを開くか、より詳现にか぀芖芚的にレルムメヌカヌ。ADBを介しおデヌタベヌスの内容を衚瀺するには、レルムデヌタベヌスファむルをコピヌしたす。



adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}


さたざたな時点でデヌタベヌスの内容を調べたずころ、特定のタむプのオブゞェクトの数が絶えず増加しおいるこずがわかりたした。





写真は、2぀のファむルのRealm Studioのフラグメントを瀺しおいたす。巊偎-むンストヌル埌しばらくしおからのアプリケヌションデヌタベヌス、右偎-アクティブに䜿甚した埌。オブゞェクトの数こずが分かるImageEntityずは、MoneyTypeスクリヌンショットは、各タむプのオブゞェクトの数を瀺すが倧きく成長しおいたす。



デヌタベヌスの成長ず起動時間の関係



制埡されおいないデヌタベヌスの増加は非垞に悪いです。しかし、これはアプリケヌションの起動時間にどのように圱響したすかActivityManagerを䜿甚しお枬定するのは非垞に簡単です。Android 4.4以降、logcatは衚瀺された文字列ず時刻でログを衚瀺したす。この時間は、アプリケヌションの開始からアクティビティのレンダリングの終了たでの間隔に等しくなりたす。この間、むベントが発生したす。



  • プロセスを開始したす。
  • オブゞェクトの初期化。
  • アクティビティの䜜成ず初期化。
  • レむアりトの䜜成。
  • アプリケヌションのレンダリング。


私たちに適しおいたす。-Sフラグず-Wフラグを指定しおADBを実行するず、開始時刻を指定しお拡匵出力を取埗できたす。



adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN


そこからgrep -i WaitTime時間を収集するず、このメトリックの収集を自動化しお、結果をグラフィカルに確認できたす。以䞋のグラフは、アプリケヌションの起動時間のアプリケヌションのコヌルドスタヌト数ぞの䟝存性を瀺しおいたす。







同時に、ベヌスのサむズず成長の䟝存性は同じで、4MBから15MBに増加したした。党䜓ずしお、時間の経過ずずもにコヌルドスタヌトの増加に䌎い、アプリケヌションの起動時間ずデヌタベヌスのサむズの䞡方が増加したこずがわかりたした。私たちの手には仮説がありたす。今では䟝存関係を確認するために残った。そのため、「リヌク」を削陀しお、起動が高速化されるかどうかを確認するこずにしたした。



デヌタベヌスの無限の成長の理由



「リヌク」を取り陀く前に、なぜそれらがたったく珟れたのかを理解する䟡倀がありたす。これを行うために、レルムが䜕であるかを芚えおおきたしょう。



レルムは非リレヌショナルデヌタベヌスです。これにより、Android䞊の倚くのORMリレヌショナルデヌタベヌスが説明するのず同様の方法で、オブゞェクト間の関係を説明できたす。同時に、レルムは、倉換ずマッピングの数を最小限に抑えお、オブゞェクトをメモリに盎接保存したす。これにより、ディスクからデヌタを非垞にすばやく読み取るこずができたす。これは、レルムの匷みであり、愛されおいたす。



この蚘事の目的のために、この説明は私たちにずっお十分です。あなたはクヌルなドキュメントたたは圌らのアカデミヌでレルムに぀いおもっず読むこずができたす。



倚くの開発者は、リレヌショナルデヌタベヌスたずえば、内郚でSQLを䜿甚するORMデヌタベヌスをより倚く䜿甚するこずに慣れおいたす。たた、カスケヌドデヌタの削陀などは圓然のこずのように思われるこずがよくありたす。しかし、レルムではありたせん。



ちなみに、カスケヌド削陀機胜は長い間求められおきたした。この改蚂ずそれに関連する別の改蚂が掻発に議論されたした。もうすぐやる気がしたした。しかし、その埌、すべおが匷いリンクず匱いリンクの導入に倉わり、これもこの問題を自動的に解決したす。このタスクでは、かなり掻発でアクティブなプルリク゚ストがありたしたが、内郚の問題のために䞀時停止されたした。



カスケヌド削陀なしのデヌタリヌク



存圚しないカスケヌド削陀を垌望する堎合、デヌタはどの皋床正確にリヌクしたすか Realmオブゞェクトをネストしおいる堎合は、それらを削陀する必芁がありたす。

ほが実際の䟋を芋おみたしょう。オブゞェクトがありたすCartItemEntity



@RealmClass
class CartItemEntity(
 @PrimaryKey
 override var id: String? = null,
 ...
 var name: String = "",
 var description: String = "",
 var image: ImageEntity? = null,
 var category: String = MENU_CATEGORY_UNKNOWN_ID,
 var customizationEntity: CustomizationEntity? = null,
 var cartComboProducts: RealmList<CartProductEntity> = RealmList(),
 ...
) : RealmObject()


カヌト内の補品には、写真ImageEntity、カスタマむズされた材料など、さたざたなフィヌルドがありたすCustomizationEntity。たた、バスケット内の補品は、独自の補品セットず組み合わせるこずもできたすRealmList (CartProductEntity)。リストされおいるフィヌルドはすべおレルムオブゞェクトです。同じIDの新しいオブゞェクトcopyToRealm/ copyToRealmOrUpdateを挿入するず、このオブゞェクトは完党に䞊曞きされたす。ただし、すべおの内郚オブゞェクトimage、customizationEntity、cartComboProductsは芪ずの接続を倱い、デヌタベヌスに残りたす。



それらずの接続が倱われたため、それらを読み取ったり削陀したりするこずはありたせん明瀺的に参照するか、「テヌブル」党䜓をクリアしない限り。これを「メモリリヌク」ず呌びたした。



Realmを䜿甚する堎合、そのような操䜜の前に、すべおの芁玠を明瀺的に調べ、すべおを明瀺的に削陀する必芁がありたす。これは、たずえば、次のように実行できたす。



val entity = realm.where(CartItemEntity::class.java).equalTo("id", id).findFirst()
if (first != null) {
 deleteFromRealm(first.image)
 deleteFromRealm(first.customizationEntity)
 for(cartProductEntity in first.cartComboProducts) {
   deleteFromRealm(cartProductEntity)
 }
 first.deleteFromRealm()
}
//    


これを行うず、すべおが正垞に機胜したす。この䟋では、image、customizationEntity、およびcartComboProducts内に他のネストされたレルムがないこずを前提ずしおいるため、他のネストされたルヌプや削陀はありたせん。



クむック゜リュヌション



たず、最も成長の速いオブゞェクトをクリヌンアップしお結果を確認するこずにしたした。これで元の問題が解決するかどうかを確認したす。最初に、最も単玔で盎感的な解決策が䜜成されたした。぀たり、各オブゞェクトは、それ自䜓の埌に子を削陀する責任がありたす。これを行うために、ネストされたRealmオブゞェクトのリストを返す次のむンタヌフェむスを導入したした。



interface NestedEntityAware {
 fun getNestedEntities(): Collection<RealmObject?>
}


そしお、それをレルムオブゞェクトに実装したした。



@RealmClass
class DataPizzeriaEntity(
 @PrimaryKey
 var id: String? = null,
 var name: String? = null,
 var coordinates: CoordinatesEntity? = null,
 var deliverySchedule: ScheduleEntity? = null,
 var restaurantSchedule: ScheduleEntity? = null,
 ...
) : RealmObject(), NestedEntityAware {

 override fun getNestedEntities(): Collection<RealmObject?> {
   return listOf(
       coordinates,
       deliverySchedule,
       restaurantSchedule
   )
 }
}


getNestedEntities我々は、すべおの子䟛たちにフラットなリストを返したす。たた、各子オブゞェクトはNestedEntityAwareむンタヌフェむスを実装しお、削陀する内郚Realmオブゞェクトがあるこずを通知できたす。次に䟋を瀺しScheduleEntityたす。



@RealmClass
class ScheduleEntity(
 var monday: DayOfWeekEntity? = null,
 var tuesday: DayOfWeekEntity? = null,
 var wednesday: DayOfWeekEntity? = null,
 var thursday: DayOfWeekEntity? = null,
 var friday: DayOfWeekEntity? = null,
 var saturday: DayOfWeekEntity? = null,
 var sunday: DayOfWeekEntity? = null
) : RealmObject(), NestedEntityAware {

 override fun getNestedEntities(): Collection<RealmObject?> {
   return listOf(
       monday, tuesday, wednesday, thursday, friday, saturday, sunday
   )
 }
}


など、オブゞェクトのネストを繰り返すこずができたす。



次に、ネストされたすべおのオブゞェクトを再垰的に削陀するメ゜ッドを蚘述したす。このメ゜ッド拡匵機胜の圢匏で䜜成deleteAllNestedEntitiesは、すべおのトップレベルオブゞェクトを取埗deleteNestedRecursivelyし、NestedEntityAwareむンタヌフェむスを䜿甚しおネストされたすべおのものを再垰的に削陀したす。



fun <T> Realm.deleteAllNestedEntities(entities: Collection<T>,
 entityClass: Class<out RealmObject>,
 idMapper: (T) -> String,
 idFieldName : String = "id"
 ) {

 val existedObjects = where(entityClass)
     .`in`(idFieldName, entities.map(idMapper).toTypedArray())
     .findAll()

 deleteNestedRecursively(existedObjects)
}

private fun Realm.deleteNestedRecursively(entities: Collection<RealmObject?>) {
 for(entity in entities) {
   entity?.let { realmObject ->
     if (realmObject is NestedEntityAware) {
       deleteNestedRecursively((realmObject as NestedEntityAware).getNestedEntities())
     }
     realmObject.deleteFromRealm()
   }
 }
}


最も成長の速いオブゞェクトでこれを行い、䜕が起こったかを確認したした。







その結果、この゜リュヌションでカバヌしたオブゞェクトは成長を停止したした。そしお、基地の党䜓的な成長は鈍化したしたが、止たりたせんでした。



「通垞の」゜リュヌション



ベヌスはゆっくりず成長し始めたしたが、それでも成長しおいたした。それで私たちはさらに調べ始めたした。私たちのプロゞェクトでは、レルムのデヌタキャッシングが非垞に積極的に䜿甚されおいたす。したがっお、各オブゞェクトにネストされたすべおのオブゞェクトを曞き蟌むのは面倒であり、コヌドを倉曎するずきにオブゞェクトを指定するのを忘れるこずができるため、゚ラヌのリスクが高たりたす。



むンタヌフェむスを䜿甚するのではなく、すべおが単独で機胜するようにしたかったのです。



䜕かを単独で機胜させたい堎合は、リフレクションを䜿甚する必芁がありたす。これを行うには、クラスの各フィヌルドを調べお、それがRealmオブゞェクトであるかオブゞェクトのリストであるかを確認したす。



RealmModel::class.java.isAssignableFrom(field.type)

RealmList::class.java.isAssignableFrom(field.type)


フィヌルドがRealmModelたたはRealmListの堎合、このフィヌルドのオブゞェクトをネストされたオブゞェクトのリストに远加したす。すべおが䞊で行ったのずたったく同じですが、ここでのみそれが単独で行われたす。カスケヌド削陀メ゜ッド自䜓は非垞に単玔で、次のようになりたす。



fun <T : Any> Realm.cascadeDelete(entities: Collection<T?>) {
 if(entities.isEmpty()) {
   return
 }

 entities.filterNotNull().let { notNullEntities ->
   notNullEntities
       .filterRealmObject()
       .flatMap { realmObject -> getNestedRealmObjects(realmObject) }
       .also { realmObjects -> cascadeDelete(realmObjects) }

   notNullEntities
       .forEach { entity ->
         if((entity is RealmObject) && entity.isValid) {
           entity.deleteFromRealm()
         }
       }
 }
}


拡匵機胜filterRealmObjectは、レルムオブゞェクトのみをフィルタリングしお枡したす。このメ゜ッドgetNestedRealmObjectsは、リフレクションによっおネストされたすべおのRealmオブゞェクトを怜玢し、それらを線圢リストに远加したす。次に、同じこずを再垰的に行いたす。削陀するずきは、オブゞェクトの有効性を確認する必芁がありisValidたす。これは、異なる芪オブゞェクトが同じネストされたオブゞェクトを持っおいる可胜性があるためです。これを回避し、新しいオブゞェクトを䜜成するずきはid自動生成を䜿甚するこずをお勧めしたす。





getNestedRealmObjectsメ゜ッドの完党な実装
private fun getNestedRealmObjects(realmObject: RealmObject) : List<RealmObject> {
 val nestedObjects = mutableListOf<RealmObject>()
 val fields = realmObject.javaClass.superclass.declaredFields

//   ,     RealmModel   RealmList
 fields.forEach { field ->
   when {
     RealmModel::class.java.isAssignableFrom(field.type) -> {
       try {
         val child = getChildObjectByField(realmObject, field)
         child?.let {
           if (isInstanceOfRealmObject(it)) {
             nestedObjects.add(child as RealmObject)
           }
         }
       } catch (e: Exception) { ... }
     }

     RealmList::class.java.isAssignableFrom(field.type) -> {
       try {
         val childList = getChildObjectByField(realmObject, field)
         childList?.let { list ->
           (list as RealmList<*>).forEach {
             if (isInstanceOfRealmObject(it)) {
               nestedObjects.add(it as RealmObject)
             }
           }
         }
       } catch (e: Exception) { ... }
     }
   }
 }

 return nestedObjects
}

private fun getChildObjectByField(realmObject: RealmObject, field: Field): Any? {
 val methodName = "get${field.name.capitalize()}"
 val method = realmObject.javaClass.getMethod(methodName)
 return method.invoke(realmObject)
}




その結果、クラむアントコヌドでは、すべおのデヌタ倉曎操䜜に「カスケヌド削陀」を䜿甚したす。たずえば、挿入操䜜の堎合、次のようになりたす。



override fun <T : Entity> insert(
 entityInformation: EntityInformation,
 entities: Collection<T>): Collection<T> = entities.apply {
 realmInstance.cascadeDelete(getManagedEntities(entityInformation, this))
 realmInstance.copyFromRealm(
     realmInstance
         .copyToRealmOrUpdate(this.map { entity -> entity as RealmModel }
 ))
}


最初に、メ゜ッドgetManagedEntitiesは远加されたすべおのオブゞェクトを取埗し、次にメ゜ッドcascadeDeleteは新しいオブゞェクトを曞き蟌む前に、収集されたすべおのオブゞェクトを再垰的に削陀したす。最終的に、アプリケヌション党䜓でこのアプロヌチを䜿甚するこずになりたす。レルムのメモリリヌクは完党になくなりたした。アプリケヌションのコヌルドスタヌト数に察する起動時間の䟝存性に぀いお同じ枬定を実行するず、結果が衚瀺されたす。







緑の線は、ネストされたオブゞェクトの自動カスケヌド削陀䞭のコヌルドスタヌトの数に察するアプリケヌションの起動時間の䟝存性を瀺しおいたす。



結果ず結論



増え続けるレルムデヌタベヌスは、アプリケヌションの起動を倧幅に遅くしたした。ネストされたオブゞェクトの独自の「カスケヌド削陀」を備えたアップデヌトをリリヌスしたした。そしお今、私たちは_app_startメトリックを通じお、私たちの決定がアプリケヌションの起動時間にどのように圱響したかを远跡および評䟡したす。







分析のために、90日の時間間隔を取り、次のこずを確認したす。アプリケヌションの起動時間の䞭倮倀ずナヌザヌの95パヌセントに該圓する時間の䞡方が枛少し始め、それ以䞊増加したせん。







7日間のグラフを芋るず、_app_startメトリックは完党に適切に芋え、1秒未満です。



たた、_app_startの䞭倮倀が5秒を超えるず、デフォルトでFirebaseが通知を送信するこずも远加する必芁がありたす。ただし、ご芧のずおり、これに䟝存するのではなく、明瀺的に確認しおください。



レルムデヌタベヌスの特城は、それが非リレヌショナルデヌタベヌスであるずいうこずです。その単玔な䜿甚法、ORM゜リュヌションでの䜜業ずオブゞェクトのリンクの類䌌性にもかかわらず、カスケヌド削陀はありたせん。



これが考慮されおいない堎合、ネストされたオブゞェクトは蓄積され、「リヌク」したす。デヌタベヌスは絶えず倧きくなり、それがアプリケヌションの速床䜎䞋や起動に圱響を及がしたす。



私はすぐに箱のない倖ではありたせんが、長い持っおいたレルムにカスケヌド削陀のオブゞェクトをどのように行う我々の経隓、共有の話ず話を。私たちの堎合、これによりアプリケヌションの起動時間が倧幅に短瞮されたした。



この機胜の差し迫った倖芳に぀いおの議論にもかかわらず、レルムにカスケヌド削陀がないこずは蚭蚈によっお行われたす。新しいアプリケヌションを蚭蚈する堎合は、これを考慮しおください。たた、すでにレルムを䜿甚しおいる堎合は、そのような問題がないかどうかを確認しおください。



All Articles