实现双进程守护

 先讲讲组件的一些注意事项。应当注意,在启动Activity中分显式和隐式启动两种,显示启动会指定需要启动的Activity的名字,隐式启动则不用。例如Intent(this, xxx.class)是显式启动。简单来说就是,看有没有指定componentName来区分显式和隐式。

        另外需要注意的是,指定componentName中应该将包名+类名一起写上,以防不同包名下存在相同的类名的情况!

        最关键的是,使用intent-filter中,有些action需要添加data才能正常使用,不然是使用不了的,这点需要特别注意!

<manifest ...> 

    <receiver ...> 

           <intent-filter> 

              <action Android:name="android.intent.action.MEDIA_MOUNTED"/> 

              <action android:name="android.intent.action.MEDIA_UNMOUNTED"/> 

              <action android:name="android.intent.action.MEDIA_SHARED"/> 

              <action android:name="android.intent.action.MEDIA_REMOVED"/> 

              <action android:name="android.intent.action.MEDIA_EJECT"/> 

              <data android:scheme="file" /> 

           </intent-filter> 

           <intent-filter> 

              <action android:name="android.intent.action.PACKAGE_ADDED"/> 

              <action android:name="android.intent.action.PACKAGE_REMOVED"/> 

              <action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/> 

              <data android:scheme="package" /> 

           </intent-filter> 

   </receiver> 

</manifest>


上面这种情况就需要添加data,否则action用不了!

        有一些需要添加category,不然是用不了

<manifest ...> 

    <receiver ...> 

            <intent-filter> 

                <action android:name="com.hilton.controlpanel.action.BUTTON_ACTION" /> 

                <category android:name="com.hilton.controlpanel.category.SELF_MAINTAIN" /> 

            </intent-filter> 

   </receiver> 

</manifest>

        对于Intent-filter,应该注意,如果自身没有action,那么不能匹配任何隐式intent,只能被显式intent匹配。而如果intent没有指定action,那么intent可以匹配任何含有action的intent-filter,而没有action的intent-filter则不行。

        总的来说,category,data都可以看成辅助action的,他们可以看成是辅助信息,帮助系统理解action的属性,所以如果定义的action具有属性的话,应该考虑使用category,data进行辅助说明,特别是action没错,而使用中却没效果的bug时,往往就是action的属性说明缺失了!

        相对于Activity,BroadcastReceiver,Service很少使用category,data属性。

        在使用组件中,最需要注意的应该是intent-filter。intent-filter必须至少要有一条action,否则任何隐式的intent都不能匹配,只能通过显式匹配,而对于intent中的action只要匹配intent-filter中的其中一条,即可开启intent-filter所属的组件


        先面说说Service如何不被系统或者第三方关闭。

        对于Service需要注意,如果在AndroidManifest注册中,android:process以冒号开头,那么这个Service的进程是这个app私有的,如果以小写字母开头,那么这个进程是全局的。应该说,以冒号开头其实最后系统会将自身的包名添加到冒号前面,例如android:process=":han",这个属性其实最后是android:process="com.han.han:han"形式的,其中冒号:后面的han是任意的。另外,对于Service的AIDL应该注意,这是为了进程通信而设计的。

        下面是一个全局进程的Service

<service android:name=".app.WifiService" android:process="com.han.han" />

        对于BroadcastReceiver,如果在实现一下代码,那么可以在手机开机之后启动BroadcastReceiver。

<receiver android:name=".app.BootReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED" >
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
    
</receiver>

这里添加了开启权限android.permission.RECEIVE_BOOT_COMPLETED,以及intent匹配android.intent.action.BOOT_COMPLETED让BroadcastReceiver在手机开机之后接收到intent进行启动。

        但是对于BroadcastReceiver,应该注意,他只有10秒的时间,时间一到如果onReceive方法中的任务没有完成,那么系统将直接判定为无响应而弹出ANR。这点需要注意!

        而其实对于广播,有各种各样的广播,例如上面的开机广播,关机广播,wifi状态发生变化广播,网络发生变化的广播等。同时注意BroadcastReceiver可以不在AndroidManifest中注册,使用Activity.startActivity()方法动态启动,而在AndroidManifest中注册的是静态广播,app启动会跟着启动。下面是静态的广播,监听开机,wifi,网络的广播

<!--静态广播在AndroidManifest中注册,然后app开启会启动广播-->
<receiver android:name=".app.BootReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED" >
    <!--开机广播ps:与此对应的还有关机广播-->
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
    <intent-filter>
        <!--网络状态发生变化-->
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
        <action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
        <action android:name="android.net.wifi.STATE_CHANGE" />
    </intent-filter>

</receiver>




        我们想要让Service不被系统杀死可以将app放在system/app路径下,放在这个路径下的应用系统会认为是系统的app,所以系统不回去杀死他,第三方也是不可以的,这一点,市面上的Root应用就会经常利用这个原理,让你怎么也删除不了这个应用。但是这个方式有限制,需要获得权限,一般需要root才行,而且也不建议使用,除非是做流氓软件。


        另外,可以使用Service.startForeground()方法,将Service变成前台服务,同时这个方法会有一个Notification参数,所以容易知道其实状态栏会有一个Notification提醒用户。但这种Service前台化的方法仅仅保证一般情况下的内存不足不会杀死Service而已,或者说内存很匮乏时,还是会杀死Serivce的,所以基本用不上这种方法。


        在onStartCommand(Intent,int,int)返回参数的方法可以让系统尝试重新创建Service,但是并不是百分之百成功的。而且关键是Service要运行的到返回才行,如果中途被kill,那么这个方法基本用不上,所以这个方法还是不行。

        这里onStartCommand()方法返回参数有四种:START_STICKY,START_NOT_STICKY,START_REDELIVER_INTENT,START_STICKY_COMPATIBILITY。

        这里START_STICKY在Service表示服务进程被kill掉之后,会保留Service在开始状态,但是不保留Intent,同时Service被销毁之后,系统会尝试重新创建Service,所以如果期间没有收到其他命令启动Service,那么Intent依旧为null;而START_NOT_STICKY则跟START_STICKY类似,只是系统不会尝试启动它的Service,它需要重新使用startService()方法启动;START_REDELIVER_INTENT在进程被kill掉之后,系统会重新启动该服务并传入Intent值;START_STICKY_COMPATIBILITY是START_STICKY的兼容版本,但是不保证Service一定能重启。在这几个参数中,START_REDELIVER_INTENT能保证Service被安排重新启动,但是是有等待重启队列的。但是无论是哪一种返回值,如果被第三方kill掉,其实都是不会在重新启动的。


        在Service.onCreate()中添加ServiceManager.startService()方法,这个方法可以阻止GC销毁Service。但是不能阻止第三方kill掉进程。


        还有一种思路:当Service销毁时会调用onDestroy()方法,在这个方法里面给广播发送Intent,然后广播再开启Service,这样就可以保证Service不会被kill掉了。但是这个方法是建立在app没退出的情况下,如果app被退出,那么这种方法基本上没用了。

        这里顺便说一下,如果外部的程序想要启动app,那么就需要外部启动应用的方法,这里使用:

