Exploring Java's Hidden Costs

Realm Java enables you to efficiently write your Android app’s model layer in a safe, persisted, and fast way.

Realm Mobile Database is an alternative to SQLite and Core Data. Thanks to its zero-copy design, Realm Mobile Database is much faster than an ORM, and often faster than raw SQLite. Get started Realm Java in minutes, not hours.

As Java 8 features come to Android, it’s important to remember that every standard library API and language feature comes with an associated cost. Even as devices get faster and have more memory, concerns of code size and performance overhead are still very relevant. This 360AnDev talk will be an exploration of hidden costs associated with some of Java’s features. We’ll focus on optimizations relevant for both library and application developers and on the tools that can be used to measure their impact.


Introduction (0:00)

In this talk, I’m going to talk about things I’ve been looking at for the last six months, and I want to disseminate some of that information. As we’re going through this, you might not get these tangible things that you can apply to your application. But wait until the end, and I’ll have some concrete tidbits of how to avoid all the stuff that we’re going to see. I’ll also be showing a lot of command-line tools that I use, and the links to all these resources will be at the end of the post.

Dex files (1:14)

We’ll start with a multiple choice question. How many methods are in this piece of code? Zero, one or two?

class Example {
}

You probably immediately have a gut reaction. Maybe it’s zero, maybe it’s one, and maybe it’s two. Let’s take a look and see if we can answer this question. First of all, there are zero methods inside this class. I’ve written no methods in the source file, so that’s perfectly valid to say. Of course, that would be a really uninteresting answer to this problem. Let’s start taking our class through the build pipeline of Android and see what comes up:

$ echo "class Example {
}" > Example.java

$ javac Example.java

$ javap Example.class
class Example {
Example();
}

We take our contents here and write it to a file, then use the Java compiler to take the source code and turn it into a class file. And then we can use this other tool from the Java development kit called javap. That is going to allow us to see inside the class file about what was compiled. If we run this on our compiled class, we see that there’s a constructor inside our example class. We didn’t write one in the source file, but Java C has taken the liberty of adding that constructor automatically. That means that there’s zero methods in source file but one method in the class. But that is not where Android builds stop:

$ dx --dex --output=example.dex Example.class

$ dexdump -f example.dex

In the Android SDK, there’s a tool called dx, which does dexing, which takes the Java class files, and turns it into Android’s Dalvik bytecode. We can run our example class through dex, and there’s another tool in the Android SDK called dexdump, which will give us a little information about what’s inside these dex files. You run it, and it’s going to print out a bunch of stuff. It’s like offsets into the file and counts and its various tables. If we look closely, one of these things stands out, and that’s the method table inside the dex file:

method_ids_size : 2

It says that there are two methods in our class. That makes no sense. Unfortunately, dexdump doesn’t give us an easy way to look at what the methods are. In response to that, I wrote a little tool which I’ll use to dump methods inside of a dex file:

$ dex-method-list example.dex
Example <init>()
java.lang.Object <init>()

If we do this, we see that it does return two methods. It returns our constructor, which we know the Java compiler created, even though we didn’t write one. But it also says that there is an object constructor. Certainly, our code is not calling new object everywhere, so where does this method come from that winds up being referenced in the dex file? If we go back to the javap tool that prints class file information, you can give it some additional flags to look more deeply inside the classes. I’m going to pass the -c flag, which is going to decompile the bytecode into something that’s a little more human readable.

$ javap -c Example.class
class Example {
    Example();
        Code:
            0: aload_0
            1: invokespecial #1 //java/lang/Object."<init>":()V
            4: return
}

At index one, it’s our generated constructor that is calling the super class constructor. That is because, even though it didn’t declare it, Example extends from Object. Every constructor has to call the super class constructor. It gets inserted automatically. That means there are two methods from our class flow.

All of these answers to my initial question were correct. The difference between these though is terminology. It’s where they live. We didn’t declare any methods. But only humans care about this. As humans, we read and write these files. We’re the only ones that really care what goes in and out of them. The other two are the ones that are more important, the number of methods that are actually compiled in the class files. These are the methods that declared or not, are inside that class.

The two methods are the number of methods that are referenced. That is inclusive in the sense that our own methods that we wrote are counted, and also all of the others that are referenced from within those methods from calling out to Android’s logger. That Log.d method that I’m referring to counts against this reference method panel, which is what’s in the dex file. That is what people traditionally refer to when you talk about method count on Android, because dex has the infamous limit for the number of methods that can be referenced.

We saw a constructor being created without us having declared one, so let’s look for some other hidden things that are generated that we might not have known were there. Nested classes are a useful construct:

// Outer.java
public class Outer {
    private class Example {
    }
}

They’re not in Java 1.0. They came in a later version. You see stuff like this for when you’re defining an adapter inside of a view or presenter:

// ItemsView.java
public class ItemsView {
    private class ItemsAdapter {
    }
}

$ javac ItemsView.java

$ ls
ItemsView.class
ItemsView.java
ItemsView$ItemsAdapter.class

If we can compile this class, this is one file with two classes. One nested inside the other. If we compile this, we see that two separate class files end up in the file system. If Java had truly nested classes, we would only get one class file. We would get ItemsView.class. But there’s no true nesting in Java, so what are in these classes? In the ItemsView, the outer class, all we have is the constructor. There’s no reference, no hint that there was an inner class:

$ javap 'ItemsView$ItemsAdapter'
class ItemsView$ItemsAdapter {
    ItemsView$ItemsAdapter();
}

If we look at the contents of the nested class, you see that it has its implicit constructor, and you know that it was inside an outer class because its name got mangled. Another important thing to note if I go back, we see that this ItemsView class is public, which is what we declared in the source file. But the inner class, the nested class, even though it’s declared as private, it’s not private in the class file. It’s package scoped. That makes sense because we had two class files that were generated, in the same package. Again, that just further proves that there’s no true nesting in Java.

// ItemsView.java

public class ItemsView {
}

// ItemsAdapter.java

class ItemsAdapter {
}

Even though you’re nesting those two class definitions, you’re effectively just creating two classes that are next to each other in the same package. You could do this if you wanted to. You can use that naming scheme as two separate source files:

// ItemsView.java

public class ItemsView {
}

// ItemsView$ItemsAdapter.java

class ItemsView$ItemsAdapter {
}

The dollar sign is a valid character for names in Java. Methods or plus names.

// ItemsView.java
public class ItemsView {
    private static String displayText(String item) {
        return ""; // TODO
    }
private class ItemsAdapter {
    }
}

However, this is really interesting because I know that I can do stuff to find a private static method in the outer class, and I can refer to that private method in the inner class:

// ItemsView.java

public class ItemsView {
    private static String displayText(String item) {
        return ""; // TODO
    }
}

