美文网首页Android 性能优化篇
Android 性能优化之问题如何定位

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

作者: Tsm_2020 | 来源:发表于2023-08-11 15:29 被阅读0次

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

    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建立这样一个缓存机制那么可以看一下他们的源码,如果你只是想了解他们使用什么方式获取的各个性能间的信息,那么上面的案例可以帮到你

    相关文章

      网友评论

        本文标题:Android 性能优化之问题如何定位

        本文链接:https://www.haomeiwen.com/subject/kpjtmdtx.html