惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
S
SegmentFault 最新的问题
美团技术团队
博客园 - 【当耐特】
博客园_首页
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
Recent Announcements
Recent Announcements
博客园 - 聂微东
P
Privacy & Cybersecurity Law Blog
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
P
Proofpoint News Feed
有赞技术团队
有赞技术团队
量子位
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
N
Netflix TechBlog - Medium
大猫的无限游戏
大猫的无限游戏
F
Full Disclosure
Microsoft Security Blog
Microsoft Security Blog
Vercel News
Vercel News
G
Google Developers Blog
Last Week in AI
Last Week in AI
D
DataBreaches.Net
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
A
About on SuperTechFans
F
Fortinet All Blogs
宝玉的分享
宝玉的分享
雷峰网
雷峰网
罗磊的独立博客
V
V2EX
Recorded Future
Recorded Future
V
Visual Studio Blog
Y
Y Combinator Blog
T
Tailwind CSS Blog
小众软件
小众软件
Blog — PlanetScale
Blog — PlanetScale
M
MIT News - Artificial intelligence
U
Unit 42

Dropbox Tech Blog

How we used DSPy to turn AI evaluations into better responses in Dash chat How Dropbox uses MCP and Dash to close the design-to-code security gap Beyond code generation: rethinking engineering productivity in the age of AI agents Introducing Nova, our internal platform for coding agents Improving storage efficiency in Magic Pocket, our immutable blob store Reducing our monorepo size to improve developer velocity How we optimized Dash's relevance judge with DSPy Using LLMs to amplify human labeling and improve Dash search relevance How low-bit inference enables efficient AI Insights from our executive roundtable on AI and engineering productivity Engineering VP Josh Clemm on how we use knowledge graphs, MCP, and DSPy in Dash Inside the feature store powering real-time AI in Dropbox Dash Building the future: highlights from Dropbox’s 2025 summer intern class Fighting the forces of clock skew when syncing password payloads Introducing Focus, a new open source Gradle plugin Making camera uploads for Android faster and more reliable How Dropbox Replay keeps everyone in sync Why we built a custom Rust library for Capture How we sped up Dropbox Android app startup by 30% Why we chose Apache Superset as our data exploration platform Revamping the Android testing pipeline at Dropbox Our counterintuitive fix for Android path normalization JQuery to React: How we rewrote the HelloSign Editor How we ensure credible analytics on Dropbox mobile apps Engineering Dropbox Transfer: Making simple even simpler Speeding up a Git monorepo at Dropbox with <200 lines of code Building for reliability at HelloSign Store grand re-opening: loading Android data with coroutines Modernizing our Android build system: Part I, the planning Modernizing our Android build system: Part II, the execution Our journey to type checking 4 million lines of Python The (not so) hidden cost of sharing code between iOS and Android Redux with Code-Splitting and Type Checking The Programmer Mindset: Main Debug Loop On working with designers Incrementally migrating over one million lines of code from Python 2 to Python 3 Crash reporting in desktop Python applications What we learned at our first JS Guild Summit How we rolled out one of the largest Python 3 migrations ever Dropbox Paper: Emojis and Exformation Creating a culture of accessibility Adding IPv6 connectivity support to the Dropbox desktop client Accelerating Iteration Velocity on Dropbox’s Desktop Client, Part 2 Accelerating Iteration Velocity on Dropbox’s Desktop Client, Part 1 DropboxMacUpdate: Making automatic updates on macOS safer and more reliable Annotations on Document Previews Open Sourcing Pytest Tools Open Sourcing Zulip – a Dropbox Hack Week Project Building Carousel, Part III: Drawing Images on Screen The Tech Behind Dropbox’s New User Experience on Mobile (Part 2) Building Dropbox’s New User Experience for Mobile, Part 1 Building Carousel, Part II: Speeding Up the Data Model Building Carousel, Part I: How we made our networked mobile app feel fast and local Scaling MongoDB at Mailbox Welcome Guido! Dropbox dives into CoffeeScript Some love for JavaScript applications Plop: Low-overhead profiling for Python Using the Dropbox API from Haskell A Python Optimization Anecdote Translating Dropbox
Detecting memory leaks in Android applications
Lily Chen · 2021-03-24 · via Dropbox Tech Blog

Memory leaks occur when an application allocates memory for an object, but then fails to release the memory when the object is no longer being used. Over time, leaked memory accumulates and results in poor app performance and even crashes. Leaks can happen in any program and on any platform, but they’re especially prevalent in Android apps due to complications with activity lifecycles. Recent Android patterns such as ViewModel and LifecycleObserver can help avoid memory leaks, but if you’re following older patterns or don’t know what to look out for, it’s easy to let mistakes slip through.

Common examples

Reference to a long-lived service

diagram showing fragment view that references a long-lived service

A fragment references an activity which references a long-lived service.

