起因
我这个人喜欢没事就去招聘网站刷刷招聘信息,也不全是为了找工作,因为我个人觉得从招聘信息中,我可以了解到行业的大方向,了解现在市场上的用人单位都需要一个程序员具备什么样的素质和技能,程序员这个职业,一辈子都需要学习和进步,如果技术固步自封,不深入底层或者不拓展技术视野,那么成为一个十年开发一年经验的程序员就不远了,于是我打开了招聘网站,随便刷了刷就发现了问题。
问题就是,对于初级程序员,市面上的公司要求不多,除了BAT,基本上回写bug就行,但是如果到了一定岁数,你想找一些中高级的Android程序员的工作时,市面上的公司就会有一些特殊的需求了,这需求其实就是门槛,现在我贴几张招聘简章的截图,大家看看:
需求各式各样,但是我发现一个共同的问题,都需要NDK或者C/C++的技能,看来NDK是未来一个学习的热点和趋势,不仅音视频、各种硬件的调用、核心算法的实现以及一些照片的处理都需要用到JNI开发。
可能你会问了,我是个Java程序员,学这C++开发相当于跨行了,其实也对,Android都没整明白呢,就跑去弄什么NDK,但是还是那句老话,技多不压身,多学习学习接触一下肯定没坏处,最起码简单的NDK开发框架会搭建,知道一些基本的配置和用法,就算以后项目里用到了基础的配置和调试也能用得到啊。
开始搭建NDK
我用的AndroidStudio的版本是3.2.1,在创建Project时如果选中了支持C++,AndroidStudio就会自动去网上帮我们下载NDK的开发环境,具体情况可以通过File->Setting->Appearance & Behavior->System Setting ->Android SDK->SDK Tools查看,主要看的就是NDK和CMake两项是否安装:
之后就新建项目,选择C++选项:
然后选择SDK版本,这个随意:
在向导的 Customize C++ Support 部分,您可以使用下列选项自定义项目:
-
C++ Standard:使用下拉列表选择您希望使用哪种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置。
-
Exceptions Support:如果您希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
-
Runtime Type Information Support:如果您希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
之后就可以看到自己项目的目录结构和平时的Android项目有所不同:
仔细观察可以发现,多了一个cpp包,而且还多了个CMakeLists.txt,打开local.properties还会发现多了个参数,其实就是ndk的指向路径:
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Wed Apr 03 09:21:33 CST 2019
ndk.dir=E\:\\AndroidSDK\\adt-bundle-windows-x86_64-20140321\\sdk\\ndk-bundle
sdk.dir=E\:\\AndroidSDK\\adt-bundle-windows-x86_64-20140321\\sdk
复制代码
-
在 cpp 组中,您可以找到属于项目的所有原生源文件、标头和预构建库。对于新项目,Android Studio 会创建一个示例 C++ 源文件 native-lib.cpp,并将其置于应用模块的 src/main/cpp/ 目录中。本示例代码提供了一个简单的 C++ 函数 stringFromJNI(),此函数可以返回字符串“Hello from C++”。
-
在 External Build Files 组中,您可以找到 CMake 或 ndk-build 的构建脚本。与 build.gradle 文件指示 Gradle 如何构建应用一样,CMake 和 ndk-build 需要一个构建脚本来了解如何构建您的原生库。对于新项目,Android Studio 会创建一个 CMake 构建脚本 CMakeLists.txt,并将其置于模块的根目录中。
我们来看看这个文件:
# 有关使用CMake在Android Studio的更多信息,请阅读文档:https://d.android.com/studio/projects/add-native-code.html
# 设置CMake的最低版本构建本机所需库
cmake_minimum_required(VERSION 3.4.1)
# 创建并命名库,将其设置为静态的
# 或共享,并提供其源代码的相对路径。
# 你可以定义多个library库,并使用CMake来构建。
# Gradle会自动将包共享库关联到你的apk程序。
add_library( # 设置库的名称
native-lib
# 将库设置为共享库。
SHARED
# 为源文件提供一个相对路径。
src/main/cpp/native-lib.cpp )
# 搜索指定预先构建的库和存储路径变量。因为CMake包括系统库搜索路径中默认情况下,只需要指定想添加公共NDK库的名称,在CMake验证库之前存在完成构建
find_library( # 设置path变量的名称
log-lib
# 在CMake定位前指定的NDK库名称
log )
# 指定库CMake应该链接到目标库中,可以链接多个库,比如定义库,构建脚本,预先构建的第三方库或者系统库
target_link_libraries( # 指定目标库
native-lib
# 目标库到日志库的链接 包含在NDK
${log-lib} )
复制代码
好了,至此,我们的NDK就算是配置完成了,现在开始尝试加入OpenCV。
引入OpenCV
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
知道了概念,现在我们来引入它
第一步:下载OpenCV的Android包
下载地址
最新版是4.0.1,然后我们点击那个Android pack,会下载好一个压缩包,解压缩后是这样的:
include文件
在下载好的OpenCV压缩包中,打开路径下的 OpenCV-android-sdk\sdk\native\jni 有一个include文件夹,把这个文件夹复制粘贴至我们的OpenCVTest项目中,路径为src/main/cpp
jni文件
然后是动态库(.so文件),打开路径下的 OpenCV-android-sdk\sdk\native ,有一个libs 文件夹,这个文件夹里面是所有版本的abi的so文件。复制粘贴到我们的项目中,路径为 src/main/jniLibs 这个文件夹需要自己手动去创建。 注意:
1. 无论是include还是libs的路径都可以自定义,习惯上是这样放,但其实只要在之后的CMakeList配置文件里面设置正确就没有问题。 2. 值得一提的是,OpenCV在最新版本中把动态库和静态库分开了,分别放在libs和staticlbs两个文件夹中,之前是放在一个文件夹里的。我们测试Demo仅需要动态库和头文件即可。
最后配置好之后文件结构如图所示(注意,这是Project方式看的):
配置Gradle
最终的配置文件如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.dhcc.www.ndkapplication"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -frtti -fexceptions"
abiFilters 'armeabi-v7a'
}
}
ndk{
abiFilters 'armeabi-v7a'
}
}
sourceSets{
main{
jniLibs.srcDirs = ['src/main/jniLibs/libs']
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
ndk{
abiFilters 'armeabi-v7a'
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
复制代码
这里有几点要讲一下:
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -frtti -fexceptions"
abiFilters 'armeabi-v7a'
}
}
ndk{
abiFilters 'armeabi-v7a'
}
复制代码
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
复制代码
cppFlags是配置预处理的设置,abiFilters是控制系统查找.SO的文件类型的过滤器,不同的CPU架构对应不同的.so文件,abiFilters关键字能够指定Android 所支持的CPU架构,一般是以上4种,最终这些.so文件会被打包进APK,所以可以根据自己的项目进行选择,比如在AS模拟器上开发APP选一个 x86 就可以了,如果是手机端,一般是arm架构,选 armeabi-v7a 即可。
然后同步项目即可。
CMakeList.txt文件
这个文件也是NDK开发最最最关键的文件,AS采用CMake脚本语法配置C编译器的环境,如果你之前有过使用CMAKE的经验,或许这并非难题,但对于初学者而言,CMAKE的脚本语法,还是略过于生涩,而且AS对该文件的配置并不友好,居然没有代码提示,于是不得不查很多文档。但好在,NDK的开发大多不是大型的C++项目,也不太需要过于复杂的设置(比如OpenCV源代码的CMAKE文件,大约有几千行的样子 Orz~)
OpenCV配置CMakeList文件的方式如下:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# 设置CMAKE的版本号
cmake_minimum_required(VERSION 3.4.1)
# 设置include文件夹的地址
include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
# 设置opencv的动态库
add_library(libopencv_java4 SHARED IMPORTED)
set_target_properties(libopencv_java4 PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/libs/${ANDROID_ABI}/libopencv_java4.so)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib libopencv_java4
# Links the target library to the log library
# included in the NDK.
${log-lib} )
复制代码
include_directories 函数设置了include文件夹的路径 add_library 函数设置库名和类型,其中libopencv_java3 是用户自定义的变量名,前后保持一致即可,SHARE 表示引入的库是动态链接库 set_target_properties 设置了OpenCV的动态链接库的路径 target_link_libraries 具有依赖关系的动态库链接到指定目标上,链接顺序需符合gcc链接规则,这里我们把libopencv_java4和log链接到了native-lib上。 更详细的CMAKE配置语法,可以参考CMAKE官方文档,我们这里仅配置动态链接库,以上四个函数足够了。
放入指定图片
将上图存储后放入路径:src/main/res/drawable 下进行测试
编写测试Demo
-
MainActivity.java
package com.dhcc.www.ndkapplication; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.view.View; import android.widget.Button; import android.widget.ImageView; public class MainActivity extends AppCompatActivity implements View.OnClickListener{ // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } private Button btn_1; private Button btn_2; private ImageView imageView; private Bitmap bitmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_1 = (Button)findViewById(R.id.button_1); imageView = (ImageView)findViewById(R.id.image); bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.lufei); imageView.setImageBitmap(bitmap); btn_1.setOnClickListener(this); btn_2 = (Button)findViewById(R.id.button_2); btn_2.setOnClickListener(this); } public void showImage(){ bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.lufei); imageView.setImageBitmap(bitmap); } public void gray(){ int w = bitmap.getWidth(); int h = bitmap.getHeight(); int[] piexls = new int[w*h]; bitmap.getPixels(piexls,0,w,0,0,w,h); int[] resultData =Bitmap2Grey(piexls,w,h); Bitmap resultImage = Bitmap.createBitmap(w,h, Bitmap.Config.ARGB_8888); resultImage.setPixels(resultData,0,w,0,0,w,h); imageView.setImageBitmap(resultImage); } @Override public void onClick(View view){ switch(view.getId()){ case R.id.button_1:showImage();break; case R.id.button_2:gray();break; } } /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */ public native int[] Bitmap2Grey(int[] pixels,int w,int h); @Override public void onResume(){ super.onResume(); } } 复制代码
-
native-lib.cpp文件
#include
#include #include using namespace cv; using namespace std; extern "C" JNIEXPORT jintArray JNICALL Java_com_dhcc_www_ndkapplication_MainActivity_Bitmap2Grey( JNIEnv *env, jobject /* this */,jintArray buf,jint w,jint h) { jint *cbuf; jboolean ptfalse = false; cbuf = env->GetIntArrayElements(buf, &ptfalse); if(cbuf == NULL){ return 0; } Mat imgData(h, w, CV_8UC4, (unsigned char*)cbuf); // 注意,Android的Bitmap是ARGB四通道,而不是RGB三通道 cvtColor(imgData,imgData,COLOR_BGRA2GRAY); cvtColor(imgData,imgData,COLOR_GRAY2BGRA); int size=w * h; jintArray result = env->NewIntArray(size); env->SetIntArrayRegion(result, 0, size, (jint*)imgData.data); env->ReleaseIntArrayElements(buf, cbuf, 0); return result; } 复制代码 这里要注意:Java_com_dhcc_www_ndkapplication_MainActivity_Bitmap2Grey 这个名字每个项目的包名不同,写法也不同,其主要是 “Java包名Activity名方法名”的组合方式,这里一定要写对了,不然会报错:
这个错其实就是引入失败或者找不到方法的错误报告,我一开始复制的是别人的代码,找了半天才发现是这里的问题,没办法,谁让自己不懂呢.....
-
activity_main.xml
xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" app:srcCompat="@drawable/lufei" /> LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/button_2" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="灰度图" /> <Button android:id="@+id/button_1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="色图" /> LinearLayout> LinearLayout> 复制代码
-
运行效果
如果一切正常,可以看到如下效果图:
点击灰度图时:
点击色图时:
至此,整个教程完毕!
相关学习链接:
Android NDK学习笔记:Android Studio3.1+CMAKE+OpenCV3.4配置
AndroidStudio3.0 NDK开发- 如何在已有项目中进行NDK开发