跳转至

10 Ways to Improve Your Android App's Performance

10 Ways to Improve Your Android App's Performance

Everyone knows how important performance is for the success of an app, but how do you get your app running at peak performance? In his talk at DroidCon NYC 2015, Boris Farber brings his wealth of experience working with Android APIs at Google to help you avoid ten common pitfalls and keep your app running as fast as humanly possible. Learn how to avoid falling victim to long launch times and bad scrolling, in order to create a responsive app with a smoother user experience.


Save the date for Droidcon SF in March — a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.


Introduction (0:00)

Hi, my name is Boris; I am a Developer Advocate at Google, and I’m focusing on performance-heavy apps. This post is the best practices and the mistakes I have seen while consulting partners on their Android apps. If you have a small app, read on, but you will likely only need this information when your app gets larger.

The rules of thumb that I have found from working with data intensive Android apps. Usually, the app that I am looking at has a long launch time, janky scrolling, or even in the worst case, unresponsiveness. We are going to see a bunch of ways how can we make those things better and faster because, at the end of the day, we all want to be successful.

Activity Leaks (1:17)

Our first friend in things that we need to fix is Activity leaks, and let’s see how they happen. Activity leaks, or memory leaks in general, are extremely dangerous. Why do they happen? If we hold the reference in our code to an unused Activity, we hold all of the Activity’s layout and, in turn, all of its views. That results in many, many pixels you don’t want to store in memory. Another really problematic thing is keeping static references. Don’t forget, both Activities and fragments have a life cycle. Once we have a static reference, this Activity, or fragment, will not be garbage collected. This is why static references are dangerous, they become dangling pointers.

m_staticActivity = staticFragment.getActivity()

I have seen this code too many times, unfortunately, and on one occasion it took two weeks of my life to understand why the app was crashing at unexpected times. Not my favorite activity to run after those bugs. I hope it is not yours. We start with the first, easiest way to make a memory leak, which is by leaking listeners. Say we have the following code, public class LeakActivity extends Activity and guess what? We have some NastyManager, which is unfortunately a singleton, and we add activity to this manager by addListener(this).

public class LeakActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    NastyManager.getInstance().addListener(this);
  }
}

If you want to see more details in the blog post that I posted in Android Developers, maybe three, four weeks ago, what happens is NastyManager holds our Activity. So, the Activity might not be presented, but it will be in memory, and this is a really, really bad idea. Let me show you how to fix it pretty easily.

@Override
public void onDestroy() {
  super.onDestroy();

  NastyManager.getInstance().removeListener(this);
}

onDestroy, for our Activity, we just unregister ourselves. Of course, we have a better discussion, should we use singletons in the first place? Usually, we shouldn’t, but sometimes it’s the only solution. That’s why software engineering is interesting. We have a tradeoff, and we have a design. So please, don’t forget to remove your listeners. In order for us deeper to understand why these problems happen, you need to remember the structure. What does it mean to be an inner class? We have a Handler, which we’ll look at shortly, which is an inner non-static class, to the scoping class, which is Activity.

As we are going to see shortly, as long as Handler alive, the bounding Activity is alive. If you don’t believe me, try to read the JVM spec, this is funny. Here is another memory leak. To represent the example, we have a Activity, and inside we have a Handler.

public class MainActivity extends Activity {
  //...
  Handler handler;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //...
    handler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
              }
  }
}

Excellent way to communicate things. Handler, asynchronous communication, thread-safe, all good things. What happens if you have the following code? handler.postDelayed next hour. What does it mean? It means, as long as a Handler has a message, it’s alive. And if a Handler is alive, it’s enclosing Activity is alive, even though we might not need it but we pay for the memory. This is what we call a memory leak. Let’s see how we fix it. I have a confession to make, the fix is a little bit tricky. We really recommend using WeakReference. What’s a WeakReference? WeakReference, if it’s the only reference to underlying object, the garbage collector will clean it. That’s it – truly simple. Here, We declare a Handler with a WeakReference, so if the Handler is alive and we don’t need Activity, and the WeakReference that the Handler holds is the only reference. The Activity is cleaned. I admit this fix is a little bit tricky. This is the ecosystem that we are living in.

private static class MyHandler extends Handler { 
  private final WeakReference<MainActivity> mActivity; 
  // ...
  public MyHandler(MainActivity activity) {
    mActivity = new WeakReference<MainActivity>(activity);
    //... 
  }

  @Override
  public void handleMessage(Message msg) { 
  }
  //...
}

To recap, we have an inner class, like Handler. Inner non-static class can’t live in isolation from the scoping class, which is Activity. So please, when you come back to your code, take a look at all your inner classes and make sure that you don’t have a memory leak there. You and your users will thank you later.

