In the middle of 2020, Kuaishou opened an online OOM monitoring and reporting framework: KOOM. Here is a brief study.

Project address: github.com/KwaiAppTeam…

I. Introduction of official projects

Description: 1.1

KOOM is a complete solution developed by the Kuaishou performance optimization team in the process of dealing with OOM problems on mobile terminals. The Android Java memory section has been greatly optimized on the basis of LeakCanary, addressing the performance issues of online memory monitoring, and collecting and parsing memory images offline without affecting the user experience. Since the launch of the main APP of Kuaishou after the Spring Festival in 2020, a large number of OOM problems have been solved. Its performance and stability have withstood the test of a large number of users and devices, so it decided to open source to give back to the community.

1.2 characteristics:

  • Richer leak scenario detection than leakCanary;
  • Better detection performance than leakCanary;
  • A full-featured closed-loop monitoring system that supports large-scale deployment online;

1.3 KOOM framework

1.4 Kuaishou KOOM core process includes:

  • Configuration delivery decision;
  • Monitor memory status;
  • Memory image collection;
  • Parse image file (hprof for short) to generate report and upload;
  • Problem aggregation alarm and allocation follow up.

1.5 Optimization of trigger mechanism of leak detection:

Leak detection trigger mechanism leakCanary is that the object WeakReference has not been added to the ReferenceQueue after GC, which may have a memory leak. This process will actively trigger GC for confirmation, which may cause user perception of lag. However, KOOM uses memory threshold monitoring to trigger image collection, delaying the determination of object leakage until the parsing. Threshold monitoring only needs to obtain several memory indicators of interest in child threads regularly, with low performance loss.

1.6 Heap dump optimization:

In traditional solutions, the application is frozen for a few seconds. KOOM forks a new process to perform dump without affecting the normal execution of the parent process. To suspend a virtual machine, you need to call the art::Dbg::SuspendVM function. Since Android 7.0, Google has restricted the use of the system library. This restriction is circumvented by caller address substitution and dl_iterate_PHdr parsing.

Memory mirrors of real users are randomly collected. The dump and fork subprocesses take the following time to block users:

And judging from the official test data, the effect seems to be very good.

Ii. Official demo

Here is the official koom-demo

Dump heap -> Heap Analysis -> Report Cache/Koom/Report / Json report is generated in the cache/koom/report of the application:

cepheus:/data/data/com.kwai.koom.demo/cache/koom/report # ls
2020-12-08_15-23-32.json
Copy the code

Simulate the simplest singleton that holds a LeakActivity instance of CommonUtils, and see what json will eventually report:

{ "analysisDone":true, "classInfos":[ { "className":"android.app.Activity", "instanceCount":4, "leakInstanceCount":3 }, { "className":"android.app.Fragment", "instanceCount":4, "leakInstanceCount":3 }, { "className":"android.graphics.Bitmap", "instanceCount":115, "leakInstanceCount":0 }, { "className":"libcore.util.NativeAllocationRegistry", "instanceCount":1513, "leakInstanceCount":0 }, { "className":"android.view.Window", "instanceCount":4, "leakInstanceCount":0 } ], "gcPaths":[ { "gcRoot":"Local variable in native code", "instanceCount":1, "leakReason":"Activity Leak", "path":[ { "declaredClass":"java.lang.Thread", "reference":"android.os.HandlerThread.contextClassLoader", "referenceType":"INSTANCE_FIELD" }, { "declaredClass":"java.lang.ClassLoader", "reference":"dalvik.system.PathClassLoader.runtimeInternalObjects", "referenceType":"INSTANCE_FIELD" }, { "declaredClass":"", "reference":"java.lang.Object[]", "referenceType":"ARRAY_ENTRY" }, { "declaredClass":"com.kwai.koom.demo.CommonUtils", "reference":"com.kwai.koom.demo.CommonUtils.context", "referenceType":"STATIC_FIELD" }, { "reference":"com.kwai.koom.demo.LeakActivity", "referenceType":"instance" } ], "signature":"378fc01daea06b6bb679bd61725affd163d026a8" } ], "runningInfo":{ "analysisReason":"RIGHT_NOW", AppVersion :"1.0", "buildModel":"MI 9 Transparent Edition", "currentPage":"LeakActivity", "dumpReason":"MANUAL_TRIGGER", "jvmMax":512, "jvmUsed":2, "koomVersion":1, "manufacture":"Xiaomi", "nowTime":"2020-12-08_16-07-34", "pss":32, "rss":123, "sdkInt":29, "threadCount":17, "usageSeconds":40, "vss":5674 } }Copy the code

