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栈异常

因为水平有限,难免有疏忽或者不准确的地方,希望大家能够直接指出来,我会及时改正。一切为了知识的分享。

后续会有更多的精彩的内容分享给大家。