深入內(nèi)存優(yōu)化

前言

內(nèi)存問題很常見 而且經(jīng)常會(huì)因?yàn)閮?nèi)存問題引起卡頓問題 在接下來的卡頓分析中 內(nèi)存也是一個(gè)很重要的方向

內(nèi)存抖動(dòng)

內(nèi)存抖動(dòng)是由頻繁gc導(dǎo)致產(chǎn)生 由于內(nèi)存空間的不足 回導(dǎo)致頻繁gc 在Profile中查看是鋸齒狀

內(nèi)存抖動(dòng)實(shí)戰(zhàn)

我們可以通過Profile來分析memory Profile的優(yōu)勢(shì)大概就是圖表非常直觀 我們一般可以配合使用mat來解決內(nèi)存泄漏的問題

 @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            for (int i = 0; i < 100; i++) {
                String args[] = new String[100000];
            }
            mHandler.sendEmptyMessageDelayed(0, 30);
        }
    };

我們通過頻繁申請(qǐng)大對(duì)象來模擬內(nèi)存抖動(dòng) 我們來觀察Profile

WechatIMG3.png

emmmm 現(xiàn)在的手機(jī)還是很厲害啊 沒有鋸齒狀 但是可以看到gc非常頻繁 這時(shí)候我們就可以dump heap
然后看一下內(nèi)存的主要消耗


WechatIMG6.jpeg

我們主要看Shallow Size 和Retained Siz

Jvm內(nèi)存分配

JVM內(nèi)存分配主要分為以下幾個(gè)部分

  • 方法區(qū) (常量 靜態(tài)變量 編譯之后代碼)
  • 程序計(jì)數(shù)器 (計(jì)算當(dāng)前線程的當(dāng)前方法執(zhí)行到多少行)
  • 虛擬機(jī)棧 (java對(duì)象引用)
  • 本地方法棧 (native對(duì)象引用)
  • 堆 (生成的對(duì)象)

工具選擇

Profile

Profile是Android Studio自帶的工具 我們可以用來查看Cpu Memory Network Energy的消耗
我們可以使用Profile來定位內(nèi)存抖動(dòng)問題 因?yàn)榭梢院苊黠@的看到鋸齒狀或者頻繁gc的情況
也可以用Cpu Profile 或者 Memory Profile來查看內(nèi)存泄漏問題
Profile提供了Fragment/Activity的監(jiān)測(cè) 還可以通過包名等方式 來查看內(nèi)存中的泄漏問題
可以定位查看 內(nèi)存中是否存在不合理的對(duì)象

MAT

MAT全稱Memory Anlyzer Tools ,是一款可以分析Hprof文件的可視化工具 我們可以使用Mat工具
查找定位我們預(yù)設(shè)的懷疑點(diǎn) 通過exclude weak soft等引用 獲取到gc到對(duì)象的引用路徑 幫助我們解決問題

LeakCanary源碼解析

在2.0之前的版本 需要我們手動(dòng)調(diào)用Install方法 在2.0之后 LeakCanary注冊(cè)了ContentProvider 不需要手動(dòng)的調(diào)用Install

LeakCanary分為兩部分 監(jiān)控和分析

監(jiān)控

查看LeakCanary源碼 發(fā)現(xiàn)AppWatcherInstaller類,繼承ContentProvider
在Oncreate方法中調(diào)用了AppWatcher.manualInstall(application)
然后AppWatcher中調(diào)用了InternalAppWatcher.install(application)

查看InternalAppWatcher.install方法

fun install(application: Application) {
    checkMainThread()
    if (this::application.isInitialized) {
      return
    }
    SharkLog.logger = DefaultCanaryLog()
    InternalAppWatcher.application = application

    val configProvider = { AppWatcher.config }
    //監(jiān)控Activity 這里傳遞了ObjectWatcher用來監(jiān)控Object對(duì)象
    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
    //監(jiān)控Fragment
    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
    onAppWatcherInstalled(application)
  }

