Android 性能优化之问题如何定位

关于如何定位这种问题其实可以把他拆分成两个问题,一个是调试阶段问题定位,另外一个就是线上问题定位,我们一步一步来分析

1.调试阶段问题定位 LeakCanary

为什么说LeakCanary 只能用于调试阶段的问题分析定位呢,那我还需要从 LeakCanary 的源码说起

整个Activity 检测过程

  1. 创建检测器任务队列
分发任务
public AndroidWatchExecutor(long initialDelayMillis) {
  mainHandler = new Handler(Looper.getMainLooper());
  HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
  handlerThread.start();
  backgroundHandler = new Handler(handlerThread.getLooper());
  this.initialDelayMillis = initialDelayMillis;
  maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis;
}

注意上面代码 HandlerThread 的创建过程,以及他所导入的Looper , HandlerThread 会启动一个looper ,并且将这个looper 的 循环 开启,将这个looper 与这个Looper 所持有的MessageQueue 交给backgroundHandler 建立联系,那么以后我们使用这个backgroundHandler 调度的任何任务已经与MainThread 里面的looper 没有关系了, 这一点很重要,理解了这个地方那么下面的代码就能很好的理解了

@Override public void execute(Retryable retryable) {
  if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
    waitForIdle(retryable, 0);//  主线程直接使用idel
  } else {
    postWaitForIdle(retryable, 0); 子线程
  }
}

private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
  mainHandler.post(new Runnable() {
    @Override public void run() {
      waitForIdle(retryable, failedAttempts);
    }
  });
}
private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
  long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
  long delayMillis = initialDelayMillis * exponentialBackoffFactor;
  backgroundHandler.postDelayed(new Runnable() {
    @Override public void run() {
      Retryable.Result result = retryable.run();
      if (result == RETRY) {
        postWaitForIdle(retryable, failedAttempts + 1);
      }
    }
  }, delayMillis);
}

在分发事件的过程中如果在主线程,就使用主线程Looper 的idel 将这个线程转换到 子线程的 looper 中,如果是在子线程就将这个任务直接在子线程中的looper 中调度

一个监视任务被触发,他都干了什么呢,如何干的呢,下面继续分析


public void watch(Object watchedReference, String referenceName) {
  if (this == DISABLED) {
    return;
  }
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  final long watchStartNanoTime = System.nanoTime();
  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);

  ensureGoneAsync(watchStartNanoTime, reference);
}


@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  long gcStartNanoTime = System.nanoTime();
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

  removeWeaklyReachableReferences();

  if (debuggerControl.isDebuggerAttached()) {
    // The debugger can create false leaks.
    return RETRY;
  }
  if (gone(reference)) {
    return DONE;
  }
  gcTrigger.runGc();
  removeWeaklyReachableReferences();
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

    File heapDumpFile = heapDumper.dumpHeap();
    if (heapDumpFile == RETRY_LATER) {
      // Could not dump the heap.
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

    HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
        .referenceName(reference.name)
        .watchDurationMs(watchDurationMs)
        .gcDurationMs(gcDurationMs)
        .heapDumpDurationMs(heapDumpDurationMs)
        .build();

    heapdumpListener.analyze(heapDump);
  }
  return DONE;
}

我们先把代码放在上面,下面我们来分析他的代码都做了哪些事情

1.创建 KeyedWeakReference 弱引用 持有这个对象,因为弱引用不会影响对象的回收,并能在没有其他类型的引用关系时被gc回收

  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);
  1. 清空引用队列中已经回收的对象
  removeWeaklyReachableReferences();

private void removeWeaklyReachableReferences() {
  // 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.
  KeyedWeakReference ref;
  while ((ref = (KeyedWeakReference) queue.poll()) != null) {
    retainedKeys.remove(ref.key);
  }
}

我先来说一下引用队列的工作原理,当我们构建一个弱引用对象,并把他与引用队列做了关联,那么在这个被弱引用所引用的对象被回收时,这个对象的一些关联信息就会被存放到引用队列中

那么他这里的工作就是 遍历现有的队列中的已经被释放的对象,并把他的关系从我们的缓存的Set 中 删除,用来明确是否有其他地方导致的gc,说白了就是在能明确已经被回收的情况下不用在做gc 的操作了

  1. 再次确认引用缓存的set 中是否还有这个key的信息,如果被删除,那么set 中的key也会被删除,
if (gone(reference)) {
     return DONE;
   }

 private boolean gone(KeyedWeakReference reference) {
   return !retainedKeys.contains(reference.key);
 }
  1. 手动触发gc
gcTrigger.runGc();

5.重复2的操作,查看改对象是否已经被回收

6.重复3的操作查看是否已经被回收 没有则发生内存泄露

7.读取内存快照

8.分析内存快照

为什么 LeakCanary 不能用于线上

  1. 频繁执行gc 导致程序卡顿更加剧烈

  2. 已经发出去版本频繁分析与解析内存快照的实际意义并不大,注意 这里说的是频繁的分析与解析, 因为内存快照可能很大,这个解析的过程也是一个比较复杂的过程,如果频繁发生内存泄露,就代表着需要频繁的上传文件, 很明显这里需要一个机制,来保证正确的内存收集已经,以及内存快照文件的上传,如果我们上传太多的这种文件,筛选和处理也是需要非常多的成本的

