Framework Size

You might be wondering why the PSPDFKit AAR is 24 MB.

The PSPDFKit SDK covers most of the PDF specification, which contains thousands of pages, making it quite complex. PSPDFKit includes a complete PDF renderer, cryptography, and many UI components. This results in a lot of code and, as such, a sizable binary, although there are certain factors that make it appear larger than it actually is. We’re working hard to ensure the framework size stays as low as possible.

Method Count

PSPDFKit builds upon mature and widely used third-party and open source software. Version 4.5.1 of PSPDFKit has a total of 23,000 methods (including references). Since most apps already ship with the Android support library and RxJava, the actual number of additional methods when using PSPDFKit is usually smaller.

Dex Method Limit

The Android dex format has a major flaw in that an app with a single dex file can only have a maximum of 65,536 method references. While this limit is hardly reachable for an app on its own, it’s very likely that your app’s method count exceeds this limit when adding several third-party dependencies — for example, Google’s support libraries, HTTP/Rest libraries, or PSPDFKit. If your app hits this limit (and you did not set any precautions) it’s likely you’ll see following error while trying to build your app:

Copy
1
2
Unable to execute dex: method ID not in [0, 0xffff]: 65536
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

The message above is created by the dx tool, which is part of the Android Build Tools and is responsible for converting your Java classes to Android’s dex format.

ProGuarding

One technique to avoid hitting this limit is to enable ProGuard for development builds. ProGuard can detect unused methods and remove them (this is called minification), thereby lowering the total method count of your app. You can enable ProGuard minification inside your app’s build.gradle file:

Copy
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
    buildTypes {
        debug {
            minifyEnabled true
            // By using a special debug ProGuard file, you can turn off obfuscation, which would
            // otherwise hinder debugging.
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro',
                			  'proguard-rules-debug.pro'
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

Now create the file app/proguard-rules-debug.pro and add the following code, which will turn off the obfuscator:

app/proguard-rules-debug.pro
1
-dontobfuscate

Important: This minification technique requires you to have already configured the main proguard-rules.pro file of your app according to the integration guide.

Multidex

Originally, Android only supported applications with a single dex file. Newer versions of Android (starting with API 21) support multiple dex files, which triages the issue. For devices prior to API 21, there is the multidex support library, which allows loading classes from different dex files packed into your app’s APK. The Google Developer document has a comprehensive guide on enabling and configuring multidex for your app.

Note: While multidex is a good solution for increasing the method limit on all newer devices, it can cause performance issues and other problems on devices prior to Lollipop. As such, we recommend using ProGuard and manual authoring of your app’s dependencies to lower the method count, and only using multidex as the second choice.

Reducing the Size of Your App

If you have extremely low APK size limits, you may decide to only ship ARMv7 native binaries without ARM64 or x86. This will force ARMv8 devices to load the ARMv7 binaries and X86 devices to load ARMv7 binaries via the libhoudini layer.

Be aware that this will cause significant performance penalties and possible bugs on devices with x86 CPUs. ARMv8 devices like the Samsung Galaxy S7, the Nexus 5X/6P, and similar will also show slower performance.

To enable ABI filtering, you need to explicitly define the set of ABIs that you would like to include in your final APK:

Copy
build.gradle
1
2
3
4
5
6
android {
    defaultConfig {
        // This will strip x86 and arm64-v8a binaries from your APK.
        ndk.abiFilters = ["armeabi-v7a"]
    }
}

ABI Split

Another technique to reduce the overall download size of your final APK is by using ABI splits. ABI splits require you to prepare your both your build setup and your Google Play entry (if you are publishing via Google Play) by doing the following:

Android App Bundles

Android App Bundles were introduced during Google I/O 2018 and provide automated modularization of your distributed app, which can yield much smaller APK download and install sizes than what was possible before. Our own trials with PDF Viewer showed an average download size reduction of 50 percent for most users. However, the final number depends upon the specific app and needs to be evaluated on per case basis.

Moreover, App Bundles allow for dynamic distribution of features inside your app, which can be used to load “secondary features” on demand, rather than installing them with the initial APK. An in-depth guide about Android App Bundles can be found at the official Android App Bundle documentation.

Third-Party Libraries

RxJava

Internally, we make heavy use of the popular RxJava and its Android adjunct RxAndroid for structuring our code, coordinating asynchronous operations, and — most importantly — performing secure and stable multi-threading and scheduling. Also, many of the public API methods of PSPDFKit are available in two flavors: a blocking call, directly returning the requested result; and an asynchronous version of the same method, returning an RxJava Observable, which is non-blocking and returns the result as soon as it is available.

Here’s an example snippet that searches a document in a non-blocking way. Search is performed on a background thread, while the results are published on the main thread, allowing you to simply update the UI:

Copy
1
2
3
4
5
6
7
8
// Perform an asynchronous search on a computation thread and update the UI on the main thread.
val searchDisposable = search.performSearchAsync(query)
    .subscribeOn(Schedulers.computation())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { nextResult ->
        // This will be called once for every SearchResult object.
        // Put your search result handling here.
    }
Copy
1
2
3
4
5
6
7
8
9
10
// Perform an asynchronous search on a computation thread and update the UI on the main thread.
final Disposable searchDisposable = textSearch.performSearchAsync(query)
    .subscribeOn(Schedulers.computation())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Consumer<SearchResult>() {
        @Override public void accept(SearchResult nextResult) {
            // This will be called once for every SearchResult object.
            // Put your search result handling here.
        }
    });

Tip: Keep an eye open for *Async() methods, which usually return an RxJava Observable or Flowable.

RxJava is very popular for Android development (CodePath lists it as one of the recommended advanced libraries) and is supported by many renown libraries. Due to the generic nature of RxJava, its API can be used throughout your whole app without the need to declare a callback type for every asynchronous method. Moreover, it allows connecting your own asynchronous code with code of any third-party library (like PSPDFKit) as long as this library also supports RxJava.

Rendering PDF

Rendering PDF documents is rather complex. We have a separate document explaining some of the challenges.