// ItemsView$ItemsAdapter.java

class ItemsView$ItemsAdapter {
    void bindItem(TextView tv, String item) {
        tv.setText(ItemsView.displayText(item));
    }
}

Now that we know that there’s no such thing as true nesting, though, how does this work in our hypothetical separated system where our ItemsAdapter class needs to refer to the private method in ItemsView? That will not compile, however this will:

// ItemsView.java

public class ItemsView {
    private static String displayText(String item) {
        return ""; // TODO
    }

    private class ItemsAdapter {
        void bindItem(TextView tv, String item) {
            tv.setText(ItemsView.displayText(item));
        }
    }
}

What’s going on here? When you go back to our tools, we can use javac again.

$ javac -bootclasspath android-sdk/platforms/android-24/android.jar \
    ItemsView.java

$ javap -c 'ItemsView$ItemsAdapter'
class ItemsView$ItemAdapter {
    void bindItem(android.widget.TextView, java.lang.String);
    Code:
        0: aload_1
        1: aload_2
        2: invokestatic #3 // Method ItemsView.access$000:…
        5: invokevirtual #4 // Method TextView.setText:…
        8: return
}

I’m referencing TextView, so I had to add the Android APIs to Java. Now I’m going to print out the contents of the nested class to see what method it has called. If you look at index two, it’s not calling the displayText method. It’s calling access$000, which we didn’t define. Is that in the ItemsView class?

$ javap -p ItemsView123

class ItemsView {
    ItemsView();
    private static java.lang.String displayText();
    static java.lang.String access$000();
}

If we look, it is. We see our private static method is still there, but we now have this additional method that we didn’t write automatically being added.

$ javap -p -c ItemsView123

class ItemsView {
    ItemsView();
        Code: <removed>

private static java.lang.String displayText();
    Code: <removed>
    
static java.lang.String access$000();
    Code:
        0: aload_0
        1: invokestatic #1 // Method displayText:…
        4: areturn
}

If we look at the contents of that method, all it does is forward the call to our original displayText method. That makes sense because we need some way to call this private method from a package scoped to a class. Java’s going to synthesize a package scoped method to facilitate that method call.

// ItemsView.java
public class ItemsView {
    private static String displayText(String item) {
        return ""; // TODO
    }

    static String access$000(String item) {
        return displayText(item);
    }
}

// ItemsView$ItemsAdapter.java
class ItemsView$ItemsAdapter {
    void bindItem(TextView tv, String item) {
        tv.setText(ItemsView.access$000(item));
    }
}

If we go back to our separated example, our manual example, we can make this compile in the exact same way. We can add that method, and we can update the other class to refer to it. The dex file has this limit of methods, and so when you have these methods that are going to be added based on the ways that you’re writing your source file, those can add up. It’s important to understand this is happening because we’re trying to access a private member somewhere that it can’t work.

More Dex (10:52)

So you might say, “Well, you only did Java C. Maybe the dex tool can see that and automatically eliminate that method for us.”

$ dx --dex --output=example.dex *.class

$ dex-method-list example.dex

ItemsView <init>()
ItemsView access$000(String)  String
ItemsView displayText(String)  String
ItemsView$ItemsAdapter <init>(ItemsView)
ItemsView$ItemsAdapter bindItem(TextView, String)
android.widget.TextView setText(CharSequence)
java.lang.Object <init>()

If we compile our two classes that are generated and list them out, you can see that that’s not the case. The dex tool compiles it like it was any other method. It does end up in your dex file.

You might say, “Well, I heard about this new Jack compiler. And the Jack compiler takes the source file directly and directly produces dex files, so maybe it can do something where it doesn’t need to generate this extra method.” There’s certainly no access method. However, there’s this -wrap0 method, which is effectively the same thing:

$ java -jar android-sdk/build-tools/24.0.1/jack.jar \
        -cp android-sdk/platforms/android-24/android.jar \
        --output-dex . \
        ItemsView.java

$ dex-method-list classes.dex

ItemsView -wrap0(String)  String
ItemsView <init>()
ItemsView displayText(String)  String
ItemsView$ItemsAdapter <init>(ItemsView)
ItemsView$ItemsAdapter bindItem(TextView, String)
android.widget.TextView setText(CharSequence)
java.lang.Object <init>()

There’s also a tool called ProGuard, which probably a lot of people are using. You might say, “Well ProGuard should be able to take care of this, right?” I can write a quick ProGuard key. I can run ProGuard on our class files and print out the methods. And I get this:

$ echo "-dontobfuscate
-keep class ItemsView$ItemsAdapter { void bindItem(...); }
" > rules.txt

$ java -jar proguard-base-5.2.1.jar \
    -include rules.txt \
    -injars . \
    -outjars example-proguard.jar \
    -libraryjars android-sdk/platforms/android-24/android.jar

$ dex-method-list example-proguard.jar

ItemsView access$000(String)  String
ItemsView$ItemsAdapter bindItem(TextView, String)
android.widget.TextView setText(CharSequence)

The constructors were removed because they weren’t being used. I’m going to add them back in because normally they would stick around:

$ dex-method-list example-proguard.jar

ItemsView <init>()
ItemsView access$000(String)  String
ItemsView$ItemsAdapter <init>(ItemsView)
ItemsView$ItemsAdapter bindItem(TextView, String)
android.widget.TextView setText(CharSequence)
java.lang.Object <init>()

You’ll see that the access method is still there. But if you look closely, we retain the access method, but displayText disappeared. What the hell’s going on here? You can unzip the jar that ProGuard produced, and then we can go back to our javap tool and look inside the ProGuarded class file:

$ unzip example-proguard.jar

$ javap -c ItemsView

public final class ItemsView {
    static java.lang.String access$000(java.lang.String);
        Code:
            0: ldc #1 // String ""
            2: areturn
}

If we look at the access method, it no longer is calling displayText. ProGuard has essentially taken the contents of displayText and moved them into the access method and deleted the displayText method. This access method was the only one referring to that private method, so it just inlined it because no one else was using it. Yes, ProGuard can kind of help. But it’s also not guaranteed to be able to do this. We got lucky because this is such a trivial example, but it is certainly not guaranteed. You might think, “Well, I don’t really nest classes that much, maybe it’s a handful. So if I’m only getting a handful of these extra methods, it’s not a big deal, right?”

Anonymous class (13:06)

Let me introduce you to our friend the anonymous class:

class MyActivity extends Activity {
    @Override protected void onCreate(Bundle state) {
        super.onCreate(state);

        setContentView(R.layout.whatever);
        findViewById(R.id.button).setOnClickListener(
            new OnClickListener() {
                @Override public void onClick(View view) {
                    // Hello!
                }
            });
    }
}

