精通安卓性能优化-第二章(三)

编译Native库

既然已经完成C代码的实现,我们可以最终通过在应用的jni路径下调用ndk-build命令编译这个shared library。

TIP:修改环境变量,使它包含NDK目录,可以很方便的调用ndk-build或者其他脚本而不用指定命令的全路径。

结果是在lib/armeabi目录下生成名为libfibonacci.so的shared library。可能需要在Eclipse中刷新工程去显示新创建的库。如果你编译和运行这个应用程序,并且调用Fibonacci.recursiveNative,它会再次因为UnsatisfiedLinkError异常crash。这是一个典型的错误,因为许多开发者忘掉在Java代码中显式的加载shared library:VM不是千里眼,需要被告知哪些库需要加载。这可以通过调用System.loadLibrary()来实现,如2-11所示。

Listing 2-11 在静态初始化块加载库

public class Fibonacci {
    static {
        System.loadLibrary("fibonacci");    // to load libfibonacci.so
    }

    public static native long recursiveNative(int n);
}

加载Native库

在静态初始化语句块里面调用System.loadLibrary()是加载库的最简单方式。这个语句块中的代码在虚拟机加载类的时候执行,在任何方法被调用之前。(需要确认一下和构造函数的调用顺序,特别是书写的顺序在构造函数之后的情况)。一个潜在的性能问题,尽管不怎么普遍,如果你的类中有几个方法,不是所有都要求一切初始化(比如,shared libraries加载)。换句话说,静态初始化块会添加显著的开销,你将会希望去避免某些函数,如Listing 2-12所示。

Listing 2-12 在静态初始化块中加载库

public class Fibonacci {
    static {
        System.loadLibrary("fibonacci");   // 加载libfibonacci.so
        
        // 在这里做其他的耗时事情,将延后superFast的执行
    }

    public static native long recursiveNative (int n);

    public long superFast (int n) {
        return 42;
    }
}

NOTE:加载库需要的时间同样依赖于库本身(比如它的大小和方法数量)。

现在,我们看到了混合Java和C/C++的基本。本地代码是否可以提升性能,部分取决于C/C++代码是如何被编译的。事实上,存在许多的编译选项,使用不同的选项结果会变化非常大。

接下来的两部分告诉你在Application.mk和Android.mk中更多的选项,现在为止还是非常基本的。

Application.mk

Listing 2-4中的Application.mk在是最简单的之一。这个文件可以指定更多的东西,可能你需要在应用中定义许多选项。Table 2-3显示了你可以在Application.mk中定义的不同的变量。

Table 2-3 Application.mk中的变量


当微调应用的性能,你需要聚焦于这些变量:
(1) APP_OPTIM
(2) APP_CFLAGS
(3) APP_CPPFLAGS
(4) APP_STI
(5) APP_ABI

APP_OPTIM是可选的,可以被设置为release或者debug。如果没有被定义它,将会自动设置,依赖于你的应用是否debugable(android:debugable在manifest中被设置为true);如果应用debugable为true,APP_OPTIM将会设置为debug,否则,将被设置为release。在debug模式下,当你调试应用程序的时候它是有意义的,默认的行为被视为大多数情况可接受的,因此,大多数时间你不需要或者显式的去在Application.mk中定义APP_OPTIM。

APP_CFLAGS(C/C++)和APP_CPPFLAGS(C++ only) 定义传递给编译器的标志位。指定标志去优化代码并不是必须的,因为它们可以简单的用来包含一个路径去查找头文件(比如,APP_CFLAGS += -I(LOCAL_PATH)/myincludefiles)。参考gcc文档的详细标志清单。最经典的性能相关的标志是-Ox系列,x指定了优化级别,从0到无优化的3,或者-Os。然而,在多数情况下,简单的定义APP_OPTIM为release或者根本不定义APP_OPTIM是足够的,因为它将为你选择一个优化级别,产生可接受的结果。