我們已經(jīng)察覺到了 關(guān)鍵代碼就在ActivityDestroyWatcher.install里面了 讓我們跟上

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
        //使用我們上面提到的objectWatcher來觀察activity
          objectWatcher.watch(
              activity, "${activity::class.java.name} received Activity#onDestroy() callback"
          )
        }
      }
    }

  companion object {
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(objectWatcher, configProvider)
        //注冊(cè)生命周期
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

我們可能發(fā)現(xiàn)了LeakCanary的原理 就是監(jiān)聽Activity的生命周期回調(diào) 在OnDestroy之后 使用objectWatcher去觀察Activity是否有被回收 如果沒有回收 就表示泄漏了

Follow Me,勝利就在前面了!!!

接著查看objectWatcherwatch方法

 @Synchronized fun watch(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    //移除一些弱引用對(duì)象
    removeWeaklyReachableObjects()
    //這里使用了隨機(jī)數(shù)生成key
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    //將觀察對(duì)象用WeakRefrence引用 并且使用RefrenceQueue來接收銷毀的對(duì)象
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
          (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
          (if (description.isNotEmpty()) " ($description)" else "") +
          " with key $key"
    }

    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
    //接著判斷
      moveToRetained(key)
    }
  }
  
   @Synchronized private fun moveToRetained(key: String) {
   //刪除不可達(dá)的對(duì)象
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      //分析內(nèi)存泄漏
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }
  
   private fun removeWeaklyReachableObjects() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    var ref: KeyedWeakReference?
    //queue中存在的對(duì)象是已經(jīng)被回收的對(duì)象
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }

上面的方法也很簡(jiǎn)單 就是將我們要觀察的對(duì)象 用WeakRefrence和RefrenceQueue對(duì)象來進(jìn)行包裝
如果gc之后 對(duì)象被回收 那么會(huì)將回收的對(duì)象放入RefrenceQueue中

如果retainedRef不為null 那么開始分析HProf

監(jiān)控總結(jié)

我們發(fā)現(xiàn) LeakCanary的監(jiān)控原理其實(shí)也比較簡(jiǎn)單 就是在OnDestroy之后用WeakRefrence來檢查Activity/Fragment是否泄漏

分析

接著分析泄漏
剛最后調(diào)用了onObjectRetainedListeners.forEach { it.onObjectRetained() }
我們發(fā)現(xiàn)AppWatcher繼承了onObjectRetainedListener

忽略一些簡(jiǎn)單方法 最后會(huì)調(diào)用到

private fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    // A tick will be rescheduled when this is turned back on.
    if (!config.dumpHeap) {
      SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
        //再gc一次
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

    //檢查保留數(shù)量
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      onRetainInstanceListener.onEvent(DebuggerIsAttached)
      //顯示通知彈窗
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(
              R.string.leak_canary_notification_retained_debugger_attached
          )
      )
      scheduleRetainedObjectCheck(
          reason = "debugger is attached",
          rescheduling = true,
          delayMillis = WAIT_FOR_DEBUG_MILLIS
      )
      return
    }

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      onRetainInstanceListener.onEvent(DumpHappenedRecently)
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
      )
      scheduleRetainedObjectCheck(
          reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
          rescheduling = true,
          delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
    //取消通知彈窗
    dismissRetainedCountNotification()
    //Dump Heap 堆轉(zhuǎn)儲(chǔ)
    dumpHeap(retainedReferenceCount, retry = true)
  }

在dumpHeap方法中 我們先看一下如何生成HProf文件(省略了一些代碼)

override fun dumpHeap(): File? {
    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null

   ......

    return try {
    //生成HProf文件
      Debug.dumpHprofData(heapDumpFile.absolutePath)
      if (heapDumpFile.length() == 0L) {
        SharkLog.d { "Dumped heap file is 0 byte length" }
        null
      } else {
        heapDumpFile
      }
    } catch (e: Exception) {
      SharkLog.d(e) { "Could not dump heap" }
      // Abort heap dump
      null
    } finally {
      cancelToast(toast)
      notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
    }
  }

