Android平台的崩溃捕获

浩浩 等级 395 0 0

1、简述

在日常开发中,崩溃日志的捕获,至关重要。有一个好的日志,有利于开发者快速定位问题并解决。

对于Android平台,我们可以使用现成的产品来捕获崩溃日志,这些产品包括BuglyFirebase友盟等,这些产品经过多年的迭代,对于日志捕获得比较全,也有很好的兼容性。

但是作为开发者,我们不能仅仅满足于使用,最好还是知道其中背后的原理。要知道原理,可以在网络上搜索崩溃捕获的相关文章,虽然网络上的文章绝大部分都是相互转载的,但是我们还是能从其中捋出一个大概的崩溃捕获的原理。注意,但是来了,但是我们仅仅通过读他人的文章来了解原理,而不是自己动手实现一遍,和纸上谈兵并无太大差异。

目前的网络上,讲解崩溃原理捕获的文章不少,但是这些文章仅仅是讲大致的原理,对于设计到的代码也只是仅仅展示片段,完全找不到一个能运行起来的并能捕获到Native崩溃栈的Demo。这就导致我们看文章理解到的原理不是串联起来的,是断断续续的。基于这个状况,我会写篇文章,首先主要讲解奔溃栈捕获的相关原理,然后主要是分析崩溃捕获的代码实现,将原理落在实处,在文章的末尾会提供我实现的能运行的并且能捕获到崩溃日志的Demo工程,希望能帮助大家对崩溃捕获原理有一个连贯认识。

接下来就先讲讲崩溃捕获的相关原理,先分析Java层的崩溃捕获,再分析Native层的崩溃捕获原理,有不对的地方,望不吝指正。

2、Java层崩溃捕获

2、1 异常发生时方法调用栈的捕获

在Java层的奔溃日志捕获比较简单,接下来看看如何操作。

首先设置默认的未捕获异常处理器:

 Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
                StringBuilder builder = new StringBuilder();
                for (StackTraceElement stackTraceElement : e.getStackTrace()) {
                    builder.append("at ")
                            .append(stackTraceElement.getClassName())
                            .append(".")
                            .append(stackTraceElement.getMethodName())
                            .append("(")
                            .append(stackTraceElement.getFileName()).append(":").append(stackTraceElement.getLineNumber())
                            .append(")")
                            .append("\n");
                }
                Log.e(TAG, "堆栈:\n 线程:" + t.getName() + " 原因" + e.getMessage() + builder.toString());
            }
        })

在上面的代码中,调用Thread的静态方法,设置了默认的未捕获异常处理程序。当一个线程发生未捕获的异常,并且没有设置过异常处理器,那么就会调用这里设置的默认的异常处理程序。

然后写一段触发崩溃的代码并调用,代码如下:

 private void crash() {
        int n = 100;
        System.out.println(n / 0);
    }

在上面的代码中,会产生异常的地方就是使用100除以了0。当代码执行到这里,会在控制台看到我们捕获并打印出来的方法调用栈:


可以看到,当异常发生的时候,我们的确是捕获到了异常发生线程的方法调用栈,然后很快就可以定位到出问题的代码。

上面介绍了当Java层崩溃日志的捕获,接下来作为补充介绍一下Java的异常机制。

2、2 Java异常机制

在目前网络上的文章,对于Java异常这一块,大部分的都是简单的讲讲Java中异常的分类,有哪些异常等。这里现提出几个问题,作为这一小节的大纲,下面会对每一个问题进行分析:

  1. Java中异常存在的意义,也就是为什么要设计异常体系。
  2. 对于不同的异常应该怎么处理?是全部catch就好了吗?
  3. 日常开发中如何使用异常?
  4. 当异常发生后JVM怎么处理的?

下面将一一分析。

1、Java中异常存在的意义

首先,我将通过两个简单程序,一个是有异常机制的Java程序,一个是没有异常机制的C程序,分别看看对于程序中的非正常情况(异常情况)都是怎么处理的。

这里以打开文件并且文件不存在的情况看看两种语言的处理:

