jnijni c调用java方法的方法能不能用sleep

13193人阅读
Android 杂谈(32)
转载请注明出处:
开发一个需要常住后台的App其实是一件非常头疼的事情,不仅要应对国内各大厂商的ROM,还需要应对各类的安全管家...虽然不断的研究各式各样的方法,但是效果并不好,比如任务管理器把App干掉,服务就起不来了...
网上搜寻一番后,主要的方法有以下几种方法,但都是治标不治本:
1、提高Service的优先级:这个,也只能说在系统内存不足需要回收资源的时候,优先级较高,不容易被回收,然并卵...
2、提高Service所在进程的优先级:效果不是很明显
3、在onDestroy方法里重启service:这个倒还算挺有效的一个方法,但是,直接干掉进程的时候,onDestroy方法都进不来,更别想重启了
4、broadcast广播:和第3种一样,没进入onDestroy,就不知道什么时候发广播了,另外,在Android4.4以上,程序完全退出后,就不好接收广播了,需要在发广播的地方特定处理
5、放到System/app底下作为系统应用:这个也就是平时玩玩,没多大的实际意义。
6、Service的onStartCommand方法,返回START_STICKY,这个也主要是针对系统资源不足而导致的服务被关闭,还是有一定的道理的。
应对的方法是有,实现起来都比较繁琐。如果你自己可以定制ROM,那就有很多种办法了,比如把你的应用加入白名单,或是多安装一个没有图标的app作为守护进程...但是,哪能什么都是定制的,对于安卓开发者来说,这个难题必须攻破~
那么,有没有办法在一个APP里面,开启一个子线程,在主线程被干掉了之后,子线程通过监听、轮询等方式去判断服务是否存在,不存在的话则开启服务。答案自然是肯定的,通过JNI的方式(NDK编程),fork()出一个子线程作为守护进程,轮询监听服务状态。守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。而守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没有改变。
那么我们先来看看Android4.4的源码,ActivityManagerService(源码/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java)是如何关闭在应用退出后清理内存的:
Process.killProcessQuiet(pid);应用退出后,ActivityManagerService就把主进程给杀死了,但是,在Android5.0中,ActivityManagerService却是这样处理的:
Process.killProcessQuiet(app.pid);
Process..uid, app.pid);就差了一句话,却差别很大。Android5.0在应用退出后,ActivityManagerService不仅把主进程给杀死,另外把主进程所属的进程组一并杀死,这样一来,由于子进程和主进程在同一进程组,子进程在做的事情,也就停止了...要不怎么说Android5.0在安全方面做了很多更新呢...
那么,有没有办法让子进程脱离出来,不要受到主进程的影响,当然也是可以的。那么,在C/C++层是如何实现的呢?先上关键代码:
* sd 之前创建子进程的pid写入的文件路径
int start(int argc, char* srvname, char* sd) {
int pid = fork();
LOGI(&fork pid: %d&, pid);
if (pid & 0) {
LOGI(&first fork() error pid %d,so exit&, pid);
} else if (pid != 0) {
LOGI(&first fork(): I'am father pid=%d&, getpid());
//exit(0);
} else { //
第一个子进程
LOGI(&first fork(): I'am child pid=%d&, getpid());
LOGI(&first fork(): setsid=%d&, setsid());
umask(0); //为文件赋予更多的权限,因为继承来的文件可能某些权限被屏蔽
int pid = fork();
if (pid == 0) { // 第二个子进程
sprintf(sd,&%s/pid&,sd);
if((fp=fopen(sd,&a&))==NULL) {//打开文件 没有就创建
LOGI(&%s文件还未创建!&,sd);
ftruncate(fp, 0);
lseek(fp, 0, SEEK_SET);
fclose(fp);
fp=fopen(sd,&rw&);
char buff1[6];
int p = 0;
memset(buff1,0,sizeof(buff1));
fseek(fp,0,SEEK_SET);
fgets(buff1,6,fp);
//读取一行(pid)
LOGI(&读取的进程号:%s&,buff1);
if(strlen(buff1)&1){ // 有值
kill(atoi(buff1), SIGTERM); // 把上一次的进程干掉,防止重复执行
LOGI(&杀死进程,pid=%d&,atoi(buff1));
fclose(fp);
fp=fopen(sd,&w&);
char buff[100];
int k = 3;
sprintf(buff,&%lu&,getpid());
fprintf(fp,&%s\n&,buff); // 把进程号写入文件
fclose(fp);
fflush(fp);
LOGI(&I'am child-child pid=%d&, getpid());
chdir(&/&); //&span style=&font-family: Arial, Helvetica, sans-&&修改进程工作目录为根目录,chdir(“/”)&/span&
//关闭不需要的从父进程继承过来的文件描述符。
if (r.rlim_max == RLIM_INFINITY) {
r.rlim_max = 1024;
for (i = 0; i & r.rlim_ i++) {
ret = pthread_create(&id, NULL, (void *) thread, srvname); // 开启线程,轮询去监听启动服务
if (ret != 0) {
printf(&Create pthread error!\n&);
int stdfd = open (&/dev/null&, O_RDWR);
dup2(stdfd, STDOUT_FILENO);
dup2(stdfd, STDERR_FILENO);
* 启动Service
void Java_com_yyh_fork_NativeRuntime_startService(JNIEnv* env, jobject thiz,
jstring cchrptr_ProcessName, jstring sdpath) {
char * rtn = jstringTostring(env, cchrptr_ProcessName); // 得到进程名称
char * sd = jstringTostring(env, sdpath);
LOGI(&Java_com_yyh_fork_NativeRuntime_startService run....ProcessName:%s&, rtn);
start(1, rtn, sd);
这里有几个重点需要理解一下:
1、为什么要fork两次?第一次fork的作用是为后面setsid服务。setsid的调用者不能是进程组组长(group leader),而第一次调用的时候父进程是进程组组长。第二次调用后,把前面一次fork出来的子进程退出,这样第二次fork出来的子进程,就和他们脱离了关系。
2、setsid()作用是什么?setsid() 使得第二个子进程是会话组长(sid==pid),也是进程组组长(pgid == pid),并且脱离了原来控制终端。故不管控制终端怎么操作,新的进程正常情况下不会收到他发出来的这些信号。
3、umask(0)的作用:由于子进程从父进程继承下来的一些东西,可能并未把权限继承下来,所以要赋予他更高的权限,便于子进程操作。
4、chdir (&/&);作用:进程活动时,其工作目录所在的文件系统不能卸下,一般需要将工作目录改变到根目录。&
5、进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。所以在最后,记得关闭掉从父进程继承过来的文件描述符。
然后,在上面的代码中开启线程后做的事,就是循环去startService(),代码如下:
void thread(char* srvname) {
check_and_restart_service(srvname);
* 检测服务,如果不存在服务则启动.
* 通过am命令启动一个laucher服务,由laucher服务负责进行主服务的检测,laucher服务在检测后自动退出
void check_and_restart_service(char* service) {
LOGI(&当前所在的进程pid=&,getpid());
char cmdline[200];
sprintf(cmdline, &am startservice --user 0 -n %s&, service);
char tmp[200];
sprintf(tmp, &cmd=%s&, cmdline);
ExecuteCommandWithPopen(cmdline, tmp, 200);
LOGI( tmp, LOG);
* 执行命令
void ExecuteCommandWithPopen(char* command, char* out_result,
int resultBufferSize) {
out_result[resultBufferSize - 1] = '\0';
fp = popen(command, &r&);
fgets(out_result, resultBufferSize - 1, fp);
out_result[resultBufferSize - 1] = '\0';
pclose(fp);
LOGI(&popen null,so exit&);
这两个启动服务的函数,里面就涉及到一些Android和linux的命令了,这里我就不细说了。特别是am,挺强大的功能的,不仅可以开启服务,也可以开启广播等等...然后调用ndk-build命令进行编译,生成so库,ndk不会的,自行百度咯~
C/C++端关键的部分主要是以上这些,自然而然,Java端还得配合执行。
首先来看一下C/C++代码编译完的so库的加载类,以及native的调用:
package com.yyh.
import java.io.DataInputS
import java.io.DataOutputS
import java.io.F
public class NativeRuntime {
private static NativeRuntime theInstance =
private NativeRuntime() {
public static NativeRuntime getInstance() {
if (theInstance == null)
theInstance = new NativeRuntime();
return theI
* RunExecutable 启动一个可自行的lib*.so文件
下午8:22:28
* @param pacaageName
* @param filename
* @param alias 别名
* @param args 参数
public String RunExecutable(String pacaageName, String filename, String alias, String args) {
String path = &/data/data/& + pacaageN
String cmd1 = path + &/lib/& +
String cmd2 = path + &/& +
String cmd2_a1 = path + &/& + alias + & & +
String cmd3 = &chmod 777 & + cmd2;
String cmd4 = &dd if=& + cmd1 + & of=& + cmd2;
StringBuffer sb_result = new StringBuffer();
if (!new File(&/data/data/& + alias).exists()) {
RunLocalUserCommand(pacaageName, cmd4, sb_result); // 拷贝lib/libtest.so到上一层目录,同时命名为test.
sb_result.append(&;&);
RunLocalUserCommand(pacaageName, cmd3, sb_result); // 改变test的属性,让其变为可执行
sb_result.append(&;&);
RunLocalUserCommand(pacaageName, cmd2_a1, sb_result); // 执行test程序.
sb_result.append(&;&);
return sb_result.toString();
* 执行本地用户命令
下午8:23:01
* @param pacaageName
* @param command
* @param sb_out_Result
public boolean RunLocalUserCommand(String pacaageName, String command, StringBuffer sb_out_Result) {
Process process =
process = Runtime.getRuntime().exec(&sh&); // 获得shell进程
DataInputStream inputStream = new DataInputStream(process.getInputStream());
DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());
outputStream.writeBytes(&cd /data/data/& + pacaageName + &\n&); // 保证在command在自己的数据目录里执行,才有权限写文件到当前目录
outputStream.writeBytes(command + & &\n&); // 让程序在后台运行,前台马上返回
outputStream.writeBytes(&exit\n&);
outputStream.flush();
process.waitFor();
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
String s = new String(buffer);
if (sb_out_Result != null)
sb_out_Result.append(&CMD Result:\n& + s);
} catch (Exception e) {
if (sb_out_Result != null)
sb_out_Result.append(&Exception:& + e.getMessage());
public native void startActivity(String compname);
public native String stringFromJNI();
public native void startService(String srvname, String sdpath);
public native int findProcess(String packname);
public native int stopService();
System.loadLibrary(&helper&); // 加载so库
} catch (Exception e) {
e.printStackTrace();
然后,我们在收到开机广播后,启动该服务。
package com.yyh.
import android.content.BroadcastR
import android.content.C
import android.content.I
import android.util.L
import com.yyh.fork.NativeR
import com.yyh.utils.FileU
public class PhoneStatReceiver extends BroadcastReceiver {
private String TAG = &tag&;
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.i(TAG, &手机开机了~~&);
NativeRuntime.getInstance().startService(context.getPackageName() + &/com.yyh.service.HostMonitor&, FileUtils.createRootPath());
} else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
Service服务里面,就可以做该做的事情。
package com.yyh.
import android.app.S
import android.content.I
import android.os.IB
import android.util.L
public class HostMonitor extends Service {
public void onCreate() {
super.onCreate();
Log.i(&daemon_java&, &HostMonitor: onCreate! I can not be Killed!&);
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(&daemon_java&, &HostMonitor: onStartCommand! I can not be Killed!&);
return super.onStartCommand(intent, flags, startId);
public IBinder onBind(Intent arg0) {
当然,也不要忘记在Manifest.xml文件配置receiver和service:
android:name=&com.yyh.activity.PhoneStatReceiver&
android:enabled=&true&
android:permission=&android.permission.RECEIVE_BOOT_COMPLETED& &
&intent-filter&
&action android:name=&android.intent.action.BOOT_COMPLETED& /&
&action android:name=&android.intent.action.USER_PRESENT& /&
&/intent-filter&
&/receiver&
android:name=&com.yyh.service.HostMonitor&
android:enabled=&true&
android:exported=&true&&
&/service&
run起来,在程序应用里面,结束掉这个进程,不一会了,又自动起来了~~~~完美~~~~跟流氓软件一个样,没错,就是这么贱,就是这么霸道!!
这边是运行在谷歌的原生系统上,Android版本为5.0...不知道会不会被国内的各大ROM干掉,好紧张好紧张~~~所以,总结一下就是:服务常驻要应对的不是各种难的技术,而是各大ROM。QQ为什么不会被杀死,是因为国内各大ROM不想让他死...
最后附上本例的源代码:
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:234593次
积分:3079
积分:3079
排名:第7859名
原创:55篇
评论:164条2756人阅读
Dalvik虚拟机(4)
在前面一文中,我们分析了Dalvik虚拟机的运行过程。从中可以知道,Dalvik虚拟机在调用一个成员函数的时候,如果发现该成员函数是一个JNI方法,那么就会直接跳到它的地址去执行。也就是说,JNI方法是直接在本地操作系统上执行的,而不是由Dalvik虚拟机解释器执行。由此也可看出,JNI方法是Android应用程序与本地操作系统直接进行通信的一个手段。在本文中,我们就详细分析JNI方法的注册过程。
老罗的新浪微博:,欢迎关注!
& & & & 在Android系统中,JNI方法是以C/C++语言来实现的,然后编译在一个SO文件里面。这个JNI方法在能够被调用之前,首先要加载到当前应用程序进程的地址空间来,如下所示:
& & & & 上述代码假设ClassWithJni类有一个JNI方法nanosleep,它实现在一个名称为libnanosleep.so的文件中,因此,在该JNI方法能够被调用之前,我们首先要将它加载到当前应用程序进程来,这是通过调用System类的静态成员函数loadLibrary来实现的。
& & & &&JNI方法nanosleep的实现如下所示:
& & & & 假设上述函数经过编译之后,就位于一个名称为libnanosleep.so的文件。
& & & & 当libnanosleep.so文件被加载的时候,函数JNI_OnLoad就会被调用。在函数JNI_OnLoad中,参数vm描述的是当前进程中的Dalvik虚拟机,通过调用它的成员函数GetEnv就可以获得一个JNIEnv对象。有了这个JNIEnv对象之后,我们就可以调用另外一个函数jniRegisterNativeMethods来向当前进程中的Dalvik虚拟机注册一个JNI方法shy_luo_jni_ClassWithJni_nanosleep。这个JNI方法即为shy.luo.jni.ClassWithJni类的成员函数nanasleep的实现。
& & & & JNI方法shy_luo_jni_ClassWithJni_nanosleep要做的事情实际上就是通过系统调用nanosleep来使得当前进程进入睡眠状态,直至seconds秒nanoseconds纳秒之后再唤醒。使用系统调用nanosleep来使得当前进程进入睡眠状态的好处它的时间精度可以达到纳秒级,但是这个系统调用有两个地方是需要注意的:
& & & & 1. 如果进程在睡眠的过程中接收到信号,那么它就会提前被唤醒,这时候系统调用nanosleep的返回值为-1,并且错误代码errno被设置为EINTR。
& & & & 2. 如果CPU的时钟中断精度达不到纳秒级别,那么nanosleep的睡眠精度也达不到纳秒级,也就是说,当前进程不一定能在指定的纳秒之后被唤醒,会有一定的延时。
& & & & 不过,JNI方法shy_luo_jni_ClassWithJni_nanosleep的实现不是我们的重点,我们的重点是分析它注册到Dalvik虚拟机的过程。
& & & & 前面提到,JNI方法shy_luo_jni_ClassWithJni_nanosleep是libnanosleep.so文件加载的时候被注册到Dalvik虚拟机的,因此,我们就从libnanosleep.so文件的加载开始,分析JNI方法shy_luo_jni_ClassWithJni_nanosleep注册到Dalvik虚拟机的过程,也就是从System类的静态成员函数loadLibrary开始分析一个JNI方法注册到Dalvik虚拟机的过程,如图1所示:
图1 JNI方法注册到Dalvik虚拟机的过程
& & & & 这个过程可以分为12个步骤,接下来我们就详细分析每一个步骤。
& & & & Step 1. System.loadLibrary
& & & & 这个函数定义在文件libcore/luni/src/main/java/java/lang/System.java中。
& & & &&System类的成员函数loadLibrary首先调用SecurityManager类的成员函数checkLink来进行安全检查,即检查名称为libName的so文件是否允许加载。注意,这是Java的安全代码检查机制,而不是Android系统的安全检查机制,而且Android系统没有使用它来进行安全检查。因此,这个检查总是能通过的。
& & & &&System类的成员函数loadLibrary接下来就再通过运行时类Runtime的成员函数loadLibrary来加载名称为libName的so文件,接下来我们就继续分析它的实现。
& & & & Step 2.&Runtime.loadLibrary
& & & & 这个函数定义在文件libcore/luni/src/main/java/java/lang/Runtime.java中。
& & & & 在Runtime类的成员函数loadLibrary中,参数libraryName表示要加载的so文件,而参数loader表示与要加载的so文件所关联的类的一个类加载器。例如,在我们这个情景中,libraryName等于“nanosleep”,与它所关联的类为shy.luo.jni.ClassWithJni。每一类有一个关联的类加载器,用来负责加载该类。在Dalvik虚拟机中,类加载器除了知道它要加载的类所在的文件路径之外,还知道该类所属的APK用来保存so文件的路径。因此,给定一个so文件名称,一个类加载器可以判断它是否存在自己的so文件目录中。
& & & & 参数libraryName只是描述要加载的so文件的部分名称,它的完整名称需要根据本地操作系统的特证来确定。由于目前Android系统都是属于Linux系统,而在Linux系统中,so文件的命名规范通常就是lib&name&.so的形式,因此,在我们这个情景中,名称为“nanosleep”的so文件的完整名称就为“libnanosleep.so”,这是通过调用System类的静态成员函数mapLibraryName来获得的。
& & & & 上面所获得的libnanosleep.so文件的名称仍然还不够完整,因为它没有包含绝对路径。在这种情况下,我们是无法将它加载到Dalvik虚拟机中去的。当参数loader的值不等于null的时候,Runtime类的成员函数loadLibrary就会调用它的成员函数findLibrary来它的so文件目录中寻找是否有一外名称为“libnanosleep.so”。如果存在的话,那么就会返回该libnanosleep.so文件的绝对路径。有了libnanosleep.so文件的绝对路径之后,就可以调用Runtime类的另外一个成员函数nativeLoad来将它加载到当前进程的Dalvik虚拟机中。注意,将参数libraryName转换为lib&name&.so的完整形式,以及获得该so文件的绝对路径,都是由参数loader所描述的一个类加载器的成员函数findLibrary来完成的。
& & & & 另一方面,如果参数loader的值等于null,那么就表示当前要加载的so文件要在系统范围的so文件目录查找。这些系统范围的so文件目录保存在Runtime类的成员变量mLibPaths所描述的一个String数组中。通过依次检查这些目录是否存在与参数libraryName对应的so文件,就可以确定参数libraryName所指定加载的so文件是否是一个合法的so文件。如果合法的话,那么同样会调用Runtime类的另外一个成员函数nativeLoad来将它加载到当前进程的Dalvik虚拟机中。注意,这里在检查参数libraryName所表示的so文件是否存在于系统范围的so文件目录之前,同样要将它转换为lib&name&.so的形式,这同样也是通过调用System类的静态成员函数mapLibraryName来完成的。
& & & & 如果最后无法在指定的APK或者系统范围的so文件目录中找到由参数libraryName所描述的so文件,或者找到了该so文件,但是在加载该so文件的过程中出现错误,那么Runtime类的成员函数loadLibrary都会抛出一个类型为UnsatisfiedLinkError的异常。
& & & & 由于加载参数libraryName所描述的so文件是由Runtime类的成员函数nativeLoad来实现的,因此,接下来我们继续分析它的实现。
& & & & Step 3.&Runtime.nativeLoad
& & & &&这个函数定义在文件libcore/luni/src/main/java/java/lang/Runtime.java中。
& & & & Runtime类的成员函数nativeLoad是一个JNI方法。由于该JNI方法是属于Java核心类Runtime的,也就是说,它在Dalvik虚拟机启动的时候就已经在内部注册过了,因此,这时候我们可以直接调用它注册其它的JNI方法,也就是so文件filename里面所指定的JNI方法。Dalvik虚拟机在启动过程中注册Java核心类的操作,具体可以参考前面一文。
& & & &&Runtime类的成员函数nativeLoad在C++层对应的函数为Dalvik_java_lang_Runtime_nativeLoad,如下所示:
& & & & 这个函数定义在文件dalvik/vm/native/java_lang_Runtime.c中。
& & & & 参数args[0]保存的是一个Java层的String对象,这个String对象描述的就是要加载的so文件,函数Dalvik_java_lang_Runtime_nativeLoad首先是调有函数dvmCreateCstrFromString来将它转换成一个C++层的字符串fileName,然后再调用函数dvmLoadNativeCode来执行加载so文件的操作。
& & & & 接下来,我们就继续分函数dvmLoadNativeCode的实现,以便可以了解一个so文件的加载过程。
& & & & Step 4.&dvmLoadNativeCode
& & & & 这个函数定义在文件dalvik/vm/Native.c中。
& & & & 函数dvmLoadNativeCode首先是检查参数pathName所指定的so文件是否已经加载过了,这是通过调用函数findSharedLibEntry来实现的。如果已经加载过,那么就可以获得一个SharedLib对象pEntry。这个SharedLib对象pEntry描述了有关参数pathName所指定的so文件的加载信息,例如,上次用来加载它的类加载器和上次的加载结果。如果上次用来加载它的类加载器不等于当前所使用的类加载器,或者上次没有加载成功,那么函数dvmLoadNativeCode就回直接返回false给调用者,表示不能在当前进程中加载参数pathName所描述的so文件。
& & & & 我们假设参数pathName所指定的so文件还没有被加载过,这时候函数dvmLoadNativeCode就会先调用dlopen来在当前进程中加载它,并且将获得的句柄保存在变量handle中,接着再创建一个SharedLib对象pNewEntry来描述它的加载信息。这个SharedLib对象pNewEntry还会通过函数addSharedLibEntry被缓存起来,以便可以知道当前进程都加载了哪些so文件。
& & & & &注意,在调用函数addSharedLibEntry来缓存新创建的SharedLib对象pNewEntry的时候,如果得到的返回值pActualEntry指向的不是SharedLib对象pNewEntry,那么就表示另外一个线程也正在加载参数pathName所指定的so文件,并且比当前线程提前加载完成。在这种情况下,函数addSharedLibEntry就什么也不用做而直接返回了。否则的话,函数addSharedLibEntry就要继续负责调用前面所加载的so文件中的一个指定的函数来注册它里面的JNI方法。
& & & & 这个指定的函数的名称为“JNI_OnLoad”,也就是说,每一个用来实现JNI方法的so文件都应该定义有一个名称为“JNI_OnLoad”的函数,并且这个函数的原型为:
& & & & 函数dvmLoadNativeCode通过调用函数dlsym就可以获得在前面加载的so中名称为“JNI_OnLoad”的函数的地址,最终保存在函数指针func中。有了这个函数指针之后,我们就可以直接调用它来执行注册JNI方法的操作了。注意,在调用该JNI_OnLoad函数时,第一个要传递进行的参数是一个JavaVM对象,这个JavaVM对象描述的是在当前进程中运行的Dalvik虚拟机,第二个要传递的参数可以设置为NULL,这是保留给以后使用的。
& & & & 从前面一文可以知道,在当前进程所运行的Dalvik虚拟机实例是通过全局变量gDvm所描述的一个DvmGlobals结构体的成员变量vmList来描述的,因此,我们就可以将它传递在前面加载的so中名称中定义的JNI_OnLoad函数。注意,定义在该so文件中的JNI_OnLoad函数一旦执行成功,它的返回值就必须等于JNI_VERSION_1_2、JNI_VERSION_1_4或者JNI_VERSION_1_6,用来表示所注册的JNI方法的版本。
& & & & 最后,&函数dvmLoadNativeCode根据上述的JNI_OnLoad函数的执行成功与否,将前面所创建的一个SharedLib对象pNewEntry的成员变量onLoadResult设置为kOnLoadOkay或者kOnLoadFailed,这样就可以记录参数pathName所指定的so文件是否是加载成功的,也就是它是否成功地注册了其内部的JNI方法。
& & & & 在我们这个情景中,参数pathName所指定的so文件为libnanosleep.so,接下来我们就继续分析它的函数JNI_OnLoad的实现,以便可以发解定义在它里面的JNI方法的注册过程。
& & & & Step 5.&JNI_OnLoad
& & & & 定义在libnanosleep.so文件中的函数JNI_OnLoad的实现可以参考文章开始的部分。从它的实现可以知道,它所注册的JNI方法shy_luo_jni_ClassWithJni_nanosleep是与shy.luo.jni.ClassWithJni类的成员函数nanosleep对应的,并且是通过调用函数jniRegisterNativeMethods来实现的。因此,接下来我们就继续分析函数jniRegisterNativeMethods的实现。
& & & & Step 6.&jniRegisterNativeMethods
& & & & 这个函数定义在文件dalvik/libnativehelper/JNIHelp.c中。
& & & & 参数env所指向的一个JNIEnv结构体,通过调用这个JNIEnv结构体可以获得参数className所描述的一个类。这个类就是要注册JNI的类,而它所要注册的JNI就是由参数gMethods来描述的。
& & & & 注册参数gMethods所描述的JNI方法是通过调用env所指向的一个JNIEnv结构体的成员函数RegisterNatives来实现的,因此,接下来我们就继续分析它的实现。
& & & & Step 7.&JNIEnv.RegisterNatives
& & & & 这个函数定义在文件dalvik/libnativehelper/include/nativehelper/jni.h中。
& & & & 从前面一文可以知道,结构体JNIEnv的成员变量functions指向的是一个函数表,这个函数表又包含了一系列的函数指针,指向了在当前进程中运行的Dalvik虚拟机中定义的函数。对于结构体JNIEnv的成员函数RegisterNatives来说,它就是通过调用这个函数表中名称为RegisterNatives的函数指针来注册参数gMethods所描述的JNI方法的。
& & & & 从前面一文可以知道,上述函数表中名称为RegisterNatives的函数指针指向的是在Dalvik虚拟机内部定义的函数RegisterNatives,因此,接下来我们就继续分析它的实现。
& & & & Step 8.&RegisterNatives
& & & & 这个函数定义在文件dalvik/vm/Jni.c中。
& & & & 参数jclazz描述的是要注册JNI方法的类,而参数methods描述的是要注册的一组JNI方法,这个组JNI方法的个数由参数nMethods来描述。
& & & & 函数RegisterNatives首先是调用函数dvmDecodeIndirectRef来获得要注册JNI方法的类对象,接着再通过一个for循环来依次调用函数dvmRegisterJNIMethod注册参数methods描述所描述的每一个JNI方法。注意,每一个JNI方法都由名称、签名和地址来描述。
& & & & 接下来,我们就继续分析函数dvmRegisterJNIMethod的实现。
& & & & Step 9.&dvmRegisterJNIMethod
& & & & &这个函数定义在文件dalvik/vm/Jni.c中。
& & & & &函数dvmRegisterJNIMethod在注册参数methodName所描述的JNI方法之前,首先会进行一系列的检查,包括:
& & & & 1. 确保参数clazz所描述的类有一个名称为methodName的成员函数。首先是调用函数dvmFindDirectMethodByDescriptor来检查methodName是否是clazz的一个非虚成员函数,然后再调用函数dvmFindVirtualMethodByDescriptor来检查methodName是否是clazz的一个虚成员函数。
& & & & 2. 确保类clazz的成员函数methodName确实是声明为JNI方法,即带有native修饰符,这是通过调用函数dvmIsNativeMethod来实现的。
& & & & 通过了前面的第1个检查之后,就可以获得一个Method对象method,用来描述要注册的JNI方法所对应的Java类成员函数。当一个Method对象method描述的是一个JNI方法的时候,它的成员变量nativeFunc保存的就是该JNI方法的地址,但是在对应的JNI方法注册进来之前,该成员变量的值被统一设置为dvmResolveNativeMethod。因此,当我们调用了一个未注册的JNI方法时,实际上执行的是函数dvmResolveNativeMethod。函数dvmResolveNativeMethod此时会在Dalvik虚拟内部以及当前所有已经加载的共享库中检查是否存在对应的JNI方法。如果不存在,那么它就会抛出一个类型为java.lang.UnsatisfiedLinkError的异常。
& & & & 注意,一个JNI方法是可以重复注册的,无论如何,函数dvmRegisterJNIMethod都是调用另外一个函数dvmUseJNIBridge来继续执行注册JNI的操作。
& & & & Step 10.&dvmUseJNIBridge
& & & &&这个函数定义在文件dalvik/vm/Jni.c中。
& & & & 一个JNI方法并不是直接被调用的,而是通过由Dalvik虚拟机间接地调用,这个用来间接调用JNI方法的函数就称为一个Bridge。这些Bridage函数在真正调用JNI方法之前,会执行一些通用的初始化工作。例如,会将当前线程的状态设置为NATIVE,因为它即将要执行一个Native函数。又如,会为即将要被调用的JNI方法准备好前面两个参数,第一个参数是一个JNIEnv对象,用来描述当前线程的Java环境,通过它可以访问反过来访问Java代码和Java对象,第二个参数是一个jobject对象,用来描述当前正在执行JNI方法的Java对象。
& & & & 这些Bridage函数实际上仍然不是直接调用地调用JNI方法的,这是因为Dalvik虚拟机是可以运行在各种不同的平台之上,而每一种平台可能都定义有自己的一套函数调用规范,也就是所谓的ABI(Application Binary Interface),这是一个API(Application Programming Interface)不同的概念。ABI是在二进制级别上定义的一套函数调用规范,例如参数是通过寄存器来传递还是堆栈来传递,而API定义是一个应用程序编程接口规范。换句话说,API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译
,而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。&
& & & & 为了使得运行在不同平台上的Dalvik虚拟机能够以统一的方法来调用JNI方法,这些Bridage函数使用了一个libffi库,它的源代码位于external/libffi目录中。Libffi是一个开源项目,用于高级语言之间的相互调用的处理,它的实现机制可以进一步参考。
& & & & 回到函数dvmUseJNIBridge中,它主要就是根据Dalvik虚拟机的启动选项来为即将要注册的JNI选择一个合适的Bridge函数。如果我们在Dalvik虚拟机启动的时候,通过-Xjnitrace选项来指定了要跟踪参数method所描述的JNI方法,那么函数dvmUseJNIBridge为该JNI方法选择的Bridge函数就为dvmTraceCallJNIMethod,否则的话,就再通过另外一个函数dvmSelectJNIBridge来进一步选择一个合适的Bridge函数。选择好Bridge函数之后,函数dvmUseJNIBridge最终就调用函数dvmSetNativeFunc来执行真正的JNI方法注册操作。
& & & & 我们假设参数method所描述的JNI方法没有设置为跟踪,因此,接下来,我们就首先分析函数dvmSelectJNIBridge的实现,接着再分析函数dvmSetNativeFunc的实现。
& & & & Step 11.&dvmSelectJNIBridge
& & & & 这个函数定义在文件dalvik/vm/Jni.c中。
& & & & Dalvik虚拟机提供的Bridge函数主要是分为两类。第一类Bridge函数在调用完成JNI方法之后,会检查该JNI方法的返回结果是否与声明的一致,这是因为一个声明返回String的JNI方法在执行时返回的可能会是一个Byte Array。如果不一致,取决于Dalvik虚拟机的启动选项,它可能会停机。第二类Bridge函数不对JNI方法的返回结果进行上述检查。选择哪一类Bridge函数可以通过-Xcheck:jni选项来决定。不过由于检查一个JNI方法的返回结果是否与声明的一致是很耗时的,因此,我们一般都不会使用第一类Bridge函数。
& & & & 此外,每一类Bridge函数又分为四个子类:Genernal、Sync、VirtualNoRef和StaticNoRef,它们的选择规则为:
& & & & 1. 一个JNI方法的参数列表中如果包含有引用类型的参数,那么对应的Bridge函数就是Genernal类型的,即为dvmCallJNIMethod_general或者dvmCheckCallJNIMethod_general。
& & & & 2. 一个JNI方法如果声明为同步方法,即带有synchronized修饰符,那么对应的Bridge函数就是Sync类型的,即为dvmCallJNIMethod_synchronized或者dvmCheckCallJNIMethod_synchronized。
& & & & 3.&一个JNI方法的参数列表中如果不包含有引用类型的参数,并且它是一个虚成员函数,那么对应的Bridge函数就是kJNIVirtualNoRef类型的,即为dvmCallJNIMethod_virtualNoRef或者dvmCheckCallJNIMethod_virtualNoRef。
& & & & 4. 一个JNI方法的参数列表中如果不包含有引用类型的参数,并且它是一个静态成员函数,那么对应的Bridge函数就是StaticNoRef类型的,即为dvmCallJNIMethod_staticNoRef或者dvmCheckCallJNIMethod_staticNoRef。
& & & & 每一类Bridge函数之所以要划分为上述四个子类,是因为每一个子类的Bridge函数在调用真正的JNI方法之前,所要进行的准备工作是不一样的。例如,Genernal类型的Bridge函数需要为引用类型的参数增加一个本地引用,避免它在JNI方法执行的过程中被回收。又如,Sync类型的Bridge函数在调用JNI方法之前,需要执行同步原始,以避免多线程访问的竞争问题。
& & & & 这一步执行完成之后,返回到前面的Step 10中,即函数dvmUseJNIBridge中,这时候它就获得了一个Bridge函数,因此,接下来它就可以调用函数dvmSetNativeFunc来执行真正的JNI方法注册操作了。
& & & & Step 12.&dvmSetNativeFunc
& & & & 这个函数定义在文件dalvik/vm/oo/Class.c中。
& & & & 参数method表示要注册JNI方法的Java类成员函数,参数func表示JNI方法的Bridge函数,参数insns表示要注册的JNI方法的函数地址。
& & & & 当参数insns的值不等于NULL的时候,函数dvmSetNativeFunc就分别将参数insns和func的值分别保存在参数method所指向的一个Method对象的成员变量insns和nativeFunc中,而当insns的值等于NULL的时候,函数dvmSetNativeFunc就只将参数func的值保存在参数method所指向的一个Method对象成员变量nativeFunc中。
& & & & 假设在前面的Step 11中选择的Bridge函数为dvmCallJNIMethod_general,并且结合前面一文,我们就可以得到Dalvik虚拟机在运行过程中调用JNI方法的过程:
& & & & 1. 调用函数dvmCallJNIMethod_general,执行一些必要的准备工作;
& & & & 2.&函数dvmCallJNIMethod_general再调用函数dvmPlatformInvoke来以统一的方式来调用对应的JNI方法;
& & & & 3.&函数dvmPlatformInvoke通过libffi库来调用对应的JNI方法,以屏蔽Dalvik虚拟机运行在不同目标平台的细节。
& & & & 至此,我们就分析完成Dalvik虚拟机JNI方法的注册过程了。这样,我们就打通了Java代码和Native代码之间的道路。实际上,很多Java和Android核心类的功能都是通过本地操作系统提供的系统调用来完成的,例如,Zygote类的成员函数forkAndSpecialize最终是通过Linux系统调用fork来创建一个Android应用程序进程的,又如,Thread类的成员函数start最终是通过pthread线程库函数pthread_create来创建一个Android应用程序线程的。
& & & & 在接下来的一篇文章中,我们就在Java代码和Native代码打通了的基础上,分析Android应用程序进程和线程与本地操作系统进程的线程的关系,也就是Dalvik虚拟机进程和线程与本地操作系统进程的线程的关系,敬请关注!
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:612268次
积分:7291
积分:7291
排名:第1978名
原创:46篇
转载:50篇
评论:551条
欢迎加入Android安全实验室QQ群交流学习:
文章:90篇
阅读:577105
(2)(4)(2)(1)(3)(3)(1)(2)(2)(2)(1)(3)(3)(2)(2)(7)(6)(4)(5)(10)(13)(18)(3)

我要回帖

更多关于 jni调用 的文章

 

随机推荐