AndroidAndroid进阶之路Android-NDK/JNI

一篇文章带你入门NDK开发

2020-09-09  本文已影响0人  涤生_Woo
NDK开发

一、C++ 基础知识

1.1 函数

int i;
int *a = &i;        //这里a是一个指针,它指向变量i
int &b = i;         //这里b是一个引用,它是变量i的引用(别名)
int * &c = a;       //这里c是一个引用,它是指针a的引用
int & *d;           //这里d是一个指针,它指向引用,但引用不是实体,所以这是错误的

在分析上面代码时,可以从变量标识符开始从右往左看,最靠近标识符的是变量的本质类型,而再往左即为对变量类型的进一步修饰。
例如:int * & a
标识符a的左边紧邻的是 &,证明 a 是一个引用变量,而再往左是 * ,可见 a 是一个指针的引用,再往左是 int,可见 a 是一个指向int类型的指针的引用。

struct Data
{
int a,b,c;
}; /*定义结构体类型*/
struct Data * p;                 /*  定义结构体指针   */
struct Data A = {1,2,3};         / *  声明结构体变量A,A即结构体名   */
int x;                               /*  声明一个变量x  */
p = &A ;                           /*   地址赋值,让p指向A    */
x = p->a;        /* 取出p所指向的结构体中包含的数据项a赋值给x   */
/* 此时由于p指向A,因而 p->a == A.a,也就是1 */

因为此处 p 是一个指针,所以不能使用.号访问内部成员(即不能 p.a),而要使用 ->。但是 A.a 是可以的,因为 A 不是指针,是结构体名。
一般情况下用 “.” 只需要声明一个结构体。格式是:结构体类型名+结构体名。然后用 结构体名加“.”加成员名 就可以引用成员了。因为自动分配了结构体的内存。如同 int a; 一样。
“->” ,则要声明一个结构体指针,还要手动开辟一个该结构体的内存(上面的代码则是建了一个结构体实例,自动分配了内存,下面的例子则会讲到手动动态开辟内存),然后把返回的地址赋给声明的结构体指针,才能用“->” 正确引用。否则内存中只分配了指针的内存,没有分配结构体的内存,导致想要的结构体实际上是不存在。这时候用 “->” 引用自然出错了,因为没有结构体,自然没有结构体的域了。
此外,(*p).a 等价于 p->a

:: 是作用域符,是运算符中等级最高的,它分为三种:

  1. 全局作用域符,用法 ::name
  2. 类作用域符,用法 class::name
  3. 命名空间作用域符,用法 namespace::name

他们都是左关联,他们的作用都是为了更明确的调用你想要的变量:

  1. 如在程序中的某一处你想调用全局变量 a,那么就写成 ::a;(也可以是全局函数)
  2. 如果想调用 class A 中的成员变量 a,那么就写成 A::a
  3. 另外一个如果想调用 namespace std 中的 cout 成员,你就写成 std::cout(相当于 using namespace std;cout)意思是在这里我想用 cout 对象是命名空间 std 中的cout(即就是标准库里边的cout);

(1)表示“域操作符”:声明了一个类 A,类 A 里声明了一个成员函数 void f(),但没有在类的声明里给出 f 的定义,那么在类外定义f时, 就要写成 void A::f(),表示这个 f() 函数是类 A 的成员函数。
(2)直接用在全局函数前,表示是全局函数:在 VC 里,你可以在调用 API 函数里,在 API 函数名前加 ::
(3)表示引用成员函数及变量,作用域成员运算符:System::Math::Sqrt() 相当于 System.Math.Sqrt()

1.2 linux内存布局

C 语言内存组成

1.3 指针数组

int* p[3];
for(int i = 0; i<3; i++){
 p[i] = &arr[i];
}

优先级高,首先说明 p 是一个指针,指向一个整型的一维数组,这个一维数组的长度是 n,也可以说是 p 的步长。执行 p+1 时,p 要跨过 n 个整型数据的长度。
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含 4 个元素的一维数组
p = a; //将该二维数组的首地址赋给 p,也就是 a[0] 或 &a[0][0]
p++; //该语句执行后,也就是 p = p+1; p 跨过行 a[0][] 指向了行 a[1][]

1.4 结构体

struct Person
{
    char c;
    int i;
    char ch;
};

int main()
{
    struct Person person;
    person.c = 8;
    person.i = 9;
}

存储变量时地址要求对齐,编译器在编译程序时会遵循两个原则:
(1)结构体变量中成员的偏移量必须是成员大小的整数倍
(2)结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数

内存对齐

1.5 共用体

union Data
{
    int i;
    float f;
    char str[20];
}data;

int main()
{
    union Data data;
    data.i = 11;
}

1.6 typedef

char *pa, *pb;//传统写法
typedef char* PCHAR; // 使用typedef 写法  一般用大写
PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;
//使用 typedef
typedef struct tagPOINT
{
int x;
int y;
}POINT;

POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候
#if __ANDROID__
typedef double SUM;
#else
typedef float SUM ;
#endif

int test() {
    SUM a;
    return 0;
}
 //原声明:
int *(*a[5])(int, char*);
//变量名为a,直接用一个新别名pFun替换a就可以了:
typedef int *(*pFun)(int, char*);
//原声明的最简化版:
pFun a[5];

1.7 类的构造和解析、友元函数

1.7.1 C++ 中头文件(.h)和源文件(.cpp)
#ifndef CIRCLE_H
#define CIRCLE_H

class Circle
{
private:
    double r;
public:
    Circle();//构造函数
    Circle(double R);//构造函数
    double Area();
};

#endif

至于 CIRCLE_H 这个名字实际上是无所谓的,你叫什么都行,只要符合规范都行。原则上来说,非常建议把它写成这种形式,因为比较容易和头文件的名字对应。

#include "Circle.h"

Circle::Circle()
{
    this->r=5.0;
}
Circle::Circle(double R)
{
    this->r=R;
}
double Circle:: Area()
{
    return 3.14*r*r;
}
#include <iostream>
#include "Circle.h"
using namespace std;

int main()
{
    Circle c(3);
    cout<<"Area="<<c.Area()<<endl;
    return 1;
}
1.7.2 构造函数和析构函数
1.7.3 友元函数、友元类
class INTEGER
{  
private:
    int num;
public:
    friend void Print(const INTEGER& obj);//声明友元函数
};
void Print(const INTEGER& obj)     //不使用friend和类::
{
    //函数体
}
void main()
{
    INTEGER obj;
    Print(obj);//直接调用
}
#include <iostream>
using namespace std;
class girl
{  
private:
    char *name;  
    int age;  
    friend class  boy;   //声明类boy是类girl的友元
public:
    girl(char *n,int age):name(n),age(age){};
};  

class boy
{  
private:
    char *name;  
    int age;  
public:  
    boy(char *n,int age):name(n),age(age){};
    void disp(girl &);   
};  

void boy::disp(girl &x)       //  该函数必须在girl类定义的后面定义,否则girl类中的私有变量还是未知的    
{ 
    cout<<"boy's name is:"<<name<<",age:"<<age<<endl;
    cout<<"girl's name is:"<<x.name<<",age:"<<x.age<<endl; 
    //借助友元,在boy的成员函数disp中,借助girl的对象,直接访问girl的私有变量
    //正常情况下,只允许在girl的成员函数中访问girl的私有变量
}
  
void main()  
{   
    boy b("aaa",8);  
    girl g("bbb",99);  
    b.disp(g); 
}

1.8 单例对象、操作符重载

#include <iostream>
using namespace std;
 
class Box
{
   public:
 
      double getVolume(void)
      {
         return length * breadth * height;
      }
      void setLength( double len )
      {
          length = len;
      }
 
      void setBreadth( double bre )
      {
          breadth = bre;
      }
 
      void setHeight( double hei )
      {
          height = hei;
      }
      // 重载 + 运算符,用于把两个 Box 对象相加
      Box operator+(const Box& b)
      {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
      }
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};
// 程序的主函数
int main( )
{
   Box Box1;                // 声明 Box1,类型为 Box
   Box Box2;                // 声明 Box2,类型为 Box
   Box Box3;                // 声明 Box3,类型为 Box
   double volume = 0.0;     // 把体积存储在该变量中
 
   // Box1 详述
   Box1.setLength(6.0); 
   Box1.setBreadth(7.0); 
   Box1.setHeight(5.0);
 
   // Box2 详述
   Box2.setLength(12.0); 
   Box2.setBreadth(13.0); 
   Box2.setHeight(10.0);
 
   // Box1 的体积
   volume = Box1.getVolume();
   cout << "Volume of Box1 : " << volume <<endl;
 
   // Box2 的体积
   volume = Box2.getVolume();
   cout << "Volume of Box2 : " << volume <<endl;
 
   // 把两个对象相加,得到 Box3
   Box3 = Box1 + Box2;
 
   // Box3 的体积
   volume = Box3.getVolume();
   cout << "Volume of Box3 : " << volume <<endl;
 
   return 0;
}

打印结果:
Volume of Box1 : 210
Volume of Box2 : 1560
Volume of Box3 : 5400

1.9 继承多态、虚函数

1.9.1 继承

基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。
基类的友元函数。

公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

#include <iostream>
using namespace std;
 
// 基类
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 派生类
class Rectangle: public Shape
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;
 
   return 0;
}

打印结果:
Total area: 35

1.9.2 虚函数

定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。


class A
{
public:
    virtual void foo()
    {
        cout<<"A::foo() is called"<<endl;
    }
};
class B:public A
{
public:
    void foo()
    {
        cout<<"B::foo() is called"<<endl;
    }
};
int main(void)
{
    A *a = new B();
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
}

1.10 类模板、函数模板

模板函数定义的一般形式如下所示:

template <typename type> ret-type func-name(parameter list)
{
   // 函数的主体
}
#include <iostream>
#include <string>
 
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
   return 0;
}

打印结果:
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World

类模板,泛型类声明的一般形式如下所示:

template <class type> class class-name {
.
.
.
}
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
 
using namespace std;
 
template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
 
template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 
 
template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 
 
template <class T>
T Stack<T>::top () const 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
    // 返回最后一个元素的副本 
    return elems.back();      
} 
 
int main() 
{ 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 
 
        // 操作 int 类型的栈 
        intStack.push(7); 
        cout << intStack.top() <<endl; 
 
        // 操作 string 类型的栈 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}

打印结果:
7
hello
Exception: Stack<>::pop(): empty stack

1.11 容器

特点
vector 头部与中间插入和删除效率较低,在尾部插入和删除效率高,支持随机访问。
deque 是在头部和尾部插入和删除效率较高,支持随机访问,但效率没有 vector 高。
list 在任意位置的插入和删除效率都较高,但不支持随机访问。
set 由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复,且插入和删除效率比用其他序列容器高。
map 可以自动建立 Key - value 的对应,key 和 value 可以是任意你需要的类型,根据 key 快速查找记录。

选择
如果需要高效的随机存取,不在乎插入和删除的效率,使用 vector
如果需要大量的插入和删除元素,不关心随机存取的效率,使用 list
如果需要随机存取,并且关心两端数据的插入和删除效率,使用 deque
如果打算存储数据字典,并且要求方便地根据 key 找到 value,一对一的情况使用 map,一对多的情况使用 multimap
如果打算查找一个元素是否存在于某集合中,唯一存在的情况使用 set,不唯一存在的情况使用 multiset

时间复杂度
vector 在头部和中间位置插入和删除的时间复杂度为 O(N),在尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
deque 在中间位置插入和删除的时间复杂度为 O(N),在头部和尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
list 在任意位置插入和删除的时间复杂度都为 O(1),查找的时间复杂度为 O(N);
setmap 都是通过红黑树实现,因此插入、删除和查找操作的时间复杂度都是 O(log N)。

1.12 命名空间

1.12.1 namespace
namespace namespace_name {
   // 代码声明
}
namespace {
   // 代码声明
}
1.12.2 using
#include <iostream>
using namespace std;
 
// 第一个命名空间
namespace first_space{
   void func(){
      cout << "Inside first_space" << endl;
   }
}
// 第二个命名空间
namespace second_space{
   void func(){
      cout << "Inside second_space" << endl;
   }
}
using namespace first_space;
int main ()
{
 
   // 调用第一个命名空间中的函数
   func();
   
   return 0;
}

using指令 使用后,可以一劳永逸,对整个命名空间的所有成员都有效,非常方便。而 using声明,则必须对命名空间的不同成员名称,一个一个地去声明。但是,一般来说,使用 using声明 会更安全。因为,using声明 只导入指定的名称,如果该名称与局部名称发生冲突,编译器会报错。using指令 导入整个命名空间中的所有成员的名称,包括那些可能根本用不到的名称,如果其中有名称与局部名称发生冲突,则编译器并不会发出任何警告信息,而只是用局部名去自动覆盖命名空间中的同名成员。特别是命名空间的开放性,使得一个命名空间的成员,可能分散在多个地方,程序员难以准确知道,别人到底为该命名空间添加了哪些名称。

二、java 调用 C/C++

//MainActivity.java
static {
        System.loadLibrary("native-lib");
}
//MainActivity.java
public native String stringFromJNI();
//native-lib.cpp
#include <jni.h>
#include <string>
//函数名的构成:Java 加上包名、方法名并用下划线连接(Java_packageName_methodName)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cppdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
# CMakeLists.txt

# 设置构建本地库所需的最小版本的cbuild。
cmake_minimum_required(VERSION 3.4.1)
# 创建并命名一个库,将其设置为静态
# 或者共享,并提供其源代码的相对路径。
# 您可以定义多个库,而cbuild为您构建它们。
# Gradle自动将共享库与你的APK打包。
add_library( native-lib       #设置库的名称。即SO文件的名称,生产的so文件为“libnative-lib.so”, 在加载的时候“System.loadLibrary("native-lib");”
             SHARED            # 将库设置为共享库。
             native-lib.cpp    # 提供一个源文件的相对路径
             helloJni.cpp      # 提供同一个SO文件中的另一个源文件的相对路径
           )
# 搜索指定的预构建库,并将该路径存储为一个变量。因为cbuild默认包含了搜索路径中的系统库,所以您只需要指定您想要添加的公共NDK库的名称。cbuild在完成构建之前验证这个库是否存在。
find_library(log-lib   # 设置path变量的名称。
             log       #  指定NDK库的名称 你想让CMake来定位。
             )
#指定库的库应该链接到你的目标库。您可以链接多个库,比如在这个构建脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries( native-lib    # 指定目标库中。与 add_library的库名称一定要相同
                       ${log-lib}    # 将目标库链接到日志库包含在NDK。
                       )
