Android's 64K Reference Limit

While many downplay the impact of breaking the 65,536 method reference barrier, I've come to give it a second look. "Just focus on shipping" is a saying all to rampant in tech. You often end up with throwaway solutions that really aren't focused on sustainability. There are few reasons I don't believe in Multidex even though I have to deal with it day to day.

  1. It slows down your build process. Not only is it using ProGuard every time you build, you have to bundle up that much more code. You can target API 21+ to bypass the ProGuard part, but you might need to target API 23+ pulling you further away from many of your users.
  2. It could slow down app start up. From personal experience and reported by others, your app could start between 15%-20% slower.
  3. It might be pointing to a larger problem.

At a bare minimum, you need to measure and track your method reference count. The good news is for most apps this is completely avoidable with a few key principals.  

  1. Don't pull in an entire library for a few classes and don't wait for ProGuard to strip out unused stuff for you. Consider grabbing the source directly or using Jar Jar to repackage the dependency.
  2. Avoid as much of Google Play Services, Support Library, and AppCompat as you can. While they provide a ton of handy stuff, you most likely won't use it all. Following Material guidelines is easy, enabling you to quickly build most components in fraction of the method count. With v25 you may be able to take bits and pieces of these libraries, but many still depend on each other.
  3. Be cautious of or avoid auto generated code. If you can't, find or make a better generator. Thrift is a great example where it includes a ton of extra methods many won't use. Also get your definitions in check to make sure Android only gets the enums it needs and not all the junk your server uses too.
  4. All too often people's first step is to search Android Arsenal or Github for a canned solution – write more of your own solutions! They'll be tailored to your app and often slimmer than a general purpose counterpart.
  5. Use ProGuard to remove unused methods. You are probably already doing this, but you may be able to tighten up the rules.
  6. Understand what happens when you (don't) access a private field from an inner class. There could be 1,000s of hidden methods you didn't write!
  7. Lastly, don't wait until you hit 64k methods. Continually check this and other key metrics like bundle size. This is even more important the larger your team gets.

View#onDetachedFromWindow

When making a custom View subclass something I often notice is overriding onAttachedToWindow and onDetachedFromWindow for lifecycle events. This isn't inherently bad, but the contents of that override could be making your View less flexible. Take this example where we are using an event bus.

public TestView(Context context) {
  super(context);
  EventBus.register(this);
}

@Override
protected void onDetachedFromWindow() {
  super.onDetachedFromWindow();
  EventBus.unregister(this);
}

This works really well if that View will always be attached, but if its removed then reattached you'd notice you no longer receive events. Thats because onAttachedToWindow and onDetatchedFromWindow need to work together here and more appropriately represent "my view does or does not have a surface to draw on", not a typical lifecycle.

In this next example, ImageView represents this quite well. It uses attach events to determine Drawable visibility states and hints the view to redraw itself if needed. The most important distinction is that the ImageView is completely reusable and does not irreparably destroy itself simply because its been detached.

@Override
protected void onAttachedToWindow() {
  super.onAttachedToWindow();
  if (mDrawable != null) {
    mDrawable.setVisible(getVisibility() == VISIBLE, false);
  }
}

@Override
protected void onDetachedFromWindow() {
  super.onDetachedFromWindow();
  if (mDrawable != null) {
    mDrawable.setVisible(false, false);
  }
}

In our event bus example, this fix is quite easy – just move the register call into onAttachedToWindow. If you need a true lifecycle matching that of an Activity or Fragment, your best bet is to pass those events through manually or by using a library like Lightcycle. Also take a close look at what that View is doing – you might be better off having the parent Controller, Activity, or Fragment do the work and passively update the view in question.

Which densities?

There are seven different DPI qualifiers recognized by Android. Just because they're there doesn't mean you should support them all. By shipping fewer sets of resources you keep your designer sane and your APK a bit leaner. My recommendation is to pick just two or three and let the system automatically scale them for you.

If resources are not available in the correct density, the system loads the default resources and scales them up or down as needed to match the current screen’s density.
— http://bit.ly/DensityConsiderations

So which do you pick? In order of importance - HDPI, XHDPI, and XXHDPI. Collectively these cover 83% of devices. The other 17% may experience inexact pixel placement once scaled and there is some cost (both memory and CPU) to the automatic scaling. In the vast majority of cases this is negligible and you ultimately should optimize for the majority. Serving emerging markets might mean focusing on MDPI and HDPI.

TIP: Have full screen tutorial images? If they absolutely must be bundled, just ship XHDPI version to save on size while maintaining decent quality. Also consider changing the design to something that uses the real app UI. If that's not an option dynamically downloading the right version would be a nice middle ground.

Android Asset Sizes

Even after Google's effort to clearly document how to create assets, every Android developer will end up getting the wrong asset from time to time. So here is a dead simple and definitive answer.

MDPI 1x
HDPI 1.5x
XHDPI 2x
XXHDPI 3x
XXXHDPI 4x

TIP: By designing your assets based on a multiple of 2 you are all but guaranteed a clean scale at the various sizes. 13px at MDPI would be 19.5px in HDPI which could lead to ugly subpixel rendering.