这一年半以来,关于 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<SimpleActivity> 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栈的最底层),无法检测出它的调用栈的内存泄漏。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容