我们知道Android系统内核是使用Linux实现的,那么在Android OS中JNI实现的本地代码中实际使用的是Linux线程,这就需要pthread支持。
一、涉及pthread方法
Linux下用C开发多线程程序,Linux系统下的多线程遵循POSIX线程接口,称为pthread。
头文件
#include<pthread.h>
1.1 pthread_create
pthread_create是UNIX环境创建线程函数
函数声明
int pthread_create(pthread_t*restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于制定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。
由 restrict 修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于第一个时,才能对对象进行存取。对对象的存取都限定于基于由 restrict 修饰的指针表达式中。 由 restrict 修饰的指针主要用于函数形参,或指向由 malloc() 分配的内存空间。restrict 数据类型不改变程序的语义。编译器能通过作出 restrict 修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。
返回值
若成功则返回0,否则返回出错编号。
参数
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
1.2 pthread_join
函数pthread_join用来等待一个线程的结束,线程间同步的操作。
函数声明
int pthread_join(pthread_t thread, void **retval);
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。
返回值
若成功则返回0,否则返回出错编号。
参数
thread: 线程标识符,即线程ID,标识唯一线程。
retval: 用户定义的指针,用来存储被等待线程的返回值。
1.3 pthread_self
pthread_self是一种函数,功能是获得线程自身的ID。
函数声明
pthread_t pthread_self(void);
获得线程自身的ID。pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则显示结果出问题。
返回值
pthread_t的类型为unsigned long int
1.4 pthread_exit
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。
函数声明
void pthread_exit(void* retval);
这个函数的作用是终止调用它的线程,并返回一个指向某个对象的指针。
二、涉及JNI方法
2.1 AttachCurrentThread
jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
将当前线程attach到Java VM。 返回JNIEnv参数中的JNI接口指针。
试图attach已经attach的线程是一个无操作。
本地线程无法同时连接到两个Java VM。
当线程attach到VM时,上下文类加载器是引导加载程序。
LINKAGE:
JavaVM接口函数表中的索引4。
PARAMETERS:
vm:当前线程将attach到的VM。
p_env:指向当前线程的JNI接口指针所在位置的指针。
thr_args:可以是NULL或指向JavaVMAttachArgs结构的指针,以指定其他信息:
从JDK/JRE 1.1开始,AttachCurrentThread的第二个参数总是一个指向JNIEnv的指针。AttachCurrentThread的第三个参数是保留的,应该设置为NULL。
从JDK/JRE 1.2开始,将NULL作为第三个参数传递和1.1保持一致,或者传递一个指向以下结构的指针来指定其他信息:
typedef struct JavaVMAttachArgs {
jint version; /* 必须至少为JNI_VERSION_1_2 */
char *name; /* 线程的名称使用修改后的UTF-8字符串,或NULL */
jobject group; /* ThreadGroup对象的全局引用,或NULL */
} JavaVMAttachArgs
RETURNS:
成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。
2.2 DetachCurrentThread
jint DetachCurrentThread(JavaVM *vm);
从Java VM分离当前线程。该线程持有的所有Java监视器都将被释放。等待此线程死亡的所有Java线程都会收到通知。
从JDK/JRE 1.2开始,主线程可以从VM分离。
LINKAGE:
JavaVM接口函数表中的索引5。
PARAMETERS:
vm: 当前线程将从其中分离的vm。
RETURNS:
成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。
三、使用本地线程
JNI接口指针(JNIEnv)仅在当前线程中有效。 如果另一个线程需要访问Java VM,它必须首先调用AttachCurrentThread()以将自身连接到VM并获取JNI接口指针。一旦连接到VM,本地线程就像在本地方法中运行的普通Java线程一样工作。本地线程保持连接到VM,直到它调用DetachCurrentThread()来分离自身。
连接的线程应该有足够的堆栈空间来执行合理的工作量。 每个线程的堆栈空间分配是特定于操作系统的。例如,使用pthreads,可以在pthread_create的pthread_attr_t参数中指定堆栈大小。
连接到VM的本地线程必须在退出之前调用DetachCurrentThread()以分离自身。如果调用堆栈上有Java方法,则线程无法分离。
以下代码片段为Java代码,运行于Android OS:
package ndk.example.com.ndkexample;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText("启动10条Native线程...");
JniCall jniCall = new JniCall();
mainThread(jniCall, 10);
}
public native void mainThread(JniCall jniCall, int num);
}
package ndk.example.com.ndkexample;
import android.util.Log;
final public class JniCall {
public void fromNativeCall(int i, int j) {
Log.d("Native", "fromNativeCall i=" + i + ",j=" + j);
}
}
本地代码如下:
#include <jni.h>
#include <stdlib.h>
#include <pthread.h>
#include <android/log.h>
#define TAG "Native"
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__))
JavaVM *g_jvm = NULL;
jobject g_obj = NULL;
struct two_parameter {
int i;
int j;
};
void *thread_fun(void *arg) {
JNIEnv *localEnv;
jclass cls;
jmethodID mid;
if (g_jvm->AttachCurrentThread(&localEnv, NULL) != JNI_OK) {
LOGE("%s: AttachCurrentThread() failed", __FUNCTION__);
return NULL;
}
cls = localEnv->GetObjectClass(g_obj);
if (cls == NULL) {
LOGE("GetObjectClass() Error.....");
goto error;
}
mid = localEnv->GetMethodID(cls, "fromNativeCall", "(II)V");
if (mid == NULL) {
LOGE("GetMethodID() Error.....");
goto error;
}
two_parameter *two;
two = (two_parameter *) arg;
LOGI("tid=%lu i=%d j=%d", (unsigned long int) pthread_self(), two->i, two->j);
localEnv->CallVoidMethod(g_obj, mid, two->i, two->j);
error:
if (g_jvm->DetachCurrentThread() != JNI_OK) {
LOGE("%s: DetachCurrentThread() failed", __FUNCTION__);
}
pthread_exit(0);
}
extern "C" JNIEXPORT void
JNICALL
Java_ndk_example_com_ndkexample_MainActivity_mainThread(JNIEnv *env, jobject /* this */, jobject jniCallObj, jint threadNum) {
env->GetJavaVM(&g_jvm);
g_obj = env->NewGlobalRef(jniCallObj);
int i;
pthread_t *pt;
pt = (pthread_t *) malloc(threadNum * sizeof(pthread_t));
for (i = 0; i < threadNum; i++) {
two_parameter *two = (two_parameter *) malloc(sizeof(two_parameter));
two->i = i;
two->j = (i + 1);
LOGI("main thread two_parameter i=%d j=%d", two->i, two->j);
pthread_create(&pt[i], NULL, &thread_fun, two);
}
for (i = 0; i < threadNum; i++) {
pthread_join(pt[i], NULL);
}
env->DeleteGlobalRef(g_obj);
LOGI("main thread exit.....");
}
我们在本地方法mainThread中创建了10条本地线程,每条本地线程都执行本地方法thread_fun,并在其中调用了AttachCurrentThread获取JNIEnv,拿到JNI环境上下文后,就可以获得Java类的jclass和jmethodID,从而可以调用Java中JniCall对象的方法fromNativeCall(int i, int j)了,最后调用了DetachCurrentThread解除了在VM中的Attach状态,将线程从VM中分离。
当然在创建线程时给执行函数传入了多个参数,所以最后传入了一个自定义的结构体指针。并且在所有线程创建以后,执行pthread_join进行等待。还将mainThread中传入的jniCallObj提升为全局引用,并使用env->GetJavaVM()拿到JavaVM的全局引用。
最后来看运行结果:
03-03 19:59:19.230 8384-8384/ndk.example.com.ndkexample I/Native: main thread two_parameter i=0 j=1
main thread two_parameter i=1 j=2
main thread two_parameter i=2 j=3
main thread two_parameter i=3 j=4
main thread two_parameter i=4 j=5
main thread two_parameter i=5 j=6
main thread two_parameter i=6 j=7
main thread two_parameter i=7 j=8
main thread two_parameter i=8 j=9
main thread two_parameter i=9 j=10
03-03 19:59:19.230 8384-8403/ndk.example.com.ndkexample I/Native: tid=1073842064 i=6 j=7
03-03 19:59:19.230 8384-8404/ndk.example.com.ndkexample I/Native: tid=1074648600 i=7 j=8
03-03 19:59:19.230 8384-8402/ndk.example.com.ndkexample I/Native: tid=1073841480 i=5 j=6
03-03 19:59:19.230 8384-8401/ndk.example.com.ndkexample I/Native: tid=1073840896 i=4 j=5
03-03 19:59:19.240 8384-8405/ndk.example.com.ndkexample I/Native: tid=1074649184 i=8 j=9
03-03 19:59:19.240 8384-8402/ndk.example.com.ndkexample D/Native: fromNativeCall i=5,j=6
03-03 19:59:19.240 8384-8401/ndk.example.com.ndkexample D/Native: fromNativeCall i=4,j=5
03-03 19:59:19.240 8384-8404/ndk.example.com.ndkexample D/Native: fromNativeCall i=7,j=8
03-03 19:59:19.240 8384-8403/ndk.example.com.ndkexample D/Native: fromNativeCall i=6,j=7
03-03 19:59:19.240 8384-8400/ndk.example.com.ndkexample I/Native: tid=1073840312 i=3 j=4
03-03 19:59:19.240 8384-8405/ndk.example.com.ndkexample D/Native: fromNativeCall i=8,j=9
03-03 19:59:19.240 8384-8400/ndk.example.com.ndkexample D/Native: fromNativeCall i=3,j=4
03-03 19:59:19.240 8384-8399/ndk.example.com.ndkexample I/Native: tid=1073839728 i=2 j=3
03-03 19:59:19.240 8384-8399/ndk.example.com.ndkexample D/Native: fromNativeCall i=2,j=3
03-03 19:59:19.240 8384-8398/ndk.example.com.ndkexample I/Native: tid=1073839144 i=1 j=2
03-03 19:59:19.240 8384-8398/ndk.example.com.ndkexample D/Native: fromNativeCall i=1,j=2
03-03 19:59:19.240 8384-8406/ndk.example.com.ndkexample I/Native: tid=1074649768 i=9 j=10
03-03 19:59:19.240 8384-8406/ndk.example.com.ndkexample D/Native: fromNativeCall i=9,j=10
03-03 19:59:19.240 8384-8397/ndk.example.com.ndkexample I/Native: tid=1073838560 i=0 j=1
03-03 19:59:19.240 8384-8397/ndk.example.com.ndkexample D/Native: fromNativeCall i=0,j=1
03-03 19:59:19.240 8384-8384/ndk.example.com.ndkexample I/Native: main thread exit.....