Gradle允许你完全控制传递依赖,你可以选择排除全部的传递依赖也可以排除指定的依赖。
exclude:可以设置不编译指定的模块,排除指定模块的依赖。后的参数有group和module,可以分别单独使用,会排除所有匹配项。

// 编译CommonSDK模块,但是去掉此模块中对com.android.support的依赖,防止重复依赖报错
compile (project(':CommonSDK')) { exclude group: "com.android.support" }
compile ('com.jakewharton:butterknife:8.5.1'){
exclude module: 'support-compat'
exclude group: 'com.android.**.***', module: '***-***'
}

transitive:用于自动处理子依赖项,默认为true,gradle自动添加子依赖项。设置为false排除所有的传递依赖,可以用来解决一些依赖冲突的问题,比如一些 Error:java.io.IOException: Duplicate zip entry 报错。

// 使用transitive属性设置为false来排除所有的传递依赖
compile project(':extras:ShimmerAndroid'){
transitive = false
}

force:强制设置某个模块的版本。

configurations.all{
resolutionStrategy{
force'org.hamcrest:hamcrest-core:1.3'
}
}

这样,应用中对org.hamcrest:hamcrest-core 依赖就会变成1.3版本。

其它相关文章:
android音频口通信(一)——2FSK信号调制
android音频口通信(二)——2FSK信号解调
android音频通信(三)——双向通信(A2B协议)之手机接收
手机音频口通信

一、手机音频通信的特点
1、 通用性强:在智能手机普及的今天,手机的对外通信接口多种多样,而其中以3.5mm的音频接口通用新最强,基本所有的手机、平板电脑都会有这个接口,所以在一些要求通用性的设备上,音频接口登上了舞台。
2、 速率低:由于手机音频部分的采样频率一般为44.1KHZ(部分国产山寨为8KHZ),这极大的限制了音频通讯的速率。我们都知道44.1KHZ的采样频率,那么最高的信号频率只能为20KHZ左右,而信号周期也不可能只有2个采样点,通常要到10个以上,这样层层下来通讯速率可想而知。
3、 小信号:音频通信的信号都是毫伏级的,各个手机厂商略有不同,但通常最大不超过200mv,通常我们通信使用的信号强度也就100mv左右,这导致信号比较容易受干扰,且在开发阶段对工具有着种种限制。

二、 手机音频通信分类
1、 无线方式:
a) 无线方式大家可能不太熟悉,容我慢慢道来。我们都知道人耳能听到的声音频率为20HZ~20KHZ,而手机通信的信号频率最高也就20KHZ,所以无线通信方式是可行的。因为虽然人耳的极限听力能到20KHZ,但普通人一般在19KHZ以上时基本就听不到了,所以如果信号的强度比较弱,且控制在19KHZ到20KHZ之间,那么我们就可以将之当做是“超声波”来看待了。
b) 其实在此提到手机音频通信的无线方式,算是给大家一种产品开发思路吧。它的通讯半径在10M左右,前景还是很广阔的,大家有兴趣的可以试试。(其实已经有这方面的产品了)
2、 有线方式:
a) 有线方式分为单向(设备→手机)和双向两种,单向的限制少,开发难度也小一些,但实际应用时会受限制。而双向通信限制多,开发难度也大一些,但实际应用时更方便些。
b) 设备→手机:曼彻斯特编码;FSK;DTMF;自定义正弦波
c) 手机→设备:由于手机输出的音频信号很小,无法直接使用,要么用运放发大到合适的范围,要么用电压比较器转换成TTL方波。

三、手机音频通信硬件通信方式分类:手机音频通信的硬件通信方式大体可分为方波和正弦波两种。
1、 方波:方波通常使用的是曼彻斯特编码方式(什么是曼彻斯特编码自己去查),它的好处是可以用单片机直接输出方波,经过衰减后即可使用,方便简单。缺点是兼容性不好,因为手机音频部分有这样一个特性,它只识别变化的电平信号,当麦克输入的信号长时间保持在某一非零电平时,手机会将其视为零,而强行拉回零电位。这就是采用方波通讯方式的兼容性不好的最大原因了,并且方波也容易受干扰。
2、 正弦波:正弦波不会出现上面所说的方波的问题,故正弦波的兼容性和稳定性更好一些。通常采用方案有FSK、DTMF、信号发生器、或方波转正弦波等。(后面会对以上方案逐一分析)
3、 通信信道分析
a) 我们知道音频接口有4根线,MIC、地、左、右声道。设备→手机用MIC,手机→设备用地、左、右声道中的任意一个。这里说一下,实际产品中,有一些厂家会更换地线,即将原本左、有声道中的一根改为地线来用,其实道理是一样的。因为音频通信的信号时交流信号,而地其实也是悬浮地,即便地线换了,最终的波形还是一样的,因为最终手机解析信号时需要的是频率和幅值。这样还剩下一个声道,通常被用来帮助设备进行上电识别,因为音频通信的设备通常都是电池供电的。
b) 另外还要在MIC和地之间并联一个4.99K的电阻,因为手机是通过检测MIC和地之间的阻抗是否为4.99K(也有其他阻值的)来判断是否有设备(耳机)插入,这一点要谨记。