In general, please prefer static to non-static inner classes. Basically the difference is non-static inner class is not bounded to its enclosing class, such as Activity. Note that both classes, Handler and Activity, have a different lifetime. This is one of the reasons for the most, most annoying bugs that I see. Most of the time I am trying to make you all successful on Android, but I also work with companies trying to sort out their problems, and many of them come to me with memory leaks.

What Can You Do About Activity Leaks? (8:37)

  • Please remove away static references.
  • Consider using event bus to decoup our listeners from senders.
  • Please unregister your listeners.
  • Please prefer static inner classes to non-static ones.
  • Please do code reviews. Based on my personal experience, the code reviews are so far the most effective way to find memory leaks.
  • Understand your application structure.
  • Use tools such as MAT, Eclipse Analyzer, LeakCanary.
  • Print logs on callbacks.

Scrolling (10:05)

The secret for effective scrolling is as follows: use UI thread only for UI. This alone would solve 99% of your problems. Never do the following on UI thread:

  • Never load images.
  • Never do networking calls.
  • Never parse JSON.
  • Never access databases.

There are means to do them. Some of them even fast. For images, networking, JSON, use libraries. There are a lot of community solutions, and some of the best presenters are even presenting in this Droidcon conference. For database access, consider using Loader, who actually do batch updates to databases, and to make you focus on what’s important in your app.

Images (11:26)

As far as image libraries go, the following typically provide the required functionality, based on our experience working with leading companies: Glide, Picasso, Fresco. Just know that they have all different design trade-offs, especially around the large images count.

Memory (12:13)

We managed to convince ourselves that the bitmaps are tricky, basically because of their large size and fragmented heap. When I was responsible for an app with a lot of images on the pre-4.0 devices, that made life hard. Also, memory management is tricky. Some of the stuff, it’s better to put in file, and some of the stuff it’s better to keep in memory. Please note, we have the class LRUCache which is an efficient cache implementation.

Networking (12:54)

First, the problem with Java networking, which made its way into Android, is the tricky APIs. In case you don’t know, many java.net API calls are blocking. This is another excellent reason to not do networking on the UI thread. Remember that stream calls are blocking, and to make things even more interesting, equals of the URL class result in DNS call – this is time that you are paying. Use libraries, or any other threading solution that fits to your program.

The hard part about the networking is they’re doing Async Http. If I remember correctly from Android 4.4, OkHttp is the part of a Android sources, however, if you feel that you need the latest version of OkHttp with some stuff that hasn’t made its way into the official Android, consider bundling your own. There is another public library which is made by Google, Volley. There is a excellent library made by Square called Retrofit for your REST calls, all the solutions you have to make your app network-friendly successful.

Large JSON (14:35)

This is another component of things you shouldn’t do on UI thread because parsing large JSON has a performance effect, especially if converted to a class with a lot of getters and setters. What we have found so far is if you have a small JSON, the GSON, also made by Google, gives the best result.However, I consider a rule of thumb, maybe less than 500k could be small, anything larger could be considered as large. And also, don’t forget, this is a moving target. On a strong device such as Nexus 6, something we consider as large is small, but if your app has to support previous devices you should take it with a grain of salt.

For large JSON, I would say the best to use or the fastest to use are Jackson and Instagram JSON Parser. They did really interesting work of generating the stream parser code. From company feedback, ig-json-parser gives the best results so far.

Looper.myLooper() == Looper.getMainLooper() is the code you can use to make sure you are not on the UI thread.

What can we do to speed up our scrolling? (16:56)

  • Please keep UI thread only for UI.
  • Understand concurrency APIs.
  • Please use libraries.
  • Use Loaders

The reason to use libraries is because the code is tricky. That doesn’t mean you can’t write a good library by yourself, however, if I were to choose should I focus on something which exists whether, say, providing more value to my business, I will definitely go for providing more value to the business. Developing a library is something which is expensive, and if you have libraries right off the shelf, consider using them. They are supported, maintained and have a community behind them.

Concurrency APIs (18:00)

I find these really important to understand in order to make your app fast and responsive. People including myself unfortunately forget that Service methods run on UI thread. Please consider using IntentService, AsyncTask, Executors, Handlers and Loopers.

Let’s focus on the trade-offs of the first three.

IntentService (19:07)

In my previous company, I used the IntentService to perform uploading to the server. IntentService is a single-threaded, simple, one job at a time. We don’t have any job system, meaning we can’t keep track for jobs, and we have essentially no normal way to stop it. If you have some large operation that doesn’t touch UI, consider using it. What I did in my previous company was doing upload to one IntentService and showing a notification icon once I’m done.

AsyncTask (19:56)