There are three main parts: class information, GC reference path, and basic running information. GcPaths shows that LeakActivity is referenced by CommonUtils.

The framework can be used here by referring to the official access document, which is not repeated here: github.com/KwaiAppTeam…

Third, framework analysis

3.1 class diagram

3.2 a sequence diagram

KOOM initialization process

KOOM executes the initialization method and, after a 10-second delay, checks the child thread of threadHandler to see if there are any unparsed files. If there are no unparsed files, it triggers parsing and monitors memory.

Heap dump process

HeapDumpTrigger

  • StartTrack: Hprof dump is automatically triggered by monitoring. If the memory monitoring function is enabled, the sub-thread triggers a heap dump check within 5s to check whether the condition is met. Conditions are organized by a series of thresholds, which are discussed in more detail later. If the threshold is met, trigger is executed by listening back to HeapDumpTrigger.
  • Trigger: Actively triggers the hprof dump operation. This is handled by the fork child process, which will be discussed later. Dump is completed through monitoring the callback trigger HeapAnalysisTrigger. StartTrack trigger heap analysis process.

Heap analysis process

HeapAnalysisTrigger

  • StartTrack triggers hprof file analysis based on policy.
  • Trigger Directly triggers hprof file analysis. Handled by a separate process of service, work content mainly share memory leak detection (activity/fragments/bitmap/Windows) and leakage consolidated data cache for json file for your report.

Four, core source code analysis

After the previous analysis, basically have a macro understanding of the use and structure of the framework, this part is going to do a brief analysis of some implementation details.

4.1 Memory Monitoring Triggers the dump rule

This is mainly to study isTrigger rule in HeapMonitor, and the trigger condition will be cyclically judged every 5S.

com/kwai/koom/javaoom/monitor/HeapMonitor.java @Override public boolean isTrigger() { if (! started) { return false; } HeapStatus heapStatus = currentHeapStatus(); if (heapStatus.isOverThreshold) { if (heapThreshold.ascending()) { if (lastHeapStatus == null || heapStatus.used >= lastHeapStatus.used) { currentTimes++; } else { currentTimes = 0; } } else { currentTimes++; } } else { currentTimes = 0; } lastHeapStatus = heapStatus; return currentTimes >= heapThreshold.overTimes(); } private HeapStatus lastHeapStatus; private HeapStatus currentHeapStatus() { HeapStatus heapStatus = new HeapStatus(); heapStatus.max = Runtime.getRuntime().maxMemory(); heapStatus.used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); HeapStatus. IsOverThreshold = 100.0 f * heapStatus. 2 / heapStatus Max > heapThreshold. Value (); return heapStatus; } com/kwai/koom/javaoom/common/KConstants.java public static class HeapThreshold { public static int VM_512_DEVICE = 510; public static int VM_256_DEVICE = 250; public static int VM_128_DEVICE = 128; public static float PERCENT_RATIO_IN_512_DEVICE = 80; public static float PERCENT_RATIO_IN_256_DEVICE = 85; public static float PERCENT_RATIO_IN_128_DEVICE = 90; public static float getDefaultPercentRation() { int maxMem = (int) (Runtime.getRuntime().maxMemory() / MB); if (maxMem >= VM_512_DEVICE) { return KConstants.HeapThreshold.PERCENT_RATIO_IN_512_DEVICE; } else if (maxMem >= VM_256_DEVICE) { return KConstants.HeapThreshold.PERCENT_RATIO_IN_256_DEVICE; } else if (maxMem >= VM_128_DEVICE) { return KConstants.HeapThreshold.PERCENT_RATIO_IN_128_DEVICE; } return KConstants.HeapThreshold.PERCENT_RATIO_IN_512_DEVICE; } public static int OVER_TIMES = 3; public static int POLL_INTERVAL = 5000; }Copy the code