四、各个通信方案对比分析
1、 设备→手机:
a) 曼彻斯特编码:在诸多通信方式中,曼彻斯特编码是最灵活简便的一种方法,编码信号可由单片机直接产生,经衰减电路衰减后便可直接使用。注意事项:曼彻斯特编码信号的生成有两种方式,一种是用PWM生成,一种是用定时器中断翻转IO,我个人比较倾向于定时器中断方式。因为我们知道曼彻斯特编码中有宽沿河窄沿之分,且宽沿和窄沿可能会灵活变化,而用PWM方式不容易精确控制宽沿、窄沿输出的变化,而定时器中断方式则非常灵活且容易控制。(后面会送上我自己写的曼彻斯特编码、解码函数)
b) FSK、DTMF方式:FSK和DTMF两种方式大同小异,使用时通常都是用集成的芯片来生成的,而这些芯片通常都是遵守固定的通信协议的的要求(FSK为Bell202或V.23协议,DTMF记不清名字了)。这两种通信方式的优点是采用正弦波通信、稳定性好且使用简便。但由于固定通信协议的限制导致通信速率、比特率也受到限制而缺乏灵活性。在这里跟他家推荐一款英国的通信芯片CMX系列,这个系列的芯片融合的FSK、DTMF的编码、解码,还是很不错的,大家有兴趣可以试试。(相关手册在附件里)
c) 信号发生器、锁相环方式:这种方式用信号发生器或者锁相环来产生方波或正玄波,由单片机来控制波形的输出,也可以实现音频通信,且十分灵活。但缺点是电路较复杂,且不同频率信号之间衔接不好掌握,用不好反而是麻烦。(相关手册在附件里)
d) 在这里送上一种我个人认为比较好的方案:就是曼彻斯特编码加低通滤波器,由单片机输出曼彻斯特编码,再经由低通滤波器将方波滤成正弦波后输出。既解决了FSK、DTMF灵活性的问题,又解决了曼彻斯特编码方波稳定性、通用性的问题。在低通滤波器方面我个人采用的是“集成低通开关电容滤波器”,它成本虽然高一些,但好处也是明显的,电路简单,使用方便,且占用的空间亦很小。(相关手册在附件里)
2、 手机→设备:
a) 放大电路方式:将手机输出信号经放大电路放大到合适的幅值,然后有锁相环或者结成FSK、DTMF芯片进行解析。该中方式难度最大,需要非常强的模拟电路功底,我个人水平有限,故采用的另一种方式。
b) 电压比较器方式:将手机输出的交流信号经电路强行拉到Vcc/2级别,然后加到电压比较器一端,另一段接比较电压Vcc/2,这样交流信号即被转化为TTL方波信号,此时再进行解析就变得很简单了。

五、研发注意事项(通讯方案分析部分由于过长,放到最后来讲)
1、 一个好手机录音软件是必须的,最好能在手机上直接看到波形的。
2、 建议用笔记本电脑进行开发,而非台式机。因为音频信号很小,容易受干扰,而台式机干扰较大,笔记本还有一个好处是必要时可将外接电源拔掉,用电池供电。
3、 一个好录音笔必不可少,有时需要得到纯净的音频信号,方便更加准确的分析。
4、 做一个转接板,一边接音频母座,一边接音频公头,将MIC、地、左、右声道4跟线用排阵引出,方便录音。
5、 做一个信号衰减电路,可将设备电路产生的信号衰减至音频接口能承受的范围内。前期调试时,我们可以用该电路将信号录进电脑进行信号分析。(推荐一个电脑音频信号分析软件:Goldwave)
6、 录音用的音频线切记不要太长,不然会给你带来不少麻烦。最好自己做,用音频裸头、杜邦线、排阵即可制作,方便好用。