The AsyncTask is a solution if you have a thing that you need, or basically something that you don’t care about the result of outside the UI. However, please note that even though AsyncTask is relatively easy, it has some dark corners. Activity life-cycles, especially during rotation, need to be closed and dismissed. Otherwise it could cause memory leaks, and as we have seen on our first bullet, Activity memory leaks are not fun. Another thing that I have problems with is it was changed rapidly around the Honeycomb and Gingerbread. As a result, I forked the AsyncTask sources from the Android source tray, bundled it in my APK, and that’s it. Sometimes, this could be solution.

Executor Framework (21:11)

This is another friend in the concurrency APIs that we got from Java 6 and on, this is essentially a thread pool managed by the system, which has callbacks, futures and mostly I use it for MapReduce tasks. Meaning, if you have a big job and you want to break it into small pieces and give each piece a thread, the Executor Framework could be your best solution. For instance, I used it when I wrote my open source git project three or four years ago. Even though it was developed on Nexus S, in the dark ages, when we ran it on Nexus 6, it performed extremely well.

What can you do about concurrency APIs? (22:07)

  • Please learn and understand the APIs and tradeoffs.
  • Please make sure that you map the correct solution to the right problem.
  • Understand what your problems are and see, which of the API that I presented before, can defeat the best.
  • Please refactor your code

Deprecation (22:42)

We all know what deprecation is and why we should avoid it. Not only is deprecation bad, system abuse is bad. Here are some classic examples:

  • Please don’t call private APIs by reflection.
  • Please don’t call private native methods from the NDK and C level.
  • Please don’t just Runtime.exec to communicate with processes.
  • adb shell am to communicate with other processes is not something we want to support.

I have seen zillions of bugs, especially when we introduced Android L, when we blocked this feature. Abusing private APIs, which are meant to be private, is bound to lead to failure.

Deprecation means that some API will be removed and, usually one or two days before the major release, your app will not work. To make things even worse, sometimes if you are dependent on the old version of a library, there is no way to update both APIs and tools. Unfortunately, I saw a company whose whole application was dependent on a library that could only be built with Android 3.0. Android 3.0 was a long, long, long time ago. This is a bad thing. Please don’t do it.

Most of the time there is a compelling reason for us to deprecate APIs to improve security, correctness, and of course, performance.

To avoid issues with deprecations:

  • Know and use proper APIs.
  • Refactor your dependencies.
  • Don’t abuse the system.
  • Update your dependencies and tools.
  • Newer is better.

Prefer Toolbar to ActionBar, and prefer RecyclerView, especially for animations, because it’s optimized for that. As a latest and greatest example, if you don’t know, but we removed the Apache Http connection at M. By the way, it’s still available as a gradle dependency. Please use HttpURLConnection. It has a simpler API, smaller size, transparent compression, better response caching, and other cool things.

Architecture (27:03)

Bugs around a lifecycle in architecture are the most prominent and annoying. To avoid these, learn the app components life cycle. What are the flags around Activities, what are fragments, what are stated fragments, what is task, and what are flags. Read documentation and add simple logs on callbacks, playing around with flags.

Please work with the framework and not against it. Framework components have a purpose, which are specific semantics, and use when these semantics are desired. There’s a reason we have broadcast receivers. There is a reason a service is different from an activity. In general, avoid over-engineering. Unfortunately, I have seen too many cases when I talk to companies about their architecture, and they just create unnecessary layers of mud above the framework components. Don’t do it. Keep it simple. Then, your app performs better, and other good things.

I’m also often asked, “what it better? Should I pick Picasso or Glide? Should I pick Volley or OkHttp?” And usually there is no right answer or 100% correct answer. However, the following checklist is something that I’m using when I’m discussing with the companies what they should do in terms of architecture.

  • Make sure it solves your problem.
  • Make sure it plays nicely with your current dependencies.
  • Please know dex method count.
    • Nowadays it’s less of a problem since we introduced the multi-dex flag, however, it’s always good to be aware of it.
  • Please check the dependencies.
    • The last thing that you want is a dependency clash. You have version Z of some library, but the third-party library requires a different version. This is what is called, at least in the Windows world, the DLL Hell. I guess in the Java world its more of a Jar hell. Try to avoid it.
  • Know the maintenance.

All in all, speaking of architecture and design, the best idea is to design your app sleeping most of the time and to be extremely responsive when it’s awake. For me, the best app is the app that gets out of my way in less than 10 seconds. I thus recommend you to be consistent, as consistency is the cornerstone of every architecture. Make sure you can get people on board quickly. It always helps to have somebody or someone who is experienced in that architecture before, and in general please pick your dependencies wisely.

原文链接: https://academy.realm.io/posts/droidcon-farber-improving-android-app-performance/