匝抽 发表于 3 天前

Android 贯彻开发过程 之 对象生命周期

Android 贯彻开发过程 之 对象生命周期

我们在开发过程当中是否会关心对象的生命周期?开发者创建对象是否是随意创建?如何更好的管理对象?对象到底应该应该迎合哪个组件的生命周期?一个对象占用内存大约是多少?接下来请听本码农细心分解。
生命周期之Hello World

介绍两个命令:
# 查看当前activity
adb shell dumpsys activity activities | grep -E 'mResumedActivity|mCurrentFocus'

# 查看栈内的activity
adb shell dumpsys activity activities | grep -E "Running activities|Hist"执行下第一个命令
$ dumpsys activity activities | grep -E 'mResumedActivity|mCurrentFocus'
        mResumedActivity: ActivityRecord{a588abe u0 io.github.customview/.activity.HomeActivity t599}堆转储并拉到pc上,拖入Android Studio Memory Profiler打开
adb shell am dumpheap io.github.customview /data/local/tmp/heapdump.hprof
adb pull /data/local/tmp/heapdump.hprof .
可以看到总共有758个类对象,19014个类实例对象。

[*]Native Size代表这些对象在native堆占用的字节总和。
[*]Shallow(浅的) Size(浅大小)表示对象本身在Java堆上占用的字节数,不包含它引用到的其他对象
[*]Retained Size: 代表该对象被回收,可以释放的内存空间,即包含它引用的其他对象。例如A引用B,C对象,则 A的 Retained Size 等于A+B+C的Shallow Size之和。
[*]Allocations: 表示在你做 Heap Dump 这一刻,当前还活着(未被 GC 回收)的该类对象的实例个数。
在内存优化的时候比较关心的是Retained Size,将他按照从大到小到小的顺序排列,依次排除。
现在进入主题:
回忆一下Activity生命周期

这便是入门Android的第一课,Activity的标准的生命周期流程,但是这并不只是入门那么简单,这个生命周期图应当贯彻Android开发的始终,使得对于生命周期拥有更加深刻的认识,包括Service, 协程,Compose副函数等等,每一个对象都有其生命周期,必须让每个对象的生命周期符合其所在的生命周期组件,不能超越生命周期的组件,否则会浪费内存,严重点内存泄漏、甚至OOM。对于Android移动应用来说内存是非常珍贵的,因为Android系统对于每个应用都是有内存限制的。并且在内存紧张的情况下JVM会进行非常频繁的GC,众所周知GC 的过程中会触发 Stop-The-World (STW) ,在 GC 的某些阶段会暂停所有 Java 线程,导致 UI 渲染和事件处理停顿,从而给用户感觉到“卡顿”,给用户带来非常糟糕的体验,有些用户吐槽android手机为什么越用越卡,这些可能就是原因之一。内存限制一般如下:

[*]低端机 / 早期设备:16 MB / 32 MB
[*]普通手机:128 MB / 256 MB
[*]高端机:512 MB 甚至更高
一个啥也不干的类将会占用8字节。
查看一个应用的内存限制
val activityManager: ActivityManager = getSystemService(ActivityManager::class.java)
Log.i(TAG, "initView -> memory: ${activityManager.memoryClass}MB")
// or
val maxMemory: Long = Runtime.getRuntime().maxMemory() / 1024 / 1024 // 最大可用内存我目前的这台机器的限制是258M,已经使用28M,当然仅仅是一个最简单的Hello World项目。
可以使用如下命令
$ adb shell dumpsys meminfoio.github.customview
Applications Memory Usage (in Kilobytes):
Uptime: 1805197131 Realtime: 1965534265
** MEMINFO in pid 1239 **
                   PssPrivatePrivateSwapPss   Heap   Heap   Heap
               Total    Dirty    Clean    Dirty   Size    Alloc   Free
                ------   ------   ------   ------   ------   ------   ------
Native Heap   6933   6868       16       58    16384   9593   6790
Dalvik Heap   1251      928      252       34   3034   1514   1520
Dalvik Other      888      888      0      0                           
      Stack       92       92      0      0                           
       Ashmem      2      0      0      0                           
    Other dev       14      0      8      0                           
   .so mmap   2661      128      188      145                           
    .apk mmap      756      0      120      0                           
    .ttf mmap       66      0      0      0                           
    .dex mmap    10451      8   7728      0                           
    .oat mmap      382      0      0      0                           
    .art mmap   4434   3904      112       14                           
   Other mmap       13      4      0      0                           
      Unknown      830      728       92      4                           
      TOTAL    29028    13548   8516      255    19418    11107   8310

App Summary
                     Pss(KB)
                        ------
         Java Heap:   4944
         Native Heap:   6868
                Code:   8172
               Stack:       92
            Graphics:      0
       Private Other:   1988
            System:   6964

               TOTAL:    29028       TOTAL SWAP PSS:      255