Here is a different threshold ratio for different memory sizes:

  • Application memory >512 MB 80%
  • Application memory >256 MB 85%
  • Application memory >128 MB 90%
  • The default value is 80% if the value is lower than 128MB

Application has been the use of memory/maximum memory more than the proportion will trigger heapStatus. IsOverThreshold. The heap dump is triggered for three consecutive times. However, memory growth is considered during this process. If the memory usage decreases or the memory usage/maximum memory usage falls below the threshold, the heap dump will be cleared.

Therefore, the rule is summarized as follows: the trigger is triggered only when the > threshold condition is met for three times and the memory is always in an upward phase. This can reduce invalid dump.

4.2 fork Performs the dump operation

Here is a comparison of the mainstream implementation of the tripartite framework on the market at present:

LeakCanaray, Matrix, and Probe are running debug. dumpHprofData(), which suspends all threads of the current process and then wakes up all threads after the hprof file has been generated. The whole process is time consuming and causes significant lag, so this pain point severely affects the ability to bring the feature to the online environment.

In KOOM, the main process forks a child process to process hprof dump. The main process itself only briefly suspends THE SUSPEND VM during the fork process. After that, time-consuming blocking occurs in the child process, which has no impact on the main process at all. The suspend VM itself takes a very short time and is completely negligible from the test results

ForkJvmHeapDumper is used by default to perform the dump.

