两种启动方式

在Android系统中有两种启动方式分别为冷启动和热启动,定义如下:

  • 冷启动:冷启动就是通过点击桌面图标启动应用,这种启动方式由于后台没有该应用的进程,系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类、创建MainActivity类、加载主题样式Theme中的windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了后最后才进行contentView的measure/layout/draw显示在界面上。
    整个过程如下所示:
Application构造 ——> attachBaseContext()——>onCreate()——>Activity的构造方法——>onCreate()——>配置主题中背景等属性——>onStart()——>onResume()——>测量布局绘制显示在界面上。
  • 热启动:当启动应用时,后台已有该应用的进程,所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这种方式叫热启动。热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application,因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。

启动时间和启动时间的测量

启动时间指的是从点击应用的启动图标开始直到我们看到了界面的第一帧,这段时间就是应用的启动时间。应用启动时间可以通过如下方式进行测量:

adb shell am start -W [packageName]/[packageName.MainActivity]

减小应用启动时间的方法

  • 在Application的onCreate()方法中不要进行耗时操作的初始化,尽量将耗时间操作放在后台线程异步处理。
  • 对于MainActivity,由于在获取到第一帧前,需要对布局进行测量绘制操作所以尽量减少布局的层次,以及采用ViewStab延迟加载策略,并在onCreate、onStart、onResume方法中避免做耗时操作。
  • 由于在冷启动时刻,WindowManager会先加载app主题样式中的windowBackground做为app的预览元素,然后再真正去加载activity的layout布局,所以可以通过给windowBackground设置一个图片在视觉上来骗过用户

推荐使用如下的解决方案,我这边尝试了下还是有效果的,但是如果本身应用在这段时间如果就没处理什么事情,就比较难看出这个效果了:
https://github.com/DreaminginCodeZH/MaterialColdStart

英文不好的可以看下下面这篇博客:
http://blog.csdn.net/ccsutofly/article/details/49990939

  1. 比较粗鲁的方式-杀进程
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
  1. 使用ActivityManager的restartPackage方法,这种方法需要注意的是需要运行在Android 1.5 API Level为3以上才可以,并且需要权限
ActivityManager am = (ActivityManager)getSystemService (Context.ACTIVITY_SERVICE);
am.restartPackage(getPackageName());

所需权限:

<uses-permission android:name="android.permission.RESTART_PACKAGES"></uses-permission>
  1. 使用FLAG_ACTIVITY_CLEAR_TOP 方式

使用FLAG_ACTIVITY_CLEAR_TOP 启动第一个Activity也就是MAIN的那个Activity然后在需要全部退出时调用finish

Intent intent = new Intent(); 
intent.setClass(xxxx,xxxxx);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
````

下面是我自己之前写的一个退出整个应用的方式使用的是ActivityLifeCycleCallback + 单例也是个人比较推崇的方式:

public abstract class MyApplication extends Application {

private static List<Activity> mActivityList = new LinkedList<Activity>();
@Override
public void onCreate() {
    super.onCreate();
    if(mActivityList!= null) {
        mActivityList.clear();
    }
    registerActivityLifecycleCallbacks (new Application.ActivityLifecycleCallbacks() {

        public void	onActivityCreated(Activity activity, Bundle savedInstanceState) {
            if(mActivityList != null) {
                mActivityList.add(activity);
            }
        }
        public void	onActivityDestroyed(Activity activity) {
            if(mActivityList != null && mActivityList.contains(activity)) {
                mActivityList.remove(activity);
            }
        }
        public void	onActivityPaused(Activity activity) {
        }

        public void	onActivityResumed(Activity activity) {

        }
        public void	onActivitySaveInstanceState(Activity activity, Bundle outState) {}
        public void	onActivityStarted(Activity activity) {
        }
        
        public void	onActivityStopped(Activity activity) {
        }
    });
}

public static void  exitAppication(MixEngineService.callbackBefore callbackBefore) {
    if(mActivityList != null) {
        int activityCount = mActivityList.size();
        for(int i =0;i<activityCount;i++) {
            Activity activity = mActivityList.get(i);
            if(activity != null) {
                activity.finish();
                activity.overridePendingTransition(0, R.anim.exitapplication_anim);
            }
        }
        System.exit(0);
    }
}

}


转眼已经工作两年了,上个月向公司递交了离职申请,第一次辞职,在离职面谈的时候HR很诧异地对我说:”公司走的人大多数都是找好下家后再提交辞职申请的,你为什么不现在这里找到下家一下再走呢“,其实这也算得上我自己对第一份工作的认真吧,拿着公司的薪水找下家,自己心里不是很舒坦,也不知道对于工作许久的人来说这算不上天真,如果算天真,也就天真一回吧,7月20号也就是明天是我在团队中上班的最后一天,也相当于对于公司的一份感恩吧,可以肯定的是这不是出于自信,但是个人始终坚信一点”认真“,”努力“,”坚持“这三样做到了,结果都不会很差,剩下就靠机遇了。接下来的一段时间会一边找工作一边借着这个空档期给自己的下一阶段充充电。

这两年发生了好多事情,一向坚强的爸爸做了心脏起搏器植入的手术,当时我连续失眠了好久,妈妈手摔骨折了,虽然都好了但是还是心有余悸,当然还有一些收获:我和相恋四年的女朋友结婚了,没有钻戒,没有婚礼,我们用自己所有的积蓄付了房子首付,希望能够在明年将爸爸妈妈接过来。

