This is a post I made on a separate issue, but I don't think that was the right place, and I'd like to make sure this topic gets some attention. I'd like to discuss the possibility of splitting up RxJava into multiple dependencies.
I'm coming at this entirely from the perspective of Android. RxJava has become very popular on the Android platform. I see more and more resumes with RxJava experience listed, and I think this library matters a great deal to the Android community.
When developing on android APK size and method count matter. There are many libraries myself or my team would like to use, but we don't simply because they are too large in size, or their method count overhead is too high. The benefits of keeping an APK size low are fairly obvious. What I'm slightly more concerned with with in RxJava 2.0 is it's method count. Android apps can have a maximum of 65536 methods before having to use something called Multi-dex which can dramatically impact compile time, and app start-up time (especially on pre 5.0 devices).
My concern is that RxJava's method count has slowly grown over time, and rxjava 2.0.0 is another significant jump in method count and overall size:
| RxJava Version | Jar Size | Method Count |
| --- | --- | --- |
| 1.0.0 | 674 KB | 3,339 |
| 1.2.1 | 1,152 KB | 5,581 |
| 2.0.0-RC3 | 1,894 KB | 8,931 |
I compiled a very basic Android App with almost no application code. All it does is include the latest appcompat-v7 library (pretty standard for any Android app), and the specified version of RxJava.
| RxJava Version | APK Size | Method Count |
| --- | --- | --- |
| none | 1,367 KB | 16,937 |
| 1.0.0 | 1,573 KB | 20,276 |
| 1.2.1 | 1,739 KB | 22,518 |
| 2.0.0-rc3 | 1,936 KB | 25,868 |
With RxJava 2.0.0 imposing an almost 9k method overhead, I do think that it's gotten to a place where splitting up the library into multiple parts would be beneficial. I think that for many teams, there are portions of the library that they won't use, and it's beneficial to be able to exclude certain portions of the library based on need in resource constrained environments like Android.
Hi. This is definitely not going to happen with 2.0.0 GA. I guess Jake will say use and configure ProGuard to strip the unnecessary parts.
Splitting the library has the problem that interoperation becomes difficult as Java doesn't have extension methods like C# and Kotlin so we have to return the full types. The second problem is the coupling between the 5 types as certain Observable and Flowable operators now return Single, Maybe or Completable.
If it were only up to me, we'd only have Flowable and 65% of the current codebase could be eliminated but people want the cardinality in their types which requires a lot of code.
Even the issue I raised a month or two ago was likely far too late in the development process for 2.x...
RxJava ProGuards extremely well since each operator is basically associated with 1-3 classes that don't cross reference each other. There's a small core of interfaces and classes used by all operator implementations, but you should see many thousands of methods fall away based on which operators you are using.
Here's an example:
$ cat Test.java
import io.reactivex.Observable;
import io.reactivex.functions.Consumer;
class Test {
public static void main(String... args) {
Observable.just("Hello").subscribe(new Consumer<String>() {
@Override public void accept(String item) {
System.out.println(item);
}
});
}
}
$ javac -cp rxjava-2.0.0-RC5.jar:reactive-streams-1.0.0.jar Test.java
$ java -cp rxjava-2.0.0-RC5.jar:reactive-streams-1.0.0.jar:. Test
Hello
$ java -jar proguard-base-5.3.1.jar \
-dontobfuscate -keep 'class Test { *; }' \
-injars . \
-outjars test.jar \
-libraryjars /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar
$ java -cp test.jar Test
Hello
$ dex-method-list test.jar | wc -l
77
$ dex-method-list test.jar
Test <init>()
Test main(String[])
Test$1 <init>()
Test$1 accept(Object)
io.reactivex.Observable <init>()
io.reactivex.Observable subscribe(Observer)
io.reactivex.Observable subscribeActual(Observer)
io.reactivex.Observer onComplete()
io.reactivex.Observer onNext(Object)
io.reactivex.Observer onSubscribe(Disposable)
io.reactivex.disposables.Disposable dispose()
io.reactivex.functions.Consumer accept(Object)
io.reactivex.internal.disposables.DisposableHelper <clinit>()
io.reactivex.internal.disposables.DisposableHelper <init>(String, int)
io.reactivex.internal.disposables.DisposableHelper dispose()
io.reactivex.internal.disposables.DisposableHelper dispose(AtomicReference) โ boolean
io.reactivex.internal.disposables.DisposableHelper setOnce(AtomicReference, Disposable) โ boolean
io.reactivex.internal.functions.Functions <clinit>()
io.reactivex.internal.functions.Functions emptyConsumer() โ Consumer
io.reactivex.internal.functions.Functions$10 <init>()
io.reactivex.internal.functions.Functions$10 run()
io.reactivex.internal.functions.Functions$10 toString() โ String
io.reactivex.internal.functions.Functions$11 <init>()
io.reactivex.internal.functions.Functions$11 toString() โ String
io.reactivex.internal.functions.Functions$12 <init>()
io.reactivex.internal.functions.Functions$12 accept(Object)
io.reactivex.internal.functions.Functions$12 toString() โ String
io.reactivex.internal.functions.Functions$13 <init>()
io.reactivex.internal.functions.Functions$13 accept(Object)
io.reactivex.internal.functions.Functions$14 <init>()
io.reactivex.internal.functions.Functions$15 <init>()
io.reactivex.internal.functions.Functions$16 <init>()
io.reactivex.internal.functions.Functions$17 <init>()
io.reactivex.internal.functions.Functions$17 call() โ Object
io.reactivex.internal.functions.Functions$18 <init>()
io.reactivex.internal.functions.Functions$18 compare(Object, Object) โ int
io.reactivex.internal.functions.Functions$9 <init>()
io.reactivex.internal.functions.Functions$9 toString() โ String
io.reactivex.internal.functions.Functions$KeyedEqualsPredicate <init>(Function)
io.reactivex.internal.functions.ObjectHelper <clinit>()
io.reactivex.internal.functions.ObjectHelper requireNonNull(Object, String) โ Object
io.reactivex.internal.functions.ObjectHelper$1 <init>()
io.reactivex.internal.observers.LambdaObserver <init>(Consumer, Consumer, Action, Consumer)
io.reactivex.internal.observers.LambdaObserver dispose()
io.reactivex.internal.observers.LambdaObserver onComplete()
io.reactivex.internal.observers.LambdaObserver onNext(Object)
io.reactivex.internal.observers.LambdaObserver onSubscribe(Disposable)
io.reactivex.internal.operators.observable.ObservableJust <init>(Object)
io.reactivex.internal.operators.observable.ObservableJust call() โ Object
io.reactivex.internal.operators.observable.ObservableJust subscribeActual(Observer)
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable <init>(Observer, Object)
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable compareAndSet(int, int) โ boolean
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable dispose()
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable get() โ int
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable lazySet(int)
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable run()
io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable set(int)
io.reactivex.plugins.RxJavaPlugins onAssembly(Observable) โ Observable
io.reactivex.plugins.RxJavaPlugins onError(Throwable)
io.reactivex.plugins.RxJavaPlugins onSubscribe(Observable, Observer) โ Observer
io.reactivex.plugins.RxJavaPlugins throwIfFatal(Throwable)
java.io.PrintStream println(String)
java.lang.Comparable compareTo(Object) โ int
java.lang.Enum <init>(String, int)
java.lang.IllegalStateException <init>(String)
java.lang.NullPointerException <init>(String)
java.lang.NullPointerException initCause(Throwable) โ Throwable
java.lang.Object <init>()
java.lang.Thread currentThread() โ Thread
java.lang.Thread getUncaughtExceptionHandler() โ Thread$UncaughtExceptionHandler
java.lang.Thread$UncaughtExceptionHandler uncaughtException(Thread, Throwable)
java.lang.Throwable printStackTrace()
java.util.concurrent.atomic.AtomicInteger <init>()
java.util.concurrent.atomic.AtomicReference <init>()
java.util.concurrent.atomic.AtomicReference compareAndSet(Object, Object) โ boolean
java.util.concurrent.atomic.AtomicReference get() โ Object
java.util.concurrent.atomic.AtomicReference getAndSet(Object) โ Object
Tools:
@akarnokd I see, I hadn't considered that. I think Jake had referenced that a split like this might hurt the api, and I couldn't think of how until you mentioned interoperation.
@JakeWharton I realize that rxjava proguards well, proguard configs for an actual project get more complicated. I don't think that slapping proguard in a project is necessarily a solution. As you've mentioned yourself in the past it's a pain to configure and use.
Something else I hadn't considered is using proguard on the final rxjava jar to create separate jar dependencies that exclude certain pieces of the library. It's somewhat brute force, but doing this would allow people to import a jar without the parts they don't want (i.e. rxjava-2.0.0-RC5-without-Flowable.jar). This could be done in a separate project by a third party like me. I imagine this would be possible with proguard, and it would still allow people who want to avoid proguard to have a reduced method count in their app. I'll play around with it as a solution.
RxJava 2 requires no ProGuard configuration, as you can see. What you describe is pointless since it's already what ProGuard does and you'll just end up breaking interop with libraries. You'll notice there's no Flowable in the method list in my example because it was not used anywhere.
You've supplied a simple example above where proguard is basically free. Anyone who's used proguard in a production product knows that it's certainly not free. My goal is to avoid the headaches of having to configure proguard for an entire Android project and actively maintain it.
But yes, a downside to shaking out portions of the library using proguard ahead of time would be that libraries couldn't use those removed parts of those libraries.
@JakeWharton you seem to have changed your tune on pro-guard since a few year's ago. What changed your mind?
I had to use the Facebook SDK โน๏ธ
I'll give Pro-Gaurd another shot with it's -dontoptimize flag and see how things go.
@ScottPierce I know this issue is closed and more than a year old, but could you share real quick how you proceeded from here, what the results were, and possibly what your latest recommendations are? Thanks! :)
The most recent version of RxJava adds more than 10k methods.
@friederbluemle Yeah, so my team successfully integrated proguard.
There were a few key points that made us successful:
internalDebug variant, we turn off proguard, and we turn multi-dex on, and for the internalRelease, and productionRelease builds, multi-dex is off, but proguard is on. This gives us fast dev builds with internalDebug, but reliable start up times in production (we've found that multi-dex can hang for several seconds sporadically on startup). We also default our min sdk version to 21 on development machines (builds a LOT faster for multi-dex as in sdk 21 and higher, multi-dex has native support), and then in our CI environment have it go back to 19, and build our APK that we actually release into production.For the most part, we had the initial setup cost of proguard, but haven't had to revisit it very often.
@friederbluemle you can check split discussion about RxJava 3.x
Most helpful comment
I had to use the Facebook SDK โน๏ธ