曼彻斯特编码的编码解码函数如下:
/**********************************************************************
注释:编码函数都是采用定时器中断的形式,以曼彻斯特编码的窄沿作为定时器周期。
发送的数据包括1个起始位、8个数据位、1个奇偶校验位、3个停止位。
***********************************************************************/
static void VIC_VECT_Fucton_00(void)//发送编码数据中断函数
{
TIMER0IS =0x0;
if((send_time%2==0) && (send_start==1))
{
switch(FSK_txState)
{
case STARTBIT:
if((GPIODATA&0x00000002)==0x00000000)//如果检测到数据发送管脚为零
send_time++;
else
{
currentSym=0;
FSK_txState = BYTE;
}
break;
case BYTE:
if(txBit < 8) { currentSym = (send_byte >> txBit) & 0x01;
txBit++;
txParity += currentSym; //奇偶校验位
}
else if (txBit == 8)
{
currentSym = txParity & 0x01; //发送奇偶校验位
txBit++;
}
else if(txBit>8 && txBit<12) { // next bit is the stop bit currentSym = 1; //发送停止位 txBit++; } else if(txBit == 12) FSK_txState = STOPBIT; break; case STOPBIT : txBit=0; FSK_txState=IDLE; send_start=0; txParity=0; send_byte=0; break; } if(lastSym!=currentSym) { timer1_num++; lastSym=currentSym; } } if(timer1_num%2==0) GPIODATA&=0xFFFFFFFD;//输出管脚复位 else GPIODATA|=0x00000002;//输出管脚置位 timer1_num++;//用来控制IO口的电平翻转 send_time++;//用来控制发送的字节的每一位 Delay++;//Delay就是延时函数 } /********************************************************************** 注释:解码函数采用外部IO中断形式(上升沿或下降沿中断,即电平电平跳变中断), 用一个定时器作为时钟,每次产生中断时便从定时器见时间值取出,并和上一次的 记录做差求出时间间隔,以此来判断当前为宽沿还是窄沿。 ***********************************************************************/ static void VIC_VECT_Fucton_04(void)//接受解码数据中断函数 { GPIOIC|=0x00000001;//清楚上一次中断内容 RX_time=TIMER1VALUE; if(RX_lasttime>=RX_time)
RX_diff=RX_lasttime-RX_time; //lasttime初始值为0
else
RX_diff=65535-RX_time+RX_lasttime;
RX_lasttime=RX_time;
switch(RX_state) //启动代码时state已经被配置为STARTBIT
{
case STARTBIT_FALL:
if ((SHORTINTERVAL> 1) + (1<<7); rxParity++; //奇偶校验位 RX_bitcounter++; //接受数据位数 } else { uartByteRx = (uartByteRx >> 1);
RX_bitcounter++;
}
}
else
{
rxParity&=0x01; //进行奇偶校验
if(rxParity==currentbit)
{
RX_bitcounter++;
RX_finish=1;
RX_state=DATAINIT;
}
else
RX_state=DATAINIT; //若奇偶校验错误则,重新检测
}
}
break;
case DATAINIT : //初始化参数状态
RX_bitcounter=0;
RX_ones=0;
rxParity=0;
currentbit=0;
RX_state=STARTBIT_FALL;
RX_times=0;
break;
default:
break;
}
}

本文详细论述如何在 JNI 编程中避免内存泄漏。论述了 JNI 编程中可能引发的明显的内存泄漏。本文的重点是阐述 JNI 编程中潜在的内存泄漏,希望读者通过本文对 Local reference 有更深刻的理解,了解 Local reference 表的存在,区分 Local reference 和局部变量,从而认识到 Local reference 可能引发的 native memory 内存泄漏。

JNI 编程简介

JNI,Java Native Interface,是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native code;在 native code 中嵌入 Java 虚拟机调用 Java 的代码。

JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点:

  1. 利用 native code 的平台相关性,在平台相关的编程中彰显优势。
  2. 对 native code 的代码重用。
  3. native code 底层操作,更加高效。

然而任何事物都具有两面性,JNI 编程也同样如此。程序员在使用 JNI 时应当认识到 JNI 编程中如下的几点弊端,扬长避短,才可以写出更加完善、高性能的代码:

  1. 从 Java 环境到 native code 的上下文切换耗时、低效。
  2. JNI 编程,如果操作不当,可能引起 Java 虚拟机的崩溃。
  3. JNI 编程,如果操作不当,可能引起内存泄漏。

JAVA 中的内存泄漏

JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏;JVM 内存中 native memory 的内存泄漏。

Java Heap 的内存泄漏

Java 对象存储在 JVM 进程空间中的 Java Heap 中,Java Heap 可以在 JVM 运行过程中动态变化。如果 Java 对象越来越多,占据 Java Heap 的空间也越来越大,JVM 会在运行时扩充 Java Heap 的容量。如果 Java Heap 容量扩充到上限,并且在 GC 后仍然没有足够空间分配新的 Java 对象,便会抛出 out of memory 异常,导致 JVM 进程崩溃。

Java Heap 中 out of memory 异常的出现有两种原因——①程序过于庞大,致使过多 Java 对象的同时存在;②程序编写的错误导致 Java Heap 内存泄漏。

多种原因可能导致 Java Heap 内存泄漏。JNI 编程错误也可能导致 Java Heap 的内存泄漏。

JVM 中 native memory 的内存泄漏

从操作系统角度看,JVM 在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。

JVM 进程空间中,Java Heap 以外的内存空间称为 JVM 的 native memory。进程的很多资源都是存储在 JVM 的 native memory 中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM 的静态数据、全局数据等等。也包括 JNI 程序中 native code 分配到的资源。

在 JVM 运行中,多数进程资源从 native memory 中动态分配。当越来越多的资源在 native memory 中分配,占据越来越多 native memory 空间并且达到 native memory 上限时,JVM 会抛出异常,使 JVM 进程异常退出。而此时 Java Heap 往往还没有达到上限。

多种原因可能导致 JVM 的 native memory 内存泄漏。例如 JVM 在运行中过多的线程被创建,并且在同时运行。JVM 为线程分配的资源就可能耗尽 native memory 的容量。

JNI 编程错误也可能导致 native memory 的内存泄漏。对这个话题的讨论是本文的重点。

JNI 编程中明显的内存泄漏

JNI 编程实现了 native code 和 Java 程序的交互,因此 JNI 代码编程既遵循 native code 编程语言的编程规则,同时也遵守 JNI 编程的文档规范。在内存管理方面,native code 编程语言本身的内存管理机制依然要遵循,同时也要考虑 JNI 编程的内存管理。

本章简单概括 JNI 编程中显而易见的内存泄漏。从 native code 编程语言自身的内存管理,和 JNI 规范附加的内存管理两方面进行阐述。

Native Code 本身的内存泄漏

JNI 编程首先是一门具体的编程语言,或者 C 语言,或者 C++,或者汇编,或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。

Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。

Global Reference 引入的内存泄漏

JNI 编程还要同时遵循 JNI 的规范标准,JVM 附加了 JNI 编程特有的内存管理机制。

JNI 中的 Local Reference 只在 native method 执行时存在,当 native method 执行完后自动失效。这种自动失效,使得对 Local Reference 的使用相对简单,native method 执行完后,它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。

而 Global Reference 对 Java 对象的引用一直有效,因此它们引用的 Java 对象会一直存在 Java Heap 中。程序员在使用 Global Reference 时,需要仔细维护对 Global Reference 的使用。如果一定要使用 Global Reference,务必确保在不用的时候删除。就像在 C 语言中,调用 malloc() 动态分配一块内存之后,调用 free() 释放一样。否则,Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,造成 Java Heap 的内存泄漏。

JNI 编程中潜在的内存泄漏——对 LocalReference 的深入理解

Local Reference 在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。对 Local Reference 的理解不够,会造成潜在的内存泄漏。

本章重点阐述 Local Reference 使用不当可能引发的内存泄漏。引入两个错误实例,也是 JNI 程序员容易忽视的错误;在此基础上介绍 Local Reference 表,对比 native method 中的局部变量和 JNI Local Reference 的不同,使读者深入理解 JNI Local Reference 的实质;最后为 JNI 程序员提出应该如何正确合理使用 JNI Local Reference,以避免内存泄漏。

错误实例 1

在某些情况下,我们可能需要在 native method 里面创建大量的 JNI Local Reference。这样可能导致 native memory 的内存泄漏,如果在 native method 返回之前 native memory 已经被用光,就会导致 native memory 的 out of memory。

在代码清单 1 里,我们循环执行 count 次,JNI function NewStringUTF() 在每次循环中从 Java Heap 中创建一个 String 对象,str 是 Java Heap 传给 JNI native method 的 Local Reference,每次循环中新创建的 String 对象覆盖上次循环中 str 的内容。str 似乎一直在引用到一个 String 对象。整个运行过程中,我们看似只创建一个 Local Reference。

执行代码清单 1 的程序,第一部分为 Java 代码,nativeMethod(int i) 中,输入参数设定循环的次数。第二部分为 JNI 代码,用 C 语言实现了 nativeMethod(int i)。

清单 1. Local Reference 引发内存泄漏

Java 代码部分
class TestLocalReference {
private native void nativeMethod(int i);
public static void main(String args[]) {
TestLocalReference c = new TestLocalReference();
//call the jni native method
c.nativeMethod(1000000);
}
static {
//load the jni library
System.loadLibrary("StaticMethodCall");
}
}

JNI 代码,nativeMethod(int i) 的 C 语言实现
#include<stdio.h>
#include<jni.h>
#include"TestLocalReference.h"
JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod
(JNIEnv * env, jobject obj, jint count)
{
jint i = 0;
jstring str;

for(; i<count; i++)
str = (*env)->NewStringUTF(env, "0");
}
运行结果
JVMCI161: FATAL ERROR in native method: Out of memory when expanding
local ref table beyond capacity
at TestLocalReference.nativeMethod(Native Method)
at TestLocalReference.main(TestLocalReference.java:9)

运行结果证明,JVM 运行异常终止,原因是创建了过多的 Local Reference,从而导致 out of memory。实际上,nativeMethod 在运行中创建了越来越多的 JNI Local Reference,而不是看似的始终只有一个。过多的 Local Reference,导致了 JNI 内部的 JNI Local Reference 表内存溢出。

错误实例 2

实例 2 是实例 1 的变种,Java 代码未作修改,但是 nativeMethod(int i) 的 C 语言实现稍作修改。在 JNI 的 native method 中实现的 utility 函数中创建 Java 的 String 对象。utility 函数只建立一个 String 对象,返回给调用函数,但是 utility 函数对调用者的使用情况是未知的,每个函数都可能调用它,并且同一函数可能调用它多次。在实例 2 中,nativeMethod 在循环中调用 count 次,utility 函数在创建一个 String 对象后即返回,并且会有一个退栈过程,似乎所创建的 Local Reference 会在退栈时被删除掉,所以应该不会有很多 Local Reference 被创建。实际运行结果并非如此。

清单 2. Local Reference 引发内存泄漏

Java 代码部分参考实例 1,未做任何修改。

JNI 代码,nativeMethod(int i) 的 C 语言实现
#include<stdio.h>
#include<jni.h>
#include"TestLocalReference.h"
jstring CreateStringUTF(JNIEnv * env)
{
return (*env)->NewStringUTF(env, "0");
}
JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod
(JNIEnv * env, jobject obj, jint count)
{
jint i = 0;
for(; i<count; i++)
{
str = CreateStringUTF(env);
}
}
运行结果
JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref
table beyond capacity
at TestLocalReference.nativeMethod(Native Method)
at TestLocalReference.main(TestLocalReference.java:9)

运行结果证明,实例 2 的结果与实例 1 的完全相同。过多的 Local Reference 被创建,仍然导致了 JNI 内部的 JNI Local Reference 表内存溢出。实际上,在 utility 函数 CreateStringUTF(JNIEnv * env)

执行完成后的退栈过程中,创建的 Local Reference 并没有像 native code 中的局部变量那样被删除,而是继续在 Local Reference 表中存在,并且有效。Local Reference 和局部变量有着本质的区别。

Local Reference 深层解析

Java JNI 的文档规范只描述了 JNI Local Reference 是什么(存在的目的),以及应该怎么使用 Local Reference(开放的接口规范)。但是对 Java 虚拟机中 JNI Local Reference 的实现并没有约束,不同的 Java 虚拟机有不同的实现机制。这样的好处是,不依赖于具体的 JVM 实现,有好的可移植性;并且开发简单,规定了“应该怎么做、怎么用”。但是弊端是初级开发者往往看不到本质,“不知道为什么这样做”。对 Local Reference 没有深层的理解,就会在编程过程中无意识的犯错。

Local Reference 和 Local Reference 表

理解 Local Reference 表的存在是理解 JNI Local Reference 的关键。

JNI Local Reference 的生命期是在 native method 的执行期(从 Java 程序切换到 native code 环境时开始创建,或者在 native method 执行时调用 JNI function 创建),在 native method 执行完毕切换回 Java 程序时,所有 JNI Local Reference 被删除,生命期结束(调用 JNI function 可以提前结束其生命期)。

实际上,每当线程从 Java 环境切换到 native code 上下文时(J2N),JVM 会分配一块内存,创建一个 Local Reference 表,这个表用来存放本次 native method 执行中创建的所有的 Local Reference。每当在 native code 中引用到一个 Java 对象时,JVM 就会在这个表中创建一个 Local Reference。比如,实例 1 中我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference 表中就会相应新增一个 Local Reference。

图 1. Local Reference 表、Local Reference 和 Java 对象的关系

图 1. Local Reference 表、Local Reference 和 Java 对象的关系

图 1 中:

⑴运行 native method 的线程的堆栈记录着 Local Reference 表的内存位置(指针 p)。

⑵ Local Reference 表中存放 JNI Local Reference,实现 Local Reference 到 Java 对象的映射。

⑶ native method 代码间接访问 Java 对象(java obj1,java obj2)。通过指针 p 定位相应的 Local Reference 的位置,然后通过相应的 Local Reference 映射到 Java 对象。

⑷当 native method 引用一个 Java 对象时,会在 Local Reference 表中创建一个新 Local Reference。在 Local Reference 结构中写入内容,实现 Local Reference 到 Java 对象的映射。

⑸ native method 调用 DeleteLocalRef() 释放某个 JNI Local Reference 时,首先通过指针 p 定位相应的 Local Reference 在 Local Ref 表中的位置,然后从 Local Ref 表中删除该 Local Reference,也就取消了对相应 Java 对象的引用(Ref count 减 1)。

⑹当越来越多的 Local Reference 被创建,这些 Local Reference 会在 Local Ref 表中占据越来越多内存。当 Local Reference 太多以至于 Local Ref 表的空间被用光,JVM 会抛出异常,从而导致 JVM 的崩溃。

Local Ref 不是 native code 的局部变量

很多人会误将 JNI 中的 Local Reference 理解为 Native Code 的局部变量。这是错误的。

Native Code 的局部变量和 Local Reference 是完全不同的,区别可以总结为:

⑴局部变量存储在线程堆栈中,而 Local Reference 存储在 Local Ref 表中。

⑵局部变量在函数退栈后被删除,而 Local Reference 在调用 DeleteLocalRef() 后才会从 Local Ref 表中删除,并且失效,或者在整个 Native Method 执行结束后被删除。

⑶可以在代码中直接访问局部变量,而 Local Reference 的内容无法在代码中直接访问,必须通过 JNI function 间接访问。JNI function 实现了对 Local Reference 的间接访问,JNI function 的内部实现依赖于具体 JVM。

代码清单 1 中 str = (*env)->NewStringUTF(env, “0”);

str 是 jstring 类型的局部变量。Local Ref 表中会新创建一个 Local Reference,引用到 NewStringUTF(env, “0”) 在 Java Heap 中新建的 String 对象。如图 2 所示:

图 2. str 间接引用 string 对象

图 2. str 间接引用 string 对象

图 2 中,str 是局部变量,在 native method 堆栈中。Local Ref3 是新创建的 Local Reference,在 Local Ref 表中,引用新创建的 String 对象。JNI 通过 str 和指针 p 间接定位 Local Ref3,但 p 和 Local Ref3 对 JNI 程序员不可见。

Local Reference 导致内存泄漏

在以上论述基础上,我们通过分析错误实例 1 和实例 2,来分析 Local Reference 可能导致的内存泄漏,加深对 Local Reference 的深层理解。

分析错误实例 1:

局部变量 str 在每次循环中都被重新赋值,间接指向最新创建的 Local Reference,前面创建的 Local Reference 一直保留在 Local Ref 表中。

在实例 1 执行完第 i 次循环后,内存布局如图 3:

图 3. 执行 i 次循环后的内存布局

图 3. 执行 i 次循环后的内存布局

继续执行完第 i+1 次循环后,内存布局发生变化,如图 4:

图 4. 执行 i+1 次循环后的内存布局

图 4. 执行 i+1 次循环后的内存布局

图 4 中,局部变量 str 被赋新值,间接指向了 Local Ref i+1。在 native method 运行过程中,我们已经无法释放 Local Ref i 占用的内存,以及 Local Ref i 所引用的第 i 个 string 对象所占据的 Java Heap 内存。所以,native memory 中 Local Ref i 被泄漏,Java Heap 中创建的第 i 个 string 对象被泄漏了。

也就是说在循环中,前面创建的所有 i 个 Local Reference 都泄漏了 native memory 的内存,创建的所有 i 个 string 对象都泄漏了 Java Heap 的内存。

直到 native memory 执行完毕,返回到 Java 程序时(N2J),这些泄漏的内存才会被释放,但是 Local Reference 表所分配到的内存往往很小,在很多情况下 N2J 之前可能已经引发严重内存泄漏,导致 Local Reference 表的内存耗尽,使 JVM 崩溃,例如错误实例 1。

分析错误实例 2:

实例 2 与实例 1 相似,虽然每次循环中调用工具函数 CreateStringUTF(env) 来创建对象,但是在 CreateStringUTF(env) 返回退栈过程中,只是局部变量被删除,而每次调用创建的 Local Reference 仍然存在 Local Ref 表中,并且有效引用到每个新创建的 string 对象。str 局部变量在每次循环中被赋新值。

这样的内存泄漏是潜在的,但是这样的错误在 JNI 程序员编程过程中却经常出现。通常情况,在触发 out of memory 之前,native method 已经执行完毕,切换回 Java 环境,所有 Local Reference 被删除,问题也就没有显露出来。但是某些情况下就会引发 out of memory,导致实例 1 和实例 2 中的 JVM 崩溃。

控制 Local Reference 生命期

因此,在 JNI 编程时,正确控制 JNI Local Reference 的生命期。如果需要创建过多的 Local Reference,那么在对被引用的 Java 对象操作结束后,需要调用 JNI function(如 DeleteLocalRef()),及时将 JNI Local Reference 从 Local Ref 表中删除,以避免潜在的内存泄漏。

总结

本文阐述了 JNI 编程可能引发的内存泄漏,JNI 编程既可能引发 Java Heap 的内存泄漏,也可能引发 native memory 的内存泄漏,严重的情况可能使 JVM 运行异常终止。JNI 软件开发人员在编程中,应当考虑以下几点,避免内存泄漏:

  • native code 本身的内存管理机制依然要遵循。
  • 使用 Global reference 时,当 native code 不再需要访问 Global reference 时,应当调用 JNI 函数 DeleteGlobalRef() 删除 Global reference 和它引用的 Java 对象。Global reference 管理不当会导致 Java Heap 的内存泄漏。
  • 透彻理解 Local reference,区分 Local reference 和 native code 的局部变量,避免混淆两者所引起的 native memory 的内存泄漏。
  • 使用 Local reference 时,如果 Local reference 引用了大的 Java 对象,当不再需要访问 Local reference 时,应当调用 JNI 函数 DeleteLocalRef() 删除 Local reference,从而也断开对 Java 对象的引用。这样可以避免 Java Heap 的 out of memory。
  • 使用 Local reference 时,如果在 native method 执行期间会创建大量的 Local reference,当不再需要访问 Local reference 时,应当调用 JNI 函数 DeleteLocalRef() 删除 Local reference。Local reference 表空间有限,这样可以避免 Local reference 表的内存溢出,避免 native memory 的 out of memory。
  • 严格遵循 Java JNI 规范书中的使用规则。

Java 本地编程接口(Java Native Interface,JNI) 易懂且容易上手,但对于一些新手甚至于一些中级开发者,在编程中都没有把 JNI 对象的生命周期考虑在内。以至于经常碰见莫名的 Crash,或者 segmentation fault。在花费了相当精力去调试后,才发现是 JNI 对象使用不当,当初保存的 JNI 对象已经被系统回收,却仍试图去使用之。本文结合实际开发中遇到的此类问题,讲述和强调了 JNI 对象生命周期的概念,并提出正确的解决方法,希望能和读者分享自己的经验和教训。

问题

在 JNI 编程中常需要从一个普通的 C/C++ 函数中调用 JNI 方法,比如:

long ProcessKeyboardEventInJava(int keyboardEvent)
{
long error = 0;
JNIEnv* env = g_env;
jclass that = g_class;
jmethodID mid = g_mid;
… …
error = env->CallStaticIntMethod(that, mid, keyboardEvent);

return error;
}

ProcessKeyboardEventInJava只是个普通的 C 函数,目的是把参数中的键盘事件交给 Java 代码处理。它并没有 JNI 参数,却需要调用一个 JNI 方法。那么调用所必需的参数 JNIEnv,jclass 和 jmethodID,是如何获得呢?从上面的代码中可以看出,它们来自保存的全局变量 g_env,g_class 和 g_mid。这样做对么?安全么?也许这段代码在某种情况下,实现了软件的需求,成功地执行了 Java 的函数,处理了这个键盘事件。在这种情况下,设计者很清楚,也深信 ProcessKeyboardEventInJava 只会在某个 Native 函数返回前被调用。对应的场景代码应该是这样的:

JNIEnv * g_env;
jclass g_class;
jmethodID g_mid;

JNIEXPORT jint JNICALL Java_SampleTest_init (JNIEnv *env, jclass cls)
{

g_class = cls;
g_env = env;
g_mid = env->GetStaticMethodID(cls, "ProcessKeyboardEventInJava", "(III)I");
while(true)
{
….
long error;
error = ProcessKeyboardEventInJava(theEvent)
….
}

}
但万一 ProcessKeyboardEventInJava 在 Java_SampleTest_init(以下简称 init)返回后的其他时机被调用了,会怎样呢?那时可就没有这么幸运了,初衷是肯定达不到了,而发生 crash 或者 segmentation fault 这类错误则几乎是一定的。事实上,如上设计,ProcessKeyboardEventInJava 也只允许在 init 函数中被调用。在其返回后被调用,就不合法了。所以,如果你是这个设计师,之前的设计中,是不是这个假设也有些太乐观了呢?

分析

调用者本来以为这个函数能帮他的大忙,现在却疑惑了,到底发生了什么?做错了什么呢?

原因其实很简单,上面三个全局变量中的 g_class 已经是非法的了,生命周期在退出 init 之后就结束了。

这里要澄清的是 g_mid,由于它不是一个 jobject,所以只要它对应的 class 没有被卸载,在退出 init 之后仍可以使用,没问题;而 g_env 对于同一个 thread 来说,它是唯一的,所以只要是 init 和 ProcessKeyboardEventInJava 处于一个 thread,初始化后它的值在这个 thread 没有中止之前,都一直是合法的。

那如何才能解决 g_class 的非法引用带来的问题呢?

这首先涉及到 Java 和 Native 代码之间函数调用时,参数如何传递的问题。简单类型,也就是内置类型,比如 int, char 等是值传递(pass by value),而其它 Java 对象都是引用传递(pass by reference),这些对象引用由 JVM 传给 Native 代码,每个都有其生命周期。

其次,Java 对象的生命周期是由它的引用类型决定的,这里的引用分两种:local reference 和 global reference。Native 函数参数中 jobject 或者它的子类,其参数都是 local reference。Local reference 只在这个 Native 函数中有效,Native 函数返回后,引用的对象就被释放,它的生命周期就结束了。若要留着日后使用,则需根据这个 local reference 创建 global reference。Global reference 不会被系统自动释放,它仅当被程序明确调用 DeleteGlobalReference 时才被回收。

解决

于是解决的办法就出来了:

在 init 函数中,不是简单地把 jcalss 参数保存,而是:

JNIEXPORT jint JNICALL Java_SampleTest_init (JNIEnv *env, jclass cls)
{

….
g_class= (jclass)(env->NewGlobalRef(cls));
….
}

这样,无论 ProcessKeyboardEventInJava 是在 init 返回前还是返回后,调用它都是安全的,可行的了。

总结

若要在某个 Native 代码返回后,还希望能继续使用 JVM 提供的参数(比如 init 函数中的 jclass), 或者是过程中调用 JNI 函数的返回值(比如 g_mid),只要它是一个 jobject, 则需要为它创建一个 global reference,以后只能使用这个 global reference;若不是一个 jobject,则无需这么做。

jclass 是由 jobject public 继承而来的子类,所以它当然是一个 jobject,需要创建一个 global reference 以便日后使用。而 jmethodID/jfieldID 与 jobject 没有继承关系,它不是一个 jobject,只是个整数,所以不存在被释放与否的问题,可保存后直接使用。JNIEnv 对于每个 thread 而言是唯一的,不能也不需要对它调用 NewGlobalReference。

1. 拉卡拉刷卡器
拉卡拉
大家都知道,拉卡拉就是一个手机刷卡器。拉卡卡、智能手机、拉卡拉软件、网络,构成了一个完整的、更强大的POS系统。为什么说更强大呢?因为在手机应用app的这一层,拉卡拉可以做很多很多方便的功能,这样借助于类似传统的刷卡服务,提供一系列现在支付宝也正在大力发展的生活服务等功能。而这个是银联的POS所严重缺乏的。目前来说,移动支付里,最方便的肯定是支付宝的快捷支付;但考虑国内目前的安全环境,用户把银行卡和密码托管给一个网站来管理,明显不如用的时候输入一下的方式更能被接受。相对于更安全的支付宝加网银的方式,拉卡拉做到了与发卡行无关,这样也不需要手机上安装各种不同的银行的网银客户端。所以,显然拉卡拉在某些应用场景比一些其他的移动支付方式更有优势。下面我们就来看看拉卡拉的技术原理是怎么回事。

2. 内部结构
94ad8e2ad40735fa0eb8e58d9b510fb30e2408f4
a89ecd0635fae6cd92fc00550ab30f2443a70ff4
ebb22cfbe6cd7b89742491b70a2442a7d8330ef4
上面左右是一个常见的拉卡拉设备,挺好看的。如果我们把它拆开来看,就如右图所示,当然除了这个东西,下面还有个耳机插头、侧面还有一个刷卡的槽。
总体来说,这个电路板很简单。

2.1 一个电池就占了绝大部分地方,导致整个拉卡拉个头偏大,可供刷卡万次以上(见参考资料1)。

2.2 左边的四根引线,对应于耳机插头上的4个区域。分别是左声道(Left)、右声道(Right)、麦克风(Mic)和接地线(Gnd)。其中L和R是接收手机往外输出信号的接口,M是输出信号到手机的接口。需要注意的是并不是所有的顺序都想右图所示。因为存在两个不一样的标准,国际标准和国家标准。像联想、中兴等大部分国产手机都是国家标准,跟右侧的图一样的顺序。而iphone、htc、三星、小米等手机都是国际标准,M和Gnd的顺序是反向的。拉卡拉的某些版本可以自动识别M和G的正反向。(见参考资料2)

2.3 电路板最右面的芯片处理输入信号和转换刷卡数据的A\D(音频\数字)处理模块,它是整个系统的核心。在手机应用里点击刷卡时,音频信号通过L或R从手机发送到电路板,通过A\D模块转换成数据信号。刷卡时电路板拿到银行卡信息,再通过A\D模块转换成音频传输给手机。当然手机APP应用里在信号出入的时候,也需要做相应的A\D编解码工作,这个也是手机APP里最重要的工作。

3. 刷卡支付流程
0a4fffcc7b899e51e4c6912047a7d933c9950df4
手机与拉卡拉音频通讯示意图
手机与拉卡拉的音频通讯大概结构见上图,大概的流程是:

3.1 先在手机上打开拉卡拉的应用。点击刷卡。

3.2 手机应用会调用android api的AudioTrack,通过L和R线路给拉卡拉手机刷卡设备发送一段通知信号(L和R表达的信息是一样的,只是波形是反相的,L的高电平对应于R的低电平,还没搞清楚拉卡拉为什么这么设计)。

3.3 芯片上的通讯模块拿到音频信号,解码后发现是刷卡通知,就等待刷卡层传来刷卡信息。

3.4 在刷卡槽刷卡后,卡的信息传递给芯片。

3.5 芯片拿到卡的信息,编码成音频信号,同M线路发送给手机。

3.6 手机APP通过AudioRecord对音频信号进行采样,拿到数字信号。

3.7 手机APP程序通过对数据信号进行解码,拿到实际的数据信息,即卡的信息。

3.8 如果刷卡失败,则手机APP拿到的是一段失败提示信息。

3.9 至此手机与刷卡器的通讯完成,手机APP再使用此卡的信息与拉卡拉的服务器端后台通讯,处理后续支付操作。
其中的技术关键点是(细节本文暂不讨论):
a) 传输上使用什么样的调制方式,采用什么样的波特率、频率。
b) 通讯上如何制定合适的协议,包括如何判断信号开始、如何握手建立连接。
c) 编码上如何编码表示数据,如何校验和纠错,如何滤波和解码数据。