Java程序:

 void main() {
        File f = new File("文件路劲");
        try {
            FileOutputStream fos = new FileOutputStream(f);
            //文件打开成功,继续执行后续逻辑
            ...
        } catch (FileNotFoundException e) {
            //文件打开失败,异常处理
        }
    } 

C程序:

void main() {
    FILE *f = fopen("文件路劲", "rb");
    if (f == NULL) {
        //文件打开失败,处理文件打开失败的情况
        //这里输出错误的原因
        printf("%s\n", strerror(errno));
    } else {
        //这里文件打开成功,继续执行
        ...
    }
}

通过对比可以看到,Java语言对于文件不存在的异常,在编码期间编译器就要求处理这种情况,不然编译不通过,而C语言则不会强制要求,通过库函数的返回值来判断文件打开是否异常。Java语言将异常的情况的处理代码放在了catch块中,和正常的逻辑的程序代码分开了,而C语言中是正常逻辑代码和异常情况代码是混在一起的。

C是一门伟大的语言,咱得客观看待不同语言的优劣,语言只是工具。

通过上面简单的对比,这里总结一下Java异常机制的意义,就会有一个比较直观的感受。

总的来说,Java异常机制能增强程序的稳定性,具体些,使用异常机制能:

  1. 正常做事的代码出了问题处理的代码分离,便于阅读和测试等;
  2. 要求检查和处理异常,增强了程序稳定性;
  3. 降低了开发者对库函数的了解要求;
  4. 异常一般携带方法调用栈,便于调试;
  5. ...

Java异常存在的意义大致分析完了,接下来分析Java对于不同的异常应该怎么处理。

2、对于不同的异常应该怎么处理

要知道对应不同的异常应该怎么处理,那么首先就得知道有哪异常,并且这些异常JDK设计出来是为了什么。首先来看看Java异常类的继承关系图,对异常的体系有一个整体的认知。


这里讲的异常不仅仅指Exception及其子类,还包括Error

可以看到,我们平常泛指的异常其实有两个直接子类:Error(错误)和Exception(异常)。

Error(错误): 当错误发生时,表示程序遇到了灾难性的错误,是程序无法控制和处理的问题。合理的程序是不应该捕获和处理错误的。

Exception(异常): 它是指阻止当前方法或作用域继续执行的问题(----Think in java),其实白话一些的说,那就是当前程序出问题了,无法正常的运行下去了。

对于Exception类型的异常,程序是可以处理的,我们应当尽量避免尽量处理这些异常,这句话看起来矛盾,接下来会有说明。Exception下有一个直接子类RuntimeException,这个子类下面的异常都是非检查异常,这类异常程序员往往都是能够预见或避免的,这类异常发生,往往是代码逻辑存在问题,更多的是应该排查代码的逻辑,而不是依赖于try catch来保证程序的运行。对于不能够预见或避免除外,这类使用catch处理,比如NumberFormatExceptionException下除了RuntimeException的其他子类几乎都是受检查异常,这些异常程序员一般是不能预见和避免的,这类异常一般和代码逻辑无关,我们只有尽量的去检查并做好异常处理。

很好区分受检查异常和非检查异常,只要写代码阶段,编译器制要求我们处理的异常就是检查异常,不要求的就是非检查异常。

上面理论讲得很空泛,下面将针对每个问题用一个实际的情况来充实一下内容。

内存溢出错误(OutOfMemoryError),它是Error的子类,当Java虚拟机由于内存不足而无法分配对象,并且垃圾回收器无法再腾出更多内存时,就会抛出该错误。对于Android程序来说,出现这个错误的原因可能很多,比如内存逐渐泄漏导致堆空间逐渐减小;比如加载过大数据,像我14年工作的时候,那时手机内存相对于现在比较小,加载图片的时候就容易出现内存溢出的错误;等等。

当出现这个错误的时候,尽管我们程序catch了它,还是会崩溃的。所以我们不应该去捕获和处理错误,而是应该排查和处理程序中可能导致该错误的代码,同时检查程序运行的环境。

空指针异常(NullPointerException),这个是我们可能经常遇到的异常,它是RuntimeException的子类,也就是运行时异常(非检查异常)。这个异常时可以预见和避免的,当这个异常出现,表示程序代码有逻辑上的错误,应该去排查代码逻辑,而不是使用try catch将可能出现空指针的代码包裹起来。

FileNotFoundException(文件找不到异常),这个异常属于检查异常,在编写文件读写代码的时候,编译器是明确要求处理该异常的,这种异常一般都是外部产生的,程序逻辑没问题。因为文件的存在与否,是和程序的运行环境有关的,程序的运行环境各种各样,这个是无法预见和避免的,所以就只有尽量的去做好异常的检查和处理。

上面讲了对于不同的异常应该怎么处理,除了处理异常,那么我们在开发中能使用异常机制吗?必须能,接下来分析日常开发中应该如何使用异常。

3、日常开发中如何使用异常

在我们日常开发中,很少会自己设计和抛出异常,几乎都是在处理异常。但是Android系统很多方法会抛出异常,JDK很多方法也会抛出异常,就说明我们这种只会处理异常,而不会使用(狭隘点可以理解为抛出)异常是不太合理的。那么在日常的开发中,应该如何较好的使用异常呢?

对于异常的如何使用,很大程度取决于开发者对异常机制的理解。下面将会阐述一下我的理解,读者可以作为一个参考。

对于这个话题,我们分两步来讨论:

  1. 什么情况下该抛异常?
  2. 应该抛出什么异常?

对于问题1,在我们编写方法的时候,首先应该明确方法的职责,如果发生了方法职责之外的情况并且不知道如何处理的时候才抛出异常。这里还需要注意的点是,不能使用异常来参与流程的控制。

下面通过两个例子来加深印象,首先看看Android 中的RemoteViews源码中的一个方法

 private Action getActionFromParcel(Parcel parcel, int depth) {
        int tag = parcel.readInt();
        switch (tag) {
            //case ...
            case SET_RIPPLE_DRAWABLE_COLOR_TAG:
                return new SetRippleDrawableColor(parcel);
            case SET_INT_TAG_TAG:
                return new SetIntTagAction(parcel);
            default:
                throw new RemoteViews.ActionException("Tag " + tag + " not found");
        }
    }

可以看到,这个方法对于很多的case都能处理,但当走到default流程的时候,表示这种情况不能处理,也不知道如何处理,然后就抛出了异常。

接下来再看一个Fragment中的方法:

public class Fragment implements ComponentCallbacks2, View.OnCreateContextMenuListener {
    //...
    public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
        try {
            Class<?> clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                if (!android.app.Fragment.class.isAssignableFrom(clazz)) {
                    throw new android.app.Fragment.InstantiationException("Trying to instantiate a class " + fname
                            + " that is not a Fragment", new ClassCastException());
                }
                sClassMap.put(fname, clazz);
            }
            android.app.Fragment f = (android.app.Fragment) clazz.getConstructor().newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.setArguments(args);
            }
            return f;
        } catch (ClassNotFoundException e) { ...}
        //一系列的catch
    }
    ...

}

这个方法的职责,就是据传入的类名创建对应的Fragment实例,当传入的类名不是Fragment的子类类名时,方法的设计者认为该方法无法处理这种情况,就抛出了异常。

对于日常开发中,一定不要随便的抛异常,比如方法中会有一个主流程,如果遇到一些分支流程就抛出异常是不可取的。要记住,当抛出异常的时候情况已经是比较严重的时候了。

对于问题2,如果要抛异常,应该抛什么类型的异常?这个问题使用《Effective Java》的一句话来回答:

对于可恢复的情况就抛出受检查异常,对于程序错误就抛出运行时异常。

Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

要能很好的使用异常机制,需要在实践中逐渐加深体会和理解。

上面介绍了日常开发中如何较好的使用异常机制的理解,接下来介绍当异常发生后JVM怎么处理的。