com/kwai/koom/javaoom/dump/ForkJvmHeapDumper.java @Override public boolean dump(String path) { boolean dumpRes = false; try { int pid = trySuspendVMThenFork(); // Suspend the VM and copy-on-write fork if (pid == 0) {debug. dumpHprofData(path); //dump hprof exitProcess(); } else {// resumeVM() in parent; // Resume Current VM dumpRes = waitPREs (pid); }} Catch (Exception e) {e.printStackTrace(); } return dumpRes; }Copy the code

Why suspendVM first and then fork? At first I understood that the main purpose was to keep memory mirrors consistent before and after forking, but it didn’t have much of an impact on memory leaks, and the demo fork didn’t seem to have any problems, especially since it did a lot of work to bypass Android N’s limitations and suspendVM was definitely necessary. Finally, it turns out that the single thread is ok because the thread has stopped and the demo plus multi-thread dump is stuck in the suspendVM. So suspendVM, fork, and resumeVM are required.

Ok, after confirming the workflow, let’s try to implement:

Native layer:

Fork, waitPid, and exitProcess are easy to ignore and focus on VM operations:

Normal operation should be:

void *libHandle = dlopen("libart.so", RTLD_NOW); Void *suspendVM = dlsym(libHandle, LIBART_DBG_SUSPEND); Void *resumeVM = dlsym(libHandle, LIBART_DBG_RESUME); void *resumeVM = dlsym(libHandle, LIBART_DBG_RESUME); // Get the resumeVM method reference dlclose(libHandle); // Close the libart.so file operation handleCopy the code

This is OK for Android versions below N, but Google has restricted the use of the system library since Android 7.0. Based on this premise, Kuaijun developed the kwai-linker component. KOOM bypassed this restriction with caller address substitution and dl_iterate_phdr parsing. The official documentation explains this restriction, so let’s look at how KOOM bypassed this restriction.

Source reference: Android 9.0

/bionic/libdl/libdl.cpp 02__attribute__((__weak__)) 103void* dlopen(const char* filename, int flag) { 104 const void* caller_addr = __builtin_return_address(0); Return __loader_dlopen(filename, flag, caller_addr); 106} /bionic/linker/dlfcn.cpp 152void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) { 153 return dlopen_ext(filename, flags, nullptr, caller_addr); 154} 131static void* dlopen_ext(const char* filename, 132 int flags, 133 const android_dlextinfo* extinfo, 134 const void* caller_addr) { 135 ScopedPthreadMutexLocker locker(&g_dl_mutex); 136 g_linker_logger.ResetState(); 137 void* result = do_dlopen(filename, flags, extinfo, caller_addr); // do_dlopen 138 if (result == nullptr) {139 __bionic_format_dlerror("dlopen failed", linker_get_error_buffer()); 140 return nullptr; 141 } 142 return result; 143} /bionic/linker/linker.cpp 2049void* do_dlopen(const char* name, int flags, 2050 const android_dlextinfo* extinfo, 2051 const void* caller_addr) { 2052 std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name); 2053 ScopedTrace trace(trace_prefix.c_str()); 2054 ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str()); 2055 soinfo* const caller = find_containing_library(caller_addr); 2056 android_namespace_t* ns = get_caller_namespace(caller); . 2141 return nullptr; 2142}Copy the code

In this case, __loader_dlopen is executed by default, but the current address of the function is passed by default. If the address of the system function is checked as a third-party address, the check fails. If the address of the system function is checked as a third-party address, the check fails.

So here’s what KOOM does:

Android version greater than N and less than Q

using __loader_dlopen_fn = void *(*)(const char *filename, int flag, void *address); void *handle = ::dlopen("libdl.so", RTLD_NOW); // open libel.so // call its __loader_dlopen method directly, It differs from dlopen in that caller address auto __loader_dlopen = can be passed reinterpret_cast<__loader_dlopen_fn>(::dlsym(handle,"__loader_dlopen")); __loader_dlopen(lib_name, flags, (void *) dlerror); // Pass the address of the dlerror system function to ensure that the caller address passes the check.Copy the code

Android Q and later versions

Since Q introduces runtime namespace, __loader_dlopen returns nullptr as handle

Dl_iterate_phdr is used to query the loaded dynamic library base object addresses in the current process.

int DlFcn::dl_iterate_callback(dl_phdr_info *info, size_t size, void *data) { auto target = reinterpret_cast<dl_iterate_data *>(data); if (info->dlpi_addr ! = 0 && strstr(info->dlpi_name, target->info_.dlpi_name)) { target->info_.dlpi_addr = info->dlpi_addr; target->info_.dlpi_phdr = info->dlpi_phdr; target->info_.dlpi_phnum = info->dlpi_phnum; // break iterate return 1; } // continue iterate return 0; } dl_iterate_data data{}; data.info_.dlpi_name = "libart.so"; dl_iterate_phdr(dl_iterate_callback, &data); CHECKP(data.info_.dlpi_addr > 0) handle = __loader_dlopen(lib_name, flags, (void *) data.info_.dlpi_addr);Copy the code

Dl_iterate_callback calls back to each dynamic library loaded by the current process, filters out the address of libart.so: data.info_.dlpi_addr, and tries to open libart.so with __loader_dlopen.

The attached:

struct dl_phdr_info { ElfW(Addr) dlpi_addr; // Base object address const ElfW(Phdr)* dlpi_phdr; // Array of Pointers ElfW(Half) dlpi_phnum; / /... };Copy the code

This is how Kuaidan’s self-developed kwe-linker component circumvent Android 7.0’s restrictions on calling system libraries through caller address substitution and dl_iterate_phdr parsing. It is also the core technical point of fork dump scheme.

4.3 Memory leak detection implementation

Memory leak detection is the core code SuspicionLeaksFinder. The find

public Pair<List<ApplicationLeak>, List<LibraryLeak>> find() { boolean indexed = buildIndex(); if (! indexed) { return null; } initLeakDetectors(); findLeaks(); return findPath(); }Copy the code
4.3.1 buildIndex ()
private boolean buildIndex() { Hprof hprof = Hprof.Companion.open(hprofFile.file()); / / choice can be a class type gcroot KClass < gcroot > [] gcRoots = new KClass [] {Reflection. GetOrCreateKotlinClass (gcroot. JniGlobal. Class), //Reflection.getOrCreateKotlinClass(GcRoot.JavaFrame.class), Reflection.getOrCreateKotlinClass(GcRoot.JniLocal.class), //Reflection.getOrCreateKotlinClass(GcRoot.MonitorUsed.class), Reflection.getOrCreateKotlinClass(GcRoot.NativeStack.class), Reflection.getOrCreateKotlinClass(GcRoot.StickyClass.class), Reflection.getOrCreateKotlinClass(GcRoot.ThreadBlock.class), Reflection.getOrCreateKotlinClass(GcRoot.ThreadObject.class), Reflection.getOrCreateKotlinClass(GcRoot.JniMonitor.class)}; / / parsing hprof files for HeapGraph object HeapGraph = HprofHeapGraph.Com panion. IndexHprof (hprof, null, kotlin.collections.SetsKt.setOf(gcRoots)); return true; } fun indexHprof( hprof: Hprof, proguardMapping: ProguardMapping? = null, indexedGcRootTypes: Set<KClass<out GcRoot>> = setOf( JniGlobal::class, JavaFrame::class, JniLocal::class, MonitorUsed::class, NativeStack::class, StickyClass::class, ThreadBlock::class, ThreadObject::class, JniMonitor::class ) ): HeapGraph {/ / confirm the corresponding record index val index = HprofInMemoryIndex. CreateReadingHprof (hprof proguardMapping, HprofHeapGraph(hprof, index)} return HprofHeapGraph(hprof, index)}Copy the code

HprofInMemoryIndex. CreateReadingHprof core logic: reading hprof files, packages different content for different record, and then a record to index the index of encapsulation, after can go through the index index to find content.

HprofReader. ReadHprofRecords () encapsulates the record

  • LoadClassRecord
  • InstanceSkipContentRecord
  • ObjectArraySkipContentRecord
  • PrimitiveArraySkipContentRecord

The index that encloses HprofInMemoryIndex. OnHprofRecord () :

  • classIndex
  • instanceIndex
  • objectArrayIndex
  • primitiveArrayIndex
class HprofHeapGraph internal constructor(
   private val hprof: Hprof,
   private val index: HprofInMemoryIndex
) : HeapGraph {
...
  override val gcRoots: List<GcRoot>
   get() = index.gcRoots()
  override val objects: Sequence<HeapObject>
   get() {
     return index.indexedObjectSequence().map {wrapIndexedObject(it.second, it.first)}
   }
  override val classes: Sequence<HeapClass>
   get() {
     return index.indexedClassSequence().map {val objectId = it.first
           val indexedObject = it.second
           HeapClass(this, indexedObject, objectId)
         }
   }
  override val instances: Sequence<HeapInstance>
   get() {
     return index.indexedInstanceSequence().map {val objectId = it.first
           val indexedObject = it.second
           val isPrimitiveWrapper = index.primitiveWrapperTypes.contains(indexedObject.classId)
           HeapInstance(this, indexedObject, objectId, isPrimitiveWrapper)
         }
   }

  override val objectArrays: Sequence<HeapObjectArray>
   get() = index.indexedObjectArraySequence().map {val objectId = it.first
     val indexedObject = it.second
     val isPrimitiveWrapper = index.primitiveWrapperTypes.contains(indexedObject.arrayClassId)
     HeapObjectArray(this, indexedObject, objectId, isPrimitiveWrapper)
   }

  override val primitiveArrays: Sequence<HeapPrimitiveArray>
   get() = index.indexedPrimitiveArraySequence().map {val objectId = it.first
     val indexedObject = it.second
     HeapPrimitiveArray(this, indexedObject, objectId)
   }
Copy the code

Hprof is transformed into HprofHeapGraph.

In short, the function of this part is mainly to parse Hrpof files into a structured index graph according to the scanned format. The indexed content is encapsulated as HprofHeapGraph, which locates each type of data through the corresponding initial index. Without going into the details of the implementation, the library that implemented this functionality was formerly Squere’s HAHA and now shark, but it provides much the same functionality.

4.3.2 initLeakDetectors () and findLeaks ()

Initialize leak detector:

private void initLeakDetectors() {
  addDetector(new ActivityLeakDetector(heapGraph));
  addDetector(new FragmentLeakDetector(heapGraph));
  addDetector(new BitmapLeakDetector(heapGraph));
  addDetector(new NativeAllocationRegistryLeakDetector(heapGraph));
  addDetector(new WindowLeakDetector(heapGraph));
  ClassHierarchyFetcher.initComputeGenerations(computeGenerations);
  leakReasonTable = new HashMap<>();
}
Copy the code

Initialize the leak detection of various types, including Activity, Fragment, Bitmap+NativeAllocationRegistry and Window.

The next step is to comb the inheritance relation string of the above objects and detect the coverage of their subclasses.

public void findLeaks() { KLog.i(TAG, "start find leaks"); < heapObject.heapInstance > instances = heapgraph.getInstances (); // Get all instances from HprofHeapGraph < heapObject.heapInstance > instances = heapgraph.getInstances (); Iterator<HeapObject.HeapInstance> instanceIterator = instances.iterator(); while (instanceIterator.hasNext()) { HeapObject.HeapInstance instance = instanceIterator.next(); if (instance.isPrimitiveWrapper()) { continue; } ClassHierarchyFetcher.process(instance.getInstanceClassId(), instance.getInstanceClass().getClassHierarchy()); for (LeakDetector leakDetector : LeakDetectors) {/ / is a subclass of object detection & satisfy corresponding leakage conditions if (leakDetector. IsSubClass (instance. GetInstanceClassId () && leakDetector.isLeak(instance)) { ClassCounter classCounter = leakDetector.instanceCount(); if (classCounter.leakInstancesCount <= SAME_CLASS_LEAK_OBJECT_GC_PATH_THRESHOLD) { leakingObjects.add(instance.getObjectId()); leakReasonTable.put(instance.getObjectId(), leakDetector.leakReason()); }}}} / / pay attention to the class and the corresponding instance number, add json HeapAnalyzeReporter. AddClassInfo (leakDetectors); findPrimitiveArrayLeaks(); findObjectArrayLeaks(); }Copy the code

Here’s a look at how each type of object detects leaks:

ActivityLeakDetector: private static final String ACTIVITY_CLASS_NAME = "android.app.Activity"; private static final String FINISHED_FIELD_NAME = "mFinished"; private static final String DESTROYED_FIELD_NAME = "mDestroyed"; public boolean isLeak(HeapObject.HeapInstance instance) { activityCounter.instancesCount++; HeapField destroyField = instance.get(ACTIVITY_CLASS_NAME, DESTROYED_FIELD_NAME); HeapField finishedField = instance.get(ACTIVITY_CLASS_NAME, FINISHED_FIELD_NAME); assert destroyField ! = null; assert finishedField ! = null; boolean abnormal = destroyField.getValue().getAsBoolean() == null || finishedField.getValue().getAsBoolean() == null; if (abnormal) { return false; } boolean leak = destroyField.getValue().getAsBoolean() || finishedField.getValue().getAsBoolean(); if (leak) { activityCounter.leakInstancesCount++; } return leak; }Copy the code

The mDestroyed and mFinish fields are true, but activities with existing instances are suspected leaks.

FragmentLeakDetector: private static final String NATIVE_FRAGMENT_CLASS_NAME = "android.app.Fragment"; // native android Fragment, deprecated as of API 28. private static final String SUPPORT_FRAGMENT_CLASS_NAME = "android.support.v4.app.Fragment"; // pre-androidx, support library version of the Fragment implementation. private static final String ANDROIDX_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"; // androidx version of the Fragment implementation private static final String FRAGMENT_MANAGER_FIELD_NAME = "MFragmentManager"; Private static Final String FRAGMENT_MCALLED_FIELD_NAME = "mCalled "; private static Final String FRAGMENT_MCALLED_FIELD_NAME = "mCalled "; //Used to verify that subclasses call through to super class. public FragmentLeakDetector(HeapGraph heapGraph) { HeapObject.HeapClass fragmentHeapClass = heapGraph.findClassByName(ANDROIDX_FRAGMENT_CLASS_NAME); fragmentClassName = ANDROIDX_FRAGMENT_CLASS_NAME; if (fragmentHeapClass == null) { fragmentHeapClass = heapGraph.findClassByName(NATIVE_FRAGMENT_CLASS_NAME); fragmentClassName = NATIVE_FRAGMENT_CLASS_NAME; } if (fragmentHeapClass == null) { fragmentHeapClass = heapGraph.findClassByName(SUPPORT_FRAGMENT_CLASS_NAME); fragmentClassName = SUPPORT_FRAGMENT_CLASS_NAME; } assert fragmentHeapClass ! = null; fragmentClassId = fragmentHeapClass.getObjectId(); fragmentCounter = new ClassCounter(); } public boolean isLeak(HeapObject.HeapInstance instance) { if (VERBOSE_LOG) { KLog.i(TAG, "run isLeak"); } fragmentCounter.instancesCount++; boolean leak = false; HeapField fragmentManager = instance.get(fragmentClassName, FRAGMENT_MANAGER_FIELD_NAME); if (fragmentManager ! = null && fragmentManager.getValue().getAsObject() == null) { HeapField mCalledField = instance.get(fragmentClassName, FRAGMENT_MCALLED_FIELD_NAME); boolean abnormal = mCalledField == null || mCalledField.getValue().getAsBoolean() == null; if (abnormal) { KLog.e(TAG, "ABNORMAL mCalledField is null"); return false; } leak = mCalledField.getValue().getAsBoolean(); if (leak) { if (VERBOSE_LOG) { KLog.e(TAG, "fragment leak : " + instance.getInstanceClassName()); } fragmentCounter.leakInstancesCount++; } } return leak; }Copy the code

There are three types of fragments:

  • android.app.Fragment
  • android.support.v4.app.Fragment
  • androidx.fragment.app.Fragment

The corresponding FragmentManager instance is null (this indicates that the fragment has been removed) and the corresponding mCalled is true, i.e., it is not in the Perform state but in the life-cycle callback state (onDestroy). However, fragments with existing instances are suspected to be leaking objects.

BITMAP_CLASS_NAME = "Android.graphics.bitmap"; BitmapLeakDetector Private Static Final String BITMAP_CLASS_NAME = "Android.graphics.bitmap"; public boolean isLeak(HeapObject.HeapInstance instance) { if (VERBOSE_LOG) { KLog.i(TAG, "run isLeak"); } bitmapCounter.instancesCount++; HeapField fieldWidth = instance.get(BITMAP_CLASS_NAME, "mWidth"); HeapField fieldHeight = instance.get(BITMAP_CLASS_NAME, "mHeight"); assert fieldHeight ! = null; assert fieldWidth ! = null; boolean abnormal = fieldHeight.getValue().getAsInt() == null || fieldWidth.getValue().getAsInt() == null; if (abnormal) { KLog.e(TAG, "ABNORMAL fieldWidth or fieldHeight is null"); return false; } int width = fieldWidth.getValue().getAsInt(); int height = fieldHeight.getValue().getAsInt(); boolean suspicionLeak = width * height >= KConstants.BitmapThreshold.DEFAULT_BIG_BITMAP; if (suspicionLeak) { KLog.e(TAG, "bitmap leak : " + instance.getInstanceClassName() + " " + "width:" + width + " height:" + height); bitmapCounter.leakInstancesCount++; } return suspicionLeak; }Copy the code

This is a judgment based on the Bitmap size. If the size exceeds 768*1366, leakage is considered.

In addition, NativeAllocationRegistryLeakDetector and WindowLeakDetector hasn’t done the specific leakage judgment rules, not to participate in the object leak detection, only do the statistics.

Conclusion: As a whole, there are two lessons to learn from KOOM:

  • 1. Trigger memory leak detection, the usual is watcher activity/fragment onDestroy, KOOM periodically check whether the current memory reaches the threshold;
  • 2. Hprof is dumped by the corresponding process. KOOM is dumped by the fork process.

Android9.0 hook dlopen problem/how to hook dlopen related function dl_iterate_phdr