在工作,技术提升方面除了公司日常工作外,下班的8个小时也被充分地利用起来的了,每天写写博客,写写代码,认识了一些产品方面的同事,在空余时间也会向他们沟通产品设计中的一些事情。我也会给他们分享一些技术上的东西。也是基于这种分享的快乐所以有了这个专门的博客。

这个博客是在今年二月份开始写的,之前都是在本地笔记上记录自己的知识积累,在读书的时候包括刚刚开始工作的时候特别讨厌写文档,那时候觉得买个技术书籍用得时候查阅就OK了何必花时间去做这种别人已经做好的工作,但是在自己工作中遇到的问题越来越多,感觉那些技术书籍包括博客上的东西就越来越凌乱了,并且很多东西别人博客上的不一定都是正确的,所以有了整理自己知识体系的想法,起初的时候用的是笔记软件,也就是这些博客的原始素材,后来在无意中好友 pf18 向我推荐了Hexo,也就在今年二月份开始,开始慢慢整理自己的笔记,并写成博文,写笔记和写技术博客是有很大的区别的,写笔记的时候会比较随意,只要自己看得明白就好,不用插图,但是博客就不一样了,必须认真地表述,认真琢磨。所以经过初期的计划,就开始利用下班时间写博客。博客计划主要分成六个部分:

目前是处于第一阶段,主要是一些Android开发中的常用技术。
第二阶段会是基于AOSP 的 源码分析由于之前是在做多媒体小组,所以会侧重于MediaPlayer 播放框架,MediaRecorder,MediaScanner,Music,Video,Camera,SoundRecorder, MediaProject,流媒体方面的源码解析。
第三阶段会是一些开源库的代码分析
第四部分会是一些自定义View和自定义动画的博客
第五部分是OpenCV,OpenGL,ffmpeg的学习总结
第六部分会是一些产品设计方面的总结
这些内容会有计划,但是不定期得发布,希望自己能够坚持下来。

下面是第一阶段博文的目录,共117篇,涵盖了开发过程中所需的各个方面技术,直到产品发布为止。在下面的博客中有参考了很多优秀的博客,也谢谢他们的分享精神让自己在技术方面有了较大的提高。也是出于感恩,有了下面的博客。

Android初步系列共有21篇

* Android 初步之Android 体系结构 
* Android 初步之Android SDK 目录结构
* Android 初步之Android组件 概图
* Android 初步之Android组件 Context
* Android 初步之Android组件 Activity
* Android 初步之Android组件 Service
* Android 初步之Android组件 BroadcastReceiver
* Android 初步之Android组件 Intent
* Android 初步之Android组件 Content Provider
* Android 初步之Android组件 Fragment
* Android 初步之布局管理
* Android 初步之资源的使用
* Android 初步之数据存储
* Android 初步之Adapter 与 ListView
* Android 初步之Timmer && AlarmManager
* Android 初步之网络与定位服务
* Android 初步之 使用Android studio 开发 AIDL
* Android studio 下使用JNI
* Android 初步之Android 安全机制
* Android 初步之剪贴板
* Android 初步之语音朗读和语音识别

Android 进阶系列共有96篇:

* Android 进阶之Android 绘图 零 大纲
* Android 进阶之Android 绘图 一 Android坐标体系及屏幕尺寸相关概念
* Android 进阶之Android 绘图 二 画笔Paint
* Android 进阶之Android 绘图 三 画布Canvs以及SurfaceView
* Android 进阶之Android 绘图 四 图像容器Bitmap Drawable以及图像变换
* Android 进阶之Android 绘图 五 硬件加速
* Android 进阶之事件交互 一 触摸事件和键盘事件
* Android 进阶之事件交互 二 滑动事件
* Android 进阶之动画 零 大纲
* Android 进阶之动画 一 视图动画
* Android 进阶之动画 二 逐帧动画
* Android 进阶之动画 三 属性动画
* Android 进阶之动画 四 布局动画
* Android 进阶之动画 五 动画集合
* Android 进阶之动画 六 动画事件
* Android 进阶之动画 七 插值器
* Android-进阶之自定义View 一 自定义View 流程
* Android-进阶之自定义View 二 View的刷新模式
* Android-进阶之自定义View 三 实现自定义View的方式
* Android 进阶之自定义View 四 自定义ViewGroup
* Android 进阶之设计模式 一 MVC模式
* Android 进阶之设计模式 二 MVP模式
* Android 进阶之设计模式 三 MVVM模式
* Android 进阶之设计模式 三 MVVM模式 DataBinding [一] 搭建开发环境
* Android 进阶之设计模式 三 MVVM模式- DataBinding[二]
* Android 进阶之设计模式 (转)Android-CleanArchitecture
* Android 进阶之设计模式 六大原则
* Android 进阶之设计模式 创建型模式-单例模式
* Android 进阶之设计模式 原型模式
* Android 进阶之设计模式 外观模式
* Android 进阶之设计模式 备忘录模式
* Android 进阶之设计模式 命令模式
* Android 进阶之设计模式 代理模式
* Android 进阶之设计模式 享元模式
* Android 进阶之设计模式 中介者模式
* Android 进阶之设计模式 工厂方法模式 && 抽象工厂模式
* Android 进阶之设计模式 适配器模式
* Android 进阶之设计模式 迭代器模式
* Android 进阶之设计模式 责任链模式
* Android 进阶之重要的控件 CardView
* Android 进阶之重要的控件 RecycleView
* Android 进阶之重要的控件 Toolbar
* Android 进阶之性能优化 UI视图优化
* Android 进阶之性能优化 内存优化
* Android 进阶之性能优化 电量优化
* Android 进阶之性能优化 网络优化
* Android 进阶之性能优化 代码级优化
* Android 进阶之第三方库的介绍 butterknife
* Android 进阶之第三方库的介绍 Picasso
* Android 进阶之第三方库的介绍 Glide
* Android 进阶之第三方库的介绍 Fresco
* Android 进阶之第三方库的介绍 Gson
* Android 进阶之第三方库的介绍 Realm [一] 基础用法
* Android 进阶之第三方库的介绍 Realm [二] 进阶用法
* Android 进阶之第三方库的介绍 Retrofit
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 一 [事件源Observable]
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 二 [事件源的过滤]
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 三 [事件源的转换]
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 四 [事件源的组合]
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 五 [线程调度]
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 六 [转]RxJava使用场景小结
* Android 进阶之第三方库的介绍 RxJava && RxAndroid 七 [学习RxJava必备的文章]
* Android 进阶之Android开发中的质量跟踪工具
* Android 进阶之工具的使用 Findbugs
* Android 进阶之工具的使用 LeakCanary
* Android 进阶之工具的使用 Lint
* Android 进阶之工具的使用Checkstyle
* Android 进阶之工具的使用 TraceView
* Android 进阶之 混淆技术
* Android 进阶之 反编译
* Android 进阶之自动化构建工具Travis CI
* Android 进阶之自动化测试 之 AndroidJunitRunner
* Android 进阶之自动化测试 Junit
* Android 进阶之自动化测试 Monkey测试
* Android 进阶之自动化测试Espreso
* Android 进阶之自动化测试 UIAutomator
* Android 进阶之自动化测试Robolectric
* Android 进阶之版本释放流程
* Android 进阶之Apk签名
* Android 进阶之多线程技术
* Android 进阶之JNI 开发 一 概述
* Android 进阶之 NDK 开发
* Android 进阶之JNI 开发 二 JNI数据类型介绍
* Android 进阶之JNI 开发 三 访问对象成员变量和成员方法
* Android 进阶之JNI 开发 四 局部引用,全局引用和弱全局引用
* Android 进阶之JNI 开发 五 JNI 异常处理

如果大家觉得写得不好或者有一些其他建议的可以给我留言,如果大家觉得写得不错的话也希望大家在转载的时候帮我推广下,在转载的时候将下面的贴到博客上方即可,因为这份坚持也需要有大家的鼓励。博客的About 是我的个人简介及简历,如果有志同道合的朋友的话可以通过上面的联系方式和我取得联系。
希望能够在技术成长的道路上能够给大家力所能及的帮助,也希望能够得到大家的意见和建议。

文/tbfungeek
网站链接:http://tbfungeek.github.io/
邮箱:tbfungeek@163.com

JNI异常和Java异常有一点很重要的区别是:

当Java中发生异常时如果没有使用try…catch来捕获,会导致程序Crash这种情况下后续的代码不会被执行。而在调用JNI接口的时候如果发生异常那么后续的代码不会停止,还会继续往下执行,所以在这种情况下需要使用return退出后续执行。
下面是一个很典型的异常处理代码片段:

JNIEXPORT void JNICALL
Java_com_idealist_myapplication_MainActivity_callMethodWithException(JNIEnv *env,
jobject instance) {
//调用会抛出异常的方法
jclass cls = (*env)->GetObjectClass(env,instance);
jmethodID methodid = (*env)->GetMethodID(env,cls,"methodWithException","()V");
(*env)->CallVoidMethod(env, instance, methodid);

//检查调用上述方法后异常是否抛出
jthrowable exception = (*env)->ExceptionOccurred(env);
//如果有异常
if(exception) {
//释放资源
(*env)->DeleteLocalRef(env,cls);
//打印异常信息
(*env)->ExceptionDescribe(env);
//清除异常
(*env)->ExceptionClear(env);
//抛出自定义的异常
jclass exclass = (*env)->FindClass(env, "java/lang/Exception");
if (exclass == NULL) {
return;
}
(*env)->ThrowNew(env, exclass, "Here is a Exception Occur!");
//将程序流终止,这里由于是最后了所以可以省略,但是如果不是处于最后那么一定不要忘记添加return终止程序运行。
return;
}
}

大家都知道,如果一个Java对象没有被其它成员变量或静态变量所引用的话,就随时有可能会被GC回收掉。所以我们在编写本地代码时,要注意从JVM中获取到的引用在使用时被GC回收的可能性。

JNI中有三种引用分别是:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)

局部引用

