相信大家在开发的过程中,都或多或少的接触过JNI,然后每次要接触JNI的时候,倒吸一口冷气,太难啦!
只有Java代码和C++代码 还好,在新建项目的时候把那个 "Include C++ support"勾选上,然后一路next,最后finish,一个简单的带有C++代码的Android项目就算完成了,然后在看下CMakeLists.txt怎么写的,照猫画虎就可以了,但是实际开发中,并不简单,因为底层(C++)那里给过来的有可能是.a静态库,也有可能是.so动态库,然后如何集成进项目里就是:一脸懵逼,二脸茫然,三脸不知所措。由于客观事实只能去百度,然后百度到的全是用Android.mk实现的居多,然而现在都Android Studio 3.1+时代了,还mk?关于CMake的资料又很少,所以就有了本文。
在去年在公司还是实习的时候,老大丢给我一个.a静态库(当时并不知道这是一个静态库),很是好奇,很想知道.a是怎么构建出来的,a和so的区别又是什么,总之一大堆疑问,在转正后,也尝试过要构建一个.a静态库,无奈!百度到的都是用mk的咯,找到一篇CMake的,但竟然要去Linux下面编译,还要什么自定义工具链,总之,麻烦的一批。
所以就有了这一系列,本系列不阐述什么是CMake,什么是NDK,什么是JNI,只是写一下在Android中使用CMake常遇的问题,解决方法是什么。
本系列涉及到的有:
- 初次使用CMake构建native项目
- 如何将现有的cpp代码集成到项目中
- 拷贝源码
- 编译成库文件
- CMake链接a静态库以及so动态库及动态库和静态库的区别
初次使用CMake构建native项目
这个就很简单啦,新建项目的时候把 include C++ support 勾选上就可以了,但,我们还是自己动手来一遍,不依靠IDE,看看可不可以,新建普通项目,名字为:AndCmake,新建完成后如下:
新项目,什么都没有,我们都知道用CMake的话,必须要有CMakeLists.txt(注意名字不能写错),第二个就是需要的cpp资源啦,这就很简单了,在src/main目录下新建就好了,为了整洁在main目录下新建cpp文件夹,且在里面新建CMakeLists.txt和native_hello.cpp文件,然后在去CMakeLists.txt简单的配置下就好了,完成后,如下:
-
../src/main/cpp/native_hello.cpp:
// // Created by xong on 2018/9/28. // #include<jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_xong_andcmake_jni_NativeFun_stringFromJNI(JNIEnv *env, jclass thiz) { std::string hello = "Hello,I from C++"; return env->NewStringUTF(hello.c_str()); } 复制代码
这里需要说明以下,Java_com_xong_andcmake_jni_是去java的一层一层的包下面去寻找,比如这个就是去com.xong.andcmake.jni下面找,后面的NativeFun_stringFromJNI就是类和方法名了。函数的形参两个必须是这样的,第二个形参可以写成"jobject",我最近写JNI的时候,提示改成"jclass"但是,"jobject"也不能算错,也是对的。
-
../src/main/cpp/CMakeLists.txt
# CMake最低版本 cmake_minimum_required(VERSION 3.4.1) # 将需要打包的资源添加进来 add_library( # 库名字 native_hello # 库类型 SHARED # 包含的cpp native_hello.cpp ) # 链接到项目中 target_link_libraries( native_hello android log ) 复制代码
这就把C++部分写好了
-
修改../app/build.gradle,修改后如下:
android { ... defaultConfig { ... externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_static' } } ... } ... externalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' } } ... } 复制代码
-
写对应的Java层代码,在com.xong.andcmake包下新建jni,然后新建NativeFun类,代码如下:
package com.xong.andcmake.jni; /** * Create by xong on 2018/9/28 */ public class NativeFun { static { System.loadLibrary("native_hello"); } public static native String stringFromJNI(); } 复制代码
-
调用NativeFun.stringFromJNI(),查看是否有正确的信息输出:这就很简单啦,在Activity中添加一个TextView然后显示下就好了,正确的话,应该会有如下显示:
OK,这样我们的第一个带有CPP代码的APP就算完成了,然后我们来思考一下,先来看native_hello.cpp的代码:
//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#include <string>
extern "C" JNIEXPORT
jstring JNICALL
// 这里可不可以优化一下?
Java_com_xong_andcmake_jni_NativeFun_stringFromJNI(JNIEnv *env, jclass thiz)
{
std::string hello = "Hello,I from C++";
// 这里呢?
return env->NewStringUTF(hello.c_str());
}
复制代码
对于我们来讲:
- 尽量不写重复代码
- 代码要高效
对于上面的函数名来讲,Java_com_xong_andcmake_jni_,假如我们在Java中对应的native类和方法,感觉在这个包中不合适,要换下包名,但是我这个类中有很多native方法,只要一换包,那么对应的cpp中的函数名就要挨个改,函数少还好办,假如有1000个,改起来真的是不要太爽,而且容易丢,那么有没有办法来尽量的减少工作量呢?类和包是相对固定的,那么我们能不能把包名抽取出来用一个表达式来表示呢?没错!就是宏定义啦!
对于下面返回的那一句,我为啥要这样写呢,因为用Android Studio创建一个带有cpp项目的时候,里面就是这样写的,然后就很自然的抄来了,但是我们想一下,有这样写的必要吗?可以看到的是在最后返回的时候,string对象又经过了一层转换点进去后可以发现,是将 string对象又转成了char数组(当然在C++中是char*指针还是const的,这里不需要知道const是啥,只需要知道,c_str()其实是又将string对象转成了char[]数组),然后我们这样写是不是就…多此一举,让计算机多做了一些事情呢?所以改完后的代码如下:
//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#define XONGFUNC(name)Java_com_xong_andcmake_jni_##name
extern "C" JNIEXPORT
jstring JNICALL
XONGFUNC(NativeFun_stringFromJNI)(JNIEnv *env, jclass thiz)
{
return env->NewStringUTF("Hello,I from C++");
}
复制代码
这样的话,假如NativeFun不在jni包下了,而是在jnia包下,那么我们只需要将上面的宏定义改下就好了;
因为我们没有对string做操作,那么我们就不需要它啦,直接返回就好了,这样计算机就不那么累了。
make project后,发生了什么?
接触过JNI的同学应该了解,项目里面添加了cpp代码,apk会变的很大,那么原因嘞,那么我就来捋一捋,cpp的代码再make project后都发生了什么,都很清楚会生成so,那么…在默认的情况下会生成几个?
打开 ../app/build/intermediates/cmake/debug/obj 目录如下:
在默认的情况下会生成4个,v7a的是最大的,x86_64是最小的,默认情况下,这4个是都会打到apk里面的(因为不清楚到底是往那台手机上安装呀,只能将所有的资源都打到apk里面了),要想缩小apk,那么就把需要的so打入apk就好了。然后,问题来了,怎么才能生成规定的so库呢?上代码,如下:
apply plugin: 'com.android.application'
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_static'
}
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
...
}
...
}
...
复制代码
在../app/build.gradle中添加ndk标签,且在里面写上需要生成的架构名称就可以了,重新build下项目,如下图:
这样打入apk中的就只有v7a和x86两种架构的so库啦! 下篇我们来讲解一下 现有的cpp代码集成到项目中有几种方式,以及如何操作。
点击跳转到下一篇:如何将现有的cpp代码集成到项目中
Demo链接:UseCmakeBuildLib
END