Android开发之NDK入门

摘要

本文主要记录了自己在开发过程中对NDK入门知识的一个学习过程,Android NDK开发涉及到的知识比较多,其中C、C++语言就需要一定的功底。另外还有JNI以及不同CPU平台对编译动态so的支持等知识。

基础知识

NDK简介

● NDK(Native Development Kit),是一个Android Native开发工具集,它的主要作用就是快速开发C、C++的动态库,并将编译生成的so打包进apk。其中Java与Native代码的交互需要使用到JNI。
● NDK使用C、C++语言开发的程序代码具有运行效率高,安全性好的特点。(编译型语言与解释型语言的区别)
● NDK开发的动态库可以移植到不同的平台

JNI简介

JNI标准成为Java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。

JNI 注册方式

  • 静态注册: 静态注册方式是通过手动编写一组 JNI 函数来完成注册。您需要在本地代码中使用特定的命名规则将本地函数与 Java 方法进行映射,并在 Java 代码中声明本地方法。然后,在 Java 代码中加载本地库时,调用 System.loadLibrary() 方法来加载本地库并触发静态注册过程。这样可以将本地函数与 Java 方法关联起来,使得 Java 代码可以调用本地函数。缺点是当包名、类名、方法名发生修改时,Native层对应的JNI方法名也需要发生修改。

  • 动态注册: 动态注册方式是通过使用 JNI 提供的函数在运行时进行注册。您需要在本地代码中编写一个 JNI_OnLoad() 函数,在该函数中调用 JNI 提供的函数来注册本地函数。然后,在 Java 代码中加载本地库时,JVM 会自动调用 JNI_OnLoad() 函数进行动态注册。

public class JNIDynamic {
    public native String  dynamicFun();
}
//指定类名
const char *jniDynamicClassName = "com/example/uiviewandrid/JNIDynamic";

//声明函数
jstring nativeDynamicFun(JNIEnv *env, jobject thiz);

//参数列表对应关系:param1:Java层方法名  param2:java层方法签名信息  param3:对应的native方法签名
static JNINativeMethod gMethods[] = {
        {"dynamicFun","()Ljava/lang/String;",(jstring)(nativeDynamicFun)},
};

jstring nativeDynamicFun(JNIEnv *env, jobject thiz){
    std::string hello = "Hello from C++ dynamic register";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    // 开始动态注册,通过JavaVM获取JNIEnv(操作杆),跟Java层大量交互都是通过JNIEnv来实现。
    JNIEnv *jniEnv = nullptr;
    int result = vm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
    // result 等于0 成功,非0 失败
    if (result != 0) {
        return -1;
    }
    // 获取MainActivity class,通过包名 + 类名 获取
    jclass jniDynamicClass = jniEnv->FindClass(jniDynamicClassName);
    // 动态注册函数, RegisterNatives(MainActivity class, 动态函数的数组, 注册动态函数数量)
    jniEnv->RegisterNatives(jniDynamicClass, gMethods,
                            sizeof(gMethods) / sizeof(JNINativeMethod));
    __android_log_print(ANDROID_LOG_DEBUG, "TAG",  "load success");
    return JNI_VERSION_1_6;
}

JNI 类型

  • 基础数据类型
Java类型JNI类型Native签名
bytejbyteB
charjcharC
shortjshortS
intjintI
floatjfloatF
longjlongJ
doublejdoubleD
booleanjbooleanZ
  • 引用数据类型和数组
Java类型JNI类型Native签名
voidvoidV
StringjstringLjava/lang/String;
objectjobjectL全限定类名;
classjclassLjava/lang/Class;
ThrowablejthrowableLjava/lang/Throwable;
Object[]jobject[L全限定类名;
int[]jintArray[I
long[]jlongArray[J
  • 方法与属性
const char *jniUserInfoClassName = "com/example/uiviewandrid/UserInfo";

jobject nativeGetUserInfo(JNIEnv *env, jobject obj) {
    //根据类名找到类
    jclass jniUserInfoClass = env->FindClass(jniUserInfoClassName);
    //寻找类的构造器方法
    jmethodID constructor = env->GetMethodID(jniUserInfoClass, "<init>", "()V");
    //调用构造器创建对象
    jobject nativeUserInfo = env->NewObject(jniUserInfoClass, constructor);
    //给对象属性赋值
    jfieldID name = env->GetFieldID(jniUserInfoClass, "name", "Ljava/lang/String;");
    jfieldID age = env->GetFieldID(jniUserInfoClass, "age", "I");
    jstring nameValue = env->NewStringUTF("Name C++");
    jint ageValue = 30;
    env->SetObjectField(nativeUserInfo, name, nameValue);
    env->SetIntField(nativeUserInfo, age, ageValue);
    //返回对象
    return nativeUserInfo;
}

CMake简介

  • C/C++语言开发的程序在编译的过程中,通常都会有四个阶段:预编译、编译、汇编、链接。
    C/C++编译过程
  • Android NDK 编译可以使用两种方式,一种是NDK Build,依赖于Application.mk与Android.mk这两个文件,比较复杂。而另一种方式就是Google官方目前推荐使用的CMake。CMake 是一个跨平台的开源构建工具,用于管理和生成各种编译环境的构建脚本。它使用一种简单的语法来描述构建过程,并可以自动生成适用于不同操作系统、编译器和构建系统的构建文件。

Android ABI 简介

在 Android 中,ABI(Application Binary Interface)指的是应用程序二进制接口,它定义了应用程序与底层操作系统和硬件交互的接口规范。Android ABI 分类是根据底层处理器架构和指令集进行的分类。下面是几种常见的 Android ABI 类型及其简介:

  • armeabi-v7a: 这是针对基于 ARMv7 架构的设备的 ABI。它支持 32 位 ARM 指令集,并提供对浮点运算和 SIMD(单指令多数据)指令的优化。这是大多数现代 ARM 设备所使用的 ABI。

  • arm64-v8a: 这是针对基于 ARMv8 架构的设备的 ABI。它同样支持 32 位 ARM 指令集,但还支持 64 位 ARM 指令集。arm64-v8a ABI 通常用于较新的 ARM 架构设备。

  • x86: 这是针对基于 x86 架构的设备的 ABI。x86 是一种常见的 PC 和服务器处理器架构。x86 ABI 适用于 Android 模拟器和一些基于 x86 的 Android 设备。

  • x86_64: 这是针对基于 x86-64 架构的设备的 ABI。它是 x86 架构的 64 位扩展,提供更高的性能和更大的内存寻址能力。x86_64 ABI 通常用于较新的 x86-64 架构设备。

  • mips: 这是针对基于 MIPS(Microprocessor without Interlocked Pipeline Stages)架构的设备的 ABI。MIPS 是一种低功耗、高性能的处理器架构,主要用于嵌入式系统和网络设备。

在构建 Android 应用时,通常需要为每个 ABI 构建相应的本机库,以便应用程序可以在不同的设备上运行。您可以通过在 Gradle 配置文件中指定目标 ABI 来进行配置,并生成适用于特定 ABI 的本机库。

请注意,随着 Android 设备的更新和演进,一些 ABI 可能会逐渐被淘汰或不再受支持。为了确保应用程序的兼容性和性能,建议根据目标用户群体的设备统计数据来选择支持的 ABI 类型。

HelloWorld

NDK环境配置

借助于AndroidStudio这一强大的Android开发神器,让我们在进行混合开发时能够得心应手,Android SDK中目前已经集成了NDK开发的整套工具,使用CMake进行编译配置,这样能够简单快速的编译出动态或静态库。我们将借助AS实现NDK的HellWorld,首先就是环境配置,我们需要配置NDK的开发环境:
SDK Manager
打开AS的SDK Manager,找到SDK Tools,我们勾选上NDK与CMake,默认最新版本,可以选择自己需要的版本进行下载,AS会自动为你下载解压到对应的路径,NDK和CMake都会被下载到Sdk路径下。

编写相关配置及代码

  1. 我们在项目对应的模块下java同级目录创建cpp文件夹,用于放置C/C++相关代码,同时创建CMakeLists.txt文件
    Alt
  2. build.gradle文件配置相关选项
android {
   ......
   defaultConfig {
       ......
       externalNativeBuild {
           cmake {
           	//提供给C++编译器的一个标志 可选 cppFlags  '-std=c++11'  例如这里可以指定C++版本
               cppFlags ''
           }
       }

       ndk{
       	//指定需要生成哪些平台的so,一共四个: 'arm64-v8a','x86','armeabi-v7a','x86_64'
           abiFilters 'arm64-v8a','x86'
       }
   }

   externalNativeBuild {
   	//指定CMakeLists.text文件的路径以及版本
       cmake {
           path file('src/main/cpp/CMakeLists.txt')
           version '3.22.1'
       }
   }
   ......
   //指定ndk的版本
   ndkVersion '21.4.7075529'
}
  1. 在cpp文件夹下创建你需要的C/C++文件,创建CMakeLists.text文件,CMakeLists.txt文件也可以放到别的文件夹下,同时需要在java文件夹下创建JNI接口函数对native函数进行声明。

  2. 编写JNI方法

package com.example.uiviewandrid

class JNILoader {

    companion object {
        init {
            System.loadLibrary("UIViewAndroid")
        }
    }

    /**
     * 在 Kotlin 中,external 是一个关键字,用于声明一个外部函数或属性。它表示该函数或属性的实现是由外部提供的,而不是在当前代码中定义的。
     * 使用 external 关键字可以在 Kotlin 中与其他语言进行交互,比如使用 Java 或 C/C++ 编写的原生代码。
     * 在这种情况下,您可以使用 external 来声明 Kotlin 函数或属性,然后在底层的原生代码中实现其具体逻辑。
     */
    external fun sayHello(): String
}

在这里插入图片描述
我们在定义完一个JNI方法之后,编译器会给出如上图所对应的提示,告诉我们创建对应JNI方法,我们点击进去他就会按照JNI的命名规则为我们创建对应的JNI函数。

  1. 编写Native对JNI方法的实现
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_uiviewandrid_JNILoader_sayHello(JNIEnv *env, jobject obj) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
  1. 定义CMakeLists.txt相关内容
# 指定最小版本
cmake_minimum_required(VERSION 3.18.1)

project(UIViewAndroid)

add_library( # Sets the name of the library. lib${name}.so
        UIViewAndroid

        # Sets the library as a shared library.指定静态还是动态
        SHARED

        # Provides a relative path to your source file(s).需要加载的cpp文件,相对路径
        NativeLib.cpp)

find_library( # Sets the name of the path variable.寻找log这个库
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

target_link_libraries( # Specifies the target library.
        UIViewAndroid

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

7.到这里我们就对项目完成了编写,接下来找个地方调用我们的JNI函数,编译项目运行查看结果即可完成一次JNI调用了。

如何处理生成的so

首先我们要根据目标机型打包对应ABI的so,根据设备类型选择对应的abiFilters,注意:使用AS编译后的so会自动打包到apk中,如果需要提供给第三方使用,可以到build/intermediates/cmake/debug/obj 目录中拷贝出来。第三方库一般直接放在main/jniLibs文件夹下,也有放在默认libs目录下的,但是必须在build.gradle中声明jni库的目录

sourceSets {
    main {
        jniLibs.srcDirs = ['jniLibs']
    }
}

JNIEnv与JavaVM

JNIEnv

  • 在JNI中,JNIEnv的定义是一个结构体,在JNI方法中,它是一个指向JNI环境的指针,他是提供给本地方法的一个关键性参数,用于native与java环境进行通信,这个结构体中定义了很多方法。例如,通过JNIEnv可以调用Java对象的实例方法、静态方法,或者创建新的Java对象等。
  • 其只在创建它的线程有效,不能跨线程传递,不同的线程的JNIEnv彼此独立。native 环境中创建的线程,如果需要访问JNI,必须调用AttachCurrentThread 进行关联,然后使用DetachCurrentThread 解除关联。
  • 访问Java对象:JNIEnv提供了一系列方法,用于访问和操作Java对象的属性、数组元素等。可以使用JNIEnv获取Java对象的Class对象、FieldID、MethodID,并通过这些信息对Java对象进行操作。
  • 异常处理:在JNI中,如果发生了异常,可以通过JNIEnv的异常处理方法来抛出、检查和清除异常。这样可以确保本地方法的异常能够被Java层捕获和处理。
  • 内存管理:JNIEnv提供了一系列方法,用于在本地代码中分配、释放内存,并管理本地引用和全局引用。这样可以确保在JNI中正确管理内存资源,避免内存泄漏和垃圾回收问题。

JavaVm

  • 跨线程通信:JavaVM可以在多个线程间共享,这意味着可以在不同的线程中使用JNIEnv进行Java操作,并且还可以在不同的本地方法之间共享Java对象。
  • 应用程序管理:JavaVM可以用于创建和销毁Java虚拟机实例,启动和停止Java应用程序等。这使得Java应用程序能够自主控制Java虚拟机,实现更加灵活的应用管理。
  • 类加载和卸载:JavaVM负责类加载和卸载,当需要使用某个类时,JavaVM会自动将该类加载到内存中并进行初始化。而当不再需要该类时,JavaVM会自动将其卸载以释放内存资源。
  • 内存管理:JavaVM管理Java堆内存和本地内存,确保Java程序能够正确分配和释放内存资源。在JNI中,JavaVM也提供了一系列方法和功能,用于管理本地内存和全局引用。

JNI的三种引用

jin.h文件中声明了大量的JNI相关的方法,其中有三组方法是用来管理引用的。

	jobject NewGlobalRef(jobject obj)
	{ return functions->NewGlobalRef(this, obj); }
	
	void DeleteGlobalRef(jobject globalRef)
	{ functions->DeleteGlobalRef(this, globalRef); }
	
	jobject NewLocalRef(jobject ref)
	{ return functions->NewLocalRef(this, ref); }
	
	void DeleteLocalRef(jobject localRef)
	{ functions->DeleteLocalRef(this, localRef); }
	
	jweak NewWeakGlobalRef(jobject obj)
	{ return functions->NewWeakGlobalRef(this, obj); }
	
	void DeleteWeakGlobalRef(jweak obj)
	{ functions->DeleteWeakGlobalRef(this, obj); }

局部引用(LocalRef)

  • 特点:局部引用是一种短暂的引用,其生命周期仅限于本地方法的调用过程中。当本地方法返回时,局部引用会被自动释放。
  • 用途:局部引用适合在本地方法中临时使用的对象,不需要长时间保持引用关系。它们不会占用太多内存资源,在本地方法执行完毕后立即释放。

全局引用(GlobalRef)

  • 特点:全局引用是一种长期有效的引用,其生命周期不受本地方法的限制,直到被显式释放或虚拟机关闭
  • 用途:全局引用适合在本地方法中创建的对象,并且需要在其他本地方法或 Java 代码中继续使用的情况。它们可以跨越多个本地方法调用,确保对象在整个应用程序中可见和可访问。

弱全局引用(WeakGlobRef)

  • 特点:弱全局引用是一种弱引用,它允许被垃圾回收器自动回收。当对象只被弱全局引用引用时,如果没有其他强引用指向该对象,垃圾回收器可能会回收这个对象。
  • 用途:弱全局引用适合在本地方法中创建的对象,并且不需要长期保持引用关系的情况。弱引用在内存不足或者紧张的时候会自动回收掉,可能会出现短暂的内存泄露,但是不会出现内存溢出的情况,建议不需要使用的时候手动调用DeleteWeakGlobalRef释放引用。
  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值