3.在 dump 内存快照信息的时候,jvm 会暂停所有操作, LeakCanary 的每次发生内存泄露的时候都会 去dump ,就导致卡顿

说道这里我们就需要知道哪些情况会导致OOM 的发生

  1. 内存泄露 无用的对象无法被回收,
  2. 内存抖动 频繁的创建和销毁对象会导致内存碎片化比较严重,内存总量是足够的 但是不连续,
  3. Thread过多 任务无法结束 或者同一时间节点创建的任务过多 也有可能是执行不频繁的线程池任务创建的主要线程数过多 ,导致主要线程池中的线程无法释放
  4. 内存不足

5.收集这些信息还用使用dump内存快照, 这时会 STOP THE WORLD ,那么如何来处理这个问题 ,

那么针对上面的四种情况我们能怎么拿到这种情况呢

  1. 内存抖动现阶段还没有一个比较成熟的方案,一种是koom 每15秒检测一下内存,发现如果内存急剧上升到危险值后获取dump来分析,通过快照文件发现哪个类的异常,另一种只能是在线下使用profiler 来查看这个类的内存表现是否存在明显的锯齿化的来判断,如果存在锯齿化这种情况在继续对他的对象来做分析,分析一个时间段哪个对象的创建和销毁比较频繁

  2. 内存泄露 内存泄露我们可以通过内存的快照文件获取他的引用信息

3.线程数 线程数可通过 读取 proc/self//status 这个文件来判断

  1. 内存不足 我们需要拿到用户手机分配给当前app 的实际情况来分析,需要查看他的 使用内存 所有内存 以及使用内存 来分析

5.使用子进程dump , 在主进程fork 子进程 , 在fork的开始时 ,linux 会将 主进程的内存区域的权限修改为 read only , 这样子进程和父进程都同时映射到了 同一块内存区域, 由于linux进程隔离,两个访问同一块内存是不合理的,如果 主进程或者子进程 任意一个修改这个内存,就会触发linux 的 copy on write ,将主进程的复制一份给子进程,所以在子进程中能dump 到主进程的信息

6 同时 由于dump 后的 hprof 这个文件比较大,存储的信息也比较多,如何筛选有用信息也是非常中用的一步

/// 各个数据的获取方式

这些分析的都是 java  Heap 的一些内存分析,同时我们还需要对 进程已经   
剩余内存      Runtime.getRuntime().freeMemory()
总分配内存      Runtime.getRuntime().totalMemory()
使用内存= 总分配内存 - 剩余内存

https://blog.csdn.net/whbing1471/article/details/105468139/ 我看了一下这博客说的很详细 关于 proc/self/state proc/meminfo proc 是 Process 简称 meminfo 是memery Info 的简称 ,都是linux 命令

我们具体查看的方式
就可以拿到系统给我们分配的内存信息以及线程信息

fun test(){
      File("/proc/self/status").forEachLineQuietly { line ->
          when {
              line.startsWith("VmSize") -> {
                  Log.i("tian.shm","VmSize${VSS_REGEX.matchValue(line)}")
              }
              line.startsWith("VmRSS") -> {
                  Log.i("tian.shm","rssInKb${RSS_REGEX.matchValue(line)}")
              }
              line.startsWith("Threads") -> {
                  Log.i("tian.shm","thread${THREADS_REGEX.matchValue(line)}")
              }
              else ->{
              }
          }
      }


      File("/proc/meminfo").forEachLineQuietly { line ->
          when {
              line.startsWith("MemTotal") -> {
                  Log.i("tian.shm","totalInKb${MEM_TOTAL_REGEX.matchValue(line)}")
              }
              line.startsWith("MemFree") -> {
                  Log.i("tian.shm","freeInKb${MEM_FREE_REGEX.matchValue(line)}")
              }
              line.startsWith("MemAvailable") -> {
                  Log.i("tian.shm","availableInKb${MEM_AVA_REGEX.matchValue(line)}")
              }
              line.startsWith("CmaTotal") -> {
                  Log.i("tian.shm","cmaTotal${MEM_CMA_REGEX.matchValue(line)}")
              }
              line.startsWith("ION_heap") -> {
                  Log.i("tian.shm","IONHeap${MEM_ION_REGEX.matchValue(line)}")
              }
              else ->{
              }
          }
      }
  }
  private fun Regex.matchValue(s: String) = matchEntire(s.trim())
      ?.groupValues?.getOrNull(1)?.toInt() ?: 0


  private fun File.forEachLineQuietly(charset: Charset = Charsets.UTF_8, action: (line: String) -> Unit) {
      kotlin.runCatching {
          // Note: close is called at forEachLineQuietly
          BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(action)
      }.onFailure { exception -> exception.printStackTrace() }
  }

线上问题检测 KOOM

快手研发的一个获取线上性能的库,给我们制定了一个获取当前运行状态的这么一个机制,这个保证了同一个版本 15天内 超过5次不会重复上传hprof 文件,以及运行时合适收集内存信息的一个机制,获取的方式就是使用我在上面所说的这样的方法,如果你需要从0-1建立这样一个缓存机制那么可以看一下他们的源码,如果你只是想了解他们使用什么方式获取的各个性能间的信息,那么上面的案例可以帮到你

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容