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 meminfo io.github.customview
- Applications Memory Usage (in Kilobytes):
- Uptime: 1805197131 Realtime: 1965534265
- ** MEMINFO in pid 1239 [io.github.customview] **
- Pss Private Private SwapPss 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这些组件当中。感谢大家,祝大家升职加薪。
有生命不足,还请大家批评指正,我会立即修改!感谢大家!
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |