Android - JNI 开发你所需要知道的基础


从 JVM 角度,存在两种类型的代码:“Java”和“native”, native 一般指的是 c/c++,为了使 java 和 native 端能够进行交互,java 设计了 JNI(java native interface)。 JNI 允许java虚拟机(VM)内运行的java代码与C++、C++和汇编等其他编程语言编写的应用程序和库进行互操作。

虽然大部分情况下我们的软件完全可以由 java 来实现,但是某些场景下使用 native 代码更加适合,比如:

  • 代码效率:使用 native 代码的性能更高
  • 跨平台特性:标准Java类库不支持应用程序所需的依赖于平台的特性,或者希望用较低级别的语言(如汇编语言)实现一小部分时间关键型代码。

native 层使用 JNI 主要可以做到:

  • 创建、检查和更新Java对象(包括数组和字符串)。
  • 调用Java方法。
  • 加载类并获取类信息。

创建 android ndk 项目

使用 as 创建一个 native c++ 项目

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

文件结构如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到生成了一个 cpp 文件夹,里面有 CMakeLists.txt, native-lib.cpp,CMakeLists后面再讲,这里先来看一下 native-lib.cpp 和 java 代码。

public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary(“native-lib”);
}

public native String stringFromJNI();
}

#include <jni.h>
#include

extern “C” JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}

可以看到在 MainActivity 中先定义了一个 native 方法,然后编译器在 cpp 文件中创建一个一个对应的方法Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI。 它的命名规则就是 Java_packageName_methodName。

接下来我们详细的解读一下 cpp 中的代码。

native 代码解读

extern “C”

在 c++ 中使用 c 代码

JNIEXPORT

宏定义:#define JNIEXPORT __attribute__ ((visibility ("default"))) 在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,定义为__attribute__ ((visibility ("default")))

GCC 有个visibility属性, 该属性是说, 启用这个属性:

  • 当-fvisibility=hidden时,动态库中的函数默认是被隐藏的即 hidden。
  • 当-fvisibility=default时,动态库中的函数默认是可见的。

JNICALL

宏定义,在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,它是个空的宏定义: #define JNICALL,所以在 android 上删除它也可以。 快捷生成 .h 代码

JNIEnv

  • JNIEnv类型实际上代表了Java环境,通过这个 JNIEnv* 指针,就可以对 Java 端的代码进行操作:
  • 调用 Java 函数
  • 操作 Java 对象
  • JNIEnv 的本质是一个与线程相关的结构体,里面存放了大量的 JNI 函数指针:

struct _JNIEnv {
/**

  • 定义了很多的函数指针
    */
    const struct JNINativeInterface
    functions;

#if defined(__cplusplus)
/// 通过类的名称(类的全名,这时候包名不是用.号,而是用/来区分的)来获取jclass
jclass FindClass(const char* name) { return functions->FindClass(this, name); }

}

JNIEnv 的结构图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

JavaVM

  • JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个

  • JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv,同时 JNIEnv 具有线程相关性,也就是 B 线程无法使用 A 线程的 JNIEnv

JVM 的结构图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

jobject thiz

这个 object 指向该 native 方法的 this 实例,比如我们在 MainActivity 调用的下面的 native 函数中打印一下 thiz 的 className:

#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR,“JNI”,VA_ARGS);

extern “C” JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = “Hello from C++”;
// 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
jclass thisclazz = env->GetObjectClass(thiz);
// 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
jmethodID mid_getClass = env->GetMethodID(thisclazz, “getClass”, “()Ljava/lang/Class;”);
// 3. 执行 getClass 方法,获得 Class 对象
jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
// 4. 获取 Class 实例
jclass clazz = env->GetObjectClass(clazz_instance);
// 5. 根据 class 的 methodID
jmethodID mid_getName = env->GetMethodID(clazz, “getName”, “()Ljava/lang/String;”);
// 6. 调用 getName 方法
jstring name = static_cast(env->CallObjectMethod(clazz_instance, mid_getName));
LOGE(“class name:%s”, env->GetStringUTFChars(name, 0));

return env->NewStringUTF(hello.c_str());
}

打印结果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

JNI 基础

数据类型

基础数据类型
Java TypeNative TypeDescription
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/A
引用类型

这里贴一张 oracle 文档中的图,虽然很丑但挺好:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Field and Method IDs

JNIEvn 操作 java 对象时利用 java 中的反射,操作某个属性都需要 field 和 method 的 id,这些 id 都是指针类型:

struct _jfieldID; /* opaque structure */
typedef struct _jfieldID jfieldID; / field IDs */

struct _jmethodID; /* opaque structure */
typedef struct _jmethodID jmethodID; / method IDs */

