聊聊元件開發過程中的記憶體優化

語言: CN / TW / HK

優化元件切換

元件化,這裡就不囉嗦了,類似的文章太多了
這裡講講怎麼自動化 核心gradle指令碼外掛https://github.com/genius158/ReferenceDump/blob/main/moduleconfig.gradle
大前提,所有模組都是AndroidStudio預設的形式生成,不去手動增減任何build.gradle內部的用於判斷元件的指令碼
例如這樣的: if (isDebug.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }
這樣的: if(isDebug.toBoolean()) { manifest.srcFile 'src/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/release/AndroidManifest.xml' }
這樣的: if(isNeedCahtModule.toBoolean()){implementation project(':chat')}

- 切換library到application

apply plugin,執行了之後對應的配置會加到project.plugins,如果我們想在後續想移除project.plugins.withType(LibraryPlugin){ project.plugins.remove(it) }會報異常java.lang.UnsupportedOperationException (no error message),就是說我們不能這麼做,這部分目前沒想到太好的方法,最終採用指令碼替換的形式

if (hookInclude) {
    project.ant.replace(
            file: build,
            token: "apply plugin: 'com.android.library'",
            value: "apply plugin: 'com.android.application'"
    )
    // 1見下文
    insertAppConfig(project)
} else {
    project.ant.replace(
            file: build,
            token: "apply plugin: 'com.android.application'",
            value: "apply plugin: 'com.android.library'"
    )
}
複製程式碼

順便一說ant對應的一些命令(zip、unzip、copy,replace ect.)超級好用,replace甚至可以去改程式碼,在javac對應的任務里加入dependsOn,拿到任務的input,也就是一些對應build下的的java目錄、檔案,在java轉class之前,去拿我們想改的類,做匹配修改,做到類似插樁的功能。
(tip:如果改非中間階段的檔案,在沒有程式碼版本管理的情況下,是非常危險的,還是需要做一些備份恢復的處理)

在看看註釋1

// 注入applicationId
p.android.defaultConfig.applicationId "com.yan.application"

// 以下主要目的在元件模組下增加main平級的module目錄插入manifest檔案
def srcDir = new File(p.projectDir.canonicalPath + "/src")
def moduleDir = new File(srcDir.canonicalPath + "/module")
def moduleManifest = new File(p.projectDir.canonicalPath + "/src/module/AndroidManifest.xml")
if (!moduleDir.exists()) moduleDir.mkdirs()

def manifest = new File(srcDir.canonicalPath + "/main/AndroidManifest.xml")
if (!moduleManifest.exists()) {
    p.ant.copy(file: "$manifest.canonicalPath", tofile: "$moduleManifest.canonicalPath")
}