#如果需要生产多个SO文件的话,写法如下
add_library( natave-lib       # 设置库的名称。另一个so文件的名称
             SHARED           # 将库设置为共享库。
             nataveJni.cpp    # 提供一个源文件的相对路径
            )
target_link_libraries( natave-lib     #指定目标库中。与 add_library的库名称一定要相同
                       ${log-lib}     # 将目标库链接到日志库包含在NDK。
                        )     
// build.gradle(:app)
android {
    compileSdkVersion 29
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.example.cppdemo"
        minSdkVersion 16
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

三、JNI 基础

3.1 JNIEnv 、JavaVM

调用 Java 函数
操作 Java 对象

3.2 数据类型

3.2.1 基础数据类型
Signature格式 Java Native Description
B byte jbyte signed 8 bits
C char jchar unsigned 16 bits
D double jdouble 64 bits
F float jfloat 32 bits
I int jint signed 32 bits
S short jshort signed 16 bits
J long jlong signed 64 bits
Z boolean jboolean unsigned 8 bits
V void void N/A
3.2.2 数组数据类型

数组简称:在前面添加 [

Signature格式 Java Native
[B byte[] jbyteArray
[C char[] jcharArray
[D double[] jdoubleArray
[F float[] jfloatArray
[I int[] jintArray
[S short[] jshortArray
[J long[] jlongArray
[Z boolean[] jbooleanArray
3.2.3 复杂数据类型

对象类型简称:L+classname +;

Signature格式 Java Native
Ljava/lang/String; String jstring
L+classname +; 所有对象 jobject
[L+classname +; Object[] jobjectArray
Ljava.lang.Class; Class jclass
Ljava.lang.Throwable; Throwable jthrowable
3.2.4 函数签名

(输入参数...)返回值参数

Signature格式 Java函数
()V void func()
(I)F float func(int i)
([I)J long func(int[] i)
(Ljava/lang/Class;)D double func(Class c)
([ILjava/lang/String;)Z boolean func(int[] i,String s)
(I)Ljava/lang/String; String func(int i)

3.3 JNI 操作 JAVA 对象、类

jclass thisclazz = env->GetObjectClass(thiz);//使用GetObjectClass方法获取thiz对应的jclass。 
jclass thisclazz = env->FindClass("com/xxx/xxx/abc");//直接搜索类名
/**
 * thisclazz -->上一步获取的 jclass
 * "onCallback"-->要调用的方法名
 * "(I)Ljava/lang/String;"-->方法的 Signature, 签名参照前面的第 3.2 小节表格。
 */
jmethodID mid_callback = env->GetMethodID(thisclazz , "onCallback", "(Ljava/lang/String;)I");
jmethodID mid_callback = env->GetStaticMethodID(thisclazz , "onCallback", "(Ljava/lang/String;)I");//获取静态方法的ID
jint result = env->CallIntMethod(thisclazz , mid_callback , jstrParams);
jint result = env->CallStaticIntMethod(thisclazz , mid_callback , jstrParams);//调用静态方法

贴一下JNI 常用接口文档,有需要可以在这里查询。

3.4 JNI 引用

3.4.1 局部引用
3.4.2 全局引用
3.4.3 弱全局引用

后续

本文参考了部分视频、书籍、博客的内容。这里就不列出来了。

上一篇 下一篇

猜你喜欢

热点阅读