Android C++ series: Summary of NDK's method of reducing the size of so library

Get into the habit of writing together! This is the 7th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

1. Background

The full-link voice SDK based on Amazon's AVS Device SDK has finally compiled dozens of dynamic libraries, and the size of a single-architecture dynamic library is tens of megabytes. It was barely running in Iot devices before, but this volume is a huge amount for mobile phone applications. The fatal thing is that each module takes a lot of time and effort to optimize the volume of a few K. I will directly give it to the last tens of megabytes, and the APP platform will definitely not accept it. But one is that there is a business requirement, and the other is that I want to push the SDK to the mobile APP to increase the number of users and verify the stability and interactive experience of the SDK, so I started a long slimming process, and finally the single architecture was compressed to less than five trillion, although It's still a bit big, but it's a big improvement over what it used to be.

2. Delete useless modules

AVS Device SDK is a console program mainly used in audio, and the code is cross-platform, so firstly, there are a lot of redundancy for cross-platform, and secondly, there are many modules that we don't use at all. For example, a dynamic library of Sqlite is introduced for local storage. We do not need local storage. We can put things like alarm clock settings in the APP layer, and even if we need storage, we can use the ones provided by Android and iOS platforms. Sqlite. Deleting unused modules is the fastest and largest package size optimization space.

3. Replacing third-party libraries to provide capabilities for Android/iOS platforms

AVS Device SDK implements an audio player based on ffmpeg decoding on the Android platform. For our scenario, we mainly use the player to play TTS, and TTS is a fixed mp3 format negotiated with the service. There is absolutely no need to introduce an mp3 decoding method. Huge ffmpeg library. Here we use the Jni layer media library provided by the Android platform for audio decoding. And even if the Android platform JNI layer does not support it, you can rely on an mp3 decoding library instead of the huge ffmpeg. For the entire package volume, third-party modules are often relatively large.

4. Use strip

使用NDK toolchain可以把调试的C++ 符号表(Symbol Table)中数据删除,我们一般我们打成APK会自动帮我们做这个工作,当然也可以手动设置:

手动的在链接选项中加入 strip参数,配置如下所示:

SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,-s")
复制代码

也可以手动执行ndk提供的aarch64-linux-android-strip命令移除动态库中的调试信息,这种方式除了前面方法外优化体积最高的方式,比如libLibSampleApp.so从48M直接优化到了992k。

4. 设置编译器的优化flag

编译器有个优化flag可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化flag设置为-Os,以便减少体积。

CMake:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
复制代码

Android.mk

LOCAL_CPPFLAGS += -Os
LOCAL_CFLAGS += -Os
复制代码

除了直接删除占用体积较大的模块外,编译器优化是排下来优化空间最大的方法。设置完-Os后占用提交较大的前几个库体积对比:

库名 优化前体积 优化后体积
libLibSampleApp.so 48M 33M
libAVSCommon.so 28M 22M
libDefaultClient.so 14M 9.9M

5. 使用 gc-sections去除没有用到的函数

有些时候代码量比较大的时候我们没办法手动发现无用的函数,这个时候可以可以开启编译器的gc-sections选项,让编译器自动的帮你做到这一点。

编译器可以配置自动去除未使用的函数和变量,以下是配置方式:

CMake:

# 去除未使用函数与变量
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
# 设置去除未使用代码的链接flag
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,--gc-sections")        
复制代码

Android.mk:

OCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections 
LOCAL_LDFLAGS += -Wl,--gc-sections
复制代码

6. 设置编译器的 Visibility Feature

Visibility Feature就是用来控制在哪些函数可以在符号表中被输入,由于C++并不是完全面向对象的,非类的方法并没有public这种修饰符,因此,要用Visibility Feature来控制哪些函数可以被外部调用。而JNI提供了一个宏-JNIEXPORT来控制这点。所以只要对函数加上这个宏,像这样:

// JNIEXPORT就是控制可见的宏
// JNICALL在NDK这里没有什么意义,只是个标识宏
JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
复制代码

然后在编译器的FLAGS选项开启 -fvisibility = hidden 就可以。这样,不仅可以控制函数的可见性,并且可以减少包体的大小。

7. 去除C++代码中的iostream等直接IO相关代码

使用STL中的iostream相关库会明显的增加包的体积,而Android本身是有预编译库(android/log.h)可以代替输入到控制台的工具的。在我们的SDK中由于之前是控制台程序所以用到了输入输出,编译的时候没有把这块排除出去,造成了一定的体积冗余。

8. STL的使用方式

对于C++的library,引用方式有2种:

  • 静态方式(static)
  • 动态方式(shared)

其中,静态方式在编译时会将用到的相关代码直接复制到目的文件中;而动态方式则会将相关的代码打成so文件,以便多次引用。由于编译器在编译时并不能知道所有被引用的地方,所以同时会打入了很多不相关的代码。

所以,如果项目中引用library的函数较多时,用动态方式可以避免多次拷贝,节省空间。相反,则直接使用静态方式会更节省空间。由于我们SDK的模块特别多,再加上整体APK里面已经有其他业务引入了动态库,所以我们用动态库的方式。

9. 不使用Exception和RTTI

关于这两点在网上看到的没有实践过,不过拿过来可以作为包体积持续优化的参考。

RTTI

通过RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型,即运行时获取对象的实际类型。C++通过下面两个操作符提供RTTI。

(1)typeid:返回指针或引用所指对象的实际类型。

(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。

RTTI的选项是默认关闭的的,而代码中其实并没有用到相关的功能,这里可以直接关闭。

Exception

Using C++ exceptions will increase the size of the package, and the current JNI support for C++ exceptions is buggy. For example, the following code will cause the program to crash (for lower versions of android NDK). Therefore, to introduce exceptions into the program, you need to implement the relevant logic yourself, but this will increase the size of the package body. For developers, exceptions can help quickly locate problems, but for users, they are not so important, so they can be removed here.

10 Summary

This article introduces removing useless modules, replacing third-party libraries with platform capabilities, using strip, setting compiler optimization flags, using gc-sections to remove unused functions, setting visibility, and removing iostreams that are helpful for dynamic library volume optimization. method.

Guess you like

Origin juejin.im/post/7084238491089027079