Anonymous classes behave the exact same way. They are effectively the same thing. It’s a nested class, but it has no name. If in these listeners, which we use all too frequently, you reference a private method in the imposing class, that’s going to generate a synthetic access method.

class MyActivity extends Activity {
    @Override protected void onCreate(Bundle state) {
        super.onCreate(state);

        setContentView(R.layout.whatever);
        findViewById(R.id.button).setOnClickListener(
            new OnClickListener() {
                @Override public void onClick(View view) {
                    doSomething();
                }
            });
    }
    
    private void doSomething() {
        // ...
    }
}

That is also true for fields:

class MyActivity extends Activity {
    private int count;

    @Override protected void onCreate(Bundle state) {
        super.onCreate(state);

        setContentView(R.layout.whatever);
        findViewById(R.id.button).setOnClickListener(
            new OnClickListener() {
                @Override public void onClick(View view) {
                    count = 0;
                    ++count;
                    --count;
                    count++;
                    count--;
                    Log.d("Count", "= " + count);
                }
            });
    }
}

I think this is the one that’s probably a lot more common. We have fields in these outer classes, these activities that we’re modifying the state of, inside of these listeners. I’ve done a completely awful implementation here, but what I’m doing is setting a value. I’m using a pre-increment, pre-decrement, post-increment, and a post-decrement, and then the log message has to read the value from the field. How many methods are we going to get from this? Maybe only two. Maybe one for reading, one for writing, and then the increment and decrement get turned into reads increments and writes. If that was only the case:

$ javac -bootclasspath android-sdk/platforms/android-24/android.jar \
MyActivity.java

$ javap MyActivity
class MyActivity extends android.app.Activity {
    MyActivity();
    protected void onCreate(android.os.Bundle);
    static int access$002(MyActivity, int); // count = 0    write
    static int access$004(MyActivity);        // ++count         preinc
    static int access$006(MyActivity);        // --count         predec
    static int access$008(MyActivity);        // count++         postinc
    static int access$010(MyActivity);        // count--         postdec
    static int access$000(MyActivity);        // count         read
}

We compile this, and one method is being generated for each of those types. So if you think that in an activity or fragment or whatever, you have four or five listeners, you have maybe ten fields that are private in the outer class. You’re getting a nice combination of explosion of access methods. You still might be unconvinced that this is a problem. Say, “Well, maybe it’s 50, maybe it’s 100. Is that really a big deal?” It turns out we can find out.

In the wild (15:03)

You can find out how prevalent these are in the wild. These commands will pull every APK from your phone. Every app that you’ve installed, that’s a third party app:

$ adb shell mkdir /mnt/sdcard/apks

$ adb shell cmd package list packages -3 -f \
| cut -c 9- \
| sed 's|=| /mnt/sdcard/apks/|' \
| xargs -t -L1 adb shell cp

$ adb pull /mnt/sdkcard/apks

We can write a script which uses this dex method list and then greps all the different numbers:

#!/bin/bash                                                 accessors.sh
set -e

METHODS=$(dex-method-list $1 | \grep 'access\$')
ACCESSORS=$(echo "$METHODS" | wc -l | xargs)
METHOD_AND_READ=$(echo "$METHODS" | egrep 'access\$\d+00\(' | wc -l | xargs)
WRITE=$(echo "$METHODS" | egrep 'access\$\d+02\(' | wc -l | xargs)
PREINC=$(echo "$METHODS" | egrep 'access\$\d+04\(' | wc -l | xargs)
PREDEC=$(echo "$METHODS" | egrep 'access\$\d+06\(' | wc -l | xargs)
POSTINC=$(echo "$METHODS" | egrep 'access\$\d+08\(' | wc -l | xargs)
POSTDEC=$(echo "$METHODS" | egrep 'access\$\d+10\(' | wc -l | xargs)
OTHER=$(($ACCESSORS - $METHOD_AND_READ - $WRITE - $PREINC - $PREDEC - $POSTINC - $POSTDEC))

NAME=$(basename $1)

echo -e "$NAME\t$ACCESSORS\t$READ\t$WRITE\t$PREINC\t$PREDEC\t$POSTINC\t$POSTDEC\t$OTHER"

Then we can run this crazy command, which is going to go over every single APK that I’ve pulled off my phone, run this script, and then make a nice pretty table:

$ column -t -s $'\t' \
<(echo -e "NAME\tTOTAL\tMETHOD/READ\tWRITE\tPREINC\tPREDEC\tPOSTINC\tPOSTDEC\tOTHER" \
&& find apks -type f | \
xargs -L1 ./accessors.sh | \
sort -k2,2nr)

You can see the table on slide 77, it sorts it by the most accessor methods by package name. And for my phone, we’re in the multiple thousands. Amazon makes up five of the top six here. The top ones are in 5,000 synthetic accessor methods. 5,000 methods, that’s an entire library. That’s like an apken pad. You have an entire apken pad of useless methods that only exist to be jumping points to some other method.

Also because we looked at ProGuard, obfuscation is going to screw with these numbers. Inlining is going to screw with them. Don’t take them as the exact number. It just puts you in the ballpark of realizing how many methods are made up. How many methods are in your application that are better than these useless accessor inputs? By the way, Twitter at the bottom here? They have 1,000. They’re ProGuarding their apps, so there’s probably a lot more. But I thought it’s interesting because they’re the lowest number reported methods for the highest number of actual methods. They have 171,000 methods in their app and only 1,000 synthetic accessors that we use. That was pretty impressive.

We can fix this. That is easy to do. We just have to not make something private. We have to make it package scoped when we’re referencing it across those boundaries. IntelliGate has an inspection for this. It’s not enabled by default, but you can go in and search for a private member. Which is fun to search for, and it will take our example, and it will highlight it yellow. You can option enter on it, and it will give you an intention action to just take that private member that you’re accessing and make it package scoped.

When you think about these nested classes, try to think of them as siblings instead of with having a parent-child relationship. You can’t really access a private member from an outer class. You have to make it package scoped. That is not going to cause problems because even if you’re a library, hopefully, these people aren’t putting classes in the same packages as you so that they can access these things that you’re making more visible. I also put in a feature request for a link check for this. Hopefully, in the future, you could potentially fail your build if you’re doing this.

I have been going through a ton of open source libraries and fixing these visibility problems so that the libraries themselves don’t impose hundreds of extra methods. That is super important for libraries that do code generation. We were able to reduce the number of methods in our application by 2700 just by changing one of our code generation steps. 2700 methods for free for generating something that was package scope instead of private scope.

Synthetic methods (18:45)

These synthetic methods are called synthetic because you didn’t write them.

// Callbacks.java

interface Callback<T> {
    void call(T value);
}