局部引用可以通过NewLocalRef和FindClass、NewObject、GetObjectClass和NewCharArray等方法进行创建,一般是在方法中使用并且值在创建它的方法内有效,它会增加引用计数从而阻止GC回收所引用的对象,但是一旦出了这个方法就变得无效了。也就是说它的生命周期只在创建该局部引用的方法内部。在用完局部引用之后可以选择不手动释放而是在本地方法执行完之后由JVM自动释放,还可以手动调用DeleteLocalRef释放。但是一般最好手动在刚用完之后就立刻释放,因为JNI会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制(Android上的JNI局部引用表最大数量是512个),就会造成局部引用表溢出,从而导致程序崩溃。并且有可能在你申请并使用完局部引用之后的操作还需要比较大的空间,这时候如果不即使释放有可能导致OOM。在使用局部引用的时候还需要注意的是局部引用不能跨线程使用,只在创建它的线程有效。不要试图在一个线程中创建局部引用并存储到全局引用中,然后在另外一个线程中使用。

全局引用

与局部引用创建方式不同的是,全局变量只能通过NewGlobalRef方法创建。JVM不会自动释放它,而是需要我们手动释放才会失效,并且它可以跨方法,跨线程使用。

jstring  
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;
...
if (stringClass == NULL) {
jclass localRefCls =
(*env)->FindClass(env, "java/lang/String");
if (localRefCls == NULL) {
return NULL; /* exception thrown */
}
/* Create a global reference */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* The local reference is no longer useful */
(*env)->DeleteLocalRef(env, localRefCls);
/* Is the global reference created successfully? */
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}

弱全局引用

调用NewWeakGlobalRef基于局部引用或全局引用创建,不能增加引用计数,不会阻止GC回收所引用的对象,也就是说它对应的Java对象生命周期依然取决于虚拟机,这就导致即便弱全局引用没有被释放,其引用的Java对象有可能已经被释放,和全局引用一样可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候进行回收而被释放。或者调用DeleteWeakGlobalRef手动释放。

引用比较与弱引用是否被回收

给定两个引用(不管是全局、局部还是弱全局引用),只需要调用IsSameObject就可以判断两个是否指向相同的对象,例如:

(*env)->IsSameObject(env, obj1, obj2)

如果obj1和obj2指向相同的对象,则返回JNI_TRUE,否则返回JNI_FALSE。但是使用IsSameObject比较弱全局引用与NULL的时候,返回值的意义是有点特别的:比如:

jboolean isWeakRefGC = (*env)->IsSameObject(env,gw_obj_ref, NULL);

在上面的例子中,如果gw_obj_ref指向的引用已经被回收,会返回JNI_TRUE,如果gw_obj_ref仍然指向一个活动对象,会返回JNI_FALSE。

访问对象的成员属性和成员方法:

下面我们通过一个例子来介绍对象成员属性和成员方法的访问:

首先我们先定义一个测试类,在这篇博客中将对测试类的testString方法进行访问和设置,我们先看下这个测试类:

public class Testjni {

private String testString;

public String getTestString() {
return testString;
}

public void setTestString(String testString) {
this.testString = testString;
}
}
//通过调用Java层方法来实现某个功能
JNIEXPORT void JNICALL
Java_com_idealist_myapplication_MainActivity_setTestString(JNIEnv *env, jobject instance,
jobject obj, jstring str_) {
//获得某个对象的类类型
jclass testCls = (*env)->GetObjectClass(env,obj);
//获得对应的方法id
jmethodID methodId = (*env)->GetMethodID(env,testCls,"setTestString","(Ljava/lang/String;)V");
if(methodId == NULL) {
return;
}
//通过方法id 调用该方法
(*env)->CallVoidMethod(env,obj,methodId,str_);
}

//通过调用Java层的方法,来获取testString
JNIEXPORT jstring JNICALL
Java_com_idealist_myapplication_MainActivity_getTestString(JNIEnv *env, jobject instance,
jobject obj) {

jclass testCls = (*env)->GetObjectClass(env,obj);
jmethodID methodId = (*env)->GetMethodID(env,testCls,"getTestString","()Ljava/lang/String;");
if(methodId == NULL) {
return NULL;
}
jstring str = (*env)->CallObjectMethod(env, obj, methodId);
return str;
}

//通过直接访问属性域,来设置testString值。
JNIEXPORT void JNICALL
Java_com_idealist_myapplication_MainActivity_setTestStrByField(JNIEnv *env, jobject instance,
jobject obj, jstring str_) {

jclass testCls = (*env)->GetObjectClass(env, obj);
jfieldID fieldId = (*env)->GetFieldID(env,testCls,"testString","Ljava/lang/String;");
if(fieldId == NULL) {
return;
}
(*env)->SetObjectField(env, obj, fieldId, str_);
}
//通过直接访问属性域,来获取testString值。
JNIEXPORT jstring JNICALL
Java_com_idealist_myapplication_MainActivity_getTestStringByField(JNIEnv *env, jobject instance,
jobject obj) {
jclass testCls = (*env)->GetObjectClass(env,obj);
jfieldID fieldId = (*env)->GetFieldID(env,testCls,"testString","Ljava/lang/String;");
if(fieldId == NULL) {
return NULL;
}
jstring str = (*env)->GetObjectField(env, obj, fieldId);
return str;
}

这个是来自JNI Programmer’s Guide and Specification文档中的一个例子:

JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)
{
jfieldID fid;/* store the field ID */
jstring jstr;
const char *str;
/* 根据jobject对象实例创建指向它的类引用 */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* 根据上述找到的jclass类型以及类名和标识符寻找fieldID 最后一个参数可以通过javap -p -s InstanceFieldAccess 来查看*/
fid = (*env)->GetFieldID(env, cls, "s","Ljava/lang/String;");
if (fid == NULL) {
return; /* failed to find the field */
}
/* 读取对象的成员属性域*/
jstr = (*env)->GetObjectField(env, obj, fid);
/* 转化为C语言的字符串形式 */
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
/* 释放掉无用的资源 */
(*env)->ReleaseStringUTFChars(env, jstr, str);
/* 创建一个新的字符串 */
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
/*设置对象属性值*/
(*env)->SetObjectField(env, obj, fid, jstr);
}

下面是访问静态成员变量的例子:

JNIEXPORT void JNICALL
Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj)
{
jfieldID fid;
/* store the field ID */
jint si;
/* Get a reference to obj’s class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the static field si in cls */
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
if (fid == NULL) {
return; /* field not found */
}
/* Access the static field si */
si = (*env)->GetStaticIntField(env, cls, fid);
printf(" StaticFieldAccess.si = %d\n", si);
(*env)->SetStaticIntField(env, cls, fid, 200);
}

下面是访问静态成员方法的例子:

JNIEXPORT void JNICALL
Java_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid =(*env)->GetStaticMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallStaticVoidMethod(env, cls, mid);
}

上面我们介绍了通过GetObjectClass来获得某个jclass,下面将通过FindClass来获取:

jobject thd = ...; /* a java.lang.Thread instance */
jmethodID mid;
jclass runnableIntf = (*env)->FindClass(env, "java/lang/Runnable");
if (runnableIntf == NULL) {
... /* error handling */
}
mid = (*env)->GetMethodID(env, runnableIntf, "run", "()V");
if (mid == NULL) {
... /* error handling */
}
(*env)->CallVoidMethod(env, thd, mid);
... /* check for possible exceptions */

访问属性和方法总结:

调用对象的静态方法的步骤

1. 使用FindClass或者GetObjectClass来获取jclass对象
2. 通过GetMethodID获取在某个类中的方法的id值。这一步需要步骤1中获取到的jclass对象,方法名,方法签名
3. 通过这个id值来调用通过CallStaticXXXMethod方法传入id,参数
4. 释放局部变量

调用对象的实例方法的步骤(没有传入jobject的情况)

1. 使用FindClass搜索某个类
2. 获取某个类的默认构造方法
3. 通过NewObject创建该类的实例
4. 通过GetMethodID查找要调用的实例对象的方法id
5. 通过CallXXXMethod调用实例的方法
6. 释放局部变量

调用对象的实例方法的步骤(有传入jobject的情况)

1. 如果有传入对象的话可以使用GetObjectClass获取jclass对象
2. 通过GetMethodID查找要调用的实例对象的方法id
3. 通过CallXXXMethod调用实例的方法
4. 释放局部变量

获取和设置实例对象的属性

1. 通过 GetObjectClass 获取类的jclass引用
2. 通过GetFieldID获取实例变量的属性id
3. 通过GetXXXField来获取某个属性变量的值
4. 通过setObjectField设置某个属性对象的值
5. 删除局部变量

获取和设置某个静态属性

1. 通过FindClass 获取类的jclass引用
2. 通过GetStaticFieldID获得静态属性id
3. 通过GetStaticXXXField来获取某个属性变量的值
4. 通过setStaticObjectField设置某个属性对象的值
5. 删除局部变量

访问父类方法和访问构造函数:

  1. 访问父类方法:

我们可以调用在父类中已经定义但是已经被覆写的实例方法,JNI 中提供了一系列的CallNonvirtual 方法用来解决这个问题,为了能够调用父类的方法需要如下处理:

  • 使用GetMethodID或者GetStaticMethodID从父类引用中获得方法id
  • 传入对象,父类,方法id到上面提到的CallNonvirtual方法中。
  1. 访问构造函数:
    在JNI中可以通过和调用实例对象的其他方法一样调用构造方法,唯一的区别是需要传入”“作为方法名”V” 作为返回类型标识。然后通过调用 NewObject并传入方法id来创建出实例对象。
    jstring
    MyNewString(JNIEnv *env, jchar *chars, jint len){
    jclass stringClass;
    jmethodID cid;
    jcharArray elemArr;
    jstring result;
    stringClass = (*env)->FindClass(env, "java/lang/String");
    if (stringClass == NULL) {
    return NULL; /* exception thrown */
    }
    /* Get the method ID for the String(char[]) constructor */
    cid = (*env)->GetMethodID(env, stringClass,"<init>", "([C)V");
    if (cid == NULL) {
    return NULL; /* exception thrown */
    }
    /* Create a char[] that holds the string characters */
    elemArr = (*env)->NewCharArray(env, len);
    if (elemArr == NULL) {
    return NULL; /* exception thrown */
    }
    (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
    /* Construct a java.lang.String object */
    result = (*env)->NewObject(env, stringClass, cid, elemArr);
    /* Free local references */
    (*env)->DeleteLocalRef(env, elemArr);
    (*env)->DeleteLocalRef(env, stringClass);
    return result;
    }

下面通过一个例子来验证上述的知识点:

JNIEXPORT void JNICALL
Java_com_idealist_myapplication_MainActivity_testSayHello(JNIEnv *env, jobject instance,
jstring name_) {
//获取ChildClass类
jclass clzz = (*env)->FindClass(env, "com/idealist/myapplication/ChildClass");
//获取ChildClass的构造函数
jmethodID contructureid = (*env)->GetMethodID(env,clzz,"<init>","()V");
//使用构造函数创建一个对象
jobject childObj = (*env)->NewObject(env, clzz, contructureid);

//通过这个对象调用ChildClass sayHello的方法
jmethodID sayhelloid = (*env)->GetMethodID(env,clzz,"sayHello","(Ljava/lang/String;)V");
(*env)->CallVoidMethod(env, childObj, sayhelloid, name_);

//通过FindClass获取FatherClass类
jclass suppercls = (*env)->FindClass(env,"com/idealist/myapplication/FatherClass");
//获取FatherClass的sayHello的id
jmethodID sayhellofromSuper = (*env)->GetMethodID(env,suppercls,"sayHello","(Ljava/lang/String;)V");
//通过子类对象,父类jclass,父类方法id来调用父类的方法。
(*env)->CallNonvirtualVoidMethod(env, childObj, suppercls, sayhellofromSuper, name_);

//释放局部变量
(*env)->DeleteLocalRef(env, clzz);
(*env)->DeleteLocalRef(env, contructureid);
(*env)->DeleteLocalRef(env, childObj);
(*env)->DeleteLocalRef(env, sayhelloid);
(*env)->DeleteLocalRef(env, suppercls);
(*env)->DeleteLocalRef(env, sayhellofromSuper);
}

输出结果:

07-05 21:52:49.212 20472-20472/com.idealist.myapplication I/xiaohai.lin: Constructure of FatherClass
07-05 21:52:49.212 20472-20472/com.idealist.myapplication I/xiaohai.lin: Constructure of ChildClass
07-05 21:52:49.212 20472-20472/com.idealist.myapplication I/xiaohai.lin: From Child class Hello Jimmy
07-05 21:52:49.212 20472-20472/com.idealist.myapplication I/xiaohai.lin: From Super class Hello Jimmy

缓存FieldID 以及methodID

每次获取FieldID以及和methodID都需要对字段、方法的名字和描述符进行一费时的检索过程。为了减小检索过程的耗时行为可以将第一次获取到的字段id缓存起来,缓存方法有两种,一种是通过将id声明为static变量在使用时缓存,另一种实在类静态初始化的时候。

  • 使用时缓存:
    JNIEXPORT void JNICALL
    Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {
    //使用静态变量缓存
    static jfieldID fid_s = NULL; /* cached field ID for s */
    jclass cls = (*env)->GetObjectClass(env, obj);
    jstring jstr;
    const char *str;
    //判断是否是第一次
    if ( fid_s == NULL) {
    fid_s = (*env)->GetFieldID(env, cls, "s","Ljava/lang/String;");
    if ( fid_s == NULL) {
    return; /* exception already thrown */
    }
    }
    printf("In C:\n");
    jstr = (*env)->GetObjectField(env, obj, fid_s );
    str = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (str == NULL) {
    return; /* out of memory */
    }
    printf(" c.s = \"%s\"\n", str);
    (*env)->ReleaseStringUTFChars(env, jstr, str);
    jstr = (*env)->NewStringUTF(env, "123");
    if (jstr == NULL) {
    return; /* out of memory */
    }
    (*env)->SetObjectField(env, obj, fid_s , jstr);
    }
  • 类初始化时缓存:
    class InstanceMethodCall {
    private static native void initIDs();
    private native void nativeMethod();
    private void callback() {
    System.out.println("In Java");
    }
    public static void main(String args[]) {
    InstanceMethodCall c = new InstanceMethodCall();
    c.nativeMethod();
    }
    static {
    System.loadLibrary("InstanceMethodCall");
    initIDs();
    }
    }

    //在类初始化的时候就获取要使用方法id或者成员变量id,并缓存起来。
    jmethodID MID_InstanceMethodCall_callback;
    JNIEXPORT void JNICALL
    Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls)
    {
    MID_InstanceMethodCall_callback =
    (*env)->GetMethodID(env, cls, "callback", "()V");
    }

那么到底用哪种方式比较合适呢?这个必须依据实际情况:
如果我们不能修改方法或者字段所在的类的源码的话,这时候就不用说得用使用时缓存,但是如果可以修改的话尽量使用静态初始化时缓存,因为在使用时缓存需要每次使用的时候都需要判断一下才能使用。这样效率往往不高。

是否需要引入jni

在我们印象中引入jni会提高运行效率,事实并不是这样的java/native 比起 JVM 内部的 java/java 来说有一个调用转换过程,在把控制权和入口切换给本地方法之前,VM 必须做一些额外的操作来创建参数和栈帧。这个步骤会导致java/native 调用比java/java 要慢。我们再来看下native/java 调用和 java/native从技术角度来看, native/java 调用和 java/native 是相似的。但实际上 native/java调用很少见,因此VM 通常不会优化 native/java 这种回调方式。
这就导致了native/java 调用的消耗可以达到 java/java 调用的好几倍。并且引入jni会对调试引入极大的难度,所以不是万不得已最好不要引入Jni。

JNI数据类型

如果需要使用Jni中定义的标准数据类型需要将jni.h include到源码中。
JNI 提供的数据类型包括两类一类是基本数据类型,一类是引用数据类型:

基本数据类型总共有八种如下所示:

引用类型就比较多了整个继承关系如下所示:

还有一个是与访问类对象成员以及类对象方法有关的jfieldID以及jmethodID,这两个方法将会在介绍类成员调用的部分进行介绍。

