这一年半以来,关于 Android,我都写了什么文章(一)

本文原创,转载请注明出处。
欢迎关注我的 ,关注我的专题 Android Class 我会长期坚持为大家收录上高质量的 Android 相关博文。

16年7月份毕业,转眼从学校到公司已经一年半的时间了。在16年年末写过一篇年终总结,当时立下一个关于写作的目标,打算17年至少每个月产出一篇。看了下我的发文记录,额。。。勉勉强强达到了吧。

今年以来我始终觉得没什么可写的,原因可以算是找到了正确获取知识的办法,关于我想写的东西,网上已经有很成熟很完善的文章了,所以自然不需要重复造轮子。

从分享知识的角度来说,确实如此,但是技术博客作为未来复习的笔记,也十分有意义。就好像以前上数学课,上课确实听懂了,但是不做习题,不做笔记,是不可能完全消化这个知识的,编程也是如此,看书/看文章获取知识,写 demo 巩固,最后在项目中应用,不断填坑,方可真正吸收。

以上说了这么多,其实就是我打算最近复习一下,这一年半我写了哪些知识点,准备复习一下,温故而知新,还是蛮有意义的。

Android 进程创建流程

Android 进程的创建可以分为三个阶段:

  1. 发起进程:比如从桌面点击图标进入,那么发起进程就是 Launcher 进程,如果是 A 应用进入到 B 应用,那么发起进程就是 A 应用所在的进程。发起进程会通过 Binder 发送一个消息到 system_service 进程。system_service 就是 framework 层的系统服务进程。
  2. system_service 进程调用 Process.start 方法,通过 socket 将消息发送的 zygote 进程。
  3. zygote 进程最终 fork 出应用真正的进程,并且通过 invokeStaticMain 方法,通过反射,调用 ActivityThread 的 main 方法,真正的进入了程序的入口。

Android 进程的种类级别

前台进程:
有正在与用户交互的 Activity;调用了 startForeground 的 Service,或者 Service bind 了一个前台交互的 Activity,Service 正在调用生命周期方法,BoradCastReceiver 正在调用 onReceive 方法。
可见进程:
Activity 调用了 onPause 方法,但是并没有调用 onStop,或者一个 Service 绑定了这样的一个 Activity。
服务进程:
当 Activity 调用了 startService 并且不属于以上两种情况下,这个进程就是服务进程。
后台进程:
当前进程的组件已经调用了 onStop,这个时候在内存紧张的时候,有可能会被系统回收,系统回收后台进程的时候,会遵循 LRU 算法,保证最近使用的那个进程,最后被回收。
空进程:
已经没有任何存活组件的进程,通常空进程都是留给下次进入做缓冲的。

Android 进程清理机制

Android 系统是基于 Linux 系统的,Linux 系统清理进程通过 Low Memory Killer 机制完成。Low Memory Killer 会分析此进程的 adj 所占的级别,以及这个进程所占的内存大小两方面来分析。不同种类的进程 adj 值是不同的,adj 值越高,越可能被系统回收,当 adj 值相同时,会先回收所占内存更大的那个。

前台进程:adj = 0;可见进程 adj = 1;服务进程 adj = 5;后台进程 adj = 7;空进程 adj = 9;

所以对于一个 app 进程如何才能“绿色”的保活呢?提升其 adj 的值才是正确的思路,这个我们未来再说。

Android 线程的管理

先来说说进程和线程的区别吧:进程就是一个拥有资源的单位,有独立的内存地址,不同进程之间,内存是不可共享的;线程是最小执行和调度的单元,不同的线程可共享进程中的堆内存;对于 Linux 系统来说,无论进程还是线程,都是一个可执行的 task_struck 结构体。

在 Android 中,尽可能的要用线程池去管理线程,不要直接 new Thread().start,因为使用线程池控制线程可以合理利用空闲线程,并且对线程进行回收,高效的利用资源。

Excutors 中有几个工厂静态方法,来创建几个不同的线程池:

newCachedThreadPool:无 corethread,maxthread 数为 Integer.MAX,空闲线程超过60s时,会被回收。

newFixedThreadPool:Thread 数量固定的线程池,线程空闲也不会被回收,当线程无空闲的时候,任务会被放在 LinkedBlockQueue 中。