class StringCallback implements Callback<String> {
    @Override public void call(String value) {
        System.out.println(value);
    }
}

They were generated automatically for you. These accessor ones are not the only ones. Generics is another thing that came post-Java 1.0, and so it had to be retrofitted into how Java works. We see this a lot in libraries and even in our own applications. We’re using these generic interfaces because they’re extremely convenient, and they allow us to retain type safety.

$ javac Callbacks.java

$ javap StringCallback
class StringCallback implements Callback<java.lang.String> {
    StringCallback();
    public void call(java.lang.String);
    public void call(java.lang.Object);
}

If we just have a method that accepts a generic value, when you compile this, you’re going to find that every method that accepts that generic is going to get turned into two methods. One that accepts a string, whatever the generic type that you’re using is, and one that’s just object. That is like what erasure is. You hear a lot of people talk about erasure, and you might not understand what’s happening. This is like the manifestation of erasure. We have to generate code that only uses object because that’s what is going to end up being called when you access this generic method.

$ javap -c StringCallback
class StringCallback implements Callback<java.lang.String> {
    StringCallback();
        Code: <removed>

    public void call(java.lang.String);
        Code: <removed>

    public void call(java.lang.Object);
        Code:
        0: aload_0
        1: aload_1
        2: checkcast #4 // class java/lang/String
        5: invokevirtual #5 // Method call:(Ljava/lang/String;)V
        8: return
}

If we look at what goes in that extra method, it’s just a cast. It casts to that year type and then it calls the real implementation that accepts the generic. Anyone that’s calling this call method is going to dispatch to this object method. Their calling code is going to pass in whatever their object is and then this code has to cast it and call into the real implementation. Every method that uses a generic, winds up turning into two methods.

// Providers.java

interface Provider<T> {
    T get(Context context);
}

class ViewProvider implements Provider<View> {
    @Override public View get(Context context) {
        return new View(context);
    }
}

That is also true of return values. If you have a method that’s returning a generic, you basically see the exact same thing.

$ javac -bootclasspath android-sdk/platforms/android-24/android.jar \
    Example.java

$ javap -c ViewProvider
class ViewProvider implements Provider<android.view.View> {
    ViewProvider();
        Code: <removed>

    public android.view.View get(android.content.Context);
        Code: <removed>

    public java.lang.Object get(android.content.Context);
        Code:
            0: aload_0
            1: aload_1
            2: invokevirtual #4 // Method get:(…)Landroid/view/View;
            5: areturn
}

Two methods get generated. One that returns. In this case, our view. Then at the bottom, one that returns objects. This one’s a lot more simple because it doesn’t have to really do anything, it just accepts the view and then re-returns it as an object type.

// Providers.java

interface Provider<T> {
    T get(Context context);
}

class ViewProvider implements Provider<View> {
    @Override public View get(Context context) {
        return new View(context);
    }
}

Another interesting thing to note here, which a lot of people don’t realize, is that this is a feature of the Java language.

class ViewProvider implements Provider<View> {
    @Override public View get(Context context) {
        return new View(context);
    }
}

class TextViewProvider extends ViewProvider {
    @Override public TextView get(Context context) {
        return new TextView(context);
    }
}

If you have a method that you’re overriding, you can change its return value to be a more specific version of that type. That is called a covariant return type. It doesn’t even have to be, like in this case, we’re implementing an interface. The base class doesn’t even have to be an interface or anything. You can just be overriding a method from a base class. You can change its return type to be something more specific.

The reason that you would do something like this is if you had other methods in our second class, which had to call into this get method, and they want the implementation type. Not the more broad, in this case, View type. It will allow them to do customizations that were only specific to TextView because they’re in that class already.

Covariant return type (21:58)

Covariant return type. I’m sure you can guess what happens here.

$ javap TextViewProvider

class TextViewProvider extends ViewProvider {
    TextViewProvider();
    public android.widget.TextView get(android.content.Context);
    public android.view.View get(android.content.Context);
    public java.lang.Object get(android.content.Context);
}

We get another method. In this case, it’s both a generic and a covariant return type. We’ve taken our one method and turned it into three by basically not doing anything. This is a python script that detects these:

#!/usr/bin/python

import os
import subprocess
import sys

list = subprocess.check_output(["dex-method-list", sys.argv[1]])

class_info_by_name = {}

for item in list.split('\n'):
    first_space = item.find(' ')
    open_paren = item.find('(')
    close_paren = item.find(')')
    last_space = item.rfind(' ')

    class_name = item[0:first_space]
    method_name = item[first_space + 1:open_paren]
    params = [param for param in item[open_paren + 1:close_paren].split(', ') if len(param) > 0]
    return_type = item[last_space + 1:]
    if last_space < close_paren:
        return_type = 'void'

    # print class_name, method_name, params, return_type

    if class_name not in class_info_by_name:
        class_info_by_name[class_name] = {}
    class_info = class_info_by_name[class_name]

    if method_name not in class_info:
        class_info[method_name] = []
    method_info_by_name = class_info[method_name]

    method_info_by_name.append({
        'params': params,
        'return': return_type
    })

count = 0
for class_name, class_info in class_info_by_name.items():
    for method_name, method_info_by_name in class_info.items():
        for method_info in method_info_by_name:
            for other_method_info in method_info_by_name:
                if method_info == other_method_info:
                    continue # Do not compare against self.
                params = method_info['params']
                other_params = other_method_info['params']
                if len(params) != len(other_params):
                    continue # Do not compare different numbered parameter lists.

                match = True
                erased = False
                for idx, param in enumerate(params):
                    other_param = other_params[idx]
                    if param != 'Object' and not param[0].islower() and other_param == 'Object':
                        erased = True
                    elif param != other_param:
                        match = False

                return_type = method_info['return']
                other_return_type = other_method_info['return']
                if return_type != 'Object' and other_return_type == 'Object':
                    erased = True
                elif return_type != other_return_type:
                    match = False

                if match and erased:
                    count += 1
                    # print "FOUND! %s %s %s %s" % (class_name, method_name, params, return_type)
                    # print " %s %s %s %s" % (class_name, method_name, other_params, other_return_type)

print os.path.basename(sys.argv[1]) + '\t' + str(count)

That took a long time to figure out, but I wanted to know how prevalent this was in applications. We can go through the same process and run it over every app that I had installed.

$ column -t -s $'\t' \
    <(echo -e "NAME\tERASED" \
        && find apks -type f | \
            xargs -L1 ./erased.py | \
            sort -k2,2nr)

It’s in the low thousands. There’s not a whole lot you can do here. I showed that ProGuard can kind of help. The advantage of this case is that if ProGuard can detect that no one is referring to the generic version of the method, the method that takes an object or returns object. It can eliminate that, so you will see hundreds or thousands of these can be removed by ProGuard. But some of them can’t because you are using them in the generic sense where you’re calling the method on the interface.

