This is a conceptual overview of how Android’s 2D Canvas rendering pipeline works. Since Android’s Canvas API is mostly a pretty thin veneer on top of Skia it should also serve as a reasonable overview of Skia’s operation, though I’ve only looked at Skia code that’s reachable from Android’s SDK, and when the Skia and Android terminology differ (which is rare, modulo “Sk” prefixes and capitalization) I’ve used the Android terminology.
How and Why I Wrote This
I wrote this overview because I’ve been doing some Android development recently, and I was getting frustrated by the fact that the documentation forandroid.graphics, particularly when it comes to all of the things that can be set in a Paint object, is extremely sparse. I Googled, and I asked a question on Stack Overflow but I couldn’t find anything that explained this stuff to my satisfaction.
This overview is based on reading what little documentation exists (often “between the lines”), doing lots of experiments to see how fringe cases work, poring over the code, and doing even more experiments to verify that I was reading the code correctly. I started writing it as notes for myself, but I figured others might benefit as well so I decided to post it here.
Caveats
I say this is a “conceptual” overview because it does not always explain the actual implementation. The implementation is riddled with special cases that attempt to avoid doing work that isn’t necessary. (I remember hearing some quote along the lines of “the fastest way to do something is to not do it at all”.) Understanding the implementation details of all of these special cases is unnecessary to understanding the actual end-result, so I’ve focused on the most general path through the pipeline. I actually avoided looking at the details of a lot of the special-case code, so if this code contains behavioral inconsistencies I won’t have seen them.
Also, there are cases, particularly in the Shading and Transfer sections, where the algorithm described here is far less efficient but easier to visualize (and, I hope, understand) than the actual implementation. For example, I describe Shading as a separate phase that produces an image containing the source colors and Transfer as a phase producing an image with intermediate colors. In reality these two “phases” are interleaved such that only a small set (often just one) of the pixels from each of these virtual images actually “exists” at any instant in time. There is also short-circuiting in this code such that the source and intermediate colors aren’t computed at all for pixels where the mask is fully transparent (0x00).
This does mean that this overview can’t give one an entirely accurate understanding of the performance (speed and/or memory) of various operations in the pipeline. For that it would be better to performing experiments and profile.

Also keep in mind that because this is documenting what is arguably “undocumented behavior” it’s hard to say how much of what is described here is stuff that’s guaranteed versus implementation detail, or even outright bugs. I’ve used some judgement in determining where to put the boundaries between phases (all of that optimization blurs the lines) based on what I think is a “reasonable API” and I’ve also tried to point out when I think a particular behavior I’ve discovered looks more like a bug than a feature to rely on.
There are still a number of cases where I’d like to do some more experimentation to verify that my reading of the code is correct and I’ve tried to indicate those below.
Recent Comments