4. 相关的一些技术问题

4.1 A/D转换的问题
耳机线传输一般是1250HZ~9600HZ之间的交流音频信号。信号的调制解调有3种方式,调幅(AM)、调频(FM)和调相(PM)三种。根据对拉卡拉的输入输出信号的分析,我们发现拉卡拉使用的是调幅方式,频率为9600HZ和4800HZ的音频信号。

4.2 耳机接口标准的问题
两种标准见2.2中所描述的。

4.3 不同android手机的问题
a) 我们发现,在个别手机上,接收到的音频波形跟其他手机相比,是反相的。即高电平的波峰变成了低电平的波谷。这个问题可以在解码的时候,根据特定的前导码来判断。
b) 手机Mic采样到的音频信号电平可能会不一样。例如同一段音频信号,使用A手机AudioRecord采样出来,波峰的值大概是32000;另外找一个手机可能是3200,根据我们的多种不同手机测试,发现可以相差10倍。处理方法是,可以额外的进行一次处理,先归一化;或者是滤波的时候,动态的根据峰值来调整阈值。
c) 有些手机的Mic不认非标准的采样率。比如三星的I9308,如果使用9600*4的采样率初始化AudioRecord,就会报错;改成44100就没有问题。

4.4 数据传输的效率
每次通讯报文在100字节左右(50个汉字左右)(见参考资料1)。因此,拉卡拉的传输效率看来不是问题。