然后調(diào)用HeapAnalyzerService.runAnalysis(application, heapDumpFile)開啟分AnalyzerService

HeapAnalyzerService這部分我們?cè)诰€上LeakCanary里 其實(shí)可以閹割掉 只需要想辦法把Hprof刪除保留有效數(shù)據(jù) 并傳回服務(wù)端就好了

AnalyzerService會(huì)調(diào)用analyzerHeap來分析內(nèi)存泄漏并生成圖表


private fun analyzeHeap(
    heapDumpFile: File,
    config: Config
  ): HeapAnalysis {
    val heapAnalyzer = HeapAnalyzer(this)

    val proguardMappingReader = try {
      ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
    } catch (e: IOException) {
      null
    }
    return heapAnalyzer.analyze(
        heapDumpFile = heapDumpFile,
        leakingObjectFinder = config.leakingObjectFinder,
        referenceMatchers = config.referenceMatchers,
        computeRetainedHeapSize = config.computeRetainedHeapSize,
        objectInspectors = config.objectInspectors,
        metadataExtractor = config.metadataExtractor,
        proguardMapping = proguardMappingReader?.readProguardMapping()
    )
  }
  

最后會(huì)調(diào)用

 Hprof.open(heapDumpFile)
          .use { hprof ->
            val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
            val helpers =
              FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
            helpers.analyzeGraph(
                metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
            )
          }

生成圖表

ARTHook監(jiān)控不合理圖片

內(nèi)存優(yōu)化的過程中 Bitmap優(yōu)化肯定是其中之一 我們可能需要監(jiān)測(cè)大圖 或者監(jiān)測(cè)重復(fù)圖 現(xiàn)在一張圖的內(nèi)存可能就占用1M 解決一張重復(fù)的 就可以省下1M內(nèi)存

優(yōu)化方法

  • 使用統(tǒng)一接口

    我們可以使用統(tǒng)一接口來設(shè)置圖片 在接口層 我們可以監(jiān)控大圖或者重復(fù)圖片

    弊端:程序員可能會(huì)忘記使用統(tǒng)一接口導(dǎo)致監(jiān)控遺漏

  • 使用ART Hook

    我們可以使用Epic框架,Epic 是一個(gè)在虛擬機(jī)層面、以 Java Method 為粒度的 運(yùn)行時(shí) AOP Hook 框架。簡(jiǎn)單來說,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以攔截本進(jìn)程內(nèi)部幾乎任意的 Java 方法調(diào)用,可用于實(shí)現(xiàn) AOP 編程、運(yùn)行時(shí)插樁、性能分析、安全審計(jì)等。

    但是在使用Epic的過程中 也遇到很多奇葩無(wú)解問題 等待作者解決

Epic使用方法

我們這邊使用Epic來Hook所有setImageBitmap來監(jiān)控Bitmap是否過大

public class ImageHook extends XC_MethodHook {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        ImageView imageView = (ImageView) param.thisObject;
        checkBitmap(imageView, ((ImageView) param.thisObject).getDrawable());
    }

    private void checkBitmap(final ImageView imageView, Drawable drawable) {
        if (imageView != null && drawable != null) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                int height = imageView.getLayoutParams().height;
                int width = imageView.getLayoutParams().width;
                if (height > 0 && width > 0) {
                    if (bitmap.getHeight() >= height << 1
                            && bitmap.getWidth() >= width << 1) {
                        warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                    }
                } else {
                    final Throwable stackTrace = new RuntimeException();
                    //還咩有初始化完成
                    imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int w = imageView.getWidth();
                            int h = imageView.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1)
                                        && bitmap.getHeight() >= (h << 1)) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
                                }
                                imageView.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }

    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();

        LogUtils.i(warnInfo);
    }
}

