前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >探索 Java 隐藏的开销--私有方法调用莫瞎写

探索 Java 隐藏的开销--私有方法调用莫瞎写

原创
作者头像
wust小吴
修改2019-12-30 18:37:05
6040
修改2019-12-30 18:37:05
举报
文章被收录于专栏:风吹杨柳风吹杨柳

随着 Android 引入 Java 8 的一些功能,请记住每一个标准库的 API 和语言特性都会带来一些相关的开销,这很重要。虽然设备越来越快而且内存越来越多,代码大小和性能优化之间仍然是有着紧密关联的

Dex 文件

我们将从一个多项选择的问题开始。下面这段代码有多少个方法?没有,一个或者两个?

class Example {
}

你可能马上就有直觉的反应了。也许没有,也许一个,也许两个。让我们看看我们是不是能回答这个问题。

首先,类里面没有方法。我在源文件里面没有任何方法,所以看起来可以这么说。

当然,这样的答案真的没有什么意思。让我们开始把我们的类在 Android 里编译一下,看看会发生什么:

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

$ javac Example.java

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

我们把内容写到一个文件里面,然后用 Java 编译器编译源代码然后把它变成 class 文件。

我们可以使用其他的非 Java 开发套件的工具,它叫做javap

这使得我们能够深入了解编译出来的 class 文件。

如果在我们编译的 class 文件上运行它,我们能看到我们的例子的 class 里面有一个构造函数。

我们没有在源文件里面编写它,但是 Java C 决定自动增加一个那样的构造函数。

这意味着源文件里面没有方法,但是 class 文件里面有一个。

是不是意味着这段代码的方法只有一个呢?,这不是 Android 编译停止的地方:

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

$ dexdump -f example.dex

在 Android SDK 中,有一个工具叫做dx,它完成dexing,这使得 Java class 文件变成 Android Dalvik 二进制码。我们通过dex运行我们的例子,Android SDK 里面还有另外一个工具叫做dexdump,这个工具会给我们一些关于 dex 文件内部的信息。

你运行它,它会打印一串东西。它们是文件的偏移量和计数器还有各种表。

如果我们详细点看看,一个明显的事情是,dex 文件里面有一个函数列表:

method_ids_size : 2

它说我们的 class 里面有两个方法。这说不通。

不幸的是,dexdump并没有给我一个简单的方法来了解这两个方法是什么。

因为如此,我写了一个小工具来输出 dex 文件里面的方法:

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

如果我们这样做,我就能看到它返回了两个方法。

它返回了我们的构造函数,我们知道它是 Java 编译器创建的,虽然我们没有去写它。

但是它还说有一个对象构造函数。当然,我们的代码没有四处调用 new 对象,所以这个方法是哪里产生的呢,

然后又在 dex 文件里面引用的呢?

如果我们回到能打印 class 文件信息的javap工具,你能通过一些额外的标志来找到 class 里面的深度信息。

我将使用-c,这会把二进制代码反编译成可读的信息。

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

在索引 1 处,是我们的对象构造函数,它被父类的构造函数调用。

这是因为,即使我们不声明它,Example 也是继承于 Object 的。

每一个构造函数都会调用它的父类的构造函数。

它是自动插入的。这意味着我们的 class 流中有两个方法。

所有这些关于我的初始问题的答案都是对的。区别就是术语不同。

这是真实的情况。我们没有定义任何方法。

但是只有人类关心它。作为人类,我们读写这些源文件。我们是唯一关心它们内部构造的人。

另外两个方法更重要,方法的个数实际上是编译进 class 文件里面了。

无论是否声明,这些方法都在 class 的内部。

这两个方法是引用方法的数目。

它和我们自己编写的方法的计数是类似的,和所有其他在函数里引用以及 Android logger 函数的调用也差不多。

我这里引用的 Log.d 函数和这个引用方法计数不一样,因为这是我们在 dex 文件里面的计数。

这也是人们经常在 Android 里面讨论方法计数时的常用方法,因为 dex 有着声名狼藉的对于引用方法的个数的限制。

我们看到一个没有声明的构造函数被创建了,所以让我们看看其他自动生成的,我们可能不知道的隐藏开销。

嵌套类是一个有用的例子:

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

Java 1.0 不支持这样做。它们是在晚些的版本里才出现的。

当你在一个视图或者展示层里面定义适配器的时候,你能看到这样的东西。

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

$ javac ItemsView.java

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

如果我们编译这个 class,这是一个有两个 class 的文件。

一个嵌套在另一个里面。如果我们编译它,我们能在文件系统中看到两个独立的 class 文件。

如果 Java 真的有内嵌类,我们就应该只能看到一个 class 文件。我们会得到ItemsView.class

但是这里 Java 没有真正的嵌套,那么这些类文件里面是什么呢?

在这个ItemsView里面,外层类,我们有的只是构造函数。这里没有引用,没有内嵌类的任何迹象:

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

如果我们看看嵌套类的内容,你可以看到它有隐式的构造函数,而且你知道它在外部类的里面,因为它的名字被扰乱了。另外一个重要的事情是如果我返回去,我能看到这个ItemsView类是公共的,这和我们在源文件里面定义的一样。

但是内部类,内嵌类,虽然它定义为私有的,在类文件里面它不是私有的。它是包作用范围的。这是对的,因为我们在同一个包中有两个生成的类文件。重申一次,这进一步证明了在 Java 里面没有真正的内嵌类。

// ItemsView.java

public class ItemsView {
}

// ItemsAdapter.java

class ItemsAdapter {
}

虽然你内嵌了两个类的定义,你可以有效地创建两个类文件,它们在同一个包里紧邻着对方。

如果你想这样做的话,你可以实现。你可以作为两个独立的文件使用命名规则:

// ItemsView.java

public class ItemsView {
}

// ItemsView$ItemsAdapter.java

class ItemsView$ItemsAdapter {
}

美元符在 Java 里面是名字的有效字符。对方法或者附加名字也有效:

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

然而,这是真正有意思的地方,因为我知道我能够做一些事情在外部类里找到一个private static方法,而且我能在内部类里面引用那个私有的方法:

// 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));
    }
}

现在我们知道没有真正的内嵌,但是,这在我们假设的独立系统里面是如何工作的呢,这里我们的ItemsAdapter类需要引用ItemsView的私有方法?这没有编译,而且它们会被编译:

// 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));
        }
    }
}

发生了什么?当你回到我们的工具的时候,我们能再次使用javac

$ 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
}

现在我将打印出内嵌类的内容,来看看哪个函数被调用了。

如果你看看索引 2,它没有调用displayText方法。它调用的是access$000,我们没有定义它。

它在ItemsView类里面吗?

$ javap -p ItemsView123

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

如果我们仔细看看,是的,它在。我们看到我们的private static方法仍然在那,但是我们现在需要这个我们没有编写的额外方法自动加入。

$ 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
}

如果我们看看这个函数的内容,它做的事情就是调用我们原来的displayText方法。

这有意义,因为我们需要一个从包的作用域到类里调用它的私有方法的途径。

Java 会合成一个包作用域的方法来帮助实现这个函数调用。

// 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));
    }
}

如果我们回到我们两个类文件的例子,我们手工的例子,我们能让编译器按照同样的方法工作。

我们能够增加方法,我们能更新另一个类,然后引用它。

dex 文件有方法的限制,所以当你有这些因为你编写源文件的方式的不同,而导致的必须要添加新的的方法加的话,这些函数的个数都是计算在内的。

理解这点是很重要的,因为我们尝试在某处访问一个私有成员是不可能的。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Dex 文件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档