4.5 数据传输的准确性
拉卡拉用了一个取巧的方式。其先发送一段9600HZ的音频信号,紧接着再发送一段表示同样数据的4800HZ的音频信号。如果手机能处理高频率的信号,后面的低频率信号就可以用来校验前面的数据。如果前面的处理有问题,直接从后面拿低频率的信号来处理也一样可以保证拿到完整的数据。当然,我们还可以在数据本身叫上校验和纠错。

4.6 数据的安全性
官方说“拉卡拉手机刷卡器内置有安全芯片,每一台刷卡器对应有唯一的银行卡磁条信息保护密钥。使用专用安全算法,保证一机一密,一次一密。用户的个人密码,采用RSA非对称加密方法进行加密”。(见参考资料1)
但是根据我们观察,貌似拉卡拉的电路板上并没有特殊的安全相关模块。整个体系也没有使用CA证书做相关的电子签名和身份认证。所以,其安全性还有待研究。

5. 更多的可能性
拉卡拉的使用场景还是太简单了。其基本上仅仅使用了一个刷卡后的信号通过音频MIC线路传输给手机APP的功能。传输的数据也很简单。其实相当于将数据从电路板发送到手机APP,通过L/R将数据从手机发送到电路板要更简单、更高效,这一块拉卡拉几乎没怎么用上。我们可以在这个系统结构上做更多双向的数据通讯,把一些安全性要求更高的操作放到硬件上来完成,从而实现更高的安全保障,提供更多的功能,适用更多的应用场景。