4、当异常发生后JVM怎么处理的

这一小节将会简单描述当异常发生后,Jvm处理异常的大致过程。

首先看一个具有try catch的简单方法:

public class Test {
    public void exceptionMethod() {
        try {
            System.out.println("try代码块");
        } catch (Exception e) {
            //...异常的处理
            e.printStackTrace();
        }
    }

}

接下来使用javap -c查看对应的字节码,这里只列出exceptionMethod方法对应的字节码命令:

public class com.example.jnicalljavatest.Test {
  //...

  public void exceptionMethod();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String try代码块
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: goto          16
      11: astore_1
      12: aload_1
      13: invokevirtual #6                  // Method java/lang/Exception.printStackTrace:()V
      16: return
    Exception table:
       from    to  target type
           0     8    11   Class java/lang/Exception
}

可以看到,下方有一个异常表( Exception table):

from: 代表异常处理器所监控范围的起始点。

to: 代表异常处理器所监控范围的结束点(该行不被包括在监控范围内,一般是 goto 指令)

target: 指向异常处理器的起始位置,在这里就是 catch 代码块的起始位置

type: 代表异常处理器所捕获的异常类型

建议先补充字节码指令相关的知识。

如果在在字节命令的执行过程中,JVM检测到会产生异常,那么Jvm会在堆上构造一个异常对象(这个对象会有异常的原因、位置和栈帧的快照等),停止当前的执行流程,将异常对象抛出,并且异常处理机制接收代码的执行,寻找合适的异常处理程序。

当程序触发了异常,如果当前方法有异常表,那么Java 虚拟机会遍历异常表,当触发的异常在这条异常处理器的监控范围内(from 和 to),且异常类型与异常表中type一致时,Java 虚拟机就会跳转到该异常处理器的起始位置(target)开始执行字节码。如果当前方法没有异常表或者没有找到匹配的异常处理程序,那么向上去调用当前方法的方法中(弹栈处理),重复前面的查找操作。如果所有的栈帧被弹出,仍然没有被处理,则抛给当前的Thread,Thread则会终止。

到这里,Java的异常机制介绍得差不多了,接下来介绍如何捕获Native层的崩溃堆栈。

3、Native层崩溃捕获

这一节可以参考这篇文章:Android 平台 Native 代码的崩溃捕获机制及实现

我们在接下来的代码中,将会使用libunwind库来解析native层的调用栈,如何编译libunwind库可以参考我这篇文章:Mac使用NDK21编译libunwind

4、崩溃捕获代码解释

在这一小节,主要是讲解Demo中的崩溃捕获代码,尽量争取让读者能看明白代码执行的大致流程。

demo地址:https://github.com/ITFlyPig/crash_sdk

这一小节主要分为几个步骤介绍源码:

  1. 崩溃捕获代码注册
  2. 发生崩溃时调用栈的解析
  3. 将Native解析到的调用栈回传到Java层
  4. Java层解析Java崩溃调用栈

1、崩溃捕获代码注册

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_crashsdk_MainActivity_initCrashSDK(JNIEnv *env, jobject thiz, jobject crash_log) {
    //创建一个全局的引用
    g_obj = env->NewGlobalRef(crash_log);
    //开启子线程,目的是在Native调用Java层的方法,将Naive层的崩溃回传到Java层
    pthread_t tid;
    int ret = pthread_create(&tid, nullptr, dumpStack, nullptr);
    if (ret) {
        return ret;
    }
    //注册崩溃处理程序
    ret = register_crash_handler();
    return ret;
}

调用Java层的initCrashSDK方法会调到上面对应的Jni方法,在该方法中,首先创建了一个全局的jobject引用,这个对象是Java层的CrashLog的实例,目的是为了后面在新的线程中,调用该对象对应的类的方法,将Native层的崩溃日志传递到Java层。

接着开启了一个新线程阻塞等待,当堆栈解析完成之后会唤醒该线程继续执行,在该线程中将Naive层的崩溃回传到Java层。为什么要开启一个新的线程来传递Naive层的崩溃日志呢?因为在实践的时候,当前线程无法成功回调Java层的方法,原因暂时不清楚。

最后注册了崩溃处理程序,进去看看:

//注册信号的处理
static int register_crash_handler() {
    //为信号处理函数注册一个栈
    stack_t stack;
    memset(&stack, 0, sizeof(stack));
    stack.ss_size = SIGSTKSZ;
    stack.ss_sp = malloc(stack.ss_size);
    stack.ss_flags = 0;
    if (stack.ss_sp == NULL || sigaltstack(&stack, NULL) != 0) {
        return ERROR;
    }
    //需要捕获的信号
    //定义信号对应的处理结构体sigaction
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
    sa.sa_sigaction = sig_handler;//注册信号处理函数

    //分配保存之前的信号处理结构体的内存
    p_sa_old = static_cast<struct sigaction *>(calloc(sizeof(struct sigaction), SIG_NUMBER_MAX));

    for (int i = 0; sig_arr[i] != 0; i++) {
        int sig = sig_arr[i];
        if (sigaction(sig, &sa, &p_sa_old[sig]) != 0) {
            LOGE("信号处理注册失败");
            return ERROR;
        }
    }
    LOGE("信号处理注册成功");
    return SUCCESS;

}

该方法中,首先为信号处理函数设置了额外的栈空间。在实践中,如果不设置,那么信号处理函数将会无限被调用,直至再次崩溃。

接着在for循环中,通过sigaction注册了我们需要处理的信号。

当系统发出我们注册了的感兴趣的信号量之后,信号处理函数就会被调用:

static void sig_handler(const int code, siginfo *siginfo, void *context) {

    //要返回的堆栈日志
    char log[1024];
    snprintf(log, sizeof(log), "signal %d (%s)", code, sig_desc(code, siginfo->si_code));
    append(stack_log, log);

    //开始解堆栈
    slow_backtrace();
    pid_t tid = gettid();
    pthread_t t = pthread_self();
    LOGE("pid_t = %d, pthread_self = %d", tid, t);
    //获取线程名称
    thread_name = get_thread_name(tid);

    //通知等待信号的线程
    pthread_mutex_lock(&mtx);
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mtx);

    //据code找到之前的信号处理
    struct sigaction old_sig_act = p_sa_old[code];
    //调用之前的处理
    old_sig_act.sa_sigaction(code, siginfo, context);

}

在该方法中主要的逻辑就是:首先通过slow_backtrace()解native层的调用栈,然后唤醒等待的线程,最后时调用之前的信号处理程序。

接下里去看看如何解native层的调用栈:

2、发生崩溃时调用栈的解析

static int slow_backtrace() {
    unw_context_t uc;
    unw_getcontext (&uc);

    unw_cursor_t cursor;
    unw_word_t pc;

    if (unw_init_local(&cursor, &uc) < 0)
        return ERROR;

    while (unw_step(&cursor) > 0) {

        if (unw_get_reg(&cursor, UNW_REG_IP, &pc) < 0)
            return ERROR;
        //尝试获取动态库的信息
        Dl_info info;
        if (dladdr((void *) pc, &info) != 0) {
            void *const nearest = info.dli_saddr;
            //相对偏移地址
            const uintptr_t addr_relative =
                    ((uintptr_t) pc - (uintptr_t) info.dli_fbase);
            char log[1024];
            snprintf(log, sizeof(log), "at %s(%s)", info.dli_fname, info.dli_sname);
            append(stack_log, log);
//            addr2line()
        }
    }
    return SUCCESS;
}

在该方法中,使用libunwind库来解native层的调用栈,每一次while循环对应解一层方法。在循环中,先通过unw_get_reg获取到寄存器pc的值,然后通过dladdr库函数,据pc值获取当前方法所在的动态库相关信息。这里获取到的信息就是native层崩溃的主要信息。

3、将Native解析到的调用栈回传到Java层

native层的崩溃信息获取到之后,会唤醒等待的线程回传到Java层,接下来去看看如何回传:

void *dumpStack(void *argv) {
    //将当前线程attach到虚拟机
    JNIEnv *env;
    if (g_jvm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
        LOGE("当前线程AttachCurrentThread失败");
        return nullptr;
    }

    while (!b_stop) {
        //等待信号
        char *stack_temp;
        pthread_mutex_lock(&mtx);
        pthread_cond_wait(&cond, &mtx);
        //将全局的数据拷贝到局部变量中
        stack_temp = static_cast<char *>(malloc(sizeof(stack_log)));
        strcpy(stack_temp, stack_log);
        pthread_mutex_unlock(&mtx);

        //将堆栈发送到Java层
        jclass clz = env->GetObjectClass(g_obj);
        jmethodID jmethodId = env->GetStaticMethodID(clz, "onNativeLog", "(Ljava/lang/String;Ljava/lang/String;)V");
        jstring jstack = env->NewStringUTF(stack_temp);

        //释放资源
        free(stack_temp);
        if (thread_name == nullptr) {
            thread_name = "";
        }
        env->CallStaticVoidMethod(clz, jmethodId, jstack, env->NewStringUTF(thread_name));
        env->DeleteLocalRef(jstack);
    }
}

该dumpStack方法会在新线程中执行,刚开始因为native堆栈数据没准备好,会一直阻塞等待,当native调用栈数据准备好后,会唤醒它继续执行。首先将准备好的调用栈数据拷贝到变本地量中,然后调用Java层的onNativeLog方法,将native的调用栈传到Java层,最后释放资源。

4、Java层解析Java崩溃调用栈

接着去Java层对应方法看看:

public class CrashLog {
    public static final String TAG = CrashLog.class.getSimpleName();

    public static void onNativeLog(String nativeLog, String threadName) {
        if (nativeLog == null) {
            nativeLog = "";
        }
        String finalStackTrace = nativeLog + dumpJavaStack(threadName);
        Log.e(TAG, "程序的崩溃堆栈:\n" + finalStackTrace);
    }
    ...

}

在Java层的代码就比较简单,通过dumpJavaStack获取对应线程的方法调用栈,然后和native层传递过来的栈信息合并,最后打印出来。


4、总结

这篇文章,首先介绍了Java层异常相关知识,然后介绍了Native层堆栈捕获的原理,最后写了一个demo,将他们串联起来,能简单的捕获到崩溃时的Native层调用栈和Java层的调用栈。这样我们的知识不至于只会纸上谈兵,整个原理通过demo完全串联起来了。

最后希望读者看完本片文章能有所收获,写得不正确的地方望指正,共同进步。

收藏
评论区

相关推荐