APP_STL用来指定应用程序使用哪个标准库。比如,NDK 6定义了4个可能的值:
(1) system
(2) stlport_static
(3) stlport_shared
(4) gnustl_static

每个库都有它的优缺点。比如:
(1) 只有gnustl_static库支持C++异常和运行时类型信息(RTTI)。STLport库在NDK r7添加了对RTTI的支持。
(2) 如果多个本地共享库使用了C++库则使用stlport_shared。(记得去用System.loadLibrary(“stlport_shared”)显式的加载这个库)。
(3) 如果在你的应用程序有只有一个shared library使用stlport_static,避免动态的加载这个库。


你可以分别通过添加-fexceptions和-frtti到APP_CPPFLAGS打开C++异常和RTTI支持。

对(几乎)所有的设备优化

如果你的应用性能很大程度上依赖于C++库的性能,用不同的库去测试并且选择最好的。选择可能不是仅仅基于性能,因为你还要考虑其他的参数,比如你的应用程序的最终大小或者你需要的C++库的功能(比如RTTI)。


我们上面编译的库(libfibonacci.so)针对armeabi ABI编译。两个问题浮出水面:
(1) 当native代码和armeabi-v7a ABI兼容的情况下,它不会对Cortex系列的处理器优化。
(2) Native代码不会和x86 ABI兼容。

Cortex系列的处理器比基于早些的ARMv5结构的处理器更加强大。一个原因是新的指令集在armv7结构中定义,对armv5编译的库将不会使用。因为编译器以armeabi ABI为目标,它保证不会使用任何armv5的处理器不理解的指令。即使你的库编译为和基于Cortex的设备兼容,将不能完全利用CPU,因此不能利用全部的潜在性能。

