为了比较方便地分析代码的动态运行情况,有时候需要在没有发生异常的情况下打印堆栈,只需插入如下一段代码即可:
Log.d(TAG, Log.getStackTraceString(new Throwable()));
可见这里堆栈是通过Log.getStackTraceString(new Throwable())获取的,我们看看里面是如何实现的。
public static String getStackTraceString(Throwable tr) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
tr.printStackTrace(pw);
pw.flush();
return sw.toString();
}
这里重点是Throwable的printStackTrace函数,如下:
private void printStackTrace(Appendable err, String indent, StackTraceElement[] parentStack)
throws IOException {
..........
StackTraceElement[] stack = getInternalStackTrace();
..........
}
可见堆栈是通过getInternalStackTrace获取的,返回的是StackTraceElement数组。
private 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,这是在哪里初始化的呢?
private 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的实现:
static 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解释器中对该字节码的处理:
HANDLE_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函数。
private static native StackTraceElement[] nativeGetStackTrace(Object stackState);
实现在java_lang_Throwable.c中,如下:
static 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获取调用栈的。
ArrayObject* 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做了些什么:
ArrayObject* 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数组,里面包含了每一层调用函数的行数,类名,方法名,文件名,只要依次打印出来即可。