1.

Intent LaunchIntent = getPackageManager().getLaunchIntentForPackage( "com.package.address" );
startActivity(LaunchIntent);

2.新建一个Intent,然后

category=LAUNCHER, action=MAIN, componentName = new ComponentName(packageName, name) setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

外部程序使用这种Intent就可以启动app了,其实就是根据包来启动!注意:需要在跳转的Activity中添加android:exported="true",这个属性依赖于intent-filter,如果有intent-filter,则为true,如果没有则为false。外部启动的话,需要谨慎,可以使用添加category或者data来避免,例如

  1. <intent-filter>  
  2.         <action android:name="android.qijian.schemeurl.activity" />  
  3.         <category android:name="android.intent.category.DEFAULT" />  
  4.         <data  
  5.             android:scheme="qijian"  
  6.             android:host="test.uri.activity" />  
  7.     </intent-filter> 

这样,通过Service检测,然后startForeground,用户点击就可以外部启动打开我们的app了。


        Service与activity的onDestroy方法中互相启动这种方法其实如果是第三方销毁的话,onDestroy方法根本就来不及调用。这个时候可以尝试使用onSavedInstanceState方法。但是还是无法保证。


        使用在intent-filter中添加Intent.ACTION_PACKAGE_RESTARTED的action判断应用状态改变,也是无法完美解决。


        总的来说,其实就算使用了上面的所有方法依旧是无法保证Service不被销毁的,特别是Service的方法onTaskRemoved(Intent)方法,虽然说在进程销毁时会回调一次,但是在这个方法里面的执行的重新启动该Service未必会马上重启,而是看系统的安排重启,也就是说,这是一个不确定的事件,所以基本上是用不了。



        这里如果需要让自己的服务不被销毁,那么只能使用双进程守护,所以这里也就需要使用到jni,ndk的知识点。


        首先说一下怎么让java使用本地的方法。首先在.java文件里面添加如下代码:

//库名
static {
    System.loadLibrary("JniTest");
}

public native String getStringFromNative();
这里System.loadLibrary("JniTest")中,JniTest为so库的名字,可以在build.gradle中进行配置,而getStringFromNative()为本地自定义的方法名称,这个方法名字前面需要添加native关键字。

        然后打开cmd并将路径定位到项目路径下,这点需要特别注意,如果没有定位到项目路径,是编译不了的!然后需要注意,需要让.class文件形成,这里虽然我使用了make Project命令和Rebuild Project命令,可还是没有形成.class文件,最后还是运行了项目一次,让系统生成了.class文件。然后再cmd命令下输入

javah -d jni -classpath D:\android/studio\sdk\platforms\android-22\android.jar;D:\workspace\CMCCEWalk\app\build\intermediates\classes\debug com.chinamobile.cmccewalk.net.Watcher命令将项目中的.class文件编译,然后生成我们需要的.h头文件。

        这里javah命令中,-d表示目录,这里由于我们在当前项目下,而-d的参数为jni,所以会在当前的module下生成一个jni文件,这个jni文件和java,res文件夹是同级的,而这里定位到的.class文件在当前module也就是app的build文件夹下,详细路径为app\build\intermediates\classes\debug,后面再跟上详细路径的class文件名,也就是包名+类名。最后编译失败的话可能是引入编译的包缺少的问题,我们可以将v7-appcompat和v4兼容包在cmd命令行中加入,使用分号隔开即可。

        需要点击刷新按钮刷新项目,这样jni文件才会正常显示,不然文件生成之后只会在路径下生成,而android studio没刷新的情况下可能不会显示,这点需要注意!

        在app这个module中,路径app\build\intermediates文件夹下会生成一个ndk文件夹,下面其实就是我们平时引入项目的包,而且是.so文件的包。这里注意路径下的Android.mk文件非常重要,后面会操作到。.mk文件用于配置.so文件的相关操作配置。


        然后点击Make Project在点击Rebuild Project,然后native的本地方法的红色警告就会消失,这里有时候需要点击本地方法与java方法之间的跳转,让系统反应过来,或者点击刷新,来消除红色警告提示。


        然后需要注意,Make Project和Rebuild Project依旧会报错,因为Android Studio有一个bug,所以我们还需要在jni文件夹下新建一个空的util.c文件。然后再添加一个.c文件来给方法添加具体实现,其实也就是给刚刚使用javah生成的.h头文件添加.c文件,这里其实.h头文件和.c文件的名字本来应该一样的,但是也可以不一样,这点需要注意!这里我讲文件命名为main.c文件,下面是代码:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#include <android/log.h>

#ifndef LOG_TAG
#define LOG_TAG "ANDROID_LAB"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#endif

/* Header for class lab_sodino_jnitest_MainActivity */

#ifndef _Included_com_chinamobile_jnitest_MainActivity
#define _Included_com_chinamobile_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: com_chinamobile_jnitest_MainActivity
 * Method: getStringFromNative
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_chinamobile_jnitest_MainActivity_getStringFromNative
        (JNIEnv * env, jobject jObj){
    LOGE("log string from ndk.");
    return (*env)->NewStringUTF(env,"Hello From JNI!");
}

#ifdef __cplusplus
}
#endif
#endif

        然后在local.properties文件中添加NDK路径,方便android studio使用NDK编译项目:

ndk.dir=D\:\\android-ndk-r10d-windows-x86_64\\android-ndk-r10d
        在gradle.properties中添加:

android.useDeprecatedNdk=true
        在module的build.gradle的default.config中添加:

ndk {
    moduleName "JniTest"
    ldLibs "log", "z", "m" // Link with these libraries!
    abiFilters "armeabi", "armeabi-v7a", "x86"
}
这里abiFilters是最终在app/build/intermediates/ndk/debug中生成的库中文件的名字,但是同时注意,这几个的名字分别对应的是硬件的属性!

        另外还可以添加:

sourceSets.main {
    jniLibs.srcDir 'src/main/myCppLibraries' // .so库的实际路径
    jni.srcDirs 'src/main/source' //源代码路径,默认为跟java文件夹同路径的jni文件夹
}
这样就可以修改native code以及最终的so库的位置了。当然也可以不添加。

        其实应该注意,在使用so库的文件中添加的system.loadLibrary("so库名")方法,这个方法中so库名字是需要配置的,不然就是当前项目名,如果是自定义名字那么在生成so库之后,才能使用这个名字。另外注意,被编译的class文件的java文件本身并不需要添加system.loadLibrary()方法,是需要用so库的文件才需要调用这个方法,这点需要特别注意!

        对于头文件和源文件的编写,最后需要点击文件右上角的Sync Now才行,这点会有提示。其实也就是使用NDK同步。

        最后运行即可。


        对于Make Project其实修改的是编译的环境,而Rebuild Project修改的是项目环境,比如使用的工具等。所以如果修改了NDK的配置或者属性,那么需要使用build->Rebuld Project而不是Make Project。

        这里,如果没有添加util.c文件,那么会报错,而报错的内容一般是app:compileDebugNdk或者ndk-build.cmd以非零数字返回。

        编辑so库会产生Android.mk文件,这个文件告诉NDK构建系统我们的native code的信息。而在Android.mk文件中添加include $(BUILD-EXECUTABLE)可以让c文件编译成系统可以运行的二进制文件,但是一般不用添加。另外,android系统的so库,在system/lib路径下,源代码在framework/base路径下。

        编译生成so库其实使用的是class文件进行的编译。注意,.a是静态库,而在windows中.dll是动态链接库,而在UNIX等系统中,.so是动态链接库。所以也就是说java/android其实是可以像windows那样使用动态库的。如果要使用native库,最好将C语言编译成Library库。另外D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\prebuilt\windows-x86_64\bin\make.exe就是用于将native code变成so文件的。


        上面是一个简单的本地调用,但是如果需要双进程守护,那么就需要掌握JNI和NDK。这里需要区分JNI和NDK,JNI是连接java和C/C++底层的接口,NDK是使用C/C++开发android程序的工具。在使用NDK之前需要注意,在以前的版本里面开发NDK,如果是Linux那么不用安装其他什么,如果是windows是需要安装cygwin环境的。但是在ndk-r7b版本之后就不再需要安装cygwin了,因为NDK包里面已经包含了。但是相比使用cygwin,这里的一些命令以及工具的路径就需要我们自己掌握了,因为NDK新版本将这些工具包含了,一般咋prebuild文件夹下的各个文件内。总的来说,使用NDK应该多掌握工具的使用。ps:使用ndk-build命令其实会调用系统会自动去寻找make工具

        需要注意,Gradle是基于ant,maven的构建工具,所以很多ant,maven的东西在gradle中有类似的使用。另外,在ant中有build.xml,而在gradle中有则是build.gradle。对于ant的build.xml以及gradle的构建,可以查看wiki百科中的gradle。


        在旧版本中,生成so库需要有build.xml文件,但是在android studio的新版本中不需要。在旧版本中,使用类似android update project -p . -s -t android-8命令生成build.xml文件。生成了build.xml之后再去编译c文件。或者直接使用ndk-build命令直接一次编译完成,这个过程是命令输入后,系统会调用make命令工具,然后make工具会将文件编译成库,我们需要的东西就在生成的库里面的lib文件夹下。

        注意,在Eclipse,ADT中使用NDK需要配置很多东西,而在android studio中仅仅需要在local.properties文件中加入NDK路径即可。所以详细的命令和工具可以自己在NDK路径下自己找,然后学习。这里关键在于掌握JNI,NDK的开发。


        这里注意,在使用jni中,h头文件需要引入jni.h包,这个包位置在D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\platforms\android-21\arch-arm\usr\include\jni.h路径下。而如果是c文件,那么引入的jni.h文件夹在%JAVA_HOME%\include\jni.h路径下,所以是不同的,这点需要注意!其实这个jni.h就是我们指的jni接口了!不过h头文件,c源文件引用的jni.h都使用D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\platforms\android-21\arch-arm\usr\include\jni.h路径下没有发生错误,其实两个路径下的jni.h文件是一样的!另外,在c源文件中还可以引入头文件,相当于说明,但是不引入也没问题。


        注意,jni提供了很多的方法,在jni.h文件中可以查找,需要掌握常用的方法!


        NDK中,C调用java的基本数据类型,也就是本地C的基本数据类型除了void之外,其他的都是基本数据类型名称前面加个“j”并且名字都是小写的,例如jdouble,jfloat,jint,jboolean,jchar,jshort,jlong,jbyte,而不是基本数据类型的例如:jstring。这点是和原本的C的数据类型是不一样的,这点需要注意!


        这里先说说编写C++头文件以及源文件的基本方式。在android studio中,已经为我们提供了创建C++类文件,源文件,头文件的方式。类文件和源文件会跟着创建头文件,而头文件只是创建头文件不会创建源文件。如果仅仅想创建源文件而不想创建头文件,可以使用创建File文件的方式自行添加尾缀,同时这个方式可以用于创建C源文件。

        这里需要说明,对于一个.h头文件,开头必须使用:

#ifndef JNITEST_NEWCPPCLASS_H
#define JNITEST_NEWCPPCLASS_H


class NewCppClass {

};


#endif //JNITEST_NEWCPPCLASS_H
这种方式就是说,开头是#ifndef 大写项目名_大写文件名_H和#define 大写项目名_大写文件名_H,当然#ifndef和#define后面不一定要按照格式来,可以是任意的,因为#ifndef意思是if not define,而#define是定义的意思。之所以这样写,仅仅是跟文件名对应而已,其实#ifndef和#define是预编译。而结束使用#endif。这样做是为了防止重复编译,如果不这样做有可能会出错。这点必须记住!同时注意到这些预编译语句可以明白到,头文件不会写具体实现,仅仅是用于预编译,也就是声明而已。

        而对于源文件,需要在开头#include “头文件名.h”以及用到的头文件。这里#include不会是源文件。


        上面这些是基本的文件构造,一般系统会帮我们添加好。但是更重要的是C++中编写代码与java的不同之处。


1.在C++中,一个头文件里面可以声明多个类,而java的文件里面只能是一个类;


2.C++中头文件中声明的类方法可以在源文件中定义,源文件中使用类名::方法名{}的方式写上具体实现;


3.在C++中使用private,public等进行分块,他们下面的变量和方法在各自分块下就表示了他们是private还是public了,而java中每个变量和方法之前必须添加private或者public等;


4.在C++中,类定义的{}之后必须添加分号“;”而java不需要;源文件的名字未必要跟头文件的名字一样,只是为了统一声明而已;


5.在android中头文件的方法名必须是Java_包名_类名_方法名形式,同时在头文件中函数的声明使用的是函数原型,也就是说参数说明里面只有类型而没有参数名字,例如Java_包名_类名_方法名(jobject),而在源文件中则可以需要添加参数名,例如Java_包名_类名_方法名(jobject obj)。最后注意,包名中的点号”.“要使用下斜杠”_“代替,也就是说,类似

Java_com_chinamobile_jnitest_MainActivity_getStringFromNative

格式;


6.在NDK中JNIEnv是java环境,通过JNIEnv可以调用java的方法,也就是jni的方法,JNIEnv全称是JNI environment,另外jobject是调用c语言方法的对象,this对象表示当前对象;在声明native的方法中,必定会引入JNIEnv,而jobject则视情况而定,如果原本方法参数没有引入Object,则引入jobject,如果有Object,则不再引入jobject,因为这个Object已经引入jobject了;


7.只有native的方法里面使用jint,jboolean这样的数据类型,因为这个是java的native方法提供给C++调用的,不是native方法的方法参数依然使用C++的数据类型,或者说C++的函数的数据类型不变;


8.C++的继承方式是class B:class A,同时需要注意的是C++与java不同的地方在于C++的继承可以多重继承,而java不可以。而C++中虚函数以virtual开头,含有虚函数的类称为虚类,所有函数都是虚函数的类称为纯虚类,而在java中称为抽象类和接口类;


9.使用#include时需要注意,使用双引号“”和双尖号<>是不一样的,<>符号其实表示引入在C++的include文件夹里面的文件,所以如果是引入本地的文件,就会出错,而“”表示现在本地查找引入的文件,然后再在C++的include文件夹下搜索。所以如果为了保证正确性,那么同意使用”“双引号是最好的选择。另外android中C++的include文件夹路径在NDK的路径中,也就是D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\platforms\android-21\arch-arm\usr\include;


10.JNIEnv只在当前线程有效,不能跨进程传递,一个native方法不能被不同的java进程调用。相同的线程所使用的JNIEnv是相同的。另外注意C++的type,const,struct等关键字以及常量指针,指针常量等。其实在jni.h文件中,分为C和C++两种文件进行的定义,这点可以在预处理中看到:

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
同意定义了C,C++各自的JNIEnv,javaVM。这里可以看到C的JNIEnv原本是JNINativeInterface的,所以我们在查找方法时,应该在struct JNINativeInterface{}中查找。而C++的,则在struct _JNIEnv{}中查找;


11.C/C++复杂变量方法可以使用”右左法则“看;


12.在NDK中断点调试是很麻烦的,所以打印日志log,如下

  1. #include <android/log.h>  
  2. #define LOG_TAG "System.out"  
  3. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)  
  4. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) 
这里android/log.h在D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\platforms\android-21\arch-arm\usr\include\android\log.h路径下。同时注意,__android_log_print方法在该文件中定义,ANDROID_LOG_DEBUG也在这个文件中定义。最后需要注意的是,#define key value,typedef 原名 别名,这两者容易被混淆,需要注意,同时他们两个也是非常重要的C/C++内容。__android_log_print方法参数分别指日志优先级,日志标签,日志内容。这里需要特别注意,可变参数个数也就是LOGD(...)里面的三个点,这里对应__VA_ARGS__,三个点有多少参数都会输入到__VA_ARGS__中,也就是说__VA_ARGS__代表”...“;


13.可以利用#ifndef __cplusplus判断系统时C还是C++的;


14.java文件的class文件在编译成头文件时,其实需要在预编译中添加类似:

#ifndef _Included_com_chinamobile_jnitest_MainActivity
#define _Included_com_chinamobile_jnitest_MainActivity
这里开头不是Java而是使用_Include连接。同时还增加了判断是否是C++的条件预处理:

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus
}
#endif
这里extern “C”的意思是使用C的手段来处理的意思。注意#ifdef __cplusplus是判断是否是C++的意思,cplusplus前面的双下划线表示私有,毕竟这是判断系统的。


15.System.loadLibrary("库名")中库名不用加后缀dll或者so,而是让系统自行判断,这样兼容性会更好。库名可以是我们自己定义的,如果没定义,那么就写当前项目名。


16.native方法编译到头文件之后,方法中有JNIEXPORT,JNICALL,这两个是JNI的关键字,表示方法是JNI调用的,并没有其他什么意思。



        这里还需要了解android项目的生成到安装到手机运行的整个过程,这样除了有利于了解android开发工具,更重要的是知道编译双进程守护需要什么工具!

        在android的SDK文件夹下有又很多的工具用于开发中的开发,调试,打包等工作。ps:查看命令行参数中,查看帮助信息时,如果横杠“-”紧跟单词,表示输入中是按照“-单词”形式输入的,如果横杠“-”跟单词分离,那么就只是提示用的,输入中不用带横杠。

1.android.bat可以用于创建项目,我们可以直接输入android --help查看参数配置。事实上,android.bat命令可以用于项目生成,AVD,设备等等的创建查看,以及更新SDK等工作。路径在SDK的tools文件夹下

2.aapt.exe工具是android资源的打包工具,一般是用于生成R文件的,还可用于打包生成资源包文件,详细的使用说明直接输入aapt即可。路径在SDK\build-tools下各个版本的文件夹下,例如sdk\build-tools\23.0.1文件夹下。

3.aidl.exe工具是用于根据.aidl文件生成java文件的工具,详细的使用说明直接输入aidl即可。路径在sdk\build-tools下各个版本的文件夹下,例如例如sdk\build-tools\23.0.1文件夹下。