Objects
               Views:       11         ViewRootImpl:      1
         AppContexts:      3         Activities:      1
            Assets:      4      AssetManagers:      3
       Local Binders:      9      Proxy Binders:       16
       Parcel memory:      3         Parcel count:       14
    Death Recipients:      0      OpenSSL Sockets:      0
            WebViews:      0

SQL
         MEMORY_USED:      0
PAGECACHE_OVERFLOW:      0          MALLOC_SIZE:      0可以看到App Summary的TOTAL的值为28M.
综上移动端的资源是非常珍贵的,稍不注意就会OOM。当然可以在发生oom的时候进行亡羊补牢、为时不晚。但是为什么不一开始就进行预防呢?
单例 or 非单例

对于移动应用来说,一个对象是否需要单例需要谨慎考虑,我们先创建一个单例类来简单看下。
创建单例类之前
val dumpFile = File(
        getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
        "home_activity.hprof"
)
Debug.dumpHprofData(dumpFile.absolutePath)将文件导入Android Studio
adb pull /storage/emulated/0/Android/data/io.github.customview/files/Download/home_activity.hprof .截图如下:

可以看到这个HomeActivity实例被销毁可以释放大约7.3kb的内存.
再来关注下Kotlin的伴生对象
internal class HomeActivity internal constructor(): AppCompatActivity() {

    internal companion object {
      internal fun startActivity(context: Context){
            context.startActivity(Intent(context, HomeActivity::class.java))
      }
    }
        //...
}它类对象占用232字节内存,类实例仅占8字节
我们删除它之后,其就不占用内存了。
kotlin 伴生类加载时机:
只要当前类被引用那么其就会初始化伴生类,或者很简单,使用如下方式即可初始化伴生类。
import android.util.Log

private const val TAG: String = "Utils"

internal class Utils internal constructor(){
    internal companion object {
      init {
            Log.i(TAG, "Utils companion object load...")
      }
    }
    internal fun hello(){
      Log.i(TAG, "Utils hello...")
    }
}在HomeActivity中
internal companion object {
        init {
                Log.i(TAG, "HomeActivity companion object load... ")
                Utils // 初始化伴生对象
        }
        @JvmStatic
        internal fun startActivity(context: Context){
                context.startActivity(Intent(context, HomeActivity::class.java))
        }
}干掉当前activity去另一个Activity会发生什么,我们对比一个两个堆转储的区别
HomeActivity
UserActivity.startActivity(context = this)
finish()
System.gc() // 手动触发
UserActivity
private fun initView(){
        binding.button.setOnClickListener {
                ///storage/emulated/0/Android/data/io.github.customview/files/Download
                val dumpFile = File(
                        getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
                        "user_activity.hprof"
                )
                Debug.dumpHprofData(dumpFile.absolutePath)
        }
        binding.gc.setOnClickListener {
                System.gc()
        }
}手动多GC几次

可以看到伴生类无法被垃圾所回收,他的生命周期伴随整个APP,它会延长其他对象的生命周期,比如Acitivity、Service.
经过上面分析,在伴生类里使用context会有如下警告
internal companion object {
        private var context1: Context? = null
}
// Do not place Android context classes in static fields; this is a memory leak有些开发者忽略警告就是干!!!
建议

[*]禁止伴生类引用Acitivity、Service
[*]在伴生类当中仅使用常量、方法,尽量避免引用实例对象。
[*]尽量少用单例对象,因为它的生命周期是自引用起到整个app被关闭
context 使用注意事项

Log.i(TAG, "initView -> this: $this baseContext: $baseContext, application: ${application}, applicationContext: $applicationContext")

[*]this:是组件本身,使用context的对象的生命周期即组件本身
[*]baseContext:和this一致
[*]applicationContext: 即Application,使用applicationContext对象的生命周期是app生命周期。
创建依赖context的对象的生命周期取决于传递的context实例的生命周期。
有些开发者在Application在中会创建一个静态的context变量在Application在onCreate的时候将Application实例赋值给context,这样做会有很多问题:
[*]Android Studio 内存泄漏警告,强迫症患者难受,但是老项目当前相当多地方在使用,你也不好修改。
[*]增加内存泄漏的风险,这样的做法使得开发者肆无忌惮的使用静态的context创建对象,在没有生命周期思维的开发者身上,会造成滥用,使得很多对象的生命周期被放大到app生命周期,严重的会造成内存泄漏,甚至OOM
结尾

在Android 移动 应用开发当中,应当提高生命周期思维,仔细分析自己手动创建的对象存活的生命周期,将生命周期的思维贯彻app开发的整个过程,而不仅仅是activity、Service这些组件当中。感谢大家,祝大家升职加薪。
有生命不足,还请大家批评指正,我会立即修改!感谢大家!

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除
页: [1]
查看完整版本: Android 贯彻开发过程 之 对象生命周期