Developers used to the Java programming language from years spent in the trenches of web, server, and even desktop computing have developed their own patterns of using the language and the ecosystem of libraries surrounding it. But writing mobile apps is not the same as writing these other kinds of applications, and good Android apps will have to take mobile constraints into account. Learn how to use the language, the runtime, and the platform effectively, and how to write better and more performant Android applications.
This talk is structured around a series of 10 articles written by Android’s platform team. All of these articles are available on Medium, the first of which is here. We will go over a bit of what is in those articles in this post, but please check them out for a more in-depth discussion of specific topics.
Why is Mobile Development So Difficult? (1:47)
Limited memory (1:47)
One of the big issues that we have with a lot of our developers at Google is a misconception about the true nature of the phones that you carry in your pocket every day. Memory is extremely limited on these devices, and the same applies to CPU, and battery. You have to understand that your app, while important to you, will not be the only app on the device. The memory is limited, but it’s also shared by the entire system. This is something that our framework team is extremely conscious about, and that’s why sometimes we suggest tips and tricks that may seem to be a little extreme. We try to think of the problems as a whole, as as an entire platform with 20, 30, 40 processes running at the same time. It can be difficult when you’re working on a single app to keep these limitations in mind.
On the devices that you have today, they may have between 1-3 GB of RAM, but that is not true of all the devices in the Android ecosystem. There are currently 1.4 billion active Android devices in the world, and a lot of these are devices that you may have been buying two or three years ago. In fact, those older devices probably account for the bulk of these 1.4 billion devices. Most of the Android devices are not in the U.S. or in Western Europe, but in countries like India or China, where the devices need to be cheaper to reach more people.
There was a big push when were were working on Android 4.3 and 4.4 to try to bring back the memory usage down to something reasonable so that Android could run successfully on limited memory devices. For the longest time, the reason so many Gingerbread devices were out there was because people wanted to ship cheap devices with 500 MB of RAM, and the only version of Android that could successfully run on it was Gingerbread. Things have ballooned since then, of course, so now there are almost no Gingerbread devices left. However, because the most recent versions of Android have bigger applications, this means a bigger framework and a bigger system.
It’s something you need to think about constantly, but it’s hard because it’s not always your priority. You may know the infamous quote by Knuth, “Premature optimization is the root of all evil.” As much as I agree with him, it’s also very difficult to go back after you’ve written your app and find 50 MBs of RAM to free in your application. We’re not saying that you should destroy the architecture of your app or sacrifice testing, but every time you can do something that helps with memory usage, please, by any means, do it.
Develop for a better user experience for the device as a whole. When your app is bigger, you kill the other apps, so you make the user experience worse for the other developers. When another developer doesn’t pay attention to their RAM usage, your app suffers too. Be very mindful, and please talk to each other and figure out how you can work on the same device.
If the system has to kill everything else on the device just to run your application, then when the user actually leaves your app, it has to cold start the other thing that the user wants to use. You can make your own app look bad when you do that. If you really want to have a nice experience when the user gets back to the application, please do whatever you can to keep it running for as long as possible by using less memory.
In some applications, it may be useful to implement memory callbacks. There’s an interface you can find pretty easily in the API where you will get this
onTrim memory callback, and there are several levels of trimming. Depending on the level of trimming, you might want to free up some of your caches, liberate some bitmaps, maybe even kill Activities, or whatever you can to help your application in the background.
CPU on mobile is obviously slower than the desktop and server. There’s an aspect of CPUs in mobile devices that maybe isn’t obvious from the outside, which is that the CPU is pretty much throttled most of the time. This means that CPUs are capable of running at some fast speeds, but still nowhere approaching what you may be used to on desktop and server systems. It can reach these max speeds, and it will during certain situations, like if the user is dragging on the screen. Otherwise, it’s running at some idle speed instead, just so that the battery can actually last as long as the user needs it.
If you buy a 2 GHz desktop or laptop, that’s what you’re getting, but if you buy a 2 GHz phone, you will only sometimes get 2 GHz. If you buy an eight core phone, you may get eight cores, but you won’t most of the time. Basically, the marketing specifications that you see on the box do not match what actually happens at runtime. It’s mostly about throttling and preserving battery, but it’s also about heat.
For GPU, all the same things apply about speed as well as throttling capabilities. Texture uploads tend to be expensive, so anything that is a bitmap, or results in a bitmap, will result in that being uploaded to the GPU. For example, if you’re drawing a path, these things go into bitmaps which get uploaded as textures, and can cost. You may suffer performance bottlenecks because of that.
There’s also a dynamic between fill rate and densities. The density of the Nexus 6P is massive, which means there’s a lot of pixels to fill on the screen. But, the rate of bandwidth performance has not kept up with that over time. The system is working harder and harder to fill all these pixels. The more you ask us to do, the longer it’s going to take.
One big tip for UI performance is around overdraw. The more times you’re telling us to fill every pixel on the screen, the more we’re going to suffer as the screens get bigger and the density gets bigger. If you have a Playstation 4 and the XBox One, they struggle to run games at 1080p at 30 frames per second. On our phones, we have a higher resolution and we try to do everything at 60 frames per second; we try to do as much or a lot more with way, way less power. That is why we insist so much on graphics performance, and how to optimize your application, because you do not have the power that you think you have in your hands.
Get more development news like this
Memory = performance (12:29)
If you have a larger application, there’s going to be more thrashing of tasks, but also slower allocations. There’s simply more for the runtime to have to wade through to figure out where it can put new objects. You will also see slower collections for the same reason; it’s just more stuff to go through every time it needs to look for memory.
There is just more garbage collector activity overall. In our talk, we take a deep dive into how the garbage collector works, and discuss the changes that are present in ART vs. Dalvik. Please see the above video at 13:06 for the full recap.
Low-End Devices (19:40)
The device you have in your pocket is guaranteed to be faster than your user’s device. What you may think of as the old, slow devices are not gone. A, they’re still in your users’ pockets, and they want those devices to last as long as possible, and B, those older, slower devices are always cheaper, which means they’re even more attractive to people in places where they may not have the means to buy the fastest devices available.
Smooth Frame Rates (21:01)
Getting a smooth frame rate is really difficult. You only have 16 milliseconds to do everything; that includes handling touch events, doing measurement, layout, drawing the frame, and swapping the buffer. 16 milliseconds is equivalent to 60 frames per second, which is what we do on Android because we are V-synced. We don’t let the screen tear, which is something you can see sometimes in games where you see two buffers at the same time for a fraction of a second. That looks horrible, so we don’t let you do that. That means that if you take just a little bit more, even 17 milliseconds, we’re going to skip a frame. Then you skip a V-sync, and instead of running at 60fps, we’re going to run at 30fps. That’s what we call jank.
Flopping back and forth between 60fps and 30fps is really bad. It makes users get the feeling that the app is really janky and inconsistent. It might actually be better for your application to always be bad, to always target 30fps consistently. That’s something that a lot of games do when they know they won’t be able to do 60, they just cap at 30.
Runtime: Language ≠ Platform (22:48)
A few weeks ago, someone asked me, “What do you do when there’s a new release of the JDK?” and I realized that they didn’t understand that the language does not equal the runtime. When there’s a new JDK, I couldn’t care less. It has nothing to do with the Android runtime. We use a common language, but that doesn’t mean we have anything to do with the runtime.
There are three elements that lead to the overall experience people have when they’re using the Java programming language: the language itself, the runtime (HotSpot, in some server environments), then the hardware it’s running on.
For those used to dealing with servers, the server runtime has some things like a moving, compacting collector, which tends to mean that temporary allocations are incredibly cheap. It’s as fast as moving a pointer around, because essentially, that’s what’s happening. Then you have the CPU, which I think we can quantify as being very fast, and then we have memory, which we could quantify very exactly as being basically infinite.
On Android, the dynamic is very different, especially if you’re used to infinite resources and a completely different runtime environment. We have Dalvik and ART. On Dalvik, we have no compaction, which means when you allocate an object, it will stay at that point in the heap. The heap can get fragmented, and it can be more expensive and more difficult to find free space for other objects in the heap. On ART, we have idle time compaction. ART never compacts the heap while the app is in the foreground, but when it goes into the background it can actually compact the heap. Over the life of the process, there’s actually some compaction that can happen, which helps a lot for finding free memory.
Compaction has a pretty critical effect on native code. If you use JNI in your application, it assumes when you grab a pointer to a Java object, that pointer is always going to be valid. In ART, if your heap gets compacted, the pointer is going to point to god-knows-what. Be very careful, because if you write JNI code, your code is probably wrong unless you were careful to begin with.
UI Thread (26:56)
Android is almost a single-threaded UI system. Technically, you can have mutliple UI threads, but it’s difficult to deal with. As far as you’re concerned, there’s one thread for the UI toolkit. Being single threaded, anything you do that might block the UI thread is going to have a deep impact on performance, jankiness, and consistency. Allocattions in particular can be bad. If you have a background thread making allocations, you will skip a frame when the VM has to block all threads (including the UI thread) for a few milliseconds. You will go to 30fps, and it’s going to be bad.
Storage performance varies. Sometimes people are storing onto SD cards, which have way different performance metrics. There’s also different performance, even on the same storage devices, once they start to fill up. If your app is dependent upon the storage speed on a particular test device, that’s probably not a good thing. Writing to flash storage means a lot of work for the controller because it has to do the collection and keep track of what’s used and what’s not. That means that if your application is in the background doing a lot of I/O operations, you might slow down everybody else. You may have experienced this when Play Store is installing updates in the background and suddenly your device feels a little slower. It does all these writes to disk, and it becomes very difficult for the application in the foreground to read its resources.
Storage sizes also vary, so don’t bet your app’s future on a particular size out there. APK size also matters. The amount of resources and assets you have in your APK can affect the time that it’s going to take to launch.
There are different ways to optimize the size of your resources. You have vector drawables now on older version of Android. If you can, you could use one of the SVG libraries for Android. It’s not suited for your icons, but it can be useful. You can use the WebP format, that uses about 20-30% less storage space than PNG. You can use even simple offline tools like PNG Crush. APT does some crunching of PNGs, but there are tools out there that go even further.
The network you use is guaranteed to be faster than a lot of your users’ networks. Many may be dependent on 2G, and they may pay a lot for that bandwidth. If your app is dependent upon constant connectivity, maybe it shouldn’t be. Maybe you need to have some intelligence where it can download cheaper content when it needs it, or not be dependent on the network for downloading that stuff, and it can get it later.
To test a crappy network, you could take a roadtrip to Utah, or you can use the new bad network emulator. It’s a handy way for you to test your applications at your desk.
Every Device is a Village (33:31)
Every device is a village, which means that everybody has to cooperate to make the user experience on that device work. You can all make it suck together, or you could make it nice together. If your app supports it, you can try to disable services and broadcast receivers and things like that in your manifest. From code, when the user does something that shows that they have the intention of using your application, then you can enable those receivers and services. A good example is an email client; if you have an email client, everything should be disabled by devault when someone installs it. Once they launch the email client and add an account, then you can enable your services and your receivers. Then the next time the user reboots the device, the framework will remember that, and your service will show up. It’s very easy to do, and it’s important, because if your service is running in the background when nobody asked you to run, you’re going to make things worse for everybody else.
The other dynamic is the “tragedy of the commons”: everybody thinks that their application is the most important. If everybody has that attitude, then everybody’s going to be as large and active as possible, and the device will be brought to its knees. Every app you add to the system that will start chipping away at the memory and the CPU makes a big difference.
Know your language (35:55)
There are developers who have years of Java experience, but they may not be using that language in the best way to take advantage of the device characteristics of mobile devices.
Don’t use Java serialization
We’re not talking about serialization in general, which is fine and super useful. However, if you ever try to use the Java serialization, it’s hard to use because you need to generate those UUIDs, and it has limitations. It’s also really slow, because it uses Reflection.
Use Android data structures
A lot of the common collections classes and approaches out there that Java developers are used to may not be appropriate for usage on Androids. There’s a lot of autoboxing going on, and there’s a lot of usage of object types instead of primitive types. There’s also a lot of allocations on the fly iterating through these collections as common practice. We created collection classes specifically to avoid some of these patterns. For keys, we use primitive types, so we don’t have any of the autoboxing associated with HashMap for just allocating an integer to hold a key value. You can use the SparseArray classes. Also, ArrayMap and SimpleArrayMap are good alternatives to HashMap.
The Android data structures are also useful to avoid some of the overhead associated with the basic structures from the Java packages. Every entry in the HashMap is going to use four times the amount of memory as your int. The more stuff you put in it, the more memory you are actually wasting. Look at some of the data structures we have, and don’t hesitate to write your own in some cases.
Be mindful of XML And JSON usage
They’re kind of large, and they’re probably more structure than you need for a specific targeted application. It would be nice if you were using a terser format on the wire, but you can gzip those things anyway over your sockets. However, if you’re going to serialize the data on disk, you might want to look at other alternatives.
Sometimes you need JNI, but not if you’re just using it out of convenience. Something interesting about JNI code: every time you cross the boundary between Java runtime and native there’s a cost because we need to validate the parameters and it has an impact on the GC behaviors. It can be pretty expensive, so when you do a lot of JNI calls, you might spend more time in the overhead of JNI than in the actual code. If you have old JNI code, you might want to revisit it.
One thing you can do in JNI that’s pretty easy is try to batch as many calls as possible. Instead of crossing that boundary back and forth all the time, do it once. For instance, in the graphics pipeline on Android, instead of calling the JNI and having the JNI extract the field data from the Java objects, we pass as many parameters as we can when we call the JNI. We pass primitives, basically, so instead of giving you wrecked objects, we pass the left, bottom, and right, so we don’t have to come back to the runtime side again.
Primitives vs. boxed primitives
Please use primitives instead of boxed primitives. One of the things that’s not obvious is that if you then use these collection classes that then take the object equivalents, we’re going to autobox every time around. We’re going to allocate an object every time we need to use that thing, because it’s going to cast into the boxed equivalent. There’s one exception though: you can use the big boolean, because there’s only two of them.
Even more than avoiding JNI, avoid Reflection. I should point out that, as much as we said to “avoid JNI”, the animation framework uses JNI just to avoid Reflection. This is in terms of both allocation overhead, because we’re going to be allocating these things and autoboxing all over the place, and the method call to mechanisms used internally through the runtime are horribly expensive.
Be careful with finalizers
We use these internally in very limited situations. The thing that’s not obvious about finalizers is that they require two full GCs to actually collect these things. If you put some asset that you want to collect inside a finalizer, we’re going to have to run a full GC twice just to get that thing back. Sometimes it’s necessary, but in other situations, maybe it’s more convenient to put this little closing thing out in a finalizer. It’s horribly expensive.
As mentioned before, your user’s network may not be great, or their network might be really expensive. Moreover, you’re just causing overhead for the system. Maybe you think your app needs that data as fast as possible, but in fact, you’re causing a lot of overhead for the system keeping your stuff alive there.
Allow delayed downloading
This is especially important on bad or expensive networks. You can also batch things up. Collect all the things you need to download and then do that single sync.
Google Cloud Messaging
GCM collects a lot of things. It’s got the transport that it works with to talk to the back end, so maybe you can actually re-use that system instead of creating your own, and every app creating its own socket.
GCM Network Manager (Job Scheduler)
This is a very good thing to use. Job Scheduler allows you to batch these things up. You can say, “I want to do the following transactions when thing are idle, or when I am plugged in, or when I’m on Wi-Fi,” and therefore collect these things into natural time slots. It’s going to be much more effective, and less obtrusive for the user.
Don’t do it, ever.
Only sync what you need to
You know what’s going on in the application, so just get the data that the app actually needs instead of everything that you think it might possibly need.
Network Quality (50:05)
Don’t assume anything about the network. Develop for low-end networks, and make sure to test your application for them too. Even if it’s just in the emulator, you should make sure to check on what we call “crappy networks”. You might be surprised by the kind of behaviors that your application might exhibit in those situations.
Data & Protocols (51:13)
If you control the server, do whatever you can to help the device. Some of that means changing the format, or changing the kind of data you send. It could also be having the device tell the server the size of the images it expects to see. We’ve seen applications internally that were sending images that were four times the size of the screen, and then were resized on the device itself. That makes no sense to have the server do that work.
Use minification and compression algorithms. One is gzip. If you can gzip the data that you send over HTTP, please do it. It’s going to help a lot, especially on crappy networks. GCM helps as well. It keeps the connection alive for you, so, one of the benefits that it’s going to lower the latency, because it won’t have to go through the handshake every single time, and mobile networks are notoriously bad in terms of latency.
Don’t hard-code your file paths
They may change. What if the user opts to put them somewhere else?
Persist relative paths only.
You know where your data is stored relative to where your APK lives, and that’s appropriate, but there are APIs to get these paths. You don’t have to hard code these things, and you should only stick with what’s relative to where your APK actually lives.
Use storage cache for temp files
Again, there are APIs for these things. Please use them.
Avoid SQLite for simple needs
SQLite tends to be kind of expensive, so if you just need something simple, like a key-value store, there are probably cheaper ways to go about that.
Avoid using too many databases
Databases are expensive overall, so don’t use too many. Maybe you can use a single database, but you can use it for several different uses.
Let the user choose content storage location
This is really important if the user has removable storage, or if they can adopt storage now, which is a new approach in the M release. If they’re actually going to adopt new storage and then move stuff there, maybe you should allow them to do that so that your app doesn’t get really confused if they do it for you.
Q: You mentioned one strategy for smoothness is to lower your frame rate. Is it possible for applications to say, “Hey, I want 30 fps instead of 60?”
Romain: Kind of. There’s the Choreographer API that you can use to sync up with the display. You could sync up every other frame. In regular applications, it’s difficult to do unless you know that you’re going to do too much work on every device out there. But, it’s different if you’re doing something using OpenGL or the Canvas and you’re rendering in your own thread and you have complete control over the rendering loop. In that case, you can time your frame, you can do a wait, you can count the V-syncs, those kind of strategies. If you really want to know the advanced techniques on how to do this, you should look up articles by game developers, because that’s the kind of stuff they do all the time.
Chet: In general, you should try to hit the 60 frames a second, and you should use a tool called GPU Profiler, which is now available through Android Studio as well as on device. This is the one that has the multiple colored bars, and you want to make sure to stay below the green line, consistently. So, please look up that tool on the developers.android.com site.
Romain: Also, as always, use systrace. Systace is kind of a low-level, low overhead, system wide profiler. It’s very useful because if your application has a performance issue anywhere, it doesn’t have to be in the rendering code, systrace will not only show you where you’re spending time, but it will show you whether your thread is scheduled, at what frequency the CPUs are running, whether you’ve been migrating from one CPU to another which can have impact on your performance, whether a background thread caused a lock to be held, and you have, maybe, the priority inversion. The documentation is also avaialble at developer.android.com. It’s an extremely useful tool.
Q: There’s a class called
android.os.memfile. It’s linked to the Linux shared memory. When I use it, I’m getting 300-400 MBs of memory in the Linux kernel that you can use to share stuff. So, I’ve used it to share captured video frames and store it in this temporary area, and I set it to be non-purgable. As it approaches 400 MBs of memory, Android will jump in and flush the memory. Do you recommend using this technique to store odd memory stuff, or not to use the shared memory?
Romain: That’s a very interesting question. I have no idea. I was not aware of this behavior, so that’s probably something we should ask the kernel team, to understand what is the behavior here, if it’s expected, and what are the consequences. Sorry I don’t have a better answer for you.
Q: I also want to point out that Android itself, when it switches states in its Activity, uses some memory from the shared memory. They found out by sitting there, monitoring the amount of shared memory in your Android device, you can actually guess which state the Activity is in, like if it’s going from
onStop, so it’s like a security hole. Apparently, Android uses a set amount of shared memory every time it switches states in an activity, something like 12 bytes. So, if you sit there and you monitor the shared memory, you can actually guess which state the Activity is in.
Romain: f you can monitor the memory, you can probably do a lot of other things on the device. Like, look at the screen. But, it’s good to know.
Q: You were talking earlier about how JNI should be avoided, then that Reflection should really be avoided, and that you avoided Reflection at all costs for animating. I wanted to know what the story was behind Object Animator, since that uses Reflection.
Chet: It does not, it uses JNI. It has Reflection code in it. Here’s the way it works:it will dive down into JNI and say, “I need a method.” So you say, I want to animate the property called “foo”. You pass in a string, and it says, “Okay, I’m looking for a setter called setFoo.” Dives into JNI, and it looks at the JNI level and says, “Is there a method with this method signature?” and if it cannot find that, it will return false, and it will use Reflection. To my knowledge, that Reflection code will never kick in. There’s no reason why the JNI should have failed. I had written a Reflection code originally, and as long as I had written it, I may use it in there, in case something weird went on in the system. Then, we still have this backup mechanism of using Reflection. But, I don’t think it should ever actually be used. It’s always going to use the JNI. It’s not obvious from some of the code, but if you look at the right pieces, it goes through a condition first where it says, “Did the JNI thing succeed? If it did, use the JNI mechanism.”
Romain: Effectively, when you’re in JNI, you can access the fields and methods of the JNI class, which is Reflection, and that’s what it does. We discovered that it was significantly faster to do it from JNI than from Java.
Chet: And part of it was allocation as well, because at least, in addition to the runtime overhead, at least JNI did zero allocation for this stuff. The method lookup is not cheap through either mechanism. But, we only do the method lookup the first time you do it. So, when you create the object animator, the first time you start it, it looks for that setter, and possibly the getter, and then it caches those. So, it doesn’t have to go back.
Romain: One of the issues that is you call a set alpha function, set alpha method, through Reflection, you call the invoke method on the method object, and that takes an object. So, you have to pass a Float, which means you have to box your float which is an allocation. But when you do it from JNI, you don’t have to do the boxing.
Chet: It also has a third mechanism, which actually does get used if you opt into it, which is the properties. This is why we added properties in a while ago. There are properties set up for this specific view, properties of alpha, there’s translation X, rotation X, all that stuff. Those actually just use direct setters. So, when you use properties, it’ll call directly into the static property object. You pass in the view instance, and it’ll call the setter on the view, and that’s cheaper than both of them.
Q: It kind of sounded like you were saying not to worry too much about Java releases, because the language is sort of disconnected from the runtime. But, I was curious what you think about things like retrolambda, where it lets you use lambdas, that are a Java 8 functionality, but it compiles them into bytecode. And then as part of dexing, it recompiles them into anonymous classes. How do you feel about that?
Romain: I’m aware of retrolambda, and I think I love the idea. I think it’s wonderful. Before using it, I would go decompile the result of retrolambda to see what exactly happens. Beause with lambda, I’m sure you could have nasty surprises. I’m pretty sure it turns into anonymous classes somewhere, so you might end up with tons of allocations depending on how they’re invoked. For instance, if I pass a lambda in a loop, what happens in terms of allocations? So, I would just go look at the disassembly and see what’s going on before I would decide. You know, the anonymous class could be cached, or it could be reallocated every time, I don’t know. I would have to look at it. It’s one of those cases where it’s important, as software engineers, to try to understand what is going on under the hood before making a decision about how to use it, because you can end up with nasty surprises down the road. And of course, in some cases like if you just using that lambda when you click on a button, that really doesn’t matter, right? Performance is not that big of an issue when you click on a button.
Q: It kind of comes back to wanting to know when it would be supporting Java 8 straight out of the box, so that you don’t have to compile it into these anonymous classes.
Romain: No idea, but honestly, these past few months we have actually quite a bit of Java 8 on the desktop side. I’ve been using the streams, and the parallel streams, and lambdas. It’s great! The code looks amazing, it’s super fun to write until you realize that, in some situations, it’s actually slower than using the old for loops. Once again, be careful, and just try to understand the tradeoffs that you’re making. As far as the Java 8 language is going to be supported in Android, I have no idea, because we haven’t communicated on it. And even if I knew, I couldn’t tell you.
Chet: It’s also worth mentioning, one of my favorite tools is Allocation Tracker in DDMS, or somewhere in Monitor now. It’s good to decompile and see what the bytecode is actually being created as, or compiled as, but it’s also good to just run the thing and see what’s happening at runtime. When we originally did the animation framework, we ran Allocation Tracker to find out that we were actually allocating things on every frame, and then eliminated those. So, if you’re using retrolambda, you want to make sure that, at critical points in your application, you’re not allocating objects that weren’t obvious from the outside.
Q: We typically work in environments where they are very feature driven, right? Internally, how do you manage to maintain the bar for performance? Do you have dedicated QA that track the code, and you look at the stack traces and notify you whenever, or is it something that you just do as a whole feature as developers before handing it out to QA?
Romain: It’s a mix. To a lot of engineers who care deeply by performance, it’s something they’re careful about during development. There’s also QA and they’re paying attention to that, and over the years, the Android team has been writing more automated tests for performance. For instance, for Project Butter, the graphics team came up with the jank dashboards where, basically for every CL, it would run some scenarios like scrolling a list in Gmail, and count the number of frames we missed. And every time that number dropped, or went up, we would get an email and someone would be blamed. Of course the app developers would get blamed, and they would send that back to frameworks saying “Oh, you guys suck, your stuff is too slow.” And we’d send it back to them, and it would go back and forth for a long time.
Chet: We also end up writing a lot of tools and improving the tools that we have. So, systrace is a great example of a tool that we actually needed and wrote and used internally, and then foisted upon the world. And, we continue to iterate on that, which is what resulted in some of the stuff that we did more recently, where there’s tips that give you information about what the problems are, in case it’s not obvious.
Romain: Actually, most of the tools, and you can tell because of their engineer UI. So things like hierarchy viewer, devel-draw, debug tool, the GPU profiler, all on the device. All of those were tools we needed internally, so we created them and then we made available to you guys. Obviously, a lot more could be done, but the big thing about performance is you should write automated tests to try to catch that. It’s difficult though. We do expose some of the internal counters, and I think that actually is documented. With adb shell dumpsys, you have access to some of that information, the time stamps of the frames, the number of janks, especially in Marshmallow and Lollipop there’s been way more added. What you could do is with something like UI automater, you could create a scenario, drive your app, then dump those counters and see what they tell you. Effectively, that’s what those dashboards we have do. They simply grab the output of those commands, and draw a pretty graph on top of it. The other thing is, really think about performance every time you do something. Easy to say, hard to do, and even harder to measure, even harder to understand what exactly you’re measuring. But, that’s the only way to do it.
Q: You talked about not using serialization. Can you talk a little bit more about that? Does it include using Serializable and Parcelable for passing data between Activities and Fragments?
Romain: That as talking about the Serializable interface. Parcelable is the variant of serialization provided by Android, which is pretty efficient, but it’s only to pass data between processes. There’s a new way now, look up “persistent parcel”, and you’ll find it. But yeah, the Serializable interface is what we are talking about.
Q: you mentioned earlier flat buffers. There’s an argument that it really doesn’t matter if you’re saving a hundred milliseconds here whenever the network request itself is going to take much longer.
Romain: The example I gave earlier is interesting because it was about using JSON as a disk format. The idea here is that you’ve already cached the data, so you already paid the costs for the network transfer, and chances are that you’re going to read from this many more times. In that case, it makes a lot of sense to use something that’s way faster. But it’s true that if it takes 10 milliseconds to parse your JSON, and 5 milliseconds to parse your flat buffers, but it took you 200 milliseconds to transfer it over the network, does it really matter? Obviously, I hope that your application is doing all the network operations on a background thread anyway. It’s about latency in the UI more than anything else. If it’s something you’re going to access a lot, you should definitely look into something like flat buffer.
There’s also Cap’n Proto, which was written by the guy who created protobuf v2. He wrote something that’s kind of like flat buffers but they go much further, because it can be used as a wire format, it can be used as an RPC mechanism, and I believe it has Java wrappers. It goes back to our prior insistance that when you consider performance, you always need to measure. Make sure that you’re fixing the right problems. It doesn’t matter if you fix something that is not ever an issue. It’s hard for us to tell you what is going to be the issue in your app, because it really depends on the application. We can only talk about the patterns that we’ve seen over and over and over again in applications, which again, doesn’t mean it’s going to be the case in your app. But yeah, so if you do serialization on disk, look at those other formats. On the wire, do what’s best for you.
About the content
This content has been published here with the express permission of the author.