4.javac.exe工具用于将.java文件编译成.class文件,详细的使用说明直接输入javac即可。路径在Java\jdk1.7.0_79\bin下。

5.dx.exe工具用于将class文件编译成一个classes.dex文件,详细使用说明直接输入dx即可。路径在SDK\build-tools下各个版本的文件夹下,例如sdk\build-tools\23.0.1文件夹下。

6.使用apkbuilder.exe生成apk,但是新版本里面放弃使用apkbuilder进行打包了,因为仅仅是简单的封装而已。

7.使用keytools,jarsigner,zipalign签名apk。


        上面是大致使用到的工具,而这里根据java文件生成class文件需要用到javac.exe工具,不然我们就得运行以便项目了。


        这样JNI的基本调用就是这样了。而NDK是使开发native code的工具,新版本放弃了cygwin环境了,NDK路径下有很多好用的工具需要了解。


1.ndk-build.cmd命令代替了原本cygwin环境时使用make命令的方式,运行ndk-build命令会自动去调用make命令,而make命令其实是make file之意,会用于NDK开发中生成android.mk文件。make.exe工具路径在android-ndk-r10d\prebuilt\windows-x86_64\bin下。

2.D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\platforms\android-21\arch-arm\usr\include\jni.h文件是我们开发中include的jni.h头文件。

3.NDK路径下的文件夹一般是prebuild文件夹下是预编译一些文件使用的,而这个文件夹下有windows的环境,例如windows-x86_64环境;而platform环境下有android的各个版本环境,这些android环境下有各种硬件属性,例如内核,这里一般使用arch-arm类型,所以arch-arm文件夹最有用;build文件夹下有各种构建工具的文件夹。


        最后注意jni.h中的方法仅仅提供一些数据的操作以及转换等,如果想要与硬件交流,还需要掌握系统框架,掌握系统提供的接口,掌握shell接口的调用。这些都需要在源码中查看,才能明白有哪些接口可以调用。所以系统架构的掌握也很重要!


        最后注意NDK工具一般不需要我们怎么关心,因为在android studio中,在项目下文件local.properties文件中添加了ndk路径之后系统会自动调用工具。但是需要注意NDK的使用容易让我们失去跨平台的特性,这是编程中最需要考虑的问题!


        总的来说,对于JNI,NDK我们基本上只需要掌握jni.h文件以及android的shell内核以及底层等即可,剩余的就是C/C++的知识点了。


        对于android的体系架构,分为Application:Java应用程序;Application Framework:Java架构;Libraries与Android Runtime:本地架构和Java运行环境;Linux Kernel:Linux操作系统以及驱动。

        而在源代码中并没有按照这种方式进行划分,在源代码中分为核心工程,扩展工程,包三种情况。

1.核心工程:在路径下各个文件夹下。是建立android的基础。

2.扩展工程:在路径下external文件夹下。是使用其他开源项目扩展的功能。

3.包:在路径下package文件夹下。提供android的应用程序以及服务。

        这三种情况中核心工程最重要也最难也是我们需要掌握的地方,其他的两个比较简单,直接打开查看就知道了。


        对于核心工程关键有以下几个文件夹需要注意:

1.bionic:[build系统]c运行时支持:libc,libm,libdl,动态linker。

2.build:[buld系统]build系统。

3.bootable:内核加载器,内核运行前运行。

4.dalvik:dalvik虚拟机。

5.development:高层的开发和调试工具。

6.framework/base:android核心的架构库。

7.framework/policies/base:架构配置策略。

8.hareware/libhareware:硬件抽象层库。

9.hareware/ril:无线接口层(radio interface layer)

10.system/core:最小可启动的环境。

11.system/extras:底层调试和检查工具。

12.kernel:kernel内核。

13.prebuild:[预编译内核]对linux,mac os编译的二进制支持。


        上面是一些常见的文件夹,但是实际上并没有必要强行记住,只需要了解即可,在需要的时候也可以搜索关键字,而且文件夹的名字很直观。最关键的是不同版本的android系统文件夹似乎有一些变化。所以需要时查看即可,不必太深究。如果NDK使用,则寻找头文件即可,因为需要包含。



ps:在实际开发中,源码的架构仅仅作为了解,因为在NDK开发中,引入的是NDK路径下的头文件,这些接口的实际代码在源码中,但是我们需要的是接口。所以我们第一步需要掌握NDK路径下的接口头文件,然后熟悉之后可以看看这些接口头文件的源代码。所以当前不熟悉的情况下不应该提早查看接口头文件,而应该先掌握NDK路径下接口:

D:\android-ndk-r10d-windows-x86_64\android-ndk-r10d\platforms\android-21\arch-arm\usr\include

这下面的文件就是#include中被包含的文件。


        下面是我的双进程守护的代码的实现过程以及原理。


1.创建项目,然后再项目中的local.properties文件夹中添加NDK路径如下:

## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Wed Oct 28 15:36:56 CST 2015
sdk.dir=D\:\\android\\studio\\sdk
ndk.dir=D\:\\android-ndk-r10d-windows-x86_64\\android-ndk-r10d


2.在gradle.properties中添加android.useDeprecatedNdk=true,这样androidstudio就可以使用旧版本的了,这样不会因为版本问题报错。如下:

# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true


3.在模块的build.gradle下添加ndk{}模块如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.han.daemonprocess"
        minSdkVersion 8
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        ndk {
            moduleName "DaemonProcess"
            ldLibs "log", "z", "m" // Link with these libraries!
            abiFilters "armeabi", "armeabi-v7a", "x86"
        }


    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.0'
    compile 'com.android.support:design:23.1.0'
}
这里defaultConfig{}中的ndk{}就是添加部分。ndk{}里面的moduleName就是我们最后so库的名字,但是注意,系统会自动在名字前面添加lib,也就是说,我们看到的是lib+so库名的形式,这点不用太在意。


4.打开cmd命令,将当前路径定位到项目中模块的src/main下,然后输入命令:javah -d jni -classpath 引用的库/包;class文件路径 包名+类名。也就是:

javah -d jni-classpath D:\android\studio\sdk\platforms\android-23\android.jar;D:\android\studio\sdk\extras\android\support\v4\android-support-v4.jar;D:\android\studio\sdk\extras\android\support\v7\appcompat\libs\android-support-v7-appcompat.jar;D:\workspace\DaemonProcess\app\build\intermediates\classes\debug com.han.daemonprocess.net.Watcher