def moduleManifestTxt = moduleManifest.getText()
if (!moduleManifestTxt.contains("<application")) {
    moduleManifestTxt = moduleManifestTxt.replaceAll("\\s/\\s", "")
    moduleManifestTxt = moduleManifestTxt.replace("</manifest>",<application>..." )
    moduleManifest.write(moduleManifestTxt)
}

// 更改manifest為元件的
p.android.sourceSets.main.manifest.srcFile("src/module/AndroidManifest.xml")
複製程式碼

上面的一段指令碼入註釋所寫,為我們的元件自動生成了module目錄,AndroidManifest指我們的模組裡的AndroidManifest,且自動加上了application標籤,同樣的原理我們的元件化測試程式碼也可以用對應的指令碼扔到module目錄下,同時增加java配置指向,這樣做主要是為了,當我們切換到lib模式,不會存在任何的程式碼檔案的增加。

- settings.gradle調整按需引入

apply from: "./moduleconfig.gradle"
if (!hookInclude || foreIncludeAll) {
    include ':test'
    ...
}
複製程式碼

主要看moduleconfig.gradle 自定義部分

ext {
    // 強制全部依賴
    foreIncludeAll = false

    // 哪些模組需要自動lib轉app
    autoModule = [":test", ":plugin2"]
}
// 只引入test模組
includeModule(":test", [":plugin2", ":router", ":moduleadapter"])
複製程式碼
// 引入我們的元件,和這個元件對應的其他模組依賴
def includeModule(module, dependencies) {
	// hookInclude在setting檔案中用於切換引用模式
    ext.hookInclude = true
    moduleListConfig[module] = dependencies
    include module
    dependencies.each { m -> include m }
}
複製程式碼

如果我們用as自動生成了一個新的模組,那麼它在setting指令碼中自動加的include是不符合我們的要求的,所以加了一個判斷,不符合提示調整指令碼,當然利用ant的replace方法我們甚至可以自動調整依賴。

def checkSettingChange() {
    gradle.projectsLoaded {
        def rootPath = rootProject.projectDir.canonicalPath
        def setting = file(rootPath + "/settings.gradle")
        def settingText = setting.getText()
        def unModulePart = settingText.replaceAll("if.*hookInclude.*\\).*\\{[^}]+}", "")
        if (unModulePart.contains("include")) {
            throw new RuntimeException("請保證在settings.gra...")
        }
    }
}
複製程式碼

怎麼依賴我們的元件

這裡我新增了一個配置modelDependencies對應原本的dependencies

// 元件由這個域引入
modelDependencies {
    implementation project(":test")
}
複製程式碼

腳本里對應以下部分

 def modelDependencies = project.getExtensions().create("modelDependencies", ModelDependencies.class)
 project.afterEvaluate {
     modelDependencies.implementations.each { module ->
         // moduleList 對應includeModule方法的moduleListConfig
         // 由modelDependencies引入的元件,判斷它目前是否處在元件化模式,
         // 是則引入,不是則跳過
         if (moduleList.find { entity -> entity.key.contains(module.name) } == null) {
             project.dependencies { implementation module }
         }
     }
 }
複製程式碼

元件記憶體優化

開發過程中記憶體可能記憶體的問題,某個物件本身非常大,或者引用很大,可能需要去分析是不是存在記憶體分配問題,能不能優化;有些物件非常多,可能需要去分析是不是有個方法短時間內大量建立臨時物件;當然還有老生常談的記憶體洩露

這些問題怎麼能方便、及時的發現?
一般我們怎麼查記憶體問題的?
利用profiler的dump Java heap?
是的多數情況下我們是用的這個as自帶的功能可以去查問題。能夠發現一些大記憶體的物件,或者記憶體洩露,然後進行些後續的優化,然而在元件開發過程中有什麼問題呢?

  1. dumpHprof給我們返回的物件太多了,我只想知道當前元件下的class檔案所產生的記憶體快照,或者某個包名下的。
  2. 或者更直觀的,我不止想知道這個物件是由別的那個類哪個物件持有的,我還想知道這些物件是由哪個方法執行產生的。
  3. 對於1,我們當然可以拿到記憶體快照後再過濾但是太慢了,使用profiler,對於大專案來說除了慢,甚至多觸發幾次,很快as就卡的不成樣子,經常還遇到記憶體不足的情況,對於一個生命週期很短的方法裡瞬間建立了一堆物件的情況很難判斷到,這種情況恨不得幾百毫秒能檢視一次記憶體快照;使用leakcanary(這裡不是說使用這個庫,主要是表述我們自己dumpHprof,自己解析),新開執行緒或者程序去做解析,確實可以解決我們使用profiler的帶來的影響開發工具本身效能的問題,但是dumpHprof後,解析過程的時間我們沒法去縮短,想看解析出來的結果,還是常常需要等上10秒20秒,之後我們再去過濾統計,整個過程就拉的更長了。

所以我想做到什麼事情呢,我們開發過程中,可以實時的檢視當前模組下的物件建立和銷燬,無需等待,或者說等待最少的時間,就可以時不時的去對比記憶體資料,發現問題的成本就非常低,無聊了就可以點兩下看看記憶體對比。

怎麼做

做法其實很簡單,就是利用插樁(asm指令級的操作),在執行NEW這個操作命令的時候,把這個物件連結到一個弱引用裡,順帶加上執行的類執行的方法。當然物件還有反射生成的,但是開發過程中,我們絕大多數情況下都不會刻意的使用反射,很多時候都是避免使用反射,從而避免反射帶來的一些效能問題,當然要記錄反射生成的物件也可以,就是在物件的建構函式裡插入程式碼去記錄,當然對系統類我們就無能為力了。
來看看對應的日誌

// 物件總個數
RefCount ---->| 23 |<----
-
//StringBuilder存在14個
CLASS - java.lang.StringBuilder count: 14
// 耗4624位元組,由App$onCreate$1#timer產生11個物件
----> size(byte): 4624 count: 11  
com.yan.referencecounttest.App$onCreate$1#timer(LBurialTimer;LString;LString;LString;J)V
//耗1064位元組,由App$onCreate$1#<clinit>()V 產生3個物件
----> size(byte): 1064 count: 3  com.yan.referencecounttest.App$onCreate$1#<clinit>()V 
// 總共佔5688位元組
----> size(byte): 5688 *
複製程式碼

怎麼發現問題,就上面這段日誌,每次檢視記憶體日誌ApponCreateonCreate1#timer路徑下的StringBuilder一直在增加,屬於臨時物件大量產生,我們就可以用一個成員變數來優化

BurialTimer.getTimer().setListener { ignore, className, methodName, des, cost ->
           if (cost>0){
               Log.e("BurialTimer","$cost   $className $methodName ")
           }
        }
複製程式碼

優化

private val stringBuilder = StringBuilder()
override fun onCreate() {
	BurialTimer.getTimer().setListener { ignore, className, methodName, des, cost ->
            if (cost > 0) {
                stringBuilder.clear()
                stringBuilder.append(cost).append("   ").append(className).append("  ").append(methodName)
                Log.e("BurialTimer", stringBuilder.toString())
                stringBuilder.clear()
            }
        }
    }

複製程式碼

git地址https://github.com/genius158/ReferenceDump