以前面试的时候总会被人问起一些Java里面的很多的东西,比如说ArrayList和Vector内部是如何实现当时我心里就一万个的草泥马,平时我们都不是只管用吗,只要会去使用就行了,管它其他的什么乱七八糟的。后来被人问多了就会慢慢的带着好奇心去看了。下面我就以android-19的api来进行分析,因为Android大部分的源代码都已经开源了而且都比较完善的,可以直接还可以看到jni层的代码实现,但是我们就看不到java的jni层的实现的。
在了解这个之前我们首先需要看看System.arraycopy()函数的使用以及底层的实现。因为ArrayList里面有一个最关键地方就是调用了该方法去实现的。
1. 数组拷贝的使用
一般我们在进行数组拷贝和移动的时候都会想到以下几种的方法
- 使用for循环依次的遍历移动
- 使用System.arraycopy()方法
- 使用Arrays.copyOf()方法
- 使用Object.clone()方法
第一种方法比较简单就是我们传统的使用for循环依次的遍历去移动赋值操作,下面我们通过使用简单的代码来实现2,3方法。
private static void SystemArrayCopy() {
int array1[] = { 10, 9, 6, 12, -9, 1 };
int array2[] = { 3, 7, -2, -6, 1, 2, 10 };
/**
* 注意: 这里的srcPos, destPos 都必须大于或等于0
* srcPos+length<= srcArray.length
* destPos + length <= destArray.length
*/
System.arraycopy(array1, 2, array2, 4, 3);
for (int num : array1) {
System.out.print(num + ",");
}
System.out.println();
}
private static void arrayCopy() {
int array1[] = { 10, 9, 6, 12, -9, 1 };
int array2[] = { 3, 7, -2, -6, 1, 2, 10 };
int array3[] = Arrays.copyOf(array1, 10);
for (int num : array3) {
System.out.print(num + ",");
}
System.out.println();
}
System arraycopy 结果: 10,9,6,12,-9,1,
Arrays copyOf 结果: 10,9,6,12,-9,1,0,0,0,0,
从上面的代码中我们可以看的出来Arrays.copyOf()
方法对于结束位置是没有要求的,但是它对于开始位置是有要求的,其实Arrays.copyOf()
还是使用了System.arrayCopy()方法去实现的
/**
* 对指定的int数组,结束位置为newLength进行数组拷贝,startPos为0
*/
public static int[] copyOf(int[] original, int newLength) {
if (newLength < 0) {
throw new NegativeArraySizeException(Integer.toString(newLength));
}
return copyOfRange(original, 0, newLength);
}
/**
* @param original 拷贝的源数组
* @param start 从数组中拷贝的开始位置
* @param end 结束位置
* @return 将最后数组拷贝之后的结果返回出去
*/
public static <T> T[] copyOfRange(T[] original, int start, int end) {
int originalLength = original.length;
//开始位置不能大于结束位置
if (start > end) {
throw new IllegalArgumentException();
}
//开始位置不能是负数并且开始位置不能大于数组的长度
if (start < 0 || start > originalLength) {
throw new ArrayIndexOutOfBoundsException();
}
int resultLength = end - start;
int copyLength = Math.min(resultLength, originalLength - start);
//这个地方是重新创建一个长度为resultLength的数组。
T[] result = (T[]) Array.newInstance(original.getClass().getComponentType(), resultLength);
//最后还是调用了native arraycopy方法将original数组的指定内容拷贝到result数组中
System.arraycopy(original, start, result, 0, copyLength);
return result;
}
其实不管数组的如何拷贝最后还是调用了System的arrayCopy的本地方法去进行操作,因为Java最后还是通过jni调用底层的C语言来实现的,由于Android是开源的所以我就直接找到了 /dalvik/vm/native/java_lang_System.cpp来看看其内部实现,如果你也想研究Java的该类底层是如何实现的话,你可以直接下载OpenJDK,因为Oracle的JDK好像是看不到底层的实现的。
首先会传进来一个整数数组的指针,该整数数组存放着四个整数,第0个整数存放的是源数组的一个封装类地址,第1个整数存放的是数组开始拷贝的起始位置,第2个整数表示的目标数组的一个封装类地址,第3个表示移动到目标数组的起始位置,第4个表示的是需要移动的个数。
static void Dalvik_java_lang_System_arraycopy(const u4* args, JValue* pResult)
{
//该类指针变量封装了源头数组
ArrayObject* srcArray = (ArrayObject*) args[0];
int srcPos = args[1];
ArrayObject* dstArray = (ArrayObject*) args[2];
int dstPos = args[3];
int length = args[4];
//检查源数组指针变量是否为空
if (srcArray == NULL) {
dvmThrowNullPointerException("src == null");
RETURN_VOID();
}
//检查拷贝到目的地数组指针是否为空
if (dstArray == NULL) {
dvmThrowNullPointerException("dst == null");
RETURN_VOID();
}
/* Make sure source and destination are arrays. */
//接着判断是否是数组类型
if (!dvmIsArray(srcArray)) {
dvmThrowArrayStoreExceptionNotArray(((Object*)srcArray)->clazz, "source");
RETURN_VOID();
}
if (!dvmIsArray(dstArray)) {
dvmThrowArrayStoreExceptionNotArray(((Object*)dstArray)->clazz, "destination");
RETURN_VOID();
}
/* avoid int overflow */
//接着继续判断位置和长度是否数组越界。个人觉得这部分可以放在java层进行处理的。
if (srcPos < 0 || dstPos < 0 || length < 0 ||
srcPos > (int) srcArray->length - length ||
dstPos > (int) dstArray->length - length)
{
dvmThrowExceptionFmt(gDvm.exArrayIndexOutOfBoundsException,
"src.length=%d srcPos=%d dst.length=%d dstPos=%d length=%d",
srcArray->length, srcPos, dstArray->length, dstPos, length);
RETURN_VOID();
}
.......
}
上面只是一些条件判断:判断传进来的数据是否是数组以及位置和长度是否会导致数组越界,接下来则会判断数组的数据类型是否一样,并且会对基本数据类型和引用数据类型分别进行操作。
static void Dalvik_java_lang_System_arraycopy(const u4* args, JValue* pResult)
{
........
ClassObject* srcClass = srcArray->clazz;
ClassObject* dstClass = dstArray->clazz;
char srcType = srcClass->descriptor[1];
char dstType = dstClass->descriptor[1];
//如果其中一个数组是基本数据类型数组,那么另外一个数组也必须是数组类型数组。
bool srcPrim = (srcType != '[' && srcType != 'L');
bool dstPrim = (dstType != '[' && dstType != 'L');
if (srcPrim || dstPrim) {
//这里需要判断两个数组的数据类型是否一样,并且是否都是基本数据类型
if (srcPrim != dstPrim || srcType != dstType) {
dvmThrowArrayStoreExceptionIncompatibleArrays(srcClass, dstClass);
RETURN_VOID();
}
//如果两个数组只要有一个是基本数据类型,并且两者的数据类型都要一样才进入数组拷贝中
switch (srcType) {
case 'B':
case 'Z':
/* byte数组和boolean数组的移动,该类型的数组的每个元素都是一个字节大小 */
memmove((u1*) dstArray->contents + dstPos,
(const u1*) srcArray->contents + srcPos,
length);
break;
case 'C':
case 'S':
/* 2 char数组和 short数组的移动,该数组的每个元素都是两个字节大小,也就是16个比特 */
move16((u1*) dstArray->contents + dstPos * 2,
(const u1*) srcArray->contents + srcPos * 2,
length * 2);
break;
case 'F':
case 'I':
/* int数组和float数组的移动,该数组的每个元素都是四个字节大小 */
move32((u1*) dstArray->contents + dstPos * 4,
(const u1*) srcArray->contents + srcPos * 4,
length * 4);
break;
case 'D':
case 'J':
/* double数组和long数组的移动,该数组的每个元素都是8个字节大小,直接调用move32方法进行*/
/*
* 8 bytes per element. We don't need to guarantee atomicity
* of the entire 64-bit word, so we can use the 32-bit copier.
*/
move32((u1*) dstArray->contents + dstPos * 8,
(const u1*) srcArray->contents + srcPos * 8,
length * 8);
break;
default:
//非法的数组类型
ALOGE("Weird array type '%s'", srcClass->descriptor);
dvmAbort();
}
}
......
RETURN_VOID();
}
上面就是基本数据类型数组的移动和拷贝,从上面我们可以看得出来如果要将数组从源数组拷贝到目标数组的话,两个数组的数据类型必须是一样的。对于每个元素大小都是一个字节大小的数组,调用的则是系统的memmove()进行数组移动的,其他的move32()和move16()其实最后还是调用的 static void memmove_words(void* dest, const void* src, size_t n)方法的,这里使用了一个宏定义。
#define move16 memmove_words
#define move32 memmove_words
通过这宏定义我们可以知道不管是move16还是move32的话,其实我们最后的是一个方法那就是 memmove_words 方法,只是命名上更加的容易的理解里面的意思的。
除了上面的基本数据类型的数组的拷贝外,其实我们还有的数组是对象的,下面接着看看对象数组的拷贝,还是以前的老方法。
static void Dalvik_java_lang_System_arraycopy(const u4* args, JValue* pResult) {
.......
//首先计算当前一个对象占用多少空间大小
const int width = sizeof(Object*);
//1、首先我们需要判断两个数组的维数是否一样,比如说都是一位数组,或者是两个都是二位数组
//2、然后判断两个数组的类型是否一样的,如果类型不一样的话,不能进行数组拷贝
if (srcClass->arrayDim == dstClass->arrayDim && dvmInstanceof(srcClass, dstClass)) {
//1、需要目的地数组位置,通过起始位置+需要的空间大小
//2、接着获取原数组的结束,也是通过起始位置+需要空间的大小
//3、获取需要拷贝数据的大小,通过length * width(表示单个对象的大小*个数)
move32((u1*)dstArray->contents + dstPos * width,
(const u1*)srcArray->contents + srcPos * width,
length * width);
//4、这里就调用Android虚拟机内部代码来进行实现了
dvmWriteBarrierArray(dstArray, dstPos, dstPos+length);
} else {
//下面就是不同对象数组或者是不同维度数组的拷贝了,由于本人能力有限就不进行分析了
Object** srcObj;
int copyCount;
ClassObject* clazz = NULL;
srcObj = ((Object**)(void*)srcArray->contents) + srcPos;
if (length > 0 && srcObj[0] != NULL) {
clazz = srcObj[0]->clazz;
if (!dvmCanPutArrayElement(clazz, dstClass))
clazz = NULL;
}
for (copyCount = 0; copyCount < length; copyCount++) {
if (srcObj[copyCount] != NULL && srcObj[copyCount]->clazz != clazz &&
!dvmCanPutArrayElement(srcObj[copyCount]->clazz, dstClass)) {
break;
}
}
move32((u1*)dstArray->contents + dstPos * width,
(const u1*)srcArray->contents + srcPos * width, copyCount * width);
dvmWriteBarrierArray(dstArray, 0, copyCount);
if (copyCount != length) {
dvmThrowArrayStoreExceptionIncompatibleArrayElement(srcPos + copyCount,
srcObj[copyCount]->clazz, dstClass);
RETURN_VOID();
}
}
}
通过上面的代码我们可以一层一层的知道上面的Api执行的原理了,也大概慢慢的明白了高级语言大概是如何执行的,对于一些喜欢研究高级语言是如何实现一个函数提供了帮助。下面总结一下Java中的System.arraycopy原理和步骤:
- 首先上层调用Java类System中的arraycopy方法,接着调用Dalvilk中的 java_lang_System.cpp类
- 然后再Jni中首先会判断传入的两个对象是否是数组类型,接着判断传入的长度和位置是否会导致数组越界问题
- 接着判断是否是基本数据类型(int,long,boolean,char,double,float)数组,并且数组维度需要相等(所谓的是数组维度是指一维数组还是二维数组)。如果是char或者是boolean类型的话直接调用系统函数 memmove 进行数据移动,否则调用其内部方法 memmove_words 进行操作的。
- 如果不是基本数据类型数组,而是对象数组的话。接着判断数组类型是否是相同类,以及维度是否相同,调用dvmWriteBarrierArray进行数据写入
- 如果不是对象数组,或者是维度也不同的话,那么进行另外的数据拷贝问题
下面我将通过一个流程图更加形象的说明
总结
通过在Android4.4 上面的代码我们可以很清晰的理解上层Java代码在进行数组拷贝时的原理和内部实现了,换位思考一下如果我们是Google的开发者,你们会怎么去实现数组的拷贝呢?其实我们在大学学习C语言的时时候候老师也会经常让我们自己去实现一个数组的拷贝的,那个时候的我们并没有这种意思,对我们写的代码进行一个封装成SDK的思想,然后以后如果我们需要使用的时候就直接引用过来了。其实细心的我们发现那些基本数据类型的数组是否也可以放到Java层去实现的呢?其实在 Android6.0中 System类中确实提供了对基本数据类型的数组的拷贝。同时由于 C语言的执行效率比Java的效率要更加的高效,当我们遇到数组拷贝和移动的个数比较多时候,内部一样还提供了 native方法来对外进行调用。
Android 6.0 System类中的数组拷贝方法:
public final class System {
private static final int ARRAYCOPY_SHORT_CHAR_ARRAY_THRESHOLD = 32;
由于有7个方法涉及到拷贝,所以这里我也不一一进行列举了,感兴趣的时候可以去看看
......
public static void arraycopy(int[] src, int srcPos, int[] dst, int dstPos, int length) {
if (src == null) {
throw new NullPointerException("src == null");
}
if (dst == null) {
throw new NullPointerException("dst == null");
}
if (srcPos < 0 || dstPos < 0 || length < 0 ||
srcPos > src.length - length || dstPos > dst.length - length) {
throw new ArrayIndexOutOfBoundsException(
"src.length=" + src.length + " srcPos=" + srcPos +
" dst.length=" + dst.length + " dstPos=" + dstPos + " length=" + length);
}
if (length <= ARRAYCOPY_SHORT_INT_ARRAY_THRESHOLD) {
// Copy int by int for shorter arrays.
if (src == dst && srcPos < dstPos && dstPos < srcPos + length) {
// Copy backward (to avoid overwriting elements before
// they are copied in case of an overlap on the same
// array.)
for (int i = length - 1; i >= 0; --i) {
dst[dstPos + i] = src[srcPos + i];
}
} else {
// Copy forward.
for (int i = 0; i < length; ++i) {
dst[dstPos + i] = src[srcPos + i];
}
}
} else {
// 这里有一个非常重要的就是当涉及到拷贝的数组个数大于 32个的时候,则会调用native的数组拷贝了
arraycopyIntUnchecked(src, srcPos, dst, dstPos, length);
}
}
......
}
通过对Android6.0 中的System类的分析我们可以得出如果涉及到数组拷贝的长度小于等于32个的时候,则调用Java的代码来进行操作,否则就调用 Native 方法进行操作
后记
由于个人能力水平有限只能对这些进行一个浅显的分析了,其目的是为了让大家养成一种刨根问底的习惯而不光只是一个会调用Api的人,在使用别人 Api的同时我们也更应该想想对方为什么会这么实现的原因和想法,同时也思考自己能不能有更好的思路来实现,其实这些东西在我们大学的时候就已经有了,只是我们那个时候我们也根本没有这种意识和想法就是对自己写的代码进行一个更好的封装,然后变成文档最后给别人或者是自己使用,这样子也可以减少平时开发中的一些重复工作量。