关于八种基本数据类型我们先不再这里进行介绍了,我们重点介绍下字符串相关的类型以及数组相关类型调用的方法:

字符串相关方法:

jstring 对应的是Java语言中的strings类型,它和标准C语言下的字符串类型是不一样的,C语言中的字符串使用的是char *,是一个指向字符串的一个指针。所以我们需要在native层代码中使用字符串之前将jstring转换为C语言类型的字符串。JNI 提供了Unicode和UTF-8字符串到C语言字符类型之间的相互转换。GetStringUTFChars 这个方法可以将jstring类型的字符串转换为char * 类型,但是这里需要注意的是我们在调用这个方法进行转换的时候不要忘了对返回值进行判空处理,因为Java虚拟机需要分配内存用于存储UTF字符串。这有可能导致内存分配失败。还需要注意的是我们在使用完这些字符后需要调用ReleaseStringUTFChars来释放之前申请的内存。如果没有及时调用ReleaseStringUTFChars或者调用失败,那么就会导致内存泄漏,这个是很严重的问题。
接下来我们还需要看下GetStringUTFChars的第三个参数:这个是用于表示当前访问的字符串是来自原始字符串的拷贝还是直接指向原始字符串,如果返回的为JNI_TRUE那么表示返回的字符串是来自原始字符串的拷贝,如果返回的是JNI_FALSE那么返回的字符串是直接指向原来的字符串,这种情况下要注意不能去修改返回值,否则原始的字符串内容也将会同时被修改。一般我们在不需要关心Java虚拟机返回的是拷贝还是指向原始位置的指针的时候我们可以传递NULL;

如果我们需要使用将C语言类型的字符串转换为Java类型的字符串,那么需要使用NewStringUTF,如果这个方法中分配内存失败那么将会抛出OutOfMemoryError异常,并且返回NULL。

JNIEXPORT jstring JNICALL
Java_com_idealist_myapplication_MainActivity_getLine(JNIEnv *env, jobject instance, jstring line_) {

jboolean isCopy = JNI_FALSE;
const char *line = (*env)->GetStringUTFChars(env, line_, &isCopy);
if(line == NULL) {
return NULL;
}
char buf[256];
stpcpy(buf," This is a String add by xiaohai ");
strcat(buf,line);
(*env)->ReleaseStringUTFChars(env, line_, line);
return (*env)->NewStringUTF(env, buf);
}

下面是字符串相关的JNI方法的说明:

方法名 说明
GetStringChars/ReleaseStringChars 获取或者释放一个指向Unicode格式字符串的内容的指针,这种情况下有可能返回字符串的拷贝
GetStringUTFChars/ReleaseStringUTFChars 获取或者释放一个指向UTF格式字符串的内容的指针,这种情况下有可能返回字符串的拷贝
GetStringLength 返回在Unicode string中的字符串个数
GetStringUTFLength 返回在UTF-8 string中的字符串个数
NewString 通过传入的Unicode C string 创建一个java.lang.String对象
NewStringUTF 通过传入的UTF-8 C string 创建一个java.lang.String对象
Get/ReleaseStringCritical 这两个方法一般成对使用,在这两个方法之间的native代码必须不能调用哪些会导致当前线程阻塞或者等待其他运行在Java虚拟机上的线程,这些限制可以避免Java虚拟机在native代码持有一个通过GetStringCritical返回的直接指针的时候进行垃圾回收
GetStringUTFRegion/SetStringRegion 由于GetStringUTFRegion没有进行内存分配所以我们不需要进行out-of-memory条件的判断,并且不需要对资源进行释放
GetStringUTFRegion/SetStringRegion 这个与上面的类似

下面是关于如何选择对应方法的说明:

数组相关方法:

使用GetIntArrayRegion方法来访问:

JNIEXPORT jint JNICALL
Java_com_idealist_myapplication_MainActivity_sumOfTheArrayByRegion(JNIEnv *env, jobject instance,
jintArray intArray_, jint size) {
jint buf[size];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, intArray_, 0, size, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}

第三个参数表示起始的index,第四个参数表示需要copy的元素的个数,一旦这些元素都被拷贝到C缓存器那么我们就可以在native层使用这些参数了。同样我们可以使用SetIntArrayRegion方法来设置某个数组的值。对于小型,大小固定的数组Get/SetArrayRegion是最有效的方式,因为在C堆栈中分配内存是十分快速的方式。

使用GetIntArrayElements方法来访问

Java_com_idealist_myapplication_MainActivity_sumOfTheArray(JNIEnv *env, jobject instance,
jintArray intArray_, jint size) {
jint *intArray = (*env)->GetIntArrayElements(env, intArray_, NULL);
if(intArray == NULL) {
return 0;
}
jint sum = 0;
jint index =0;
for(index = 0;index<size;index++) {
sum += intArray[index];
}
(*env)->ReleaseIntArrayElements(env, intArray_, intArray, 0);
return sum;
}

GetArrayLength 返回数组中的元素长度
NewArray 创建一个指定长度的数组

之前写过在Android 使用NDK进行开发的博客,Android Studio 下使用JNI 但是这种方式很麻烦,我觉得NDK只是一个工具不应该在配置上弄得十分麻烦,所以我在看到gradle-experimental插件进行JNI开发的时候我就好不犹豫地进行了尝试,下面我们就来看下如何使用gradle-experimental进行JNI开发:

  • 添加NDK路径

  • 配置Project的build.gradle