JNI 操作 java 对象

操作 jarray

将一个 Java int[] 对象传入 C++ 中,如何操作这个数组呢?

JNIEXPORT void JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_setArray(JNIEnv *env, jobject thiz, jintArray array) {

// 1.获取数组长度
jint len = env->GetArrayLength(array);
LOGE(“array.length:%d”, len);

jboolean isCopy;
// 2.获取数组地址
// 第二个参数代表 javaArray -> c/c++ Array 转换的方式:
// 0: 把指向Java数组的指针直接传回到本地代码中
// 1: 新申请了内存,拷贝了数组
// 返回值: 数组的地址(首元素地址)
jint *firstElement = env->GetIntArrayElements(array, &isCopy);
LOGE(“is copy array:%d”, isCopy);
// 3.遍历数组(移动地址)
for (int i = 0; i < len; ++i) {
LOGE(“array[%i] = %i”, i, *(firstElement + i));
}
// 4.使用后释放数组
// 第一个参数是 jarray,第二个参数是 GetIntArrayElements 返回值
// 第三个参数代表 mode
env->ReleaseIntArrayElements(array,firstElement,0);

// 5. 创建一个 java 数组
jintArray newArray = env->NewIntArray(3);
}

  • mode = 0 刷新java数组 并 释放c/c++数组
  • mode = JNI_COMMIT (1) 只刷新java数组
  • mode = JNI_ABORT (2) 只释放c/c++数组

操作 jstring

extern “C”
JNIEXPORT void JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_setString(JNIEnv env, jobject thiz, jstring str) {
// 1.jstring -> char

// java 中的字符创是 unicode 编码, c/C++ 是UTF编码,所以需要转换一下。第二个参数作用同上面
const char *c_str = env -> GetStringUTFChars(str,NULL);

// 2.异常处理
if(c_str == NULL){
return;
}

// 3.当做一个 char 数组打印
jint len = env->GetStringLength(str);
for (int i = 0; i < len; ++i) {
LOGE(“c_str: %c”,*(c_str+i));
}

// 4.释放
env->ReleaseStringUTFChars(str,c_str);
}

调用完 GetStringUTFChars 之后不要忘记安全检查,因为 JVM 需要为新诞生的字符串分配内存空间,当内存空间不够分配的时候,会导致调用失败,失败后 GetStringUTFChars 会返回 NULL,并抛出一个OutOfMemoryError 异常。JNI 的异常和 Java 中的异常处理流程是不一样的,Java 遇到异常如果没有捕获,程序会立即停止运行。而 JNI 遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要用 return 语句跳过后面的代码,并立即结束当前方法。

操作 jobject
  • c/c++ 操作 java 中的对象使用的是 java 中反射,步骤分为:
  • 获取 class 类
  • 根据成员变量名获取 methodID / fieldID
  • 调用 get/set 方法操作 field,或者 CallObjectMethod 调用 method
操作 Field
  • 非静态成员变量使用: GetXXXField,比如 GetIntField,对于引用类型,比如 String,使用 GetObjectField
  • 对于静态成员变量使用: GetStaticXXXField,比如 GetStaticIntField

在 java 代码中,MainActivity 有两个成员变量:

public class MainActivity extends AppCompatActivity {

String testField = “test1”;

static int staticField = 1;
}

// 1. 获取类 class
jclass clazz = env->GetObjectClass(thiz);

// 2. 获取成员变量 id
jfieldID strFieldId = env->GetFieldID(clazz,“testField”,“Ljava/lang/String;”);
// 3. 根据 id 获取值
jstring jstr = static_cast(env->GetObjectField(thiz, strFieldId));
const char* cStr = env->GetStringUTFChars(jstr,NULL);
LOGE(“获取 MainActivity 的 String field :%s”,cStr);

// 4. 修改 String
jstring newValue = env->NewStringUTF(“新的字符创”);
env-> SetObjectField(thiz,strFieldId,newValue);

// 5. 释放资源
env->ReleaseStringUTFChars(jstr,cStr);
env->DeleteLocalRef(newValue);
env->DeleteLocalRef(clazz);

// 获取静态变量
jfieldID staticIntFieldId = env->GetStaticFieldID(clazz,“staticField”,“I”);
jint staticJavaInt = env->GetStaticIntField(clazz,staticIntFieldId);

GetFieldID 和 GetStaticFieldID 需要三个参数:

  • jclass
  • filed name
  • 类型签名: JNI 使用 jvm 的类型签名
