Android开发Android开发经验谈Android技术知识

还不上车?十年老司机带你入门NDK开发,想下车都难!

2020-09-10  本文已影响0人  木木玩Android

原文链接:https://juejin.im/post/6870678713361661966

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 语言内存组成

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

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 容器

特点

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

选择

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

时间复杂度

  1. vector 在头部和中间位置插入和删除的时间复杂度为 O(N),在尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
  2. deque 在中间位置插入和删除的时间复杂度为 O(N),在头部和尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
  3. list 在任意位置插入和删除的时间复杂度都为 O(1),查找的时间复杂度为 O(N);
  4. 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 对象;

JNIEnv 内部结构 JavaVM 结构

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 弱全局引用
上一篇 下一篇

猜你喜欢

热点阅读