代碼很簡(jiǎn)單 就是獲取Bitmap和ImageViewwidthheight 然后對(duì)比是否超過兩倍大

內(nèi)存必解Bitmap

其他內(nèi)存優(yōu)化點(diǎn)

  1. 設(shè)備分級(jí)

    我們可以對(duì)設(shè)備性能進(jìn)行分級(jí) 4G內(nèi)存的手機(jī)和1G內(nèi)存的手機(jī)運(yùn)行肯定是不一樣的
    比如一些動(dòng)畫我們可以在13年之后的手機(jī)開啟 10年之前的手機(jī)不開啟任何動(dòng)畫
    可以參考device-year-class庫(kù)

  2. 緩存管理

    我們需要一套統(tǒng)一的緩存管理機(jī)制 當(dāng)遇到LMK時(shí) 果斷釋放占有內(nèi)存 減小被殺幾率 我們可以使用OnTrimMemory回調(diào) 根據(jù)不同的狀態(tài)決定釋放不同的內(nèi)存

  3. 進(jìn)程模型

    減少應(yīng)用啟動(dòng)的進(jìn)程數(shù),常駐的進(jìn)程數(shù) 有節(jié)操的保活 對(duì)低端機(jī)優(yōu)化很有效

  4. 安裝包大小

    安裝包的代碼 資源 圖片以及so都跟占有內(nèi)存有很大的關(guān)系 所以我們可以針對(duì)低端機(jī)型退出Lite版本

  5. 統(tǒng)一圖片庫(kù)

    收攏圖片庫(kù)的使用 統(tǒng)一使用自研庫(kù)或者Glide,Fresco等 低端機(jī)使用565,更嚴(yán)格的縮放策略
    而且可以進(jìn)一步的將Bitmap.createBitmap,BitmapFactory相關(guān)接口收攏 方便監(jiān)控

  6. 統(tǒng)一監(jiān)控

    可以采用接口的方式 也可以采用ARTHook的方式 不過ART Hook在實(shí)驗(yàn)室環(huán)境沒什么關(guān)系 在實(shí)驗(yàn)室環(huán)境,遇到內(nèi)存不合理或者圖片合理 可以立即彈窗提醒開發(fā)人員解決 但是在線上環(huán)境 我們要更多的考慮穩(wěn)定性和容錯(cuò)性

線上監(jiān)控方案

  • java內(nèi)存泄漏

    我們可以簡(jiǎn)歷類似LeakCanary的線上方案 裁剪大部分圖片對(duì)應(yīng)的byte數(shù)組 再使用壓縮 進(jìn)一步提高文件上傳的成功率

  • native內(nèi)存泄漏

    native內(nèi)存泄漏往往很難采集 可以參考 《微信Android終端內(nèi)存優(yōu)化實(shí)踐》

  • 采集方式

    用戶在前臺(tái)時(shí) 我們可以每五分鐘采集一次PSS,JAVA堆,圖片總內(nèi)存,建議按照用戶抽樣 而不是按照次抽樣

容災(zāi)方案參考

我們可以在一些特殊的時(shí)間點(diǎn) 重啟應(yīng)用 釋放一些已經(jīng)泄漏的內(nèi)存 可以更好的提高用戶體驗(yàn) 下面參考自微信:

  • 微信是否在主界面退到后臺(tái) 且 位于后臺(tái)的時(shí)間超過 30 分鐘

  • 當(dāng)前時(shí)間為凌晨 2~5 點(diǎn)

  • 不存在前臺(tái)服務(wù)(存在通知欄,音樂播放欄等情況)

  • java heap 必須大于當(dāng)前進(jìn)程最大可分配的 85% || native 內(nèi)存大于 800M || vmsize 超過了 4G(微信 32bit)的 85%

  • 非大量的流量消耗(每分鐘不超過 1M) && 進(jìn)程無(wú)大量 CPU 調(diào)度情況

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容