NOTE:arm v7比arm v5更加强大有许多理由,指令集是一个理由。参考ARM网址(http://www.arm.com)不同体系的更多信息。

第二个问题更加严重,因为一个针对ARM ABI编译的库将不能用在一个基于x86结构的设备上。如果本地代码对你的应用程序是必须的话,将不能在任何基于Intel的Android设备上运行。在我们的示例中,System.loadLibray(“Fibonacci”)将会以一个UnsatisfiedLinkError的异常失败,意味着库不可以被加载。


这两个问题可以简单的修复,因为APP_ABI可以为native代码指定一系列的ABI,如Listing 2-12所示。通过指定多个ABI,你可以保证本地代码不止可以支持所有的结构,而且针对每一个都做优化。

Listing 2-12 Application.mk指定三个ABI

APP_ABI := armeabi armeabi-v7a x86

使用这个新的Application.mk重新编译你的应用程序之后,libs目录下将包含3个子目录。除了armeabi,还有两个新的目录armeabi-v7a和x86。你可以轻松的猜测出,两个新的目录涉及到应用现在支持的两个新的ABI。每个目录包含一个libfibonacci.so。

TIP:在编辑Application.mk或者Android.mk后使用ndk-build –B V=1去强制库重新编译,并且显示编译命令。这种方式可以验证你的改变在编译的过程中是否起到了期望的效果。

应用文件变得更大,因为现在它包含了同一个库的3个实例,每一个针对一个不同的ABI。Android的package manager将在安装应用的时候决定哪个库会被安装。Android系统定义了一个首选的ABI和一个可选的辅助的ABI。Package manager将会首先安装针对ABI的首选库,如果辅选库已经定义了,并且找不到首选的库的话,将会选择安装辅选库。比如,一个基于Cortex的安卓设备定义首选ABI为armeabi-v7a,辅选ABI为armeabi。表2-4给出了所有设备的首选和备选ABI。

Table 2-4 首选和辅选ABI


辅选的ABI提供了一个新的Android设备兼容老的Android应用的机制,因为ARMv7ABI向后兼容ARMv5。

NOTE:安卓系统将来可能定义比首选和备选ABI更多的选择,比如ARM设计一个新的ARMv8架构向后兼容ARMv7和ARMv5。

支持所有的设备

一个问题仍然存在。尽管现在应用支持所有的NDK支持的ABI,Android可以(而且很可能)将被移植到新的架构上。比如,我们之前提到的MIPS手机。Java的承诺是“写一次,在任何地方运行”(字节码是平台无关的,不需要去为了新的平台重新编译代码),本地代码是针对特定平台的,我们产生的3个库没有一个和基于MIPS的安卓系统兼容。有两种方式去解决这个问题:
(1) 当NDK支持一种新的ABI的时候为你的应用编译新的库
(2) 当package manager安装本地代码失败的情况下,你可以提供一个默认的Java实现

第一种方案是相当琐细的,因为它仅仅涉及到安装新的NDK,修改应用的Application.mk,重新编译应用,发布更新(比如,在安卓市场上)。然而,官方的Android NDK不会总是支持所有的安卓已经移植的ABI或者将被移植的。作为结果,建议你也实现第二个方案;换句话说,Java实现同样需要提供。

NOTE:MIPS技术提供了一个单独的NDK,允许你为MIPS ABI单独编译库。参考:http://develper.mips.com/android。

Listing 2-13给出了当加载本地库失败的情况下如何提供一个默认的Java实现。

Listing 2-13 提供一个默认的Java实现

public class Fibonacci {
    private static final boolean useNative;
    
    static {
        boolean success;
        
        try {
            System.loadLibrary("fibonacci");  // 加载libfibonacci.so
            success = true;
        } catch (Throwable e) {
            success = false;
        }
        
        useNative = success;
    }
    
    public static long recursive (int n) {
        if (useNative) return recursiveNative(n);
        return recursiveJava(n);
    }
    
    private static long recursiveJava (int n) {
        if (n>1) return recursiveJava(n-2) + recursiveJava(n-1);
        return n;
    }
    
    private static native long recursiveNative (int n);
}

一个替代的设计是使用策略模式:
(1) 设计一个策略接口
(2) 定义两个类去实现这个接口(一个使用本地代码,一个只使用Java)
(3) 基于System.loadLibrary()的结果实例化正确的类

Listing 2-14给出了替代设计的实现。


Listing 2-14 使用策略模式提供一个默认的Java实现

// FibonacciInterface.java

public interface FibonacciInterface {
    public long recursive (int n);
}

// Fibonacci.java

public final class FibonacciJava implements FibonacciInterface {
    public long recursive(int n) {
        if (n > 1) return recursive(n-2)+recursive(n-1);
        return n;
    }
}

// FibonacciNative.java

public final class FibonacciNative implements FibonacciInterface {
    static {
        System.loadLibrary("fibonacci");
    }
    
    public native long recursive (int n);
}

// Fibonacci.java

public class Fibonacci {
    private static final FibonacciInterface fibStrategy;
    
    static {
        FibonacciInterface fib;
        try {
            fib = new FibonacciNative();
        } catch (Throwable e) {
            fib = new FibonacciJava();
        }
        
        fibStrategy = fib;
    }
    
    public static long recursive (int n) {
        return fibStrategy.recursive(n);
    }
}

NOTE:因为本地函数现在在FibonacciNative.java中声明而不是Fibonacci.java,你需要去重新创建native库,这次使用com_apress_proandroid_FibonacciNative.c和com_apress_proandroid_FibonacciNative.h。(Java_com_apress_proandroid_FibonacciNative_recursiveNative将是从Java调用的函数名字)。如果使用之前的库将会有UnsatisfiedLinkError异常。

就性能表现而言,两个实现之间的细微差别足够安全的被忽略:
(1) 第一个实现每次recursive()被调用的时候需要一个判断
(2) 第二个实现在recursive()被调用的时候需要在静态初始化块有一个对象分配和virtual函数的调用。


从设计角度看,推荐你使用策略模式:
(1) 你仅需要选择一次合适的策略,不会有忘记if(useNative)测试的风险
(2) 可以简单的通过修改两行代码去改变策略
(3) 在不同的文件维护策略,使得维护更容易
(4) 添加一个方法到策略接口强制你在所有的实现类中实现该方法


就像你可以看到的,配置Application.mk并不是一个必须的任务。然而,你将很快认识到大多数时间所有的应用程序使用同样的参数,简单的复制已有的Application.mk到新的应用通常可以的。

Android.mk

Application.mk允许你指定整个应用的通用变量,Android.mk用来指定你要编译什么模块和怎么去编译他们,所有的这一切都是难以忍受的细节。Table 2-5列出了在NDK 6可用的变量。

Table 2-5 在Android.mk可以定义的变量



我们再次聚焦于影响性能的几个变量:
(1) LOCAL_CFLAGS
(2) LOCAL_CPPFLAGS
(3) LOCAL_ARM_MODE
(4) LOCAL_ARM_NEON
(5) LOCAL_DISABLE_NO_EXECUTE

LOCAL_CFLAGS和LOCAL_CPPFLAGS相似于APP_CFLAGS和APP_CPPFLAGS,但是仅仅会影响当前的模块,而在Application.mk中定义的标志影响所有的模块。推荐不要在Android.mk中设置优化级别而是依赖于Application.mk中的APP_OPTIM。

LOCAL_ARM_MODE可以用来强制生成ARM模式的二进制码,即使用32位的指令。而代码密度可能会是Thumb模式(16位指令),设置为ARM代码模式会比Thumb模式性能更好。比如,Android本身的Skia库显式的被编译为ARM模式。显然,这仅仅应用到ARM ABI,即armeabi和armeabi-v7a。如果你只希望特定的文件使用ARM模式,你可以在LOCAL_SRC_FILES列出他们,带有.arm扩展名,比如,file.c.arm而不是file.c。

LOCAL_ARM_NEON指出了是否可以在代码中使用Advanced SIMD指令或者内部函数,编译器是否可以在native code中生成NEON指令。尽管性能可以通过NEON指令显著的提升,NEON仅在ARMv7架构引入并且是可选项。NEON不是在所有设备上可用。比如,三星的Galaxy Tab 10.1不支持NEON,但是Nexus S支持。LOCAL_ARM_MODE单独的文件支持NEON,使用.neon扩展名。第三章覆盖到了NEON扩展并提供了实例代码。

TIP:可以在LOCAL_SRC_FILES联合.arm和.neon扩展名,比如,file.c.arm.neon。如果所有的扩展名都使用到了,保证.arm是第一个,否则,它将不会被编译。

LOCAL_DISABLE_NO_EXECUTE本身不会对性能有影响。然而,熟练地开发者喜欢动态生成代码的时候关闭NX bit(很可能达到更好的性能)。这不是一个普遍的事情,你很可能从来没有在Android.mk中指定这个标志位,NX bit默认是打开的。关闭NX bit同样需要考虑安全风险。

Android.mk可以指定多个模块,每个模块可以使用不同的标志和源文件。Listing 2-15给出了一个Android.mk文件,编译两个不同的模块使用了不同的标志。

Listing 2-15 Android.mk中的两个模块定义

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := fibonacci
LOCAL_ARM_MODE := thumb
LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c fibonacci.c
include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := fibonarmcci
LOCAL_ARM_MODE := arm
LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c fibonacci.c
include $(BUILD_SHARED_LIBRARY)

Application.mk、Android.mk可以用很多方式配置。在这两个文件里选择正确的变量值是达到好的性能的关键,而不用求助于更加增强的和复杂的优化。NDK经常升级,你需要参考最新的在线文档,因为新版本会增加最新的变量,有些可能被淘汰。当新的NDK发布,推荐你重新编译应用并发布更新,特别是新版本带有新的编译器的情况下。

NOTE:在使用不同的tool chain(新的SDK或者NDK)重新编译后再次测试你的应用。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值