java是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由Sun Microsystems公司于1995年5月推出的Java程序设计语言和Java平台(即JavaEE, JavaME, JavaSE)的总称。本站提供基于Java框架struts,spring,hibernate等的桌面应用、web交互及移动终端的开发技巧与资料
保持永久学习的心态,将成就一个优秀的你,来 继续搞起java知识。
为了比较方便地分析代码的动态运行情况,有时候需要在没有发生异常的情况下打印堆栈,只需插入如下一段代码即可:
1Log.d(TAG, Log.getStackTraceString(new Throwable()));
可见这里堆栈是通过Log.getStackTraceString(new Throwable())获取的,我们看看里面是如何实现的。
1public static String getStackTraceString(Throwable tr) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); pw.flush(); return sw.toString(); }
这里重点是Throwable的printStackTrace函数,如下:
1private void printStackTrace(Appendable err, String indent, StackTraceElement[] parentStack) throws IOException { .......... StackTraceElement[] stack = getInternalStackTrace(); .......... }
可见堆栈是通过getInternalStackTrace获取的,返回的是StackTraceElement数组。
1private StackTraceElement[] getInternalStackTrace() { if (stackTrace == EmptyArray.STACK_TRACE_ELEMENT) { stackTrace = nativeGetStackTrace(stackState); stackState = null; return stackTrace; } else if (stackTrace == null) { return EmptyArray.STACK_TRACE_ELEMENT; } else { return stackTrace; } }
可见是通过nativeGetStackTrace来获取调用栈的,这是个native函数,其中要注意的是参数是stackState,这是在哪里初始化的呢?
1private transient volatile Object stackState; public Throwable() { this.stackTrace = EmptyArray.STACK_TRACE_ELEMENT; fillInStackTrace(); } public Throwable fillInStackTrace() { stackState = nativeFillInStackTrace(); stackTrace = EmptyArray.STACK_TRACE_ELEMENT; return this; }
原来stackState是通过nativeFillInStackTrace来设置的,而nativeFillInStackTrace是在Throwable构造函数中调到的。我们来看看nativeFillInStackTrace的实现:
1static void Dalvik_java_lang_Throwable_nativeFillInStackTrace(const u4* args, JValue* pResult) { Object* stackState = NULL; stackState = dvmFillInStackTrace(dvmThreadSelf()); RETURN_PTR(stackState); } INLINE Object* dvmFillInStackTrace(Thread* thread) { return (Object*) dvmFillInStackTraceInternal(thread, true, NULL); } void* dvmFillInStackTraceInternal(Thread* thread, bool wantObject, int* pCount) { ArrayObject* stackData = NULL; int* simpleData = NULL; void* fp; void* startFp; int stackDepth; int* intPtr; if (pCount != NULL) *pCount = 0; fp = thread->curFrame; while (fp != NULL) { const StackSaveArea* saveArea = SAVEAREA_FROM_FP(fp); const Method* method = saveArea->method; if (dvmIsBreakFrame(fp)) break; if (!dvmInstanceof(method->clazz, gDvm.classJavaLangThrowable)) break; fp = saveArea->prevFrame; } startFp = fp; stackDepth = 0; while (fp != NULL) { const StackSaveArea* saveArea = SAVEAREA_FROM_FP(fp); if (!dvmIsBreakFrame(fp)) stackDepth++; fp = saveArea->prevFrame; } if (wantObject) { stackData = dvmAllocPrimitiveArray('I', stackDepth*2, ALLOC_DEFAULT); intPtr = (int*) stackData->contents; } else { simpleData = (int*) malloc(sizeof(int) * stackDepth*2); intPtr = simpleData; } if (pCount != NULL) *pCount = stackDepth; fp = startFp; while (fp != NULL) { const StackSaveArea* saveArea = SAVEAREA_FROM_FP(fp); const Method* method = saveArea->method; if (!dvmIsBreakFrame(fp)) { *intPtr++ = (int) method; if (dvmIsNativeMethod(method)) { *intPtr++ = 0; /* no saved PC for native methods */ } else { *intPtr++ = (int) (saveArea->xtra.currentPc - method->insns); } stackDepth--; // for verification } fp = saveArea->prevFrame; } bail: if (wantObject) { dvmReleaseTrackedAlloc((Object*) stackData, dvmThreadSelf()); return stackData; } else { return simpleData; } }
这里首先回溯到首个不是break frame和throwable的栈帧,然后计算出栈的深度,接下来创建一个int数组,长度是栈深度的两倍,为什么是两倍呢?因为数组中既要保存Method的地址,又要保存pc的相对偏移,这个相对偏移的作用是可以计算出该偏移处所在的代码行数。值得注意的是如果是native函数,则pc偏移为0,因为这个相对偏移的概念只是针对interpreted code的字节码的。这里会给数组返回,保存在Throwable类的stackState中。也就是说,当我们抛出一个Throwable时,在其构造函数中就会计算好调用栈并设置到stackState中。之后如果需要,可以随时printStackTrace。
这里有个问题,就是saveArea->xtra.currentPc是在哪里设置的呢?答案是在new Throwable时。这里new关键字对应着字节码中的OP_NEW_INSTANCE,我们看Dalvik解释器中对该字节码的处理:
1HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/) { ClassObject* clazz; Object* newObj; EXPORT_PC(); vdst = INST_AA(inst); ref = FETCH(1); clazz = dvmDexGetResolvedClass(methodClassDex, ref); if (clazz == NULL) { clazz = dvmResolveClass(curMethod->clazz, ref, false); if (clazz == NULL) GOTO_exceptionThrown(); } if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) GOTO_exceptionThrown(); newObj = dvmAllocObject(clazz, ALLOC_DONT_TRACK); SET_REGISTER(vdst, (u4) newObj); } FINISH(2); OP_END
这里通过EXPORT_PC将当前pc导出到saveArea->xtra.currentPc中,其实很多指令码的处理开头都会这样做,就是为了记录执行路径。在异常发生时便于回溯。好了我们回到nativeGetStackTrace,这是个native函数。
1private static native StackTraceElement[] nativeGetStackTrace(Object stackState);
实现在java_lang_Throwable.c中,如下:
1static void Dalvik_java_lang_Throwable_nativeGetStackTrace(const u4* args, JValue* pResult) { Object* stackState = (Object*) args[0]; ArrayObject* elements = NULL; elements = dvmGetStackTrace(stackState); RETURN_PTR(elements); }
可见是通过dvmGetStackTrace获取调用栈的。
1ArrayObject* dvmGetStackTrace(const Object* ostackData) { const ArrayObject* stackData = (const ArrayObject*) ostackData; const int* intVals; int stackSize; stackSize = stackData->length / 2; intVals = (const int*) stackData->contents; return dvmGetStackTraceRaw(intVals, stackSize); }
这里的intVals就是之前我们说的Throwable的stackState数组,里面保存了调用栈的Method地址和PC偏移。接下来看看dvmGetStackTraceRaw做了些什么:
1ArrayObject* dvmGetStackTraceRaw(const int* intVals, int stackDepth) { ArrayObject* steArray = NULL; int i; if (!dvmIsClassInitialized(gDvm.classJavaLangStackTraceElement)) dvmInitClass(gDvm.classJavaLangStackTraceElement); steArray = dvmAllocArray(gDvm.classJavaLangStackTraceElementArray, stackDepth, kObjectArrayRefWidth, ALLOC_DEFAULT); for (i = 0; i < stackDepth; i++) { Object* ste; Method* meth; StringObject* className; StringObject* methodName; StringObject* fileName; int lineNumber, pc; const char* sourceFile; char* dotName; ste = dvmAllocObject(gDvm.classJavaLangStackTraceElement,ALLOC_DEFAULT); meth = (Method*) *intVals++; pc = *intVals++; if (pc == -1) // broken top frame? lineNumber = 0; else lineNumber = dvmLineNumFromPC(meth, pc); dotName = dvmDescriptorToDot(meth->clazz->descriptor); className = dvmCreateStringFromCstr(dotName); free(dotName); methodName = dvmCreateStringFromCstr(meth->name); sourceFile = dvmGetMethodSourceFile(meth); if (sourceFile != NULL) fileName = dvmCreateStringFromCstr(sourceFile); else fileName = NULL; JValue unused; dvmCallMethod(dvmThreadSelf(), gDvm.methJavaLangStackTraceElement_init, ste, &unused, className, methodName, fileName, lineNumber); dvmReleaseTrackedAlloc(ste, NULL); dvmReleaseTrackedAlloc((Object*) className, NULL); dvmReleaseTrackedAlloc((Object*) methodName, NULL); dvmReleaseTrackedAlloc((Object*) fileName, NULL); if (dvmCheckException(dvmThreadSelf())) goto bail; dvmSetObjectArrayElement(steArray, i, ste); } bail: dvmReleaseTrackedAlloc((Object*) steArray, NULL); return steArray; }
这里传入了stackState,里面保存了调用栈的每一层的Method和PC。首先创建StackTraceElement数组,然后遍历stackState,新建StackTraceElement,从stackState中取出Method和pc,获取lineNumber、className、methodName、fileName,调用StackTraceElement的构造函数将这些参数都设置到Java类中。最后返回这个StackTraceElement数组。
至此,Java类中已经获取到了调用栈的StackTraceElement数组,里面包含了每一层调用函数的行数,类名,方法名,文件名,只要依次打印出来即可。
android栈异常
因为水平有限,难免有疏忽或者不准确的地方,希望大家能够直接指出来,我会及时改正。一切为了知识的分享。
后续会有更多的精彩的内容分享给大家。