前言
本博客写于2017/08/11, 博主非专业搞安卓开发, 只是工作的需要倒腾了下Android NDK相关的开发, 博文中有什么不正确、不严格的地方欢迎指正哈 本文后续也许还会有删改, 就这样。
一、工具、开发环境
博主的操作系统是Windows 10 x64位,虽然感觉Windows 7更适合用来搞开发, 但是用着Win 10也是挺好使的(没必要在操作系统上有很大纠结,Win 10和Win 7都可以)。
1.搭建并测试JAVA开发环境
首先要搭建Java开发环境,Java开发环境的搭建教程网上可以找到,这里不予赘述; 测试是否搭建成功的时候,需要打开命令提示符依次输入java、javac、javah命令进行测试,显示如下界面结果就成功了:
2.搭建Android开发环境
很长一段时间内,Eclipse + ADT插件是Android开发的主流,不过现在流行的是Android Studio。 还记得Android Studio刚出来的时候很是不被看好, 不过现在它也是很好用的安卓开发工具了,此文介绍的所需要的工具只有Android Studio。
网上有介绍说设置ndk-build环境变量的, 但是要弄明白一件事, 他们设置这个变量是因为他们的"NDK"这个依赖包是单独下载的, 并没有在Android Studio里面下载, 本文的"NDK"依赖包是在Android Studio里面下载的, 因此并不需要设置ndk-build环境变量。
开始正式搭建环境,我用的是Android Studio 2.2.3版本。 安装完Android Studio之后, 选择"File" -> "Settings" -> "Appearence & Behavior" -> "System Settings" -> "Android SDK", 配置 "SDK Platforms"
和 "SDK Tools" :
(1)"SDK
Platforms":这个是选择安卓SDK版本,根据自己想法跟需要下载,我下载了安卓4.4、5.0、7.1.1
Studio的时候默认有什么包我忘了,"Android SDK Build-Tools"这个包应该不用自己勾选,默认就有。NDK开发现在提供了CMake的方式来进行编译调试,通过下载"CMake"跟"LLDB"这俩(可选)就可以用CMake来开发了,本文未基于CMake的方式进行编译调试,而是采用的传统的gradle方式(相对而言,我其实更推荐使用CMake的方式)。
这一步就是为了更方便的进行gradle方式的开发而写的。
$JDKPath$/bin/javah
Parameters:
-d
../jni -jni $FileClass$
Working directory:
$SourcepathEntry$\..\java
最后单击"OK"按钮进行保存。
2) 添加ndk-build,它用于构建so包
你的NDK目录\build\ndk-build.cmd
Parameters: 什么都不用填
Working directory:
$ModuleFileDir$\src\main
最后单击"OK"按钮进行保存。
3) 添加ndk-build
clean,它用于清理so包
你的NDK目录\build\ndk-build.cmd
Parameters:
clean
Working directory:
$ModuleFileDir$\src\main
最后单击"OK"按钮进行保存。
3.来一个栗子
一个NDK开发项目总体就分为生成so库、调用so库两部分,本博客栗子就是先将基于OpenCV的C++代码编译生成so库,再通过jni接口来实现对安卓摄像头的灰度化处理。
(1)首先新建一个工程
1)选择新建项目:
切换到"Project"视图界面,
NDK开发将C/C++代码生成so库, 留一个对单幅图片进行灰度化的接口, 然后我们在Java层获取摄像头数据, 通过JNI调用所生成so库的接口, 对每一帧摄像头数据进行灰度化。
2)新建一个类OpencvClass,负责与C/C++代码对接。
public native static int convertGray(long matAddrRgba, long matAddrgray);
接下来,右键单击类OpencvClass,来生成C/C++头文件
上一步生成了头文件, 此头文件内容是:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_hoos_grayprocessing_OpencvClass */
#ifndef _Included_com_hoos_grayprocessing_OpencvClass
#define _Included_com_hoos_grayprocessing_OpencvClass
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_hoos_grayprocessing_OpencvClass
* Method: convertGray
* Signature: (JJ)I
*/
JNIEXPORT jint JNICALL Java_com_hoos_grayprocessing_OpencvClass_convertGray
(JNIEnv *, jclass, jlong, jlong);
#ifdef __cplusplus
}
#endif
#endif
其中, JNIEXPORT是接口声明, 我们需要在cpp源文件里将它实现, 这里当然需要我们有C/C++基础, 可以写出cpp文件的框架了:
//
// Created by HooS on 2017/8/14.
//
#include <com_hoos_grayprocessing_OpencvClass.h>
JNIEXPORT jint JNICALL Java_com_hoos_grayprocessing_OpencvClass_convertGray
(JNIEnv *, jclass, jlong, jlong){
}
在实现时, 接口的两个jlong类型需要用来传输OpenCV里面的Mat类型数据, 所以要略微修改一下那里:
//
// Created by HooS on 2017/8/14.
//
#include <com_hoos_grayprocessing_OpencvClass.h>
JNIEXPORT jint JNICALL Java_com_hoos_grayprocessing_OpencvClass_convertGray
(JNIEnv *, jclass, jlong addrRgba, jlong addrGray){
}
然后完善代码,完善JNI接口数据转换,实现单幅图像的灰度化功能:
//
// Created by HooS on 2017/7/25.
//
#include <com_hoos_ndkopencvtest_OpencvNativeClass.h>
JNIEXPORT jint JNICALL Java_com_hoos_ndkopencvtest_OpencvNativeClass_convertGray
(JNIEnv *, jclass, jlong addrRgba, jlong addrGray){
// 实现jlong类型到Mat类型的转换
Mat& mRgb = *(Mat*)addrRgba;
Mat& mGray = *(Mat*)addrGray;
// 定义 int 和 jint 类型来接收函数返回值
int conv;
jint retVal;
// 此处调用了toGray函数来对图片进行灰度化处理
conv = toGray(mRgb, mGray);
retVal = (jint)conv;
return retVal;
}
看注释可以知道, 我们还需要实现toGray函数来实现单幅图片的灰度化, 这个实现代码很简单, 为啥非得再麻烦些折腾些呢, 因为大项目往往会有好多源文件, 相互调用, 所以之前最好有多源文件编译的机会来锻炼一下, nao, 自己创造机会。
接下来,新建gray.h、gray.cpp文件来实现toGray这个函数。按照刚才新建.c/.cpp文件的操作来新建:
// this is gray.h
#include <stdio.h>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
bool toGray(Mat img, Mat& gray);
// this is gray.cpp
#include "gray.h"
bool toGray(Mat img, Mat& gray)
{
if (img.channels() == 3)
cvtColor(img, gray, CV_BGR2GRAY);
else if (img.channels() == 4)
cvtColor(img, gray, CV_BGRA2GRAY);
else if (img.channels() == 1)
gray = img;
if(gray.rows == img.rows && gray.cols == img.cols)
return true;
else
return false;
}
然后再修改下com_hoos_ndkopencvtest_OpencvNativeClass.h文件就好了,添加如下代码(在哪里添加应该都懂):
#include "gray.h"
using namespace cv;
using namespace std;
至此,C/C++源文件搞定。
5)写 Android.mk 和 Application.mk 文件
因为本文采用的gradle的方式来进行NDK开发的, 所以我们会用到 Android.mk 和 Application.mk文件,开始新建这两个文件:
# this is Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#opencv
OPENCVROOT:= D:/Usual/Android/OpenCV-2.4.9-android-sdk #这句代码等号右边写上自己的OpenCV安卓开发包的位置
OPENCV_CAMERA_MODULES:=on
OPENCV_INSTALL_MODULES:=on
OPENCV_LIB_TYPE:=SHARED
#思考下,参考我的修改一下就好了
include D:/Usual/Android/OpenCV-2.4.9-android-sdk/sdk/native/jni/OpenCV.mk
#这里写上自己的开发需要的源文件, 我这里需要两个
LOCAL_SRC_FILES := com_hoos_grayprocessing_OpencvClass.cpp gray.cpp
#设置生成的so库的名字 前面的lib和后缀名不用写
LOCAL_MODULE := MyLibs
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
# this is Application.mk
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
#这句是设置生成的cpu指令类型,可以根据自己需求来设置生成的平台, 一般这两个就够了
APP_ABI := armeabi-v7a x86
APP_PLATFORM := android-8 #这句是设置最低安卓平台,可以不弄
6)开始生成so库
源内容如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.hoos.grayprocessing.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
根据实际情况, 我们不需要TextView, 而需要一个JavaCameraView来显示摄像头帧, 控件ID为"java_camera_view", 修改如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.hoos.grayprocessing.MainActivity">
<org.opencv.android.JavaCameraView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/java_camera_view"
/>
</RelativeLayout>
2)AndroidManifest.xml
原内容是:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hoos.grayprocessing">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
因为应用需要摄像头权限, 所以需要在这个文件添加摄像头权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hoos.grayprocessing">
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
3)gradle.propertise
需要添加对以前版本的支持,在此文件内容的最后添加一行:
android.useDepredcatedNdk=true
4)build.gradle
原内容:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.hoos.grayprocessing"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
testCompile 'junit:junit:4.12'
compile project(':openCVLibrary249')
}
添加内容, 指明生成的so库的路径:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.hoos.grayprocessing"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets.main{
jniLibs.srcDirs = ['src/main/libs']
jni.srcDirs = []
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
testCompile 'junit:junit:4.12'
compile project(':openCVLibrary249')
}
5)MainActivity.java
部分注释在代码中,
package com.hoos.grayprocessing;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.JavaCameraView;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2{
private static String TAG = "MainActivity";
// 初始化一个实例,用来获取摄像头帧
JavaCameraView javaCameraView;
Mat mRgba; // 用来存储原始摄像头数据
Mat mGray; // 用来存储灰度化后的帧数据
// 静态加载之前生成的so库——Mylins
static{
System.loadLibrary("MyLibs");
}
// 回调开初始化帧数据
BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch(status){
case BaseLoaderCallback.SUCCESS:
javaCameraView.enableView();
break;
default:
super.onManagerConnected(status);
break;
}
super.onManagerConnected(status);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 与java_camera_view控件绑定
javaCameraView = (JavaCameraView)findViewById(R.id.java_camera_view);
javaCameraView.setVisibility(View.VISIBLE);
javaCameraView.setCvCameraViewListener(this);
}
@Override
protected void onPause(){
super.onPause();
if(javaCameraView!=null)
javaCameraView.disableView();
}
protected void onDestroy(){
super.onDestroy();
if(javaCameraView!=null)
javaCameraView.disableView();
}
// 判断是否加载成功OpenCV库
protected void onResume(){
super.onResume();
if(OpenCVLoader.initDebug()){
Log.i(TAG, "Opencv loaded successfully");
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
else{
Log.i(TAG, "Opencv not loaded");
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_9, this, mLoaderCallback);
}
}
@Override
public void onCameraViewStarted(int width, int height) {
// 初始化这两个Mat类型
mRgba = new Mat(height, width, CvType.CV_8UC4);
mGray = new Mat(height, width, CvType.CV_8UC1);
}
@Override
public void onCameraViewStopped() {
mRgba.release();
}
@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
// 得到摄像头原始帧数据
mRgba = inputFrame.rgba();
// 调用OpencvClass类的方法来对帧数据进行灰度化
OpencvClass.convertGray(mRgba.getNativeObjAddr(), mGray.getNativeObjAddr());
return mGray;
}
}
此外, 出现了如下提示"Gradle files have changed ...", 请单击右边的"Sync Now"来进行更新。
木有问题
建议创建x86的虚拟机):