In this case, we have a standard setup with an activity that holds a reference to some long-living service, then a fragment and its view that hold references to the activity. For example, say that the activity somehow creates a reference to its child fragment. Then, for as long as the activity sticks around, the fragment will continue living too. This causes a leak for the duration between the fragment’s onDestroy and the activity’s onDestroy.

diagram showing memory leak caused by fragment referencing a long-lived service

The fragment will never be used again, yet it persists in memory.

Long-lived service which references a fragment’s view

What if, in the other direction, the service obtained a reference to the fragment’s view? First, the view would now stay alive for the entire duration of the service. Furthermore, because the view holds a reference to its parent activity, the activity now leaks as well.

diagram showing memory leak in long-lived service that references a fragment view

As long as the Service lives, the FragmentView and Activity will squander memory.

Detecting memory leaks

Now that we know how memory leaks happen, let’s discuss what we can do to detect them. An obvious first step is to check if your app ever crashes due to OutOfMemoryError. Unless there’s a single screen that eats more memory than your phone has available, you have a memory leak somewhere.

app crashes due to OutOfMemoryError

This approach only tells you the existence of the problem—not the root cause. The memory leak could have happened anywhere, and the crash that’s logged doesn’t point to the leak, only to the screen that finally tipped memory usage over the limit. 

You could inspect all the breadcrumbs to see if there’s some similarity, but chances are the culprit won’t be easy to discern. Let’s explore other options.

LeakCanary

One of the best tools out there is LeakCanary, a memory leak detection library for Android. We simply add a dependency on our build.gradle file. The next time we install and run our app, LeakCanary will be running alongside it. As we navigate through our app, LeakCanary will pause occasionally to dump the memory and provide leak traces of detected leaks.

This one step is vastly better than what we had before. But the process is still manual, and each developer will only have a local copy of the memory leaks they’ve personally encountered. We can do better!

LeakCanary and Bugsnag 

LeakCanary provides a very handy code recipe for uploading found leaks to Bugsnag. We’re then able to track memory leaks just as we do any other warning or crash in the app. We can even take this one step further and use Bugsnag’s integrations to hook it up to project management software such as Jira for even more visibility and accountability.

screenshot showing BugSnag integration with Jira

Bugsnag connected to Jira

LeakCanary and integration tests

Another way to improve automation is to hook up LeakCanary to CI tests. Again, we are given a code recipe to start with. From the official documentation: 

LeakCanary provides an artifact dedicated to detecting leaks in UI tests which provides a run listener that waits for the end of a test, and if the test succeeds then it looks for retained objects, trigger a heap dump if needed and perform an analysis.

Be aware that LeakCanary will slow down testing, as it dumps the heap after each test to which it listens. In our case, because of our selective testing and sharding set up, the extra time added is negligible. 

Our end result is that memory leaks are surfaced just as any other build or test failure on CI, with the leak trace at the time of the leak recorded.

Running LeakCanary on CI has helped us learn better coding patterns, especially when it comes to new libraries, before any code hits production. For example, it caught this leak when we were working with MvRx mocks: 

<failure>Test failed because application memory leaks were detected: ==================================== HEAP ANALYSIS RESULT ==================================== 4 APPLICATION LEAKS References underlined with "~~~" are likely causes. Learn more at https://squ.re/leaks. 198449 bytes retained by leaking objects Signature: 6bf2ba80511dcb6ab9697257143e3071fca4 ┬─── 
│ GC Root: System class 
│ ├─ com.airbnb.mvrx.mocking.MockableMavericks class 
│ Leaking: NO (a class is never leaking) 
│ ↓ static MockableMavericks.mockStateHolder 
│                            ~~~~~~~~~~~~~~~ 
├─ com.airbnb.mvrx.mocking.MockStateHolder instance 
│ Leaking: UNKNOWN 
│ ↓ MockStateHolder.delegateInfoMap 
│                   ~~~~~~~~~~~~~~~ 
├─ java.util.LinkedHashMap instance 
│ Leaking: UNKNOWN 
│ ↓ LinkedHashMap.header 
│                 ~~~~~~ 
├─ java.util.LinkedHashMap$LinkedEntry instance 
│ Leaking: UNKNOWN 
│ ↓ LinkedHashMap$LinkedEntry.prv 
│                             ~~~ 
├─ java.util.LinkedHashMap$LinkedEntry instance 
│ Leaking: UNKNOWN 
│ ↓ LinkedHashMap$LinkedEntry.key 
│                             ~~~ 
╰→ com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment instance 
   Leaking: YES (ObjectWatcher was watching this because com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null) 
   key = 391c9051-ad2c-4282-9279-d7df13d205c3 
   watchDurationMillis = 7304 
   retainedDurationMillis = 2304 198427 bytes retained by leaking objects 
   Signature: d1c9f9707034dd15604d8f2e63ff3bf3ecb61f8

It turned out that we hadn’t properly cleaned up the mocks when writing the test. Adding a few lines of code avoids the leak:

   @After
    fun teardown() {
        scenario.close()
        val holder = MockableMavericks.mockStateHolder
        holder.clearAllMocks()
    }