newSingleThreadPool:单一线程,不会被超时回收,任务会依次执行。

newScheduleThreadPool:创建一个线程池,可以指定核心线程数和最大线程数,每间隔一定时间去执行任务。

线程的任务处理方法,首先判断核心线程有没有满,如果没有满,即使有空闲的,也会创建一个新线程,如果满了,则优先复用空闲线程,其次才会创建新线程,如果线程达到了最大线程数,则将任务放入任务队列中,如果最后任务队列都满了,那就会抛出异常,或者进行其他处理。

其实这几个线程池都是对 ThreadPoolExecutor 的封装,设置不同的参数罢了。

对于非常消耗 CPU 的任务,尽量不要让核心线程数超过 CPU 核心数 。Runtime.getRuntime().availableProcessors

对于I/O操作频繁的任务,可以尽可能多的创建线程数。

以上就是对 Android 线程池的创建和使用的总结。

Context 的原理用法以及和四大组件的关系

Context 是一个顶层抽象类,掌管着 Android 的资源和组件。
直接子类:ContextWrapper(包装类) ContextImpl(实现类) MockContext(测试用的)

ContextWrapper 的直接子类:Application、Service、ContextThemeWrapper,ContextThemeWrapper 的子类是 Activity(因为 Activity 中有 theme 属性)

ContextWrapper 本身并没有真正实现 Context 的方法,而是持有了一个 mBase 变量,这个 mBase 就是 ContextImpl,通过 mBase 来实现 Context 的方法,这就是装饰模式。

ContextWrapper 和 ContextImpl 是何时建立联系的呢?在 ActivityThread 的 main 方法中,调用了 mThread.attach 方法,将 AMS(Activity Service 等调度者) 与 ApplicationThread(四大组件事件接受者,通过 mH 通知到主线程) 建立联系。

当 BIND_APPLICATION、LAUNCH_ACTIVITY、START_SERVICE 事件到来之时,会 new 一个 ContextImpl 然后调用 setOuterContext,这样就将二者建立了联系。

未来我们再去详细探究 Application、Service、Activity 的启动流程,和被调度的流程吧。

Context 的使用

不同对象的 Context 在使用上是有区别的,比如只有 Activity 的 context 才可以 show a dialog。对于和 View 相关的 context,只可以用 Activity 的,但其他的 Context,比如调数据库、系统服务等等,尽量还是使用 Application 的 Context,毕竟 Application 的 Context 生命周期是伴随着进程本身的,不会造成内存泄漏。

context.getApplicationContext

与 Context 相关联的资源都是同一份,通过 ResourceManager 获取,都指向了同一个文件夹下,只不过不同的 Context 对这个资源处理的方式不同才导致了这个差异。

内存泄漏

刚刚我们提到了 Context 使用不当导致的内存泄漏,内存泄漏在 java 中就是“该松手时不松手”,本来应该被 java gc 掉的对象,但是依然有着引用可达它。所以这个时候造成了内存泄漏。在 Android 中,内存泄漏有 90% 都是因为 Activity 已经 onDestory 了,但是依然有其他对象引用着它,导致它无法被 gc 回收掉。

所以理所当然的,当这个 Activity 中有一些对象引用着它,并且其生命周期超过了 Activity 本身之时,就会造成内存泄漏。

Handler:
Handler 通常都会怎么用呢?

Handler mHandler = new Handler()

如果在 Activity 中这样声明一个内部类的话,是可能造成内存泄漏的,为什么呢?因为在 Java 中非静态的内部类会持有外部类的引用,当 Activity 已经 onDestory 了,但是主线程的 MessageQueue 依然还有没处理的消息,则 Handler 一直抓着 Activity 不让其被 GC 回收。

正确的用法:

private SafeHandler mHandler = new SafeHandler(this);

private static class SafeHandler extands Handler {
    private final WeakReference mRef;
    public SafeHandler(SimpleActivity activity){
        mRef = new WeakReference<>(activity);
    }
    
    @Override
    public void handleMessage(Message msg) {
        SimpleActivity activity = mRef.get();
        if(activity != null){
        }
    }
}

