On Static Binary Translation of ARM/Thumb Mixed ISA Binaries
4 stars based on
The video was transcribed by Realm and is published here with the permission of the conference organizers. Through a deep dive into technical decisions the team makes, Ty show what his team has learned about developing an SDK for stability, testability, performance, overall footprint size, and, most importantly, exceptional ease of implementation.
Whether you are building an SDK now or in the future, you will find some great ideas in this talk for API design and usability. SDKs are at the heart of great applications and great developer experience goes a long way when making great apps. At Fabric we care deeply about the developer experience, we spend enormous amounts of time trying to make our products easy and fun to use. What exactly is Fabric? Just last month we announced multiple external partners who are now building on top of the Fabric platform as well, with many more coming soon.
As we were building out the Fabric SDKs, we kept several goals in mind that helped guide us as developers. These principles compelled us when making API and programming decisions. Before we hop into code though, we have to think about a few considerations that I think you should make before you try to build a library or an SDK.
Is it for internal developers or public developers? Is there already a solution on the market? If so, you should be contributing to that instead. Considering open source versus closed source is also a big question. Open sourcing will generally give you better adoption from the community, more stable software, and more enthusiastic internal engineers. Therefore, consider your license carefully as well.
A more flexible license might be something like Apache 2 or MIT. You have three types to consider. The first is a standard library project, which is where the developer just includes your source code and links it in their IDE.
Jars are another great example in the Java world — very standard binary packaging. The last consideration is where to host your artifacts. If a customer repository is used if you have a proprietary binarythen the developer must manually add the repository in their build script. We believe that great SDKs go a long way in delivering on all of these points. One of the big areas of focus is usability.
We believe that developer products should be easy and easy to use. What do we mean by easy to use? We wanted to create the easiest way possible for people to get started with Fabric right away in their application. With just one line in your application file, you can get going. But as easy as that is, many developers want more customization. To do that, we use the builder pattern to set commonly used but optional parameters, like a listener to notify you when the app previously crashed.
With Fabric we require an API key as a dependency to authenticate against our web service. This is something that we wanted to handle for the developer to minimize the amount of work needed to get started. Our standard approach is to provision that key through our build plug-in and inject that into the manifest. Once Fabric is in the initialization process we can parse the application info provided by the package manager to retrieve the API key and continue along in that process.
We do, however, allow alternative approaches as well to managing the API key, and this can work much better for open source libraries.
Aside from the implementation details that I just mentioned, we like to think about these sort of traits when designing APIs. The first is being intuitive. If an API call acts exactly the way a developer expects without having to reference documentation, it will avoid surprises. Use common language in the method signatures and similar design patterns within your SDK. Also use naming conventions according to the platform, for example, iOS or Android.
Lastly, APIs that are hard to misuse prevent bugs from happening. This is one of the hardest but deepest effects of what it feels like to use an API. Here we would expect equals to perform some sort of normalized string comparison.
Would anyone expect that? Blocking the caller thread is an example of unexpected behavior that should be made very explicit in the API. In initializing either Fabric or Crashlytics, two different binaries, we allow the same pattern builder pattern as we saw before. The user can get the defaults by using a no parameter constructor or a helper method defined, however, both classes offer a builder that can be used to heavily customize the objects.
Lastly is hard to misuse. For example, the constructor from the builder of Fabric like we covered earlier requires the context to be set, while other setters are options. The developer consuming the API cannot proceed without providing the context, but can use the others at their leisure. We believe this is hard to misuse.
How do we get these qualities though? We create a design doc before any implementation work is done so that we can discuss the pros and cons of the different approaches across both of the platforms that we work on. Even if we discover the bug early and we fix it fairly quickly, it may still take up to a month, during which time your users have a degraded experience.
However, if an SDK has a critical bug, the timeline is a lot longer. It may take months before users of an application using your SDK get that bug fixed. Since app developers may take weeks before noticing or upgrading your SDK to the version that has the bug fix, ensuring high stability in an SDK is one of our highest priorities. What can we do as developers to ensure the highest stability possible? There are some things that are key parts of our development process.
Code reviews are always important, but we take them way seriously. Being able to automatically have some sort of guarantee of basic correctness, can also help catch bugs early so unit tests are incredibly useful. This way they may be able to catch bugs in the integration with your SDK.
Lastly, continuous integration and dog-fooding also add another layer that may help identify problems early and quickly. There are some tricks to making your SDKs testable and by testable, I mostly mean mockable in this case. A mock class is just a dummy class that represents the real one, but has no ops and allows for overriding returns for the method calls and other types of verification on the calls.
By avoiding static methodsyou allow any method calls to also operate on the mock instances. Many mocking libraries also have problems with final classes so be thoughtful about your class extensions. Use interfaces around your public API. The interfaces allow developers to override with behavior to hit mock servers or in memory storage instead of expensive and inconsistent real operations.
Lastly, think about architecting your code in such a way that a tester would never need to mock more than one level deep. This encourages tests to actually be written and provides more stability to your testing framework. A class that is hard to test in mock is one that is final, creates its dependencies on its own, and is a singleton, meaning state-based. It makes it very challenging to test in isolation. So, what could we do to this to fix it?
With just a few modifications we can make this a lot more testable. Remove the final modifier in the class so that Mockito or other frameworks can mock it and allow the SDK to take its dependencies at construction time.
Developers are impatient and inquisitive, so the faster something fails, the better. However, in production your SDK should never fail, preserving the trust of your developers is the only way to keep your code in their application. As a developer, you use SDKs that add value to your app, the worst thing that someone could do is take away value or destroy the user experience altogether.
In addition to being stable, users are less likely to download big apps and this means that binary size matters. Unlimited data plans have gone in and out of fashion around the world, many users pay per kilobyte, so downloads cost them money even if your app is free.
For example, popular image loading libraries on Android vary greatly in size. Fresco, for example, is an order of magnitude larger than any of the others. However, it has much better support for older devices, it bundles an entire native image process pipline for processing, and it supports progressive JPEGs. As an SDK you should strive to balance your size with your functionality. So, be mindful of including third-party libraries to make sure that they only meet the goals that are needed.
This generally provides a much better experience than rolling your own. If you just weigh yourself everyday, you will lose weight. We take this approach to our SDK sizes. Has anyone in the room actually run into the Dalvik 65k limit? Okay, few of you. The problem surfaces when the dex tool, at compile time, tries to write all the method references to a certain amount of space allocated for this in the dex file and it fails. It is even incorrectly implemented on certain popular Samsung devices and causes app crashes on launch every single time, consistently.
These specific devices have millions and millions of devices that are active in Europe and Asia, so that says something. But if a developer runs into this problem, though, before they go with multidex or something similar, often times they decide to audit their choice of third-party library to see if they can minimize their app size. Our goal, and my advice to other library or SDK developers is to limit your impact here as much as possible and be extremely modular and lean.
We use a great library called dex-method-counts on our build script written by an engineer at Quip, and it wraps some of the Android build tools to give a detailed data analysis on method count per package. This allows us quick insight into our size and the size of our dependencies. We have this set up on CI to run every build and report the data just like we do with the binary sizes.