然后按刷新按钮,就可以开到项目main文件夹下多了一个jni文件夹,下面有我们需要的头文件。


5.在jni文件夹下添加util.c文件夹,这个文件夹名字和后缀都是不能变的,是android studio的bug。

        然后再添加一个跟上一步生成的头文件对应的源文件。这里需要特别注意,因为C源文件是不能调用类的,所以如果你在jni文件夹下同时使用c源文件和cpp源文件,那么就需要特别注意了,不能再C源文件下调用cpp源文件的类进行实例化,不然会报错。

        同时需要特别注意的是,这里我们创建的源文件中不能将上一步生成的头文件包含进来,也就是说这个文件不能加#include “头文件名”,否则会出错,会提示没有native的方法!同时需要注意,这个文件是最特别的文件,它是介于java和C/C++之间的文件,不能按照C/C++的原理来设计。这里并没有按照C/C++的规则进行设计,这里我命名为main.cpp,然后并没有因为使用类名:方法名{}这种方式进行方法的实现,但是需要注意的是,这个文件里面任何的错误,android studio都基本不会提示,所以需要特别注意!

        同时注意:

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus
}
#endif

这个部分也是不能省的,不然也会出错!

        这需要特别注意的是#include <jni.h>和#include <android/log.h>的加入,这里即使没加入也不会报错,但是最后运行就会出错!最好的方式是将上一步的头文件赋值粘贴到当前源文件下,然后我们在进行修改。这里我的文件最后如下:

#include <jni.h>
#include <android/log.h>

#ifndef LOG_TAG
#define LOG_TAG "ANDROID_LAB"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#endif


#ifndef _Included_com_han_daemonprocess_net_Watcher
#define _Included_com_han_daemonprocess_net_Watcher
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jboolean JNICALL Java_com_han_daemonprocess_net_Watcher_createWatcher
        (JNIEnv *, jobject, jstring)
{
  return JNI_TRUE;
}

JNIEXPORT jboolean JNICALL Java_com_han_daemonprocess_net_Watcher_connectToMonitor
        (JNIEnv *, jobject)
{
  return JNI_FALSE;
}

JNIEXPORT jint JNICALL Java_com_han_daemonprocess_net_Watcher_sendMsgToMonitor
        (JNIEnv *, jobject, jstring)
{
  return 5;
}

#ifdef __cplusplus
}
#endif
#endif
里面的函数体是需要修改的。


6.make project,rebuild project。在这之前使用这两个命令都是会报错的,直到现在D:\workspace\DaemonProcess\app\build\intermediates下会多出一个ndk文件夹,下面就是生成的so库。这个so库就是我们需要的,也就是system.loadLibrary("so库名")中so库名加载的so库。



        下面是实际代码中遇到的注意实现:

1.android系统使用的是类UNIX的系统,虽然说所有的类UNIX系统都遵循POSIX标准,但android这里基本大部分遵循,所以调用的底层函数需要注意是否在android遵循的POSIX标准内,同时查看该函数是否是POSIX的,如果是POSIX的,那么兼容性基本不用担心,如果不是,那么最好不要使用,因为不同的函数版本可能有不同的实现方式。这里signal函数就不属于POSIX标准,所以在android不同版本中有不同的实现,所以这里最好不要使用signal函数。可以使用sigaction函数代替,同时sigaction函数是POSIX标准之内的函数。

2.对于signal函数的第二个函数是函数指针,传递中不用带括号,仅仅传递名字即可,例如signal(SIGCHILD, operate_child)

3.尽快提到技术水平,查看源代码是个很好的选择,这里首先需要提到的有三种选择:1.Linux的源代码;2.NDK的源代码;3.android的源代码。这里android虽然是类Linux的系统,但是在实际使用中,却很容易发现,实际上有部分是不相同的。而对于NDK的源代码仅仅只有头文件,所以其实不可以说有源代码。而android的源代码需要下载完成的源代码才行。总的来说,如果需要查看源代码,那么最好的选择时使用android的源代码,也可以配合NDK查看头文件。

4.其实无论是何种系统的源代码,我们使用到的头文件基本上都是放在include文件夹下,而且需要注意的是,底层源代码跟java源代码不一样,不会有项java源代码一样添加注释,而且头文件中方法的实现未必就会放在跟头文件同名的源文件下。

5.注意,在android中sigaction函数的实现跟Linux的不大一样,这里sigaction函数使用上虽然没什么区别,但是其中的函数指针参数sa_handler与sa_sigaction两个函数被放在union体中,所以只能选择其中一个,不能同时使用。

6.守护进程是涉及到Linux C++的底层,内核等东西,但是实际上在java,android中已经提供了接口进行实现。我们可以使用Process等类进行Zygote守护进程的创建

7.使用Linux检索有关底层的知识,因为android底层是Linux的。使用C++检索C++方面的知识。

8.如果需要android的进程不被杀死,其实做法原理是守护进程DaemonProcess的创建。这里主进程创建进程A,然后进程A创建进程B,进程B变成守护进程中会杀死进程A,这也就是守护进程的脱壳了。这种做法需要多创建两个进程,所以才叫做双进程守护。

9.如果底层代码需要调用java的代码,那么要找到JNIEnv,类和对象,对调用的方法进行方法签名。这里如果不对方法进行签名,那么底层是找不到方法的。方法签名可以通过javap命令实现。这里定位到class文件路径并使用javap -s 类名即可。注意,javap是JDK提供的反汇编工具。

10.native底层调用java变量,方法的关键是JNIEnv的方法。这里关键是类似callXXMethod()类型的函数,例如void(*CallStaticVoidMethodA)(JNIEnv*, jclass, jmethodID, jvalue*);。调用参数需要JNIEnv,类名,方法名,参数,所以需要用到JNIEnv的FindClass函数,GetMethodID函数等。下面是一个在native方法的方法体部分,详细调用如下:

jclass clazz =NULL;

jobject jobj =NULL;

jmethodID mid_construct = NULL;  

jmethodID mid_instance = NULL;  

jstring str_arg =NULL;  

// 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象

clazz = (*env)->FindClass(env,"com/study/jnilearn/ClassMethod");

if (clazz == NULL) {

printf("找不到'com.study.jnilearn.ClassMethod'这个类");

return;

}  

