Categories
android

Smooth Operator: Using StrictMode to make your Android App ANR free

Performing any kind of long blocking operations or disk IO operations on the Android Main thread can cause ANR issues. (Application Not Responding). You may not even realise that you have a potential ANR until it is too late and is already in your user’s hands.

If you are lucky, the library or framework you are using will not allow you to perform disk operations on the main thread (Room for instance, makes it explicit when you want to turn it off).

But how can you pick up on these issues in your app, when the libraries or frameworks don’t explicitly prevent this kind of operation? Luckily there is a class in Android called StrictMode that can help you find these issues.

What is StrictMode? 🚨

StrictMode is a developer tool that you enable on start up of your application, which can help pick up operations that are happening on the main thread. It can automatically terminate your app (or log it to logcat), when a violation has occurred.

This can help prevent ANR’s from happening and overall make your app a smoother experience, since you will be made more aware of the potential issues your app has.

Enabling StrictMode

To enable StrictMode doesn’t require much. In your Application class, add the following (Make sure to only enable it for debug/developer mode in your app):

class CustomApplication : OverApplication() {

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG){
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()
                    .build()
            )
            StrictMode.setVmPolicy(
                StrictMode.VmPolicy.Builder()
                    .detectAll()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

You can choose how you want StrictMode to react when certain violations occur. In the above case for the threading policy, I’ve set it to penaltyLog(), which will only log the violations in logcat. You can also set it to penaltyDeath() if you would prefer the application to crash when a violation has occurred. I would recommend first setting penaltyLog() and once you have fixed all your StrictMode issues, set it to  penaltyDeath() since the errors will then be more visible when adding new code.

Now, run your application and use it as per usual, if a violation occurs, you should see the StrictMode violation logged out to the Android logger.

Example of a StrictMode violation ⛔️

Here is an example of a Disk I/O operation violation reported by StrictMode in an app of mine. You can see it points out that its a DiskReadViolation and points to the specific line in my code where I am reading the disk on the main thread:

StrictMode policy violation; ~duration=154 ms: android.os.strictmode.DiskReadViolation
        at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1556)
        at libcore.io.BlockGuardOs.access(BlockGuardOs.java:69)
        at libcore.io.ForwardingOs.access(ForwardingOs.java:73)
        at android.app.ActivityThread$AndroidOs.access(ActivityThread.java:7246)
        at java.io.UnixFileSystem.checkAccess(UnixFileSystem.java:281)
        at java.io.File.exists(File.java:815)
        at android.app.ContextImpl.ensurePrivateDirExists(ContextImpl.java:645)
        at android.app.ContextImpl.ensurePrivateDirExists(ContextImpl.java:636)
        at android.app.ContextImpl.getFilesDir(ContextImpl.java:681)
        at android.content.ContextWrapper.getFilesDir(ContextWrapper.java:243)
        at dev.riggaroo.ImageLoader.loadImage(ImageLoader.kt:41)

Below is the snippet of code that StrictMode has highlighted. Although I am loading the actual image off the main thread using Glide, accessing context.getFilesDir() is happening on the main thread:

val file = File(context.filesDir, "/path/to/image.jpg")
GlideApp.with(itemView.context)
    .load(file)
    .into(filterImageView)

To fix this wasn’t as simple as one would hope. I ended up creating a custom class that would be passed to Glide and then Glide would run a custom ModelLoader, which gets access to the filesDir inside of it. This is the class I created which doesn’t do any disk access when its created:

data class FilesDirPath(val path: String) {
    fun getUri(context: Context): Uri {
        return Uri.fromFile(File(context.filesDir, path))
    }
}

Then I needed to implement a custom ModelLoader which took the class above and used it to load up the image. It is worth noting that buildLoadData runs off the main thread, so it is a good place to access the file system:

class CustomFilesDirImageLoader(
    private val context: Context,
    private val modelLoader: ModelLoader<Uri, InputStream>
) : ModelLoader<FilesDirPath, InputStream> {

    override fun buildLoadData(model: FilesDirPath, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
        val uri: Uri = model.getUri(context)
        return modelLoader.buildLoadData(uri, width, height, options)
    }

    override fun handles(model: FilesDirPath): Boolean = true

    class Factory(
        private val applicationContext: Context
    ) : ModelLoaderFactory<FilesDirPath, InputStream> {

        override fun build(
            multiFactory: MultiModelLoaderFactory
        ): ModelLoader<FilesDirPath, InputStream> {
            val modelLoader = multiFactory.build(Uri::class.java, InputStream::class.java)
            return CustomFilesDirImageLoader(
                applicationContext,
                modelLoader
            )
        }

        override fun teardown() {}
    }
}

Then in our custom AppGlideModule, we need to register this custom ModelLoader that should be used when the FilesDirPath is used.

@GlideModule
class CustomGlideAppModule : AppGlideModule() {
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        super.registerComponents(context, glide, registry)

        registry.append(FilesDirPath::class.java,
            InputStream::class.java,
            CustomFilesDirImageLoader.Factory(context))
    }
}

Then I create a FilesDirPath object where I was previously accessing the file system on the main thread:

val filesDirPath = FilesDirPath("/path/to/image.jpg")
GlideApp.with(itemView.context)
    .load(filesDirPath)
    .into(filterImageView)

And this removes the StrictMode DiskReadViolation error! We can confirm that we are indeed fixing the StrictMode issues by running our app again and seeing if anything is logged.

Summary

There are plenty of other ways in which you can be violating StrictMode policies, this is just one such example of an issue and how I resolved it.

I’ve found StrictMode to be an extremely valuable tool to help with these issues. You may think you are doing everything off the main thread, but sometimes small things can creep in and cause these issues. StrictMode helps keep our apps in check and should definitely be enabled whilst developing your applications.

Have something to add or ask? Find me on Twitter @riggaroo.