The last example that I want to look at for methods deals with something that’s new and upcoming in Android which is the the Java 8 language features.

class Greeter {
    void sayHi() {
        System.out.println("Hi!");
    }
}

class Example {
    public static void main(String... args) {
        Executor executor = Executors.newSingleThreadExecutor();
        final Greeter greeter = new Greeter();
        executor.execute(new Runnable() {
            @Override public void run() {
                greeter.sayHi();
            }
        });
    }
}

We’ve had retro-lamina for awhile. But now the Jack compiler is also implementing these in the same spirit, which is allowing them to be used in a way that’s back portable. But is there an associated cost to these new language features?

I have a simple class which says, Hi when we call its method. What I want to do is take my Greeter and make it say hello on this Executor. Executor has a single method called run, and it accepts a Runnable. In the current world, we would make that creator type final. Then we would create a new runnable and call the method directly.

class Greeter {
    void sayHi() {
        System.out.println("Hi!");
    }
}

class Example {
    public static void main(String... args) {
        Executor executor = Executors.newSingleThreadExecutor();
        Greeter greeter = new Greeter();
        executor.execute(() -> greeter.sayHi());
    }
}

In Lambda World it’s just reducing the verbosity of doing that. That will still create a Runnable, but it does so implicitly. You don’t have to specify the type. You don’t have to know the actual method name or the argument types.

The last one is method references. These are a little more interesting because it infers that this is a method that returns nothing and accepts no arguments. I can turn that into a Runnable automatically because I know all I have to do is call that method.

class Greeter {
    void sayHi() {
        System.out.println("Hi!");
    }
}

class Example {
    public static void main(String... args) {
        Executor executor = Executors.newSingleThreadExecutor();
        Greeter greeter = new Greeter();
        executor.execute(greeter::sayHi);
    }
}

How much do these cost? (24:45)

That’s fun, but how much do these cost? What are the costs of applying these language features? I set up a Retrolambda toolchain here and a toolchain using Jack.

Retrolambda toolchain

$ javac *.java

$ java -Dretrolambda.inputDir=. -Dretrolambda.classpath=. \
    -jar retrolambda.jar

$ dx --dex --output=example.dex *.class

$ dex-method-list example.dex


Jack toolchain 

$ java -jar android-sdk/build-tools/24.0.1/jack.jar \
        -cp android-sdk/platforms/android-24/android.jar \
        --output-dex . *.java

$ dex-method-list classes.dex

Neither of these is using ProGuard, or minification in Jack’s case, because it doesn’t affect the results. In the anonymous class case, the case that we’ve been using for so long, it’s always two methods.

Example$1 <init>(Greeter)
Example$1 run()

$ javap -c 'Example$1'
final class Example$1 implements java.lang.Runnable {
    Example$1(Greeter);
        Code: <removed>

    public void run();
        Code:
        0: aload_0
        1: getfield #1 // Field val$greeter:LGreeter;
        4: invokevirtual #3 // Method Greeter.sayHi:()V
        7: return
}

If we compile our example, we see that this is our anonymous class, given assigned a monotonically increasing number here. We see the constructor. The constructor’s going to take in the Greeter class for us and then it has a run method, which if we decompile it, all it does is call the method. That is exactly what we expect, very straightforward.

When we do a lambda, if you’re using an old version of retrolambda, this is really expensive. That one little tiny line of code can be six or seven methods to produce that functionality. Thankfully, the current version’s down to four. And Jack even bests it with three, so only one worse than the anonymous class. But what’s the difference? Why is there that extra method?

We know how to figure that out. This is retrolambda, which has two additional methods:

Example lambda$main$0(Greeter)
Example$$Lambda$1 <init>(Greeter)
Example$$Lambda$1 lambdaFactory$(Greeter)  Runnable
Example$$Lambda$1 run()

$ javap -c 'Example$$Lambda$1'

final class Example$$Lambda$1 implements java.lang.Runnable {
    public void run();
        Code:
            0: aload_0
            1: getfield #15 // Field arg$1:LGreeter;
            4: invokestatic #21 // Method Example.lambda$main$0:
            7: return
}

The one that’s new is the top one. What’s happening here is, you’re defining a block of code inside the lambda, and that code has to go somewhere. It’s not encoded in the method that defines the lambda because that would be weird. It doesn’t belong to that method. It has to be stored somewhere so that it can be passed into whatever you’re calling.

That’s what this top method is. It just copies and pastes whatever you write into the lambda into method in that same class. You can see the implementation here. All it does is delegate to the sayHi method. Very similar to what our runnable implementation did. We still have our constructor. We still have our run method. Except the repair of the run method is different. Instead of calling Greeter directly, it’s going to call back into the original class and call that lambda method. That is the extra method. Here’s where retrolambda screws it up.

Example lambda$main$0(Greeter)
Example$$Lambda$1 <init>(Greeter)
Example$$Lambda$1 lambdaFactory$(Greeter)  Runnable
Example$$Lambda$1 run()

Instead of calling the constructor directly to create this generated class, it generates another method, which is a static factory method that calls the constructor. Jack’s basically the same thing except it doesn’t have that additional static method.

Example -Example_lambda$1(Greeter)
Example <init>()
Example main(String[])
Example run(Runnable)
Example$-void_main_java_lang_String__args_LambdaImpl0 <init>(Greeter)
Example$-void_main_java_lang_String__args_LambdaImpl0 run()
Greeter <init>()
Greeter sayHi()
java.io.PrintStream println(String)
java.lang.Object <init>()
java.lang.Runnable run()

We still get the lambda one, although it’s named kind of crazy, and then the generated class is named very creatively with the entire type signature in the name. So that’s it. Three methods. Lambdas cause that additional method at the top to be created. That’s why they cost one additional method.

Method references are also really interesting. Retrolambda and Jack are essentially tied. Retrolambda sometimes has to generate an extra method, and that’s because you might be referring to a private method, and so it’s not retrolambda generating it. Java generates one of those synthetic accessor methods because if you’re passing a reference to a private method to another class, it’s going to have no way of calling that. That’s what the fourth one is.

Jack, interestingly enough, generates three for every single method reference. It should be generating two for every single one except for that private case. In that case, it should only be generating three. Currently, it generates an accessor method for every single method reference. There’s a bug filed for that too, so hopefully we’ll see Jack get down to two methods. That’s really important because that puts the method reference on the same par as the anonymous class. It makes it a zero cost abstraction from switching from the anonymous class to the method reference, which will be nice.