AT大牛带你深度剖析Android 10大开源框架
做了几年的Android开发,也面试过不少公司,被面试过,也面试过不少人,其实Android的技术真的是无边界,不管你做过多牛的项目,不管你多久的经验在Android开发中不可能是停滞不前的,需要不断的学习及总结,否则难以解公关技术问题,下面把我压箱底的视频资料贡献给大家; 一,android视频教程 Android视频教程: 1,Android入门
Android Service 流程分析
启动Service过程 Android Service启动时序图 (https://imghelloworld.osscnbeijing.aliyuncs.com/039313fdaaf1e7dea3bde222b3ec9934.png) Android Service启动时序图.png 上图就是Android
Android开发 常见异常和解决办法(一)
Android Studio是Android开发的理想工具,但是由于版本的更新和配置的差异,会出现很多问题,下面是以《第一行代码 第二版》为基础进行开发学习可能遇见的一些问题及其解决办法。 1.Android Studio 3.0及以上版本找不到Android Device Monitor: 解决办法: (1)在Android Studio中打开终端,如下
Android 自学必备网站
一,Android 自学网站给 Android 自学者朋友推荐几个自学网站:1. Android Developers作为一个Android开发者,官网的资料当然不可错过,从设计,培训,指南,文档,都不应该错过,在以后的学习过程中慢慢理解体会。网站:https://developer.android.com/2. Android专业中文社区Android专业
Android系统启动流程(一)解析init进程启动过程
作为“Android框架层”这个大系列中的第一个系列,我们首先要了解的是Android系统启动流程,在这个流程中会涉及到很多重要的知识点,这个系列我们就来一一讲解它们,这一篇我们就来学习init进程。"tag: Android框架层 Android系统启动categories: Android框架层本文首发于微信公众号「刘望舒」
理解Android.bp
介绍Android最新的编译系统 一、简介早期的Android系统都是采用Android.mk的配置来编译源码,从Android 7.0开始引入Android.bp。很明显Android.bp的出现就是为了替换掉Android.mk。再来说一说跟着Android版本相应的发展演变过程: Android 7.0引入ninja和kati
Android AOSP基础(三)Android系统源码的整编和单编
AOSP基础 Android框架层本文首发于微信公众号「刘望舒」 前言在上一篇文章中,我们顺利的将AOSP下载了下来,很多时候我们不仅仅需要去查看源码,还有以下的几个需求: 动态调试Android系统源码 定制Android系统 将最新版本的Android系统刷入到自己的Android设备中 将系统源码导入到Android Studio中为了实现这些需求,就
Android深入理解JNI(一)JNI原理与静态、动态注册
Android框架层 Android深入理解JNI Android框架层本文首发于微信公众号「刘望舒」 前言JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在Java Framework层。这一个系列我们来一起深入学习JNI。<!more 1.JNI概述Android系统按语言来划分的
Android深入四大组件(一)应用程序启动过程(前篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「后厂技术官」 前言在此前的文章中,我讲过了Android系统启动流程和Android应用进程启动过程,这一篇顺理成章来学习Android 7.0的应用程序的启动过程。分析应用程序的启动过程其实就是分析根Activity的启动过程。<!more 1
Android深入四大组件(一)应用程序启动过程(后篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「刘望舒」 1.ActivityManageService到ApplicationThread的调用流程AMS的startActivity方法中return了startActivityAsUser方法:<!moreframeworks/base/s
Android深入四大组件(六)Android8.0 根Activity启动过程(前篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「刘望舒」 前言在几个月前我写了和这两篇文章,它们都是基于Android 7.0,当我开始阅读Android 8.0源码时发现应用程序(根Activity)启动过程照Android 7.0有了一些变化,因此又写下了本篇文章,本篇文章照此前的文章不仅
Android深入四大组件(七)Android8.0 根Activity启动过程(后篇)
Android框架层 Android深入四大组件categories: Android框架层本文首发于微信公众号「刘望舒」 前言在几个月前我写了和这两篇文章,它们都是基于Android 7.0,当我开始阅读Android 8.0源码时发现应用程序(根Activity)启动过程照Android 7.0有了一些变化,因此又写下了本篇文章,本篇文章照此前的文章不仅
Android解析WindowManager(一)WindowManager体系
Android框架层 Android系统服务 WindowManagercategories: Android框架层本文首发于微信公众号「刘望舒」 前言WindowManagerService(WMS)和AMS一样,都是Android开发需要掌握的知识点,同样的,WMS也很复杂,需要多篇文章来进行讲解,为何更好的理解WMS,首先要了解WindowManage
Android解析WindowManagerService(一)WMS的诞生
Android框架层 Android系统服务 WindowManagerService Android框架层本文首发于微信公众号「后厂技术官」 前言此前我用多篇文章介绍了WindowManager,这个系列我们来介绍WindowManager的管理者WMS,首先我们先来学习WMS是如何产生的。本文源码基于Android 8.0,与Android 7.1.2
全靠这份Android知识点PDF大全,月薪30K
第一阶段:Android 基础知识回顾: 回顾Android 开发编程,深入理解Android系统原理和层次结构,深入分析Handler源码和原理; 回顾Java,C/C++,Kotlin、dart 在Android开发中必用的语言,熟悉一下几种语言混淆后的特性; 回顾Android IPC和JNI的底层原理和热更新技术回顾Native开发要点,使用C++结