将项目build.gradle配置文件的dependencies节点中中classpath的值改为对应的gradle-experimental插件。如果你不知道gradle-experimental最新的版本可以使用如下方式查询,它会返回目前的最新插件版本。

dependencies {
classpath 'com.android.tools.build:gradle-experimental:0.7.2'
}

修改app目录下的build.gradle

//将插件名由'com.android.application'换成'com.android.model.application'。
apply plugin: 'com.android.model.application'
//新增加model节点,将原来android除了defaultConfig外的其他节点全部移到android节点外并与android并列,并且前面的名字加上android.。
model {
//该节点下的全部都换成 “ = ”
android {
compileSdkVersion = 23
buildToolsVersion = "23.0.3"
defaultConfig {
applicationId = "com.idealist.myapplication"
//这里修改为minSdkVersion.apiLevel
minSdkVersion.apiLevel = 15
//这里修改为targetSdkVersion.apiLevel
targetSdkVersion.apiLevel = 23
versionCode = 4
versionName = "1.0.1"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
}
android.buildTypes {
release {
minifyEnabled false
//修改为proguardFiles.add
proguardFiles.add(file("proguard-rules.txt"))
}
}
//新增节点
android.ndk {
moduleName "testndk"
ldLibs.addAll(['log'])
cppFlags.add("-std=c++11")
cppFlags.add("-fexceptions")
stl 'gnustl_shared'
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha1'
testCompile 'junit:junit:4.12'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support:support-annotations:23.3.0'
}
  • 打开MainActivity 定义native方法,这时候方法会提示错误的信息,按下Alt + Enter打开备选解决方案:Create function java_com_idealist_myapplication_MainActivity_getStringFromNative
  • 这时候会在java的同级目录下自动新建一个jni的目录,并新建了一个c++源码文件:
  • 源码内容如下,我们修改这个方法通过它来返回一个字符串如下所示:
  • 使用loadLibrary引入库,在MainActivity中使用getStringFromNative

为什么使用JNI

我们知道Java语言是一个跨平台的语音,而C/C++这是一个对平台有依赖的语言,所以不是万不得已个人并不推荐在项目中引入项目中,一旦引入JNI那么我们将失去跨平台的特性,对特定的平台会产生依赖,并且NE是开发中最难解决的问题,一旦抛出Native 层的错误将会增加我们定位问题的难度。那么我们一般什么时候才需要使用到JNI技术呢?

  • 我们在编程中需要用到某个功能,这个功能之前已经用C/C++等本地语言进行编写了,这时候为了避免重复造轮子,所以使用JNI在Java层调用这些已经封装好的方法。
  • 有些功能需要非常严格的时效性,或者某些方法需要和特定的硬件进行交互,比如某些缓存的操作等。这种情况下就需要使用到JNI了。
  • 为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码来实现,然后封装成so动态库文件,并提供Java接口供应用层调用,从而防止被反编译。

JNI在整个开发中所扮演的角色

JNI顾名思义Java Native Interface,是Java和Native层的一个接口,但是要注意的是这是一个双向接口,也就是说不但运行在Java层调用Native层,也允许在Native层调用Java层。

使用Android Studio开发JNI

在Android Studio中进行JNI开发环境的搭建,请查看我之前的一篇博客《Android 进阶之 NDK 开发》这里就不重复介绍了。

最简单的例子介绍

public class MainActivity extends AppCompatActivity {
// [1]
private native String getStringFromNative();
// [2]
static {
System.loadLibrary("testndk");
}

private TextView mTextView = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.demojni);
mTextView.setText(getStringFromNative());
}
}
#include <jni.h>
JNIEXPORT jstring JNICALL
//[3]
Java_com_idealist_myapplication_MainActivity_getStringFromNative(JNIEnv *env, jobject instance) {
return (*env)->NewStringUTF(env, "Hello JNI");
}

下面将会上面标注的三点进行讲解,给大家一个简单的入门,这个例子中通过调用getStringFromNative这个native方法返回一个”Hello JNI”,并将其显示到TextView上。
[1]. 所有的Native方法在定义的时候都需要添加native标识。
[2]. 在Linux 环境下编译后会生成一个”lib”+模块名+“.so”的库文件,在这个例子中模块名为testndk所以会生成libtestndk.so,而在Window环境下会生成一个”lib”+模块名+“.dll”的库文件,在需要使用这些库文件里面的方法的时候需要使用loadLibrary将库引入。
[3]. Java_com_idealist_myapplication_MainActivity_getStringFromNative这个方法是自动生成的,它的生成规律是Java_包名将所有的分隔符由点换成下划线_native方法声明所处的类库_native方法名。
我们接下来看下这个jni方法的参数(JNIEnv *env, jobject instance) 第一个参数是一个方法指针指向JNI提供的方法,如果下图所示:

第二个参数要视Java层上声明的方法为静态方法还是非静态方法,如果是静态方法则传入的是jclass表示指向某个类,如果是动态方法者传入的是jobject类型参数,指向的是调用的对象。
JNIEXPORT 和 JNICALL 宏定义用于确保外部能够从native库中看到这个方法。

如果上面看不是很懂,没事可以在接下来的博客的例子中慢慢体会。