Lambda’s, unfortunately, will likely never get down to the same amount. The reason that that will never happen is that you also could be referring to private members or private methods inside of the lambda. It can’t copy that into the generated runnable. Because again, it doesn’t have any way of accessing those things. We can count these as well.

Lambdas in the wild (30:05)

Let’s look and see how many lambdas are being used in the wild. Same deal. By the way, this takes a long time.

#!/bin/bash             lambdas.sh
set -e

ALL=$(dex-method-list $1)

RL=$(echo "$ALL" | \grep ' lambda\$' | wc -l | xargs)
JACK=$(echo "$ALL" | \grep '_lambda\$' | wc -l | xargs)

NAME=$(basename $1)

echo -e "$NAME\t$RL\t$JACK"
$ column -t -s $'\t' \
    <(echo -e "NAME\tRETROLAMBDA\tJACK" \
        && find apks -type f | \
            xargs -L1 ./lambdas.sh | \
            sort -k2,2nr)

NAME                         RETROLAMBDA     JACK
com.squareup.cash             826             0
com.robinhood.android         680             0
com.imdb.mobile             306             0
com.stackexchange.marvin     174             0
com.eventbrite.attendee     53                 0
com.untappdllc.app             53                 0

It takes around 10 minutes. It also depends on how many apps that you end up pulling. If you wind up doing this for whatever reason, don’t get impatient. It will take a long time, but you will get a result.

Unfortunately, apparently not a lot of people are using lambdas, and I was excited to see that we pay the most. We have 826 lambdas. That’s the number of lambdas and not the number of methods. The number of methods that we have from our lambdas is 826 times three or maybe four.

No one’s using Jack yet, or at least, of the applications I installed, no one’s using Jack with lambdas. They might be using Jack and not using lambdas, which would be weird. Or additionally, they could be ProGuarding.

So again, this ProGuard completely hides these lambda classes and names of the methods. If you’re a popular application using lambdas, and you ProGuard, that’s probably why you’re not on this list. Or I just don’t like your app. That was all on methods.

The reason I was looking into this was one, to stave off hitting the 65K limit. But those methods have a run time cost as well. There are costs for loading additional bytecode. There’s also costs for the extra trampoline that you have to go through when you’re executing them. The private field one is my favorite because a lot of times you see that happen inside these anonymous listeners. Those are usually running as the result of the UI interaction on the main thread.

What you don’t want is a computationally expensive piece of code that you have to run on the main thread, whether it’s for animation, size calculation, whatever. You don’t want those, every single time you’re referencing those fields; you don’t want to be jumping through that extra method. Looking up a field is fairly fast. Invoking a method and then looking up a field is still going to be fast, but it’s 100% of the time going to be slower than just looking up the field. You’re introducing these indirections which you’re not going to be dropping frames all of a sudden because these accessor methods exist. But they’re useless methods that the only thing they serve to do is bloat your APK and very, very subtly slow down your application.

Collections (33:21)

조금 수정하여 런타임에 초점을 맞춰 얘기해보겠습니다. 컬렉션과 관련된 것인데요.

HashMap<K, V>                ArrayMap<K, V>
HashSet<K, V>                ArraySet<V>
HashMap<Integer, V>            SparseArray<V>
HashMap<Integer, Boolean>    SparseBooleanArray
HashMap<Integer, Integer>    SparseIntArray
HashMap<Integer, Long>        SparseLongArray
HashMap<Long, V>            LongSparseArray<V>

여러분의 앱에 이런 것을 쓰고 계시다면, 필요 이상으로 리소스를 낭비하고 있을지 모릅니다.

많은 분들이 익히 알고 계시듯, 안드로이드는 이런 특별한 컬렉션을 가지고 있습니다. 세부 구현은 다르지겠만 흔하게 접하게 되는 상황들에 특화되어 있습니다. 예를 들어, 맵에서 어떤 값을 나타내주는 인티저 인덱스가 있다고 하면, 여기에 사용할 수 있도록 특화된 컬렉션이 있습니다.

많은 사람들이 이런 오토박싱(autoboxing)에 대해 많이 얘기했습니다. 오토박싱이 생소하신 분들을 위해 잠시 설명드리겠습니다.

HashMap<Integer, V>
int ---> Integer.valueOf ---> put(Integer, V)
getKey() ---> Integer.intValue ---> int

인티저 키를 받을 수 있는 해쉬맵이 있고 특정 인티저 값을 맵에 넣고자 하는 상황을 가정해 보겠습니다. 아래 경우처럼, 엔트리를 순회하면서 그리고 키로부터 밸류를 꺼내고 싶다면 이 컨버전은 간단하지 않습니다. 이 때 오토박싱이라고 하는 추가 단계를 거치면서 primitive 값을 받아서 클래스 버전으로 돌려줍니다. 지금 상황에서는 인터저겠죠.

아래의 경우 타입을 언랩핑하므로 비용이 높지 않습니다. 그러나 위의 경우 비용이 커질 가능성이 높습니다. 적은 수에 대해서는 캐쉬가 있으니 그리 나쁘지 않은데, 만약 대량의 랜덤 인티저를 처리해야하는 경우라면 메서드를 부를 때마다 매번 오브젝트를 할당하게 됩니다. 이것은 제네릭이면서 많은 I 인티저를 받습니다. 이게 많은 사람들이 장점으로 꼽는 이유입니다. 그런데 이것 말고도 잘 알려지지 않은 다른 두 개의 큰 장점이 있습니다.

첫째는 데이터 인디렉션입니다. 해쉬맵의 내부 구현을 보면 노드들의 배열로 되어있고 고유 사이즈를 갖습니다. 밸류를 넣거나 찾으려면 이 배열로 와야합니다. 이 것이 해싱 단계로, 해쉬를 찾고나서 차감을 계산하는 과정에서 비용과 시간이 소요됩니다. 한편 이는 노드의 배열로 이뤄지는데, 노드타입은 키와 밸류를 모두 갖고 있습니다. 또한 추가 노드를 가리키는 포인터를 갖는 해쉬를 가지고 있죠.

배열이 있고, 노드에 대한 참조를 찾았으니 이제 그 노드로 가야할 차례입니다. 해당 밸류를 원한다면 그 노드 안을 살펴야겠죠. 즉, 밸류에 대한 참조값을 갖고 그 내부 값을 살펴보는 인디렉션들을 거쳐야 합니다. 이들이 메모리의 다른 공간에 들어있으므로 여러분은 하나의 키에 대한 값을 얻거나 값을 넣기 위해 이동을 해야 하죠. 이 경우 상황은 더 나빠질 수 있습니다.