静态变量:
静态变量有什么特性呢?随着类(ClassLoader)的创建而创建,被所有类的对象共用,也就是说可能会有一些线程安全问题。随着类的销毁而被销毁,类什么时候被销毁呢?是在 ClassLoader 销毁这个类的时候,也就是在这个进程被杀死的时候。

所以一个静态变量的生命周期是与所在的 app 进程同步的,比如你在一个 activity 中保有了一个 static 的 Context(this),即使 Activity 调用了 onDestory ,这个 Context 依然是无法回收的。

与之情况相似的还有单例模式,因为单例模式也是占用内存,无法释放,所以如果传入了 Context,尽量使用 Application 的 Context 防止内存泄漏。

匿名内部类:
比如 new Thread(new Runnable()) 的这种情况所造成的内存泄漏,也是匿名内部类会持有外部类的强引用,导致 Activity 无法被回收。

View 和属性动画导致的内存泄漏

因为动画持有 View 的引用,View 持有 Activity 的引用,所以要避免 Activity 被销毁了,但是 View 依然抓着它不让它回收。比如要在 activity onDestory 之前 dissmiss dialog,停止循环的属性动画等等。

LeakCanary 原理分析

既然上面说到了内存泄漏,我们也知道在 Android 上有一个非常好用的开源库检测内存泄漏,那就是 LeakCanary,本着知其所以然的态度,大致看了下 LeakCanary 的源码,分析下原理:

  1. 在一个 Activity 调用 onDestory 之时,是 LeakCanary 开始进行检测的入口,检测的类是 RefWatcher 类 的 watch 方法。首先会给这个 Activity 创建一个唯一的 ReferenceKey,并且使用一个带 ReferenceQueue 参数的构造方法,创建一个 WeakReference,其目的是,当 Activity 被 GC 回收之时,会出现在这个 ReferenceQueue 中。如果反复 GC ,Activity 的对象都不在队列里,说明就可能发生了内存泄漏,进行进一步分析。
  2. 接着调用了 watchExecutor.execute 方法,其目的是向主线程推一个消息执行 ensureGone 方法,为了不影响主线程其他任务的调用,这个消息只有在主线程空闲的时候执行。
  3. ensureGone 方法:
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    ...

    removeWeaklyReachableReferences();
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
        return;
    }
    gcTrigger.runGc();      // 手动执行一次gc
    removeWeaklyReachableReferences();
    if (!gone(reference)) {

        long startDumpHeap = System.nanoTime();
        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

        File heapDumpFile = heapDumper.dumpHeap();
        if (heapDumpFile == null) {
            // Could not dump the heap, abort.
            Log.d(TAG, "Could not dump the heap, abort.");
            return;
        }
        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

        heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
                watchDurationMs, gcDurationMs, heapDumpDurationMs));
    }
  }

removeWeaklyReachableReferences 这个方法的目的就是移除弱引用,如果当前 Activity 的弱引用还在 retainedKeys 中,那么说明就还没被移除,则手动调用一次 GC,重复上述的判断,如果发现 Activity 的引用还在 retainedKeys 中,也就是说,这个 Activity 对象并没有按照期望,被回收到 ReferenceQueue 中,则说明它极有可能发生了内存泄漏。

  1. 既然判断出现了内存泄漏,那么下一步就是定位分析了。上面代码中 heapDumpFile 这个就是我们要分析的内存文件,因为这个步骤比较耗时,所以被放进了一个新的进程里,HeapAnalyzerService 这个服务就跑在这个进程里进行内存泄漏分析,HeapAnalyzerService 通过调用 HeapAnalyzer 使用 HAHA 解析这个内存文件。
  2. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
  3. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
  4. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

上面就是 LeakCanary 的原理分析,LeakCanary 的问题是无法检测出 Service 的内存泄漏的。如果最底层的MainActivity一直未走onDestroy生命周期(它在Activity栈的最底层),无法检测出它的调用栈的内存泄漏。

先复习这么多吧,一篇一篇的来。我发现很多知识点一年以后重新看,理解会比当时更加深刻,更全面,所以时常复习是很重要的。

你可能感兴趣的:(这一年半以来,关于 Android,我都写了什么文章(一))