// 2、获取类的默认构造方法ID

mid_construct = (*env)->GetMethodID(env,clazz,"<init>","()V");

if (mid_construct == NULL) {

printf("找不到默认的构造方法");

return;

}

// 3、查找实例方法的ID

mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod","(Ljava/lang/String;I)V");

if (mid_instance == NULL) {

return;

}

// 4、创建该类的实例

jobj = (*env)->NewObject(env,clazz,mid_construct);

if (jobj == NULL) {

printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");

return;

}

// 5、调用对象的实例方法

str_arg = (*env)->NewStringUTF(env,"我是实例方法");

(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);

// 删除局部引用

(*env)->DeleteLocalRef(env,clazz);

(*env)->DeleteLocalRef(env,jobj);

(*env)->DeleteLocalRef(env,str_arg);

其实对于底层代码调用java的方法,其实核心就是通过jni.h头文件中提供的方法接口进行的调用,调用其实是回调。


        在将java的native接口转化为头文件中的函数接口之后,这里源文件名字没有跟头文件的名字一致,而是使用main.c文件对应这个头文件。下面是main.c源文件的代码:

//
//
//
//
//
//
//
// Created by Administrator on 2015/11/5.
//

#include "ProcessManager.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <android/log.h>
#include <stdlib.h>
#include <signal.h>


#ifndef LOG_TAG
#define LOG_TAG "Native"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#endif

void on_child_terminated(int sig)
{
    LOGE("child process is terminated");

}



bool ProcessManager::create_process() {


    struct sigaction sa_usr;

    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = on_child_terminated;
    sigaction(SIGCHLD, &sa_usr, NULL);



    int count = 0;
    pid_t pid = fork();
    int status = -1;

    if (pid < 0) {
        LOGE("fork error for %m\n", errno);
    } else if (pid > 0) {
        LOGE("this is parent ,pid = %d\n", getpid());
        wait(&status);//父进程执行到此,马上阻塞自己,直到有子进程结束。当发现有子进程结束时,就会回收它的资源。

    } else {
        LOGE("this is child , pid = %d , ppid = %d\n", getpid(), getppid());

        for (int i = 0; i < 10; i++) {
            count++;
            sleep(1);
            LOGE("count = %d\n", count);
        }

        exit(5);

    }
    LOGE("child exit status is %d\n", WEXITSTATUS(status));//status是按位存储的状态信息,需要调用相应的宏来还原一下

    LOGE("end of program from pid = %d\n", getpid());

    return pid < 0;


}
这里仅仅是创建了一个子进程的代码,效果是即使移除android app子进程依旧是可以运行的。


        上面是简答的分进程,下面改进上面的代码,使其实现守护进程。


        这里需要注意,Linux的信号机制。对于父进程可以操作子进程,特别是子进程销毁时,子进程会向父进程发送SIGCHLD信号,而父进程的数据子进程是不能操作的,所以他们两个的关系是解耦的。也就是说,子进程会自动向父进程发送销毁的信号,而父进程不会。

        prctl函数的作用是修改进程的行为,我们可以修改当前进程的名字,而这里我们关键是,使用prctl函数,然后父进程死亡之后,shell内核会发送一个信号给父进程的子进程。这里使用

prctl(PR_SET_PDEATHSIG, SIGHUP);

其中SIGHUP信号是可以修改的。这个函数语句意思是当父进程死亡时,发送一个SIGHUP信号给他的子进程。这样,我们就可以通过捕获信号知道父进程知否死亡了。


        下面是在上面代码修改之后的代码,实现了父进程和子进程死亡都可以互相通知。


//
//
//
//
// 1.注意,Linux是操作系统,很多函数的引用不用通过类引用,这个跟java/android有很大的区
// 别,所以需要特别注意哪一类函数是不需要通过类进行引用的,这些函数一般都是系统的函数,
// 就像jni接口一样,例如:IO的操作特别是文件操作,并发操作特别是进程操作。总的来说,就
// 是除了C/C++部分之外,系统的函数都是直接调用函数的
//
// 2.可以使用init_daemon函数生成守护进程
//
//
// Created by Administrator on 2015/11/5.
//

#include "ProcessManager.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <android/log.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <pthread.h>
#include <sys/ptrace.h>
#include <signal.h>


#ifndef LOG_TAG
#define LOG_TAG "Native"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#endif

void on_child_terminated(int sig)
{
    LOGE("child process is terminated");

}

void on_parent_terminated(int sig)
{
    LOGE("parent process is terminated");

}

//调用java方法
void check_wifi(JNIEnv * env)
{

    jclass clazz= NULL;
    jstring str_arg = NULL;
    jmethodID mid = NULL;
    jmethodID mstruct = NULL;
    clazz = env->FindClass("com/han/daemonprocess/daemon/DaemonProcess");
    if(clazz != NULL) {
        mid = env->GetMethodID(clazz, "setString", "(Ljava/lang/String;)V");
        if(mid != NULL) {
            mstruct = env->GetMethodID(clazz, "<init>", "()V");
            jstring str = env->NewStringUTF("hangertesting");
            jobject mobj = env->NewObject(clazz, mstruct);

            env->CallVoidMethod(mobj, mid, str);

        }
    }

}