打开OmniGraffle之后,选择“文件”->“资源浏览器”->“型版”->“流程图”,双击打开。

在新打开的页面中,就可以看到我们熟悉的,函数流程图中需要的各种模块了。

复制需要的模块,粘贴到新建的画布上就可以了。

如果在上述的菜单下,找不到“流程图”的型版,可以点击“型版”页面,右下角的“设置”菜单,选择“恢复”->“型版”->“软件”->“流程图”,来恢复。

在SVN工程的根目录下执行

#!/bin/sh

#功能:导出SVN上两个给定的节点间修改的文件,输出到指定的目录中
#示例:sh svn_export_diff.sh rev1 rev2 outFolder

#获取SVN服务器地址
url=`svn info|awk -F ': ' '{if($1 == "URL"){print $2}}'`
#结尾添加斜杠/
SVN_ROOT=$url"/"
echo $SVN_ROOT

if [ $# -eq 3 ];then
svn diff --summarize -r $1:$2|awk '{print $2}' > $3/temp.txt
cat $3/temp.txt | while read line
do
mkdir -p $3/$line
rm -r $3/$line
svn export -r $2 $SVN_ROOT$line $3/$line
done
else
echo "./$0 rev1 rev2 outFoler"
fi


#!/bin/bash

#功能:导出本地修改的文件
#需要指定存放拷贝文件的目标目录,例如
#sh svn_export_local_files.sh ~/Downloads/temp/

files=`svn status | awk -F ' ' '{print $2}'`

n=0
for file in $files
do
#创建目录
d=${file%/*}
if [ -d "$d" ];then
mkdir -p $1$d
fi

#拷贝文件
if [ -f "$file" ];then
cp $file $1$file
fi
((n++))
done