类型签名一览表
TypeSignature Java Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
Vvoid
L fully-qualified-class;fully-qualified-class
[typetype[]
(arg-types) ret-typemethod type
  • 基本数据类型的比较好理解,不如要获取一个 int ,GetFieldID 需要传入签名就是 I;

  • 如果是一个类,比如 String,签名就是 L+全类名; :Ljava.lang.String;

  • 如果是一个 int array,就要写作 [I

  • 如果要获取一个方法,那么方法的签名是:(参数签名)返回值签名,参数如果是多个,中间不需要加间隔符,比如: | java 方法|JNI 签名| |–|–| |void f (int n); |(I)V| |void f (String s,int n); |(Ljava/lang/String;I)V| |long f (int n, String s, int[] arr); |(ILjava/lang/String;[I)J|

操作 method

操作 method 和 filed 非常相似,先获取 MethodID,然后对应的 CallXXXMethod 方法

Java层返回值方法族本地返回类型NativeType
voidCallVoidMethod()(无)
引用类型CallObjectMethod( )jobect
booleanCallBooleanMethod ( )jboolean
byteCallByteMethod( )jbyte
charCallCharMethod( )jchar
shortCallShortMethod( )jshort
intCallIntMethod( )jint
longCallLongMethod()jlong
floatCallFloatMethod()jfloat
doubleCallDoubleMethod()jdouble

在 java 中我们要想获取 MainActivity 的 className 会这样写:

this.getClass().getName()

可以看到需要先调用 getClass 方法获取 Class 对象,然后调用 Class 对象的 getName 方法,我们来看一下如何在 native 方法中调用:

extern “C” JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = “Hello from C++”;
// 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
jclass thisclazz = env->GetObjectClass(thiz);
// 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
jmethodID mid_getClass = env->GetMethodID(thisclazz, “getClass”, “()Ljava/lang/Class;”);
// 3. 执行 getClass 方法,获得 Class 对象
jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
// 4. 获取 Class 实例
jclass clazz = env->GetObjectClass(clazz_instance);
// 5. 根据 class 的 methodID
jmethodID mid_getName = env->GetMethodID(clazz, “getName”, “()Ljava/lang/String;”);
// 6. 调用 getName 方法
jstring name = static_cast(env->CallObjectMethod(clazz_instance, mid_getName));
LOGE(“class name:%s”, env->GetStringUTFChars(name, 0));

// 7. 释放资源
env->DeleteLocalRef(thisclazz);
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(clazz_instance);
env->DeleteLocalRef(name);

return env->NewStringUTF(hello.c_str());
}

创建对象

首先定义一个 java 类:

public class Person {
private int age;
private String name;

public Person(int age, String name){
this.age = age;
this.name = name;
}

public void print(){
Log.e(“Person”,name + age + “岁了”);
}
}

然后我们再 JNI 中创建一个 Person 并调用它的 print 方法:

// 1. 获取 Class
jclass pClazz = env->FindClass(“com/wangzhen/jnitutorial/Person”);
// 2. 获取构造方法,方法名固定为
jmethodID constructID = env->GetMethodID(pClazz,“”,“(ILjava/lang/String;)V”);
if(constructID == NULL){
return;
}
// 3. 创建一个 Person 对象
jstring name = env->NewStringUTF(“alex”);
jobject person = env->NewObject(pClazz,constructID,1,name);

jmethodID printId = env->GetMethodID(pClazz,“print”,“()V”);
if(printId == NULL){
return;
}
env->CallVoidMethod(person,printId);

// 4. 释放资源
env->DeleteLocalRef(name);
env->DeleteLocalRef(pClazz);
env->DeleteLocalRef(person);

JNI 引用

JNI 分为三种引用:

  • 局部引用(Local Reference),类似 java 中的局部变量
  • 全局引用(Global Reference),类似 java 中的全局变量
  • 弱全局引用(Weak Global Reference),类似 java 中的弱引用

上面的代码片段中最后都会有释放资源的代码,这是 c/c++ 编程的良好习惯,对于不同 JNI 引用有不同的释放方式。

局部引用

创建

JNI 函数返回的所有 Java 对象都是局部引用,比如上面调用的 NewObject/FindClass/NewStringUTF 等等都是局部引用。

释放
  • 自动释放 局部引用在方法调用期间有效,并在方法返回后被 JVM 自动释放。
  • 手动释放
手动释放的场景

有了自动释放之后为什么还需要手动释放呢?主要考虑一下场景:

  • 本机方法访问大型Java对象,从而创建对Java对象的局部引用。然后,本机方法在返回到调用方之前执行附加计算。对大型Java对象的本地引用将防止对该对象进行垃圾收集,即使该对象不再用于计算的其余部分。
  • 本机方法创建大量本地引用,但并非所有本地引用都同时使用。因为 JVM 需要一定的空间来跟踪本地引用,所以创建了太多的本地引用,这可能导致系统内存不足。例如,本机方法循环遍历一个大型对象数组,检索作为本地引用的元素,并在每次迭代时对一个元素进行操作。每次迭代之后,程序员不再需要对数组元素的本地引用。

所以我们应该养成手动释放本地引用的好习惯。

手动释放的方式
  • GetXXX 就必须调用 ReleaseXXX。

在调用 GetStringUTFChars 函数从 JVM 内部获取一个字符串之后,JVM 内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用ReleaseStringUTFChars 函数通知 JVM 这块内存已经不使用了。

  • 对于手动创建的 jclass,jobject 等对象使用 DeleteLocalRef 方法进行释放

全局引用

创建

JNI 允许程序员从局部引用创建全局引用:

static jstring globalStr;
if(globalStr == NULL){
jstring str = env->NewStringUTF(“C++”);
// 从局部变量 str 创建一个全局变量
globalStr = static_cast(env->NewGlobalRef(str));

//局部可以释放,因为有了一个全局引用使用str,局部str也不会使用了
env->DeleteLocalRef(str);
}

释放

全局引用在显式释放之前保持有效,可以通过 DeleteGlobalRef 来手动删除全局引用调用。

弱全局引用

与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象

所以在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象

创建

static jclass globalClazz = NULL;
//对于弱引用 如果引用的对象被回收返回 true,否则为false
//对于局部和全局引用则判断是否引用java的null对象
jboolean isEqual = env->IsSameObject(globalClazz, NULL);
if (globalClazz == NULL || isEqual) {
jclass clazz = env->GetObjectClass(instance);
globalClazz = static_cast(env->NewWeakGlobalRef(clazz));
env->DeleteLocalRef(clazz);
}

释放

删除使用 DeleteWeakGlobalRef

线程相关

局部变量只能在当前线程使用,而全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。

加载动态库

在 android 中有两种方式加载动态库:

  • System.load(String filename) // 绝对路径
  • system library path // 从 system lib 路径下加载

比如下面代码会报错,在 java.library.path 下找不到 hello

static{
System.loadLibrary(“Hello”);
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以使用下面代码打印出 java.library.path ,并且吧 hello 拷贝到改路径下:

public static void main(String[] args){
System.out.println(System.getProperty(“java.library.path”));
}

JNI_OnLoad

调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数则调用。 JNI_OnLoad 必须返回 JNI 的版本,比如 JNI_VERSION_1_6、JNI_VERSION_1_8。

动态注册

JNI 匹配对应的 java 方法有两种方式:

  • 静态注册: 之前我们使用的 Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI 来进行与java方法的匹配就是静态注册
  • 动态注册:就是将 java 中的方法在代码中动态的与 JNI 方法对应起来

静态注册的名字需要包名,太长了,可以使用动态注册来缩短方法名。

比如我们再 Java 中有两个 native 方法:

public class MainActivity extends AppCompatActivity {
public native void dynamicJavaFunc1();

public native int dynamicJavaFunc2(int i);
}

在 native 代码中,我们不使用静态注册,而使用动态注册

void dynamicNativeFunc1(){
LOGE(“调用了 dynamicJavaFunc1”);
}
// 如果方法带有参数,前面要加上 JNIEnv *env, jobject thisz
jint dynamicNativeFunc2(JNIEnv *env, jobject thisz,jint i){
LOGE(“调用了 dynamicTest2,参数是:%d”,i);
return 66;
}

// 需要动态注册的方法数组
static const JNINativeMethod methods[] = {
{“dynamicJavaFunc1”,“()V”,(void*)dynamicNativeFunc1},
{“dynamicJavaFunc2”,“(I)I”,(int*)dynamicNativeFunc2},
};
// 需要动态注册native方法的类名
static const char *mClassName = “com/wangzhen/jnitutorial/MainActivity”;

jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
// 1. 获取 JNIEnv,这个地方要注意第一个参数是个二级指针
int result = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
// 2. 是否获取成功
if(result != JNI_OK){
LOGE(“获取 env 失败”);
return JNI_VERSION_1_6;
}
// 3. 注册方法
jclass classMainActivity = env->FindClass(mClassName);
// sizeof(methods)/ sizeof(JNINativeMethod)
result = env->RegisterNatives(classMainActivity,methods, 2);

if(result != JNI_OK){
LOGE(“注册方法失败”)
return JNI_VERSION_1_2;
}

return JNI_VERSION_1_6;
}

这样我们再 MainActivity 中调用 dynamicJavaFunc1 方法就会调用 native 中的 dynamicNativeFunc1 方法。

native 线程中调用 JNIEnv*

前面介绍过 JNIEnv* 是和线程相关的,那么如果在 c++ 中新建一个线程A,在线程A 中可以直接使用 JNIEnv* 吗? 答案是否定的,如果想在 native 线程中使用 JNIEnv* 需要使用 JVM 的 AttachCurrentThread 方法进行绑定:

JavaVM *_vm;

jint JNI_OnLoad(JavaVM* vm, void* reserved){
_vm = vm;
return JNI_VERSION_1_6;
}

void* threadTask(void* args){
JNIEnv *env;
jint result = _vm->AttachCurrentThread(&env,0);
if (result != JNI_OK){
return 0;
}

// …

// 线程 task 执行完后不要忘记分离
_vm->DetachCurrentThread();
}

extern “C”
JNIEXPORT void JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_nativeThreadTest(JNIEnv *env, jobject thiz) {
pthread_t pid;
pthread_create(&pid,0,threadTask,0);
}

交叉编译

在一个平台上编译出另一个平台上可以执行的二级制文件的过程叫做交叉编译。比如在 MacOS 上编译出 android 上可用的库文件。 如果想要编译出可以在 android 平台上运行的库文件就需要使用 ndk。

两种库文件

linux 平台上的库文件分为两种:

  • 静态库: 编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,linux中后缀名为”.a”。
  • 动态库: 在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。linux 中后缀名为”.so”,gcc在编译时默认使用动态库。

Android 原生开发套件 (NDK):这套工具使您能在 Android 应用中使用 C 和 C++ 代码。 CMake:一款外部编译工具,可与 Gradle 搭配使用来编译原生库。如果您只计划使用 ndk-build,则不需要此组件。 LLDB:Android Studio 用于调试原生代码的调试程序。

NDK

原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库。 我们可以在 sdk/ndk-bundle 中查看 ndk 的目录结构,下面列举出三个重要的成员:

  • ndk-build: 该 Shell 脚本是 Android NDK 构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了。
  • platforms: 该目录包含支持不同 Android 目标版本的头文件和库文件, NDK 构建系统会根据具体的配置来引用指定平台下的头文件和库文件。
  • toolchains: 该目录包含目前 NDK 所支持的不同平台下的交叉编译器 - ARM 、X86、MIPS ,目前比较常用的是 ARM。 // todo ndk-depends.cmd

ndk 为什么要提供多平台呢? 不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。更具体的内容参考官方文档

使用 ndk 手动编译动态库

在 ndk 目录下的 toolchains 下有多个平台的编译工具,比如在 /arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin 下可以找到 arm-linux-androideabi-gcc 执行文件,利用 ndk 的这个 gcc 可以编译出在 android(arm 架构) 上运行的动态库:

arm-linux-androideabi-gcc -fPIC -shared test.c -o libtest.so

参数含义 -fPIC: 产生与位置无关代码 -shared:编译动态库,如果去掉代表静态库 test.c:需要编译的 c 文件 -o:输出 libtest.so:库文件名

独立工具链 版本比较新的 ndk 下已经找不到 gcc 了,如果想用的话需要参考独立工具链。 比如执行 $NDK/build/tools/make_standalone_toolchain.py --arch arm --api 21 --install-dir/$yourDir 可以产生 arm 的独立工具链

$NDK 代表 ndk 的绝对路径, $yourDir 代表输出文件路径

当源文件很多的时候,手动编译既麻烦又容易出错,此时出现了 makefile 编译。

makefile

makefile 就是“自动化编译”:一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,如何进行链接等等操作。 Android 使用 Android.mk 文件来配置 makefile,下面是一个最简单的 Android.mk:

源文件在的位置。宏函数 my-dir 返回当前目录(包含 Android.mk 文件本身的目录)的路径。

LOCAL_PATH := $(call my-dir)
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后如何才能让我们在面试中对答如流呢?

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴?为此我整理了一份Android学习资料路线:

这里是一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套BAT大厂面试资料专题包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家。

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

最后,祝愿即将跳槽和已经开始求职的大家都能找到一份好的工作!

这些只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢再关注一下

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后如何才能让我们在面试中对答如流呢?

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴?为此我整理了一份Android学习资料路线:

[外链图片转存中…(img-9fhuaDNL-1713708624301)]

这里是一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套BAT大厂面试资料专题包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家。

[外链图片转存中…(img-EVLSJd3a-1713708624302)]

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

最后,祝愿即将跳槽和已经开始求职的大家都能找到一份好的工作!

这些只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢再关注一下

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值