bool ProcessManager::create_process(JNIEnv * env)
{


    check_wifi(env);

    struct sigaction sa_usr;

    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = on_child_terminated;
    //父进程接受信号函数,当子进程退出时,会自动给父进程发送SIGCHLD信号
    sigaction(SIGCHLD, &sa_usr, NULL);


    int count = 0;
    pid_t pid = fork();
    int status = -1;

    if (pid < 0) {
        LOGE("fork error for %m\n", errno);
    } else if (pid > 0) {
        LOGE("this is parent ,pid = %d\n", getpid());
        wait(&status);//父进程执行到此,马上阻塞自己,直到有子进程结束。当发现有子进程结束时,就会回收它的资源。

    } else {
        LOGE("this is child , pid = %d , ppid = %d\n", getpid(), getppid());

        //此处将子进程设置成组长,但是未脱壳,也就是没有销毁父进程
        setsid();
        //设置进程可以操作的路径为根目录
        chdir("/");

        //这个函数会请求shell当父进程死亡时给当前进程发送SIGHUP信号
        prctl(PR_SET_PDEATHSIG, SIGHUP);
        sa_usr.sa_flags = 0;
        sa_usr.sa_handler = on_parent_terminated;
        sigaction(SIGHUP, &sa_usr, NULL);//接受信号函数


        for (int i = 0; i < 10; i++) {
            count++;
            sleep(1);
            LOGE("count = %d\n", count);
        }
        exit(5);

    }
    LOGE("child exit status is %d\n", WEXITSTATUS(status));//status是按位存储的状态信息,需要调用相应的宏来还原一下

    LOGE("end of program from pid = %d\n", getpid());

    return pid < 0;


}
 
    注意,对于wait函数,如果在fork函数之前使用wait函数,那么wait函数返回的是-1,而正常情况下返回的是PID。如果我们仅仅是回收僵尸进程,那么使用wait(NULL)即可,这个时候成功则返回PID,失败则返回-1,并将errno置为ECHILD。
    需要注意的是,使用wait函数的话,如果参数不是NULL,wait会将结束状态赋值给这个参数。注意,由于状态被存储在整数的不同二进制位中,所以平常的读取比较麻烦,使用macro宏来读取:WIFEXITED和WEXITSTATUS,这两个宏像函数一样使用。WIFEXITED正常退出的话,返回非0值;WEXITSTATUS返回值是exit函数参数,如果不是正常退出,那么返回的是0。
 

        下面是改进上面代码让进程间重启。

        这里应该先了解,对于android,本身就是一个简化过的Linux系统。android和Linux一样,可执行程序在system/bin下,但是android相比Linux少了一些命令,如果需要在这个文件夹下写入可执行文件,可以下载busybox,然后加载上去。在Linux中有gdb,这是GNU调试桥的意思,跟android中的adb相对应。在Linux中有内核shell的操作,在android中,我们通过adb shell命令进入。最后需要注意的是,android相比Linux增加了一些执行程序,例如am命令,这里我们可以在源代码中查找到am.java进行查看。在android中除了从界面上启动程序还可以通过命令行启动程序,使用am命令行工具就可以实现。格式为:

# am start -n {包(package)名}/{包名}.{活动(activity)名称}

例如:

# am start -n com.example.android.helloactivity/com.example.android.helloactivity.HelloActivity

这里如果是在NDK中,想让android被销毁的主进程重启,只能通过使用命令

execlp( "am",            "am",            "startservice",            "--user",            g_userId,            "-n",            SERVICE_NAME, //注意此处的名称            (char *)NULL);

这里

//服务名称static const char* SERVICE_NAME = "com.han.daemonprocess/com.han.daemonprocess.MyService";

这里,使用exec函数开启了android进程的Service服务,当然是用am命令可以开启android的组件。只要组建开启,进程也就会开启的,毕竟组件就是在进程中的。

        这里开启android进程关键就是am命令!

        注意,双进程守护中,子进程未必需要原来的进程,也就是说,未必需要进程复活。我们可以重新开一个子进程就可以了,死掉的回收。毕竟使用Linux的进程重启命令是比较麻烦的!

        在Linux中,守护进程有test.c与init.c两个部分,可以使用int.c的init_daemon函数生成守护进程。但是在android中尽管源码有init.c文件,但是却没有了init_daemon函数!

        在android系统的根目录下/proc/下有运行的进程的文件,其中进程号就是文件名,名字为1的是init的进程的文件夹。

        注意,在NDK中开启的进程是跟android的进程不一样的,NDK中开启的进程是查看不到的,没有图标显示,即使在系统的设置中查看也查不到。在NDK中,或者说Linux中,如果想要重启进程,最好的方式就是通过命令行,当然通过编程也可以实现。

        这里使用exec函数重启进程其实过程跟使用命令是一样的,例如:使用shell命令执行ps命令,实际上shell命令调用fork函数创建一个子进程,然后使用exec函数将新进程完全替换成ps进程。Linux中使用exec函数进程替换,新进程的PID会跟被替换进程的一样。

        重启进程原理:/proc/下面的进程文件就是我们需要加载的程序,所以使用exec函数,将参数path或者file设置成想要重启的进程的文件,就可以重启进程了。

        尽管单纯的java/android方式不能实现,但是不要忘记,java/android给我们操作进程和调用系统的命令以及接口都留下了方法。这里需要先了解的有android的Zygote机制,Process类,Runtime类,以及android的系统命令接口Runtime.exec()方法,结合adb shell运行android shell内核,这也是创建进程的方式,尽管没什么用。

        对于Zygote是系统的东西,只能被系统调用,用于生成每一个app的进程,而android.os.Process这个类的start方法是不对外公开的,仅仅是Zygote等在内部生成进程时会调用它,而如果我们想调用他,只能通过反射调用,但是方法非常复杂,因为涉及到系统内部的东西。最关键的是创建的进程跟java创建进程的方式差不多,但是关键是创建的进程并不像创建线程一样能进行控制。所以不建议使用!

        如果想要使用应用层的代码创建进程,那么最好是使用java多进程,也就是使用Runtime.exec方法。这里需要注意,java中多进程可以使用ProcessBuilder以及Runtime两种形式,官方ProcessBuilder例子:

Process process = new ProcessBuilder()
*       .command("/system/bin/ping", "android.com")
*       .redirectErrorStream(true)
*       .start();
*   try {
*     InputStream in = process.getInputStream();
*     OutputStream out = process.getOutputStream();
*
*     readStream(in);
*
*   } finally {
*     process.destroy();
*   }
* }
这里返回的Process本身是抽象类java.lang.Process的子类java.lang.ProcessImpl。

而Runtime的使用一般如下:

  1. Process process = Runtime.getRuntime().exec("/system/bin/ping");  
  2. OutputStream stdout = process.getOutputStream();  
  3. InputStream stderr = process.getErrorStream();  
  4. InputStream stdin = process.getInputStream();  
  5. BufferedReader reader = new BufferedReader(new InputStreamReader(stdin));  
  6. BufferedReader err= new BufferedReader(new InputStreamReader(stderr));  
  7. BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdout));  

另外,还可以通过Runtime.exec方法运行命令创建进程,但是这些方式基本上都是没什么用的。


        使用应用层的java/android代码创建的进程非常的受限制,因为得到的是已经被系统封装好的进程,不能修改,所以基本上这些方法是完全没用的。


        所以最好使用NDK的方式创建多进程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值