You may be wondering: Since this memory leak only happens in tests, is it really that important to fix? Well, that’s up to you! Like linters, leak detection can tell you when there’s code smell or bad coding patterns. It can help teach engineers to write more robust code—in this case, we learned about the existence of clearAllMocks(). The severity of a leak and whether or not it’s imperative to fix are decisions an engineer can make.

For tests on which we don’t want to run leak detection, we wrote a simple annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SkipLeakDetection {
    /**
     * The reason why the test should skip leak detection.
     */
    String value();
}

and in our class which overrides LeakCanary’s FailOnLeakRunListener():

override fun skipLeakDetectionReason(description: Description): String? {
    return when {
        description.getAnnotation(SkipLeakDetection::class.java) != null ->
            "is annotated with @SkipLeakDetection"
        description.testClass.isAnnotationPresent(SkipLeakDetection::class.java) ->
            "class is annotated with @SkipLeakDetection"
        else -> null
    }
}

Individual tests or entire test classes can use this annotation to skip leak detection.

Fixing memory leaks

Now that we’ve gone over various ways to find and surface memory leaks, let’s talk about how to actually understand and fix them. 

The leak trace provided by LeakCanary will be the single most useful tool for diagnosing a leak. Essentially, the leak trace prints out a chain of references associated with the leaked object, and provides an explanation of why it’s considered a leak. 

LeakCanary already has great documentation on how to read and use its leak trace, so there’s no need to repeat it here. Instead, let’s go over two categories of memory leaks that I mostly found myself dealing with.

Views

It’s common to see views declared as class level variables in fragments: private TextView myTextView;  or, now that more Android code is being written in Kotlin: private lateinit var myTextView: TextView—common enough for us not to realize that these can all cause memory leaks. 

Unless these fields are nulled out in the fragment’s onDestroyView, (which you can’t do for a lateinit variable), the references to the views now live for the duration of the fragment’s lifecycle, and not the fragment’s view lifecycle as they should. 

    The simplest scenario of how this causes a leak: We are on FragmentA. We navigate to FragmentB, and now FragmentA is on the back stack. FragmentA is not destroyed, but FragmentA’s view is destroyed. Any views that are tied to FragmentA’s lifecycle are now held in memory when they don’t need to be.

For the most part, these leaks are small enough to not cause any performance issues or crashes. But for views that hold objects and data, images, view/data binding and the like, we are more likely to run into trouble. 

So when possible, avoid storing views in class-level variables, or be sure to clean them up properly in onDestroyView.

Speaking of view/data binding, Android’s view binding documentation tells us exactly that: the field must be cleared to prevent leaks. Their code snippet recommends we do the following: 

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

This is lot of boilerplate to put in every fragment (also, avoid using !! which will throw a KotlinNullPointerException if the variable is null. Use explicit null handling instead.) We addressed this issue is by creating a ViewBindingHolder (and DataBindingHolder) that fragments can then implement:

interface ViewBindingHolder<B : ViewBinding> {

    var binding: B?

    // Only valid between onCreateView and onDestroyView.
    fun requireBinding() = checkNotNull(binding)

    fun requireBinding(lambda: (B) -> Unit) {
        binding?.let {
            lambda(it)
        }}

    /**
     * Make sure to use this with Fragment.viewLifecycleOwner
     */
    fun registerBinding(binding: B, lifecycleOwner: LifecycleOwner) {
        this.binding = binding
        lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                owner.lifecycle.removeObserver(this)
                this@ViewBindingHolder.binding = null
            }
        })
    }
}

interface DataBindingHolder<B : ViewDataBinding> : ViewBindingHolder<B>

This provides an easy and clean way for fragments to:

  • Ensure binding is present when it’s required
  • Only execute certain code if the binding is available
  • Clean up binding on onDestroyView automatically

Temporal leaks

These are leaks that only stick around for a short duration of time. In particular, one that we ran into was caused by an EditTextView's async task. The async task lasted just longer than LeakCanary’s default wait time, so a leak was reported even though the memory was cleaned up properly soon afterward. 

If you suspect you are running into a temporal leak, a good way to check is to use Android Studio’s memory profiler. Once you start a session within the profiler, take the steps to reproduce the leak, but wait for a longer period of time before dumping the heap and inspecting. The leak may be gone after the extra time.

screenshot of Android Studio’s memory profiler

Android Studio’s memory profiler shows the effect of temporal leaks that get cleaned up.

Test often, fix early

We hope that with this overview, you’ll feel empowered to track down and tackle memory leaks in your own application! Like many bugs and other issues, it’s much better to test often and fix early before a bad pattern gets deeply baked into the codebase. As a developer, it’s important to remember that while memory leaks may not always affect your own app performance, users with lower-end models and lower-memory phones will appreciate the work you’ve done on their behalf. Happy leak hunting!