이를 해쉬 충돌 문제라고 하는데, 두개의 아이템이 하나의 버킷에 해쉬될 때 발생하는 것입니다. 해쉬맵은 버킷 안의 링크드 리스트로 바뀌며, 그 링크드 리스트를 따라가 정확히 매치되는 해쉬를 찾아야 합니다. sparse 배열의 재배열 예제로 한 번 살펴 볼까요? 미리 언급하자면 앞서 말한 또 다른 장점은 오버헤드와 관련이 있습니다. 이 컬렉션들이 사용하는 메모리 상의 오버헤드를 염두에 두고 한번 살펴봅시다. 아래 예제에는 두 개의 클래스가 있습니다.

$ java -jar jol-cli-0.5-full.jar internals java.util.HashMap
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
java.util.HashMap object internals:
OFFSET     SIZE     TYPE DESCRIPTION                 VALUE
0         4         (object header)                 01 00 00 00
4         4         (object header)                 00 00 00 00
8         4         (object header)                 9f 37 00 f8
12         4         Set AbstractMap.keySet             null
16         4         Collection AbstractMap.values     null
20         4         int HashMap.size                 0
24         4         int HashMap.modCount             0
28         4         int HashMap.threshold             0
32         4         float HashMap.loadFactor         0.75
36         4         Node[] HashMap.table             null
40         4         Set HashMap.entrySet             null
44         4         (loss due to the next object alignment)

Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Java object layout이라고 하는 도구를 사용하면 오브젝트를 메모리에 생성하는 오버헤드에 대해 알 수 있습니다. 해쉬맵 위에서 동작하는 이 도구는 많은 정보들을 프린트해줍니다. 필드별로 비용을 보여준 비용을 살펴볼까요? 중요한 숫자는 아래에 있는데, 해쉬맵의 모든 인스턴스가 있는 곳입니다. 노드나, 키, 밸류 등이 아닌 바로 해쉬맵입니다. 해쉬맵 자체는 48 bytes입니다. 나쁘지 않은 크기네요.

$ java -jar jol-cli-0.5-full.jar internals 'java.util.HashMap$Node'
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.

java.util.HashMap$Node object internals:
OFFSET     SIZE     TYPE DESCRIPTION     VALUE
0         12         (object header)     N/A
12         4         int Node.hash         N/A
16         4         Object Node.key     N/A
20         4         Object Node.value     N/A
24         4         Node Node.next         N/A
28         4         (loss due to the next object alignment)

Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

이제 노드 오브젝트를 돌려 볼까요? 같은 과정을 통해 4개의 필드를 확인할 수 있습니다. 맵의 각 노드별로 32 bytes입니다. 맵의 각각의 아이템, 키 밸류 쌍은 이 노드들 중의 하나에 속하게 됩니다. 전체 항목 수의 32배가 되죠.

값을 입력하기 시작할때, 런타임시 실제 오브젝트의 사이즈를 파악하기 위해 이런 공식을 사용할 수 있습니다. 어레이 같은 것들도 계산해야 하므로 완전히 정확한 것은 아닙니다. 노드를 홀드해야 하는 어레이가 있다면 어레이가 차지할 공간도 당연히 계산해야 할테니 복잡한 문제입니다. 각 어레이를 참조하는 하나의 인티저는 4이며, 어레이 크기에 따라 배가 됩니다.

문제는 해쉬맵이 로드팩터라고 하는 것을 가지고 있다는 것입니다. 끝의 8은 모든 어레이의 상수 오버헤드입니다. 그런데 로드 팩터는 가득 채워지지 않습니다. 어느 정도로 차있는 수준을 유지합니다. 그래서 그 적정 선에 도달하면 어레이 리스트와 마찬가지로 커지게 됩니다. 해쉬맵 역시 빈 공간을 유지하기 위해 커집니다.

그 이유는, 이렇게 하지 않는다면 많은 충돌과 성능 저하를 경험하게 될 것이기 때문입니다. 아마도 로드 팩터가 어떻든 많은 엔트리를 갖는 해쉬맵 밸류가 될 겁니다. 우리는 이게 메모리에서 얼만큼의 bytes를 사용하는지 확인할 수 있습니다. 그런데 디폴트 로드 팩터는 75%입니다. 해쉬맵은 3/4만큼만 채워진 상태를 유지하려 합니다.

Sparse array(희소 행렬)는 이렇게 해쉬맵을 재배치할 때 꼭 사용해야만 하는 것입니다. 해쉬맵에서 봤던 2개의 경우를 볼까요. sparse array는 형제인 2개의 어레이를 가집니다. 하나는 키이고, 다른 하나는 밸류입니다. 이 맵에서 밸류를 찾거나 밸류를 넣기위해 처음 해야하는 것은 해쉬맵에서와 달리 인티저 어레이로 가는 것입니다. 해시맵과 달리 상수시간이 걸리는 작업이 아닙니다. 어레이 안에서 바이너리 서치를 해서 밸류 어레이로 갈 수 있고,밸류가 있는 셀을 돌려받을 수 있습니다. 즉, 밸류 어레이이므로 반환받은 후 바로 레퍼런스로 갈 수 있고 값을 반환받을 수 있다는 것이죠.

메모리에 대해서는 덜 간접적인 것들이 많습니다. 인티저 어레이는 연속적이며 링크드 리스트가 없고, 바로 밸류 어레이 내부로 들어갈 수 있습니다. 우리가 언랩해서 밸류에 접근해야 하는 노드 오브젝트도 없고, 간접성도 줄어듭니다. 그렇지만, 완전히 상수 시간이 아닌 작업의 속도가 더 느려질 수 있습니다. 이 때문에 상대적으로 작은 맵을 유지하고자 하게 되죠. 작다는 정도는 수백 개 정도 규모의 엔트리들을 뜻하는데, 만약 수천 개를 넣는다면 바이너리 서치의 성능은 느려지게 될 것입니다. 아마 이제 해쉬맵의 오버헤드가 성능면에서 매력적으로 보이기 시작하실테죠.

$ javac SparseArray.java

$ java -cp .:jol-cli-0.5-full.jar org.openjdk.jol.Main \
internals android.util.SparseArray

# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.

android.util.SparseArray object internals:
OFFSET     SIZE     TYPE DESCRIPTION                 VALUE
0         4         (object header)                 01 00 00 00
4         4         (object header)                 00 00 00 00
8         4         (object header)                 1a 69 01 f8
12         4         int SparseArray.mSize             0
16         1         boolean SparseArray.mGarbage     false
17         3         (alignment/padding gap)         N/A
20         4         int[] SparseArray.mKeys         [0, 0, 0, 0, 0, 0, ]
24         4         Object[] SparseArray.mValues     [null, null, null, ]
28         4         (loss due to the next object alignment)

Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

이 경우는 클래스들은 완전히 동일하며, JVM은 64bit입니다. 그리고 안드로이드도 이제 64bit입니다. 이게 싫다면 근사치로 처리하고 20% 정도의 variance를 허용해주세요. 분명히 안드로이드 자체에서 오브젝트 사이즈를 구하는 불가능한 일을 시도하는 것보다 쉬운 길입니다.

sparse array를 위한 오브젝트 자체는 32bytes로 조금 작습니다. 그 사이즈는 문제가 되지 않지만, 이것들이 단지 32 plates가 아니라는 점이 문제가 됩니다. 또 엔트리 문제 역시 있습니다. 이 어레이 역시 계산돼야 하죠. 즉, 키를 위한 인티저들 그리고 엔트리 수의 4배, 여기에 8을 더해야 합니다. 그 후 밸류를 위한 어레이가 또 있습니다. 똑같이 엔트리의 4배에 8을 더해야 하죠.

여기서 다른 점은 sparse array는 로드팩터를 가지지 않는데, 이 어레이들은 바이너리 트리이므로 암묵적으로 로드팩터를 갖는다는 점입니다. 이들은 채워지지 않고, 인접하지 않으며, 내부에 사용되지 않는 공간이 있습니다. 로드팩터와 유사한 임시방편적인 요소(fudge factor)를 가지고 살펴볼까요? 이 경우는 맵에 넣는 데이터에 전적으로 의존합니다. 같은 밸류를 사용하면서, 어레이들이 75%정도로 채워진다고 안전하게 가정해 보겠습니다.

SparseArray<V>
32 + (4 * entries + 4 * entries) / 0.75

HashMap<Integer, V>
48 + 32 * entries + 4 * (entries / loadFactor) + 8

이제 이들을 직접적으로 비교할 수 있습니다. 앞서 indirection jump를 카운트 할 수 있음을 설명드렸죠. 이제 간단한 식들을 사용해서 인스턴스들이 차지하는 실제 메모리의 값들을 비교할 수 있습니다.

SparseArray<V>
32 + (4 * 50 + 4 * 50) / 0.75 = 656

HashMap<Integer, V>
48 + 32 * 50 + 4 * (50 / 0.75) + 8 = 1922

해쉬맵에 디폴트값인 .75를 사용하겠습니다. 엔트리 수를 위해 수를 선택할 수 있는데, 여기서는 50을 선택했습니다. 아마도 미국 주에 대한 맵일 수 있겠네요. 이제 계산을 실행합니다. sparse array가 전체 사이즈의 1/3사이즈라는 것을 볼 수 있을 겁니다. 약간의 성능 오버헤드가 있음을 기억하세요. 왜냐하면 각각의 오퍼레이션은 더 이상 상수 시간이 아니기 때문입니다. 50개 요소가 있으니까 바이너리 트리에서 검색하는 수는 매우 빠를 것입니다.

결론 (44:18)

많은 작업을 했습니다만, 결국은 이런 오버헤드를 피하기 위해서 컴파일 타임이나 런타임에 해야 할 아주 사소한 것들이 있다는 겁니다.

첫번째는 이미 말씀드린대로 private 멤버를 절대 무시하지 말고 잘 조사해 보시라는 점입니다. 모든 싱글 타입에 대해 할 필요도, 한번에 모든 것을 다 찾아 해야 할 필요도 없습니다. 여러분의 앱에서 하시듯 하면 되겠죠. 반대로 라이브러리라면 조금 더 중요하겠습니다.

라이브러리라면 여러분은 필히 APK사이즈와 런타임 성능에 미치는 영향을 최소화해야 할 것입니다. deck 사이즈와 런타임 성능도 신경써야 합니다. 라이브러리를 가지고 계신다면, 아마 이런 모든 것들을 찾기를 원하실 겁니다. 라이브러리에 dex 파일에서 공간을 낭비하는 synthetic accessor 메서드들이 존재해야 할 이유는 없습니다. 런타임에서 시간을 낭비하는 이런 메서드를 발견하면 버그 리포트를 하세요.

만약 retrolambda를 사용하고 계신다면 제발, 제발, 제발 최신 버전으로 업그레이드하세요. 아니면 아마도 수천개의 메서드를 낭비하게 될겁니다. 만약 오픈소스 라이브러리를 작성한다면, 익명 클래스를 잘 다루고 받아들이셔야 합니다. 아주 어려운 일은 아닙니다. 그런데 한 번 더, 여러분이 앱 개발자들에게 미치는 영향을 최소화하고 싶다면, 라이브러리라서 문제가 되진 않습니다.

Jack에 대해 말해보자면, 이건 꽤 큰 일입니다. 여전히 빠르게 개발이 진행 중이며, 많은 개발자들이 적용할 수 있는 많은 것들을 놓치고 있습니다. 하지만 이보다 더 진부하거나 빌드타임에 더 색다른 작업을 하는 어플리케이션도 물론 존재하겠죠.

버그를 무시하지 마세요. 스위치 하지 말고, 충돌나는 것을 찾으면서, 어쨌든 ‘에이, 2년 안에는 고쳐야지’ 하면서 넘어가지 마세요. 여러분은 할 수 있습니다. Java C 인덱스로 돌아가기 전에 버그를 리포트 할 수 있습니다. 이건 나중에 할 일이라면서 귀를 닫고 눈을 감을 수 있겠지만, 이건 항상 벌어지는 일이므로 가능한 빨리 찾는게 더 낫습니다. 이런 상황에서 ProGuard가 도움이 됩니다.

과도하게 룰을 사용하지는 마세요. ProGaurd 룰 파일에서 * * 를 본다면, 이건 100% 틀린 것이므로 쓰지 말아야 합니다. 왜냐하면 이를 사용하면 ProGuard에서 얻은 장점들이 없어지기 때문입니다. 여러분은 “맞아, 난 OkHttp에서 풀링하지니까 지금 쓰지 않는 HTTP/2의 이 메서드들을 원하지는 않아. 그래도 그것들을 버리진 않을거야. 만약을 위해 그것들을 가지고 있겠어”라고 할 수는 있겠지만, 현명한 방법은 아니죠. 만약 오픈 소스 라이브러리에서 이것들을 발견하신다면 버그 리포팅을 하세요. 만약 여러분 앱에서도 가지고 있다면 왜인지부터 알아 내고, 이를 제거하고 나서 ProGuard의 실패에 대해 살펴보세요. 또 여러분의 룰을 바꿀 수 있는 더 구체적인 것이 있는지 보시고요. 만약 더 깊게 알아보고 싶다면 아래 프레젠테이션들을 참고해 보세요.

Resources


Jake Wharton

Jake Wharton

Jake Wharton is an Android developer at Square working on Square Cash. For the past 5 years he's been living with a severe allergy to boilerplate code and bad APIs. He speaks at conferences all around the world to educate more about this terrible plague that afflicts many developers.