Android 面试指南(2022最新版)

本文深入剖析Android面试重点,涵盖Handler的常见问题,如Handler的工作原理、生命周期及其与主线程的关系。同时,详细讲解Activity的生命周期、启动模式以及特殊场景下的变化。探讨Fragment的生命周期、回退栈与通信机制,以及Context的使用。此外,还涉及性能优化,包括内存泄漏、布局优化和ListView的高效加载。最后,概述了Android中的跨进程通信方式和Binder机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先看下目录(有点长哈),点击跳转至相应位置

文章目录

Android 知识点

基础

导致内存泄露的原因有哪些?

Android 性能优化

内存泄露的根本原因:长生命周期的对象持有短生命周期的对象。短周期对象就无法及时释放。

集合类泄漏:List集合add()元素之后,会引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露。当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。
单例:通常是由于引用的context是生命周期短造成,可通过ApplicationContext替代
匿名内部类/非静态内部类(Handler):可通过弱引用
资源未关闭造成的内存泄漏:
    网络、文件等流忘记关闭
    手动注册广播时,退出时忘记 unregisterReceiver()
    Service 执行完后忘记 stopSelf()
    EventBus 等观察者模式的框架忘记手动解除注册
    注意 Bitmap,用完及时 Recycle()

OOM

1、什么是OOM?
  程序申请内存过大,虚拟机无法满足我们,然后自杀了。
2、为什么会有OOM?
  因为Android系统的APP每个进程或者虚拟机有最大内存限制,一旦超过这个限制系统就会抛出OOM错误。跟手机剩余内存是否充足没有多少关系。
3、为什么Android会有APP的内存限制
  (1) 要开发者使用内存更加合理。限制每个应用可用内存上限,避免恶意程序或单个程序使用过多内存导致其他程序的不可运行。有了限制,开发者就必须合理使用资源,优化资源使用
  (2) 屏幕显示内容有限,展示给用户的数据是有限的。大部分信息都是处于准备显示状态,所以没必要给予太多heap内存。
  (3) Android多个虚拟机Davlik的限制需要。android设备上的APP运行,每打开一个应用就会打开至少一个独立虚拟机。这样可以避免系统崩溃,但代价是浪费更多内存。
4、有GC自动回收资源,为什么还会有OOM?
  Android的GC会按照特定的算法来回收不使用的资源,但是gc一般回收的是无主的对象内存或者软引用资源。
  使用软引用的图片资源在一定程度上可以避免OOM。
  ps:不用的对象设置为null,是一个好习惯。不过更好的方法是,不用的图片直接recycle。因为有时候通过设置null让gc来回收还是来不及。
5、怎么来避免OOM产生呢?
  简单通过SoftReference引用方式管理图片资源
  建一个SoftReference的hashmap,使用图片时,先检查这个hashmap是否有softreference,softreference的图片是否为空,如果为空将图片加载到softreference并加入haspmap。

ANR的原因

在Android中,应用的响应性被活动管理器(Activity Manager)和窗口管理器(Window Manager)这两个系统服务所监视,当用户操作的在一定时间内应用程序没能做出反应,就会ANR。
响应时间限制:
    应用在5秒内未响应用户的输入事件(如按键或者触摸)会发生ANR
    BroadcastReceiver未在10秒内完成相关的处理会发生ANR
    前台Service在20秒内无法处理完成会发生ANR
    后台Service在200s内没有处理完成会发生ANR
解决方案是对于耗时的操作,比如访问网络、访问数据库等操作,需要开辟子线程,在子线程处理耗时的操作,主线程主要实现UI的操作

anr定位及监控?
目前流行的ANR检测方案有开源的BlockCanary 、ANR-WatchDog、SafeLooper, 还有根据谷歌原生系统接口监测的方案:FileObserver(/data/anr文件夹下会记录)。

Handler

Android 关于Handler面试的坑

https://amarao.blog.csdn.net/article/details/88654938

主线程子线程通信,handler

https://blog.csdn.net/github_36719758/article/details/77333903

有以下五种方式:
1.Activity.runOnUiThread(Runnable)
2.View.post(Runnable)、View.postDelayed(Runnable, long)
4.Handler
5.AsyncTask

handler机制,四个组成部分及源码解析

Message:需要传递的消息,可以传递数据; 
MessageQueue:消息队列,但是它的内部实现并不是用的队列,实际上是通过 一个单链表的数据结构来维护消息列表,因为单链表在插入和删除上比较有优势。主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next); 
Handler:消息辅助类,主要功能向消息池发送各种消息事件 (Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);
Looper:不断循环执行(Looper.loop),从 MessageQueue 中读取消息,按分发机制将消息分发给目标处理者。

首先创建一个Handler对象 ,并重写handleMessage方法。然后创建一个Message对象,并通过handler发送这条消息出去。
之后这条消息被加入到MessageQueue队列中等待被处理,通过Looper对象会一直轮询消息队列,尝试从Message Queue中取出待处理的

消息,最后分发会Handler的handler Message方法中。

Handler一定要在主线程实例化吗?new Handler()和new Handler(Looper.getMainLooper())的区别

如果你不带参数的实例化:Handler handler = new Handler();那么这个会默认用当前线程的looper
一般而言,如果你的Handler是要来刷新操作UI的,那么就需要在主线程下跑。
情况:
1.要刷新UI,handler要用到主线程的looper。那么在主线程 Handler handler = new Handler();如果在其他线程,也要满足这个功能的话,要Handler handler = new Handler(Looper.getMainLooper());
2.不用刷新ui,只是处理消息。 当前线程如果是主线程的话,Handler handler = new Handler();不是主线程的话,Looper.prepare(); Handler handler = new Handler();Looper.loop();或者Handler handler = new Handler(Looper.getMainLooper());
若是实例化的时候用Looper.getMainLooper()就表示放到主UI线程去处理。
如果不是的话,因为只有UI线程默认Loop.prepare();Loop.loop();其他线程需要手动调用这两个,否则会报错。

Handler能不能在子线程使用

可以。
方式一:new Handler(Looper().getMainLoopr());
在new Handler()中传入Looper().getMainLoopr()
因为在主线程中会默认调用一个Looper.prepareMainLooper()方法,它会调用Looper.prepare(),完成Looper对象的创建。

方式二:
Looper.prepare(); 
Handler handler = new Handler();
Looper.loop();
首先要先要Looper.prepare()进行初始化,不然直接new Handler()会报错,提示Looper为Null。

message.what,message.arg1,message.arg2,message.obj,setData,Messenger他们在之间有什么区别呢?

what就是一般用来区别消息的,比如你传进去的时候msg.what = 3;然后处理的时候判断msg.what == 3是不是成立的,是的话,表示这个消息是干嘛干嘛的(自己能区别开)
arg1,arg2,其实也就是两个传递数据用的,两个int值,看你自己想要用它干嘛咯。如果你的数据只是简单的int值,那么用这两个,比较方便。
msg.obj呢,这个就是传递数据了,msg中能够携带对象,在handleMessage的时候,可以把这个数据取出来做处理了。
setData(Bundle),上面两个arg是传递简单int的,这个是传递复杂数据的。
Messenger:如果是同一个进程,最好用上面的setData就行了。Messenger类来用来跨进程传递可序列化的对象的,这个比起上面的来,更消耗性能一些。

当messageQueue为空的时候,怎么进行阻塞?

https://blog.csdn.net/qq_24531461/article/details/72972416

MessageQueue是个单项链表,它包含一个Message应,用于保存链表的第一个元素,Message对象有个next字段保存列表中的下一条消息。
在MessageQueue的next()方法循环体内首先调用nativePollOnce(ptr, nextPollTimeoutMillis)方法,
它是通过Native层的MessageQueue阻塞nextPollTimeoutMillis毫秒的时间。
如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时),如果期间有程序唤醒会立即返回。
如果有消息需要处理,先判断时间有没有到,
    如果没到的话设置一下阻塞时间nextPollTimeoutMillis,进入下次循环的时候会调用nativePollOnce(ptr, nextPollTimeoutMillis)阻塞;
    如果时间到了的话把消息返回给调用者,并且设置mBlocked = false代表目前没有阻塞。
阻塞了有两种方式唤醒,
    一种是超时了,
    一种是被主动唤醒了。MessageQueue入队列的时候应该会去唤醒,加入链表的时候按时间顺序从小到大排序,然后判断是否需要唤醒,如果需要唤醒则调用nativeWake(mPtr);来唤醒之前等待的线程。

屏障postSyncBarrier(long when)

https://mp.weixin.qq.com/s/CNpnS6y2IYobzDa7rAjy1Q

这个方法直接在MessageQueue中插入了一个Message,并且未设置target。
它的作用是插入一个消息屏障,这个屏障之后的所有同步消息都不会被执行,即使时间已经到了也不会执行。
next方法,如果发现了一个消息屏障,会循环找出第一个异步消息,所有同步消息都将忽略。
平常发送的一般都是同步消息,可以通过setAsynchronous(boolean async)设置为异步消息。
可以通过removeSyncBarrier(inttoken)来移除这个屏障,参数是post方法的返回值。

使用场景:
https://blog.csdn.net/cpcpcp123/article/details/115374057
https://blog.csdn.net/u013728021/article/details/105033368/

Handler 是怎样收到消息的

Message 中有个taregt来记录Handler,
在消息Handler中调用enqueueMessage入队的时赋值,Looper.loop中通过从MessageQueue.next()中出获取消息,并调用msg.target.dispatchMessage(msg)来进行分发消息。
dispatchMessage中判断,如果msg的callback非空,则调用msg的callback.run,空则判断handler.handleCallback是否为空,
非空便调用到handleCallback.handleMessage,空则直接调用handleMessage,该方法默认为空,Handler 子类通过覆写该方法来完成具体的逻辑。最终时消息会在handlerMessage中接收到。

处理顺序:msg.callback > handler.mCallback >  handleMessage
msg.callback为空时
mCallback为空
或mCallback.handleMessage(msg)返回fasle才会执行handleMessage


Handler对于消息的处理大概分为这三种优先级:
    Message如果自带了callback处理,则交给callback处理,最常见的就是Handler.post(Runnable r)函数,r就是在callback
    Handler如果设置了全局的mCallback,则交给mCallback处理,这个mCallback需要返回true表示消息已经被处理
    如果上述都没有(1中消息没有自带的callback且2为空返回false),则交给Handler子类实现的handleMessage来处理,这里要求重写handleMessage函数
// Handler.dispatchMessage
public void dispatchMessage(@NonNull Message msg) {
    // 调用 Handler 的 post 系列方法执行handleCallback(msg)
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        // 创建 Handler 传入 Callback
        if (mCallback != null) {
            // 返回true表示消息已经被处理
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //msg.callback或mCallback为空或mCallback返回false,才执行
        handleMessage(msg);
    }
}

http://www.voidcn.com/article/p-shbywnir-kd.html

https://blog.csdn.net/zgaoq/article/details/80295518?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242

https://wongzhenyu.cn/2019/09/16/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Android%EF%BC%9AMessageQueue/

https://www.jianshu.com/p/c2dd89000cec

创建Message对象的几种方法比较

https://blog.csdn.net/haoyuegongzi/article/details/102984557

方式1、Message message = new Message();
方式2、Message message = Message.obtain();
方式3、Message message = new Handler().obtainMessage(); // 本质上是调用的Message.obtain();

new Message();
    每new一次,就会在内存中创建一个Message对象。
Message.obtain();
    Handler的sendEmptyMessage系列方法和post系列方法内部的message对象都是使用这个方式获取的。
    使用Message.obtain()创建Message对象的好处是Message对象可以重复使用,可以免除一直new Message对象造成无谓的内存压力。
    当我们使用完通过Message.obtain()创建的Message对象后,系统会自动回收Message对象,但这里的回收并不是直接将Message对象清空置为null,而是清空Message对象里面的所有信息,然后再将其添加到系统的sPoolSize中去,方便下次直接使用。
new Handler().obtainMessage(); 
    本质上是调用的Message.obtain();

view.post 和handler发送消息的区别

https://blog.csdn.net/u014313623/article/details/80740636

 view.post也是往主线程发消息,只是发消息要看view的状态。
    如果view已经被关联到window的话,直接通过handler发送,
    如果不是的话,则把消息添加到RunQueue.mActions中,到下次view绘制时再把mAction中的消息发到主线程消息队列。
    这里如果aAction中的runnable对象没有被及时发出去的话,会导致内存泄露,因为runnable常常作为匿名内部类,会持有对外部类的引用.
    
从使用上看在有view引用变量的情况下,使用view.post方便,不需要自己去定义一个handler,定义handler一不小心就会引起内存泄露。

Handler 发送消息sendMessageAtTime()中为什么调用 SystemClock.uptimeMillis()而不是System.currenttime来计算?

System.currenttime:是获取当前的绝对时间。 
SystemClock.uptimeMillis()是获取系统从开机启动到现在的相对时间,期间不包括休眠的时间。
handler会受到阻塞,挂起状态,睡眠等,这些时候是不应该执行的;
如果使用绝对时间的话,就会抢占资源来执行当前 handler 的内容,显然这是不应该出现的情况,所以要避免。

Handler发送的消息DELAY准确吗

http://www.dss886.com/2016/08/17/01/

handler内部计算消息的时间用是用的SystemClock.uptimeMillis()
SystemClock.uptimeMillis是获取系统从开机启动到现在的相对时间,期间不包括休眠的时间。
handler会受到阻塞,挂起状态,睡眠等影响。
如果系统休眠就会挂起。

Handler、Thread和HandlerThread的差别

https://blog.csdn.net/chenliguan/article/details/54585646

Handler:在android中负责发送和处理消息,通过它可以实现其他支线线程与主线程之间的消息通讯。

Thread:Java进程中执行运算的最小单位,执行处理机调度的基本单位。主要用于执行耗时操作。

HandlerThread:继承于Thread,所以它本质就是个Thread。它与普通Thread的差别就在于:有自己的Looper,默认调用了Looper.prepare和Looper.loop,可以在自己的线程中分发和处理消息。

HanderThread退出时,需要释放loop吗

https://blog.csdn.net/zgaoq/article/details/80295518

http://www.voidcn.com/article/p-shbywnir-kd.html

http://www.androidmst.com/1074.html

需要。不然loop会一直死循环。
HandlerThread使用完后需要退出,调用下面代码即可
    sHandlerThread.getLooper().quit();
    或
    sHandlerThread.quit();
    // HandlerThread.quit
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }
那如何终止消息循环呢?
我们可以调用Looper的quit方法或quitSafely方法,二者稍有不同。
相同点:
    将不在接受新的事件加入消息队列。
不同点:
    Looper.quit,实际上执行了MessageQueue.removeAllMessagesLocked方法。
    该方法的作用是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息还是非延迟消息。
    
    Looper.quitSafely方,实际上执行了MessageQueue.removeAllFutureMessagesLocked方法。
    该方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有的非延迟消息派发出去让Handler去处理。

quit方法会将消息队列中的所有消息移除(延迟消息和非延迟消息)。
quitSafely会将消息队列所有的延迟消息移除,非延迟消息派发出去让Handler去处理。

如果我们在 activity中start了一个HandlerThread,那么这个线程会启动一个looper消息循环,当activity退出了,这个HandlerThread线程并没有终止,还是在那里做looper死循环,这当然不是我们愿意看到的,多个线程占用资源,进程会出现crash.
使用quit方法,具体调用形式如下:
  mHandlerThread.getLooper().quit();
注意:不能用myHandler.stop(),无效。stop是Thread的方法

IdleHandler cpu闲置非闲置任务

https://blog.csdn.net/qq_36523667/article/details/80028168

https://www.cnblogs.com/mingfeng002/p/12091628.html

IdleHandler:空闲监听器
在每次next获取消息进行处理时,发现没有可以处理的消息(队列空,只有延时消息并且没到时间,同步阻塞时没有异步消息)都会通知这些订阅者。
queueIdle返回值:
如果为false则执行完毕之后移除这条消息(一次性完事),
如果为true则保留,等到下次空闲时会再次执行(重复执行)。
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
               //耗时操作
               
                return false;
            }
        });
看MessageQueue的源码可以发现有两处关于IdleHandler的声明,分明是:
    存放IdleHandler的ArrayList(mIdleHandlers),
    还有一个IdleHandler数组(mPendingIdleHandlers)。它里面放的IdleHandler实例都是临时的,也就是每次使用完(调用了queueIdle方法)之后,都会置空(mPendingIdleHandlers[i] = null)
大概流程是这样的:
    如果本次循环拿到的Message为空,或者这个Message是一个延时的消息而且还没到指定的触发时间,那么,就认定当前的队列为空闲状态,
    接着就会遍历mPendingIdleHandlers数组(这个数组里面的元素每次都会到mIdleHandlers中去拿)来调用每一个IdleHandler实例的queueIdle方法,
    如果这个方法返回false的话,那么这个实例就会从mIdleHandlers中移除,也就是当下次队列空闲的时候,不会继续回调它的queueIdle方法了。
    处理完IdleHandler后会将nextPollTimeoutMillis设置为0,也就是不阻塞消息队列,当然要注意这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时肯定会影响后面的message执行。

场景
1.Activity启动优化:onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间
2.想要在一个View绘制完成之后添加其他依赖于这个View的View,当然这个用View#post()也能实现,区别就是前者会在消息队列空闲时执行
3.发送一个返回true的IdleHandler,在里面让某个View不停闪烁,这样当用户发呆时就可以诱导用户点击这个View,这也是种很酷的操作
4.一些第三方库中有使用,比如LeakCanary,Glide中有使用到,具体可以自行去查看

主线程Loop死循环为什么不卡主界面

https://www.zhihu.com/question/34652589

https://www.cnblogs.com/linguanh/p/6412042.html

要完全彻底理解这个问题,需要准备以下4方面的知识:Process/Thread,Android Binder IPC,Handler/Looper/MessageQueue消息机制,Linux pipe/epoll机制。

1.对于Activity 的生命周期的函数为什么不会因为 Looper.loop()里的死循环卡死而永无机会执行。

Activity 的生命周期函数都是在 Looper 里面的死循环中被 ActivityThread 内部的 Handler 的 handleMessage 方法里被处理的。
因为本身在循环里面调用,也就不会被阻塞。

2.在 1 的基础上,View 的绘制到底是怎样完成的,它又为什么不会因为 Looper.loop()里的死循环卡死而永无机会刷新。

View 的底层绘制是通过Binder机制完成的,
由底层 SurfaceFlinger 的工作线程中的handler事件机制(包含 handler ,looper,messageQueue)来进行接收、处理,
它和 ActivityThread 的 Handler 没关系,即是与 ActivityThread 几乎无关,
在ActivityThread 里面调用View的相关函数,如 setText(.),最终触发到View其它底层函数,它将会将这些信息发送到 SurfaceFlinger 的事件机制中去,被对应处理,最终刷新到界面。
SurfaceFlinger:

https://www.cnblogs.com/blogs-of-lxl/p/11272756.html

https://blog.csdn.net/afxstar/article/details/38661581

用来绘制应用程序的UI,SurfaceFlinger服务运行在System进程中,与应用程序通过Binder机制通信。

子线程真的不能刷新 UI ?

https://www.cnblogs.com/linguanh/p/7898996.html

https://www.jianshu.com/p/c39203884209

子线程不能更新UI的限制是viewRootImpl.java内部限制了,并不是子线程不能刷新UI
viewRootImpl内部有一个checkThread方法,会检测当前执行方法的进程是不是viewRootImpl的创建进程,如果不是则抛出异常。
而ViewRootImpl是在主线程中创建的,所以会出现主线程不能更新UI的说法。
// ViewRootImpl
void checkThread() {
    // mThread 是创建 ViewRootImpl 的线程
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}
* 对组件 Activity 而言,viewRootImpl 的初始化在 onCreate 之后,onResume 之前。
* 如果你的子线程更新代码在满足下面的条件下,那么它可以顺利运行:
修改应用层的 viewRootImpl.java 源码,解除限制
把你更新代码写在 onResume 之前,例如 onCreate 里面,且更新之际要赶在 viewRootImpl 初始化之前。
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        title = (TextView) findViewById(R.id.title_tips);
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        // 子线程更新UI
                        title.setText("我 tm 妥妥地执行完毕");
                    }
                }
        ).start();
    }

Activity

Activity 声明周期介绍

在这里插入图片描述

在正常情况下,一个Activity从启动到结束会以如下顺序经历整个生命周期:

onCreate()->onStart()->onResume()->onPause()->onStop()->onDestory()。

包含 了六个部分,还有一个onRestart()没有调用,下面我们一一介绍这七部分内容。

Android子线程更新View的探索

  1. onCreate():当 Activity 表示activity正在被创建,此时的UI操作不会更新UI,所以此时在子线程调用setText等方法,不会报错。详见Android子线程更新View的探索。 在这个方法中,可以做一些初始化工作,比如调用setContentView去加载界面布局 资源,初始化Activity所需的数据。当然也可借助onCreate()方法中的Bundle对象来 回复异常情况下Activity结束时的状态(后面会介绍)。

  2. onRestart():表示Activity正在重新启动。一般情况下,当前Activity从不可见 重新变为可见状态时,onRestart就会被调用。这种情形一般是用户行为导致的,比 如用户按Home键切换到桌面或打开了另一个新的Activity,接着用户又回到了这个 Actvity。(关于这部分生命周期的历经过程,后面会介绍。)

  3. onStart(): 表示Activity正在被启动,即将开始,这时Activity已经出现了,但是还 没有出现在前台,无法与用户交互。这个时候可以理解为Activity已经显示出来, 但是我们还看不到。

  4. onResume():表示Activity已经可见了,并且出现在前台并开始活动。需要和 onStart()对比,onStart的时候Activity还在后台,onResume的时候Activity才显示到 前台。

  5. onPause():表示 Activity正在停止,仍可见。onPause中不能进行耗时操作,会影响到新Activity的显示。因为onPause必须执行完,新的Activity的onResume才会执行。因为该Activity的onPause方法执行完了,才回去执行新Activity的onResume。

  6. onStop():表示Activity即将停止,不可见,位于后台。可以做稍微重量级的回收 工作,同样不能太耗时。

  7. onDestory():表示Activity即将销毁,这是Activity生命周期的最后一个回调,可以 做一些回收工作和最终的资源回收。
    在平常的开发中,我们经常用到的就是 onCreate()和onDestory(),做一些初始化和 回收操作。

有两个activity分别是A、B,从A跳转到B,再从B返回到A,分析生命周期的变化?

A->B
A--->onPause()
B--->onCreate()
B--->onStart()
B--->onResume()
-----------------------------------------
当B可见,A执行onStop()方法
A--->onStop()
(1)----------------B按返回键back------------
B->A
B--->onPause()
A--->onRestart()
A--->onStart()
A--->onResume()
-----------------------------------
当A再次可见,B执行onStop(),onDestory()方法
B---->onStop()
B--->onDestory()

onNewIntent

Android Activity切换的生命周期与onNewIntent()触发机制

如下所示为onNewIntent调用时机图。

在这里插入图片描述

调用时机
* Activity在当前的任务栈中,Intent的Flag中如果包含CLEAR_TOP则调用
* Activity在当前的任务栈中,launchMode为singleTop且处于栈顶或者是singleTask在栈内。
* Activity在当前的任务栈中,launchMode为singleInstance

这时候再次启动ActivityA时,便不会调用onCreate()去产生新的实例,而是调用onNewIntent()并重用返回栈里的A实例。

* 如果A在栈顶,launchMode为singleTop或singleTask,那么方法回调的调用顺序依次为A.onPause() -> A.onNewIntent() -> A.onResume()。
* 如果A不在栈顶,launchMode是singleTask,此时它处于A.onStop()状态,当再次启动时,调用顺序依次是A.onStop() -> A.onNewIntent() -> A.onRestart() -> A.onStart() -> A.onResume()。

在这里插入图片描述

Activity的启动模式

Android提供了四种Activity启动方式:

1.标准模式(standard)

特点 : 
启动新的活动时, 会创建一个新的Activity实例并置于栈顶, 无论该实例是否存在,之前的活动会被压入栈底。

谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。

在这里插入图片描述

特殊情况,如果在Service或Application中启动一个Activity,其并没有所谓的任务栈,可以使用标记位Flag来解决。
解决办法:为待启动的Activity指定 FLAG_ACTIVITY_NEW_TASK标记位,创建一个新栈。

2.栈顶复用模式(singleTop)

特点 : 
启动新的活动时, 会检查返回栈, 
如果该活动在栈顶, 就会启动该实例, 而不重新创建实例. 
如果该活动不在栈顶,就会创建该Activity新的实例,并放入栈顶。

并回调如下方法:

protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent); 
}

在这里插入图片描述

应用场景: 在通知栏点击收到的通知,然后需要启动一个Activity,这个Activity就可以用singleTop,否则每次点击都会新建一个Activity。

3.栈内复用模式(singleTask)

特点 : 
启动新的活动时, 会检查返回栈, 
如果该活动在栈中, 就会启动该活动实例. 
如果该活动不在栈顶, 会将它之前的所有实例踢出栈, 自己占据栈顶.

该模式是一种单例模式,即一个栈内只有一个该Activity实例。
可以通过在 AndroidManifest文件的Activity中通过taskAffinity指定该Activity需要加载到那个栈中。
<activity android:name=".Activity1" 
    android:launchMode="singleTask" 
    android:taskAffinity="com.lvr.task" 
    android:label="@string/app_name"
</activity>
应用场景:大多数App的主页。
对于大部分应用,当我们在主界面点击回退按钮的时候都是退出应用。那么当我们第一次进入主界面之后,主界面位于栈底,以后不 管我们打开了多少个Activity,只要我们再次回到主界面,都应该使用将主界面 Activity上所有的Activity移除的方式来让主界面Activity处于栈顶,而不是往栈顶新 加一个主界面Activity的实例,通过这种方式能够保证退出应用时所有的Activity都能 报销毁。
在跨应用Intent传递时,如果系统中不存在singleTask Activity的实例,那 么将创建一个新的Task,然后创建SingleTask Activity的实例,将其放入新的Task 中。

4.堆内单例模式(singleInstance)

特点 :
如果活动实例不存在,在启动时会单独创建一个Task, 并且将这个活动放到这个栈中. 
如果已经存在这个活动实例, 就会启动这个实例.

应用场景: 呼叫来电界面。
这种模式的使用情况比较罕见,在Launcher中可能使 用。
或者你确定你需要使Activity只有一个实例。建议谨慎使用。

关于taskAffinity的值

指定Activity存放的任务栈,设置此属性时不能和包名相同,因为默认就是包名。

如果Activity指定的栈不存在,则创建一个栈,并把创建的Activity压入栈内。

如果一个Activity没有显式的指明该Activity的taskAffinity,那么它的这个属性就等于Application指明的taskAffinity,如果Application也没有指明,那么该 taskAffinity的值就等于包名。

Activity的启动标志位

(1)FLAG_ACTIVITY_NEW_TASK 使用一个新的Task来启动Activity。 

(2)FLAG_ACTIVITY_SINGLE_TOP 其效果与指定Activity为singleTop模式一致。

(3)FLAG_ACTIVITY_CLEAR_TOP 当启动此Activity时,在同一个任务栈中所有位于它上面的Activity都要出栈。

如果和singleTask模式一起出现, 若被启动的Activity已经存在栈中,则清除其之上的Activity,并调用该Activity的 onNewIntent方法。

如果被启动的Activity采用standard模式,那么该Activity连同之上的所有Activity出栈,然后创建新的Activity实例并压入栈中。

(4)FLAG_ACTIVITY_NO_HISTORY
    新的Actvity不会保存在历史任务栈中。一旦用户离开它,就会退出。

(5)FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    该Activity不会出现在最近任务列表。

生命周期的几种普通情况

1. 针对一个特定的Activity,第一次启动,回调如下:onCreate()->onStart()- >onResume()
2. 用户打开新的Activiy的时候,上述Activity的回调如下:onPause()->onStop() 
3. 再次回到原Activity时,回调如下:onRestart()->onStart()->onResume()
4. 按back键回退时,回调如下:onPause()->onStop()->onDestory() 
5. 按Home键切换到桌面后又回到该Actitivy,回调如下:onPause()->onStop()->onRestart()->onStart()->onResume() 
6. 调用finish()方法后,回调如下:onDestory()(以在onCreate()方法中调用为例,不同方法中回调不同,通常都是在onCreate()方法中调用)

特殊情况下的生命周期

1. 横竖屏切换:见下一条标题。
2. 资源内存不足导致优先级低的Activity被杀死 
    Activity优先级的划分和下面的Activity的三种运行状态是对应的。
    (1) 前台Activity——正在和用户交互的Activity,优先级最高。
    (2) 可见但非前台Activity——比如Activity中弹出了一个对话框,导致Activity可见但 是位于后台无法和用户交互。
    (3) 后台Activity——已经被暂停的Activity,比如执行了onStop,优先级最低。
当系统内存不足时,会按照上述优先级从低到高去杀死目标Activity所在的进程。

横竖屏切换的生命周期

onPause()->onSaveInstanceState()-> onStop()- >onDestroy()->onCreate()->onStart()->onRestoreInstanceState->onResume()
    
默认情况下,屏幕会旋转并且会重新走生命周期。
    可以通过在AndroidManifest文件的Activity中指定如下属性,来避免横竖屏切换时,重新走生命周期。
# 最好这三个都配置,否则不能适配所有机型或sdk版本,当清单上用户指定的设置改变时,Activity会自己处理这些变化。
android:configChanges="keyboardHidden|orientation|screenSize" 

如果禁止屏幕旋转,请配置下面属性

android:screenOrientation="portrait" # landscape:横屏,portrait:竖屏

为什么横屏切换竖屏会调用两次生命周期

参考链接

从 API 级别 13 开始,当设备在纵向和横向之间切换时,“屏幕尺寸”也会发生变化。
因此,在开发针对 API 级别13或更高版本系统的应用时,若要避免由于设备方向改变而导致运行时重启,则除了"orientation"值以外,您还必须添加 "screenSize"值。
在API 级别 12 或更低版本的系统,则 Activity 始终会自行处理此配置变更。

一、Android 3.2 (API 级别 13)以前
1. 不设置configChanges时,切横屏执行1次,切竖屏执行2次。
2. 设置configChanges="orientation"时,切横竖屏时都只会执行1次。
3. 设置configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法。
注:不设置Activity的android:configChanges时,切换竖屏activity的各个生命周期执行两次

二、从 Android 3.2 (API级别 13)开始
1. 不设置configChanges,或configChanges="orientation",configChanges="orientation|keyboardHidden",切横竖屏时都只会执行1次。
2. 配置 configChanges="orientation|keyboardHidden|screenSize",才不会销毁 activity,且只调用 onConfigurationChanged方法。

onSaveInstanceState和onRestoreInstanceState。

onSaveInstanceState:

在Activity“可能被系统杀死”之前或由于异常情况下终止时,系统会调用onSaveInstanceState来保存当前 Activity的状态。
这个方法的调用是在onStop之前,它和onPause没有既定的时序关系,该方法只在Activity被异常终止的情况下调用。

onRestoreInstanceState:

当异常终止的Activity被重建以后,系统会调用onRestoreInstanceState。
并且把Activity销毁时onSaveInstanceState方法所保存的Bundle对象参数同时传递给onRestoreInstanceState和onCreate方法。
因此,可以通过onRestoreInstanceState方法来恢复Activity的状态,该方法的调用时机是在onStart之后。
其中onCreate和onRestoreInstanceState方法来恢复Activity的状态的区别: 
    onRestoreInstanceState回调则表明其中Bundle对象非空,不用加非空判断。 
    onCreate需要非空判断。建议使用onRestoreInstanceState。

onSaveInstanceState的调用时机

在“可能被系统杀死”之前调用,有以下几个场景,其实是在onStop调用后基本都有可能调用
* 当用户按下手机home键的时候。
* 手机息屏时。
* 打开新Activity时,原Activity就会调用。再返回原Activity时,新Activity不会调用
* 默认情况下横竖屏切换时。
* 切换暗黑

Activity 启动过程

一个APP从启动到主页面显示经历了哪些过程?

在这里插入图片描述

总结三步:
    1.创建App进程
    2.绑定Application
    3.显示界面
    
1.点击桌面App图标,Launcher进程通过Binder机制向AMS进程发起startActivity请求;
2.AMS进程接收到请求后,通过socket方式向Zygote进程发送创建进程的请求;
3.Zygote进程收到请求后会fork出新的App进程;

4.App进程创建后,会通过Binder向AMS进程发起attachApplication请求,
5.AMS进程在收到请求后,会调用到ApplicationThreadProxy,ApplicationThreadProxy通过Binder向App进程发送scheduleLaunchActivity请求。
6.App进程收到请求后,会调用ApplicationThread的bindApplication方法。
7.bindApplication方法经过准备后,会通过Handler向主线程发送BIND_APPLICATION信息。
8.主线程收到BIND_APPLICATION后,会调用handleBindApplication,去完成Application的绑定。
9.handleBindApplication方法最后会调用到scheduleRelaunchActivity方法,此方法通过Handler发送RELAUNCH_ACTIVITY信息。

7.主线程在收到消息后,通过反射机制创建目标Activity,并回调 Activity.onCreate()等方法。
8.到此,App便正式启动,开始进入Activity生命周期,直到执行完 onResume方法,UI渲染结束后便可以看到App的主界面。

Zygote 是什么,有什么作用?

参考链接

Android是基于Linux系统的。
Linux在内核加载完成后会启动一个init进程,所有的进程都是有init进程fork出来的,zygote进程也是由init进程fork出来的。
Zygote进程在启动时会创建一个Dalvik虚拟机实例。

在Android系统里面,所有的App都是一个单独的进程,有一个单独的dalvik虚拟机,而这些进程都是由zygote孵化出来的。

为什么由zygote来创建应用进程

本质上是为了资源共用和更快的启动速度。
每个Android应用都运行在各自独立的Dalvik虚拟机中。
Zygote进程在启动时会创建一个Dalvik虚拟机实例。
每当它孵化一个新的App进程时,都会将这个Dalvik虚拟机实例复制到新的App进程里面去。
从而使得每一个App进程都有一个独立的Dalvik虚拟机实例,进而共享虚拟机内存和框架层资源,这样大幅度提高应用程序的启动和运行速度。

SystemService是什么?有什么用?与zygote的关系

参考链接

SystemService是一个由zygote进程fork出来的进程。
它们两个是AndroidFramwork里面的两大重要进程。
通过 ps 命令,可知SystemService进程名为system_server,Zygote的进程名称为app_process。
SystemServer进程主要的作用是启动各种系统服务,比如ActivityManagerService,PackageManagerService,WindowManagerService等服务

简述Android framework之AMS、PMS、WMS

简述Android framework之AMS、PMS、WMS
初探Android PMS服务

主要是ActivityManagerService(AMS), WindowManagerService(WMS),PackageM anerService(PMS)

AMS 主要负责App进程的启动、切换和调度、四大组件的启动和管理,以及管理Activity的生命周期。
WMS 管理各个窗口的启动,隐藏,显示,动画等
PMS 用来管理跟踪所有应用APK,解析,安装,卸载,权限控制等.

什么是ActivityManagerService

AMS 是系统的引导服务,App进程的启动、切换和调度、四大组件的启动和管理都需要 AMS 的支持。
以及负责系统中所有Activity的生命周期,如Activity的开启、暂停、关闭。
ActivityManagerService进行初始化的时机很明确,就是在SystemServer进程开启 的时候,就会初始化ActivityManagerService。

什么是WindowManagerService

WMS是系统的其他服务,它的职责主要有以下几点:
    窗口管理,负责窗口的启动、添加和删除,窗口的大小和层级也是由WMS进行管理的。
    窗口动画,窗口动画由WMS的动画子系统来负责,动画子系统的管理者为WindowAnimator。
    Surface管理,窗口并不具备有绘制的功能,因此每个窗口都需要有一块Surface来供自己绘制。为每个窗口分配Surface是由WMS来完成的。

AMS与ActivityThread的关系

在Android系统中,任何一个Activity的启动都是由AMS和App进程(主要是ActivityThread)相互配合来完成的。
AMS服务统一调度系统中所有进程的Activity启动。
每个Activity的启动过程则由其所属的进程具体来完成。

Android Framework的客户端服务器架构

Framework初识
Android 系统介绍和架构一览

在这里插入图片描述

Application 层
Home、Contacts、Phone、Browser 等。

Java Framework 层
ActivityManager、WindowManager、ContentProvider、ViewSystem、NotificationManager、PackageManager、TelephonyManager、ResourceManager、LocationManager、XMPPManager 等。

Libraries & Android Runtime 层
Libraries: Surface Manager、MediaFramework (c++ Framework)、SQLite、OpenGL/ES、FreeType、Webkit、SGL、SSL、Libc 等。
Android Runtime: Core Libraries、Dalvik VM / ART VM。

Linux Kernel 层
Display Driver、Camera Driver、Bluetooth Driver、Flash Mem Driver、Binder (IPC) Driver、USB Driver、Keypad Driver、WIFI Driver、Audio Driver、Power Management 等。

App进程和AMS(SystemServer进程)还有zygote进程分属于三个独立的进程,他们之间如何通信呢?

App进程与AMS通过Binder进行IPC通信
AMS(SystemServer进程)与zygote通过Socket进行IPC通信。

Launcher

当我们点击手机桌面上的图标的时候,App就由Launcher开始启动了。
Launcher本质上也是一个应用程序,和我们的App一样,也是继承自Activity。
Launcher实现了点击、长按等回调接口,来接收用户的输入。
比如我们点击图标的时候,是怎么开启应用的呢?捕捉图标点击事件,然后startActivity()发送对应的Intent请求。

Instrumentation和ActivityThread

ActivityThread负责与AMS的外交工作。

每个Activity都持有Instrumentation对象的一个引用,但是整个进程只会存在一个 Instrumentation对象。 
Instrumentation是完成对Application和Activity初始化和生命周期的工具类。

整个系统的android framework进程启动流程如下:

init进程 –> Zygote进程 –> SystemServer进程 –>各种应用进程

那这些系统服务是如何开启的

在zygote开启时,会调用ZygoteInit.main()进行初始化。
1.ZygoteInit.main():
// 加载首个zygote时,会传入初始化参数,使得startSystemServer = true
boolean startSystemServer = false;
String zygoteSocketName = "zygote";
String abiList = null;
boolean enableLazyPreload = false;
for (int i = 1; i < argv.length; i++) {
    if ("start-system-server".equals(argv[i])) {
        startSystemServer = true;
    } else if ("--enable-lazy-preload".equals(argv[i])) {
        enableLazyPreload = true;
    } else if (argv[i].startsWith(ABI_LIST_ARG)) {
        abiList = argv[i].substring(ABI_LIST_ARG.length());
    } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
        zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());
    } else {
        throw new RuntimeException("Unknown command line argument: " + argv[i]);
    }
}
。
。
。
// 开始forkSystemServer进程
if (startSystemServer) {
    Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer); // Prepare the arguments and forks for the system server process.
    。。。
}

2.ZygoteInit.forkSystemServer:
/* Hardcoded command line to start the system server 
ZygoteInit.main(String argv[])中的argv就是通过此处传递的
*/
String args[] = {
        "--setuid=1000",
        "--setgid=1000",
        "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,"
                + "1024,1032,1065,3001,3002,3003,3006,3007,3009,3010",
        "--capabilities=" + capabilities + "," + capabilities,
        "--nice-name=system_server",
        "--runtime-args",
        "--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT,
        "com.android.server.SystemServer",
};
ZygoteArguments parsedArgs = null;
int pid;
try {
    parsedArgs = new ZygoteArguments(args);
    Zygote.applyDebuggerSystemProperty(parsedArgs);
    Zygote.applyInvokeWithSystemProperty(parsedArgs);
    boolean profileSystemServer = SystemProperties.getBoolean(
            "dalvik.vm.profilesystemserver", false);
    if (profileSystemServer) {
        parsedArgs.mRuntimeFlags |= Zygote.PROFILE_SYSTEM_SERVER;
    }
    /* Request to fork the system server process 
    此处fork出来的SystemService*/
    pid = Zygote.forkSystemServer(
            parsedArgs.mUid, parsedArgs.mGid,
            parsedArgs.mGids,
            parsedArgs.mRuntimeFlags,
            null,
            parsedArgs.mPermittedCapabilities,
            parsedArgs.mEffectiveCapabilities);
} catch (IllegalArgumentException ex) {
    throw new RuntimeException(ex);
}

Fragment

Fragment 声明周期

在这里插入图片描述

Fragment比Activity多了几个生命周期的回调方法

  • onAttach(Activity) 当Fragment与Activity发生关联的时候调用,Fargment附加到Activity以后,再次调用setArguments就不生效了。
  • onCreate: 此时Fragment还没有获得Activity的onCreate已完成的通知,所以不将依赖于Activity视图层次结构存在性的代码放入此回调方法中。
  • onCreateView(LayoutInflater, ViewGroup, Bundle) 创建该Fragment的视图。

注意:不要将视图层次结构附加到传入的 ViewGroup父元素中,该关联会自动完成。
如果在此回调中将碎片的视图层次结构附加到父元素,很可能会出现异常。
就是不要把初始化的view视图主动添加到container里面,所以 inflate函数的第三个参数必须填false,而且不能出现container.addview(v)的操作。

inflater.inflate(R.layout.activity_main,container,false)
  • onActivityCreated(Bundle) 在Activity的onCreated方法完成后调用。
  • onDestroyView() 当该Fragment的视图被移除时调用
  • onDestory() 不再使用Fragment时调用。(备注:Fragment仍然附加到Activity并任然可以找到,但是不能执行其他操作)
  • onDetach() 当Fragment与Activity取消关联时调用

PS:注意:除了onCreateView,其他的所有方法如果你重写了,必须调用父类对 于该方法的实现

FragmentTransaction对象的主要方法

- add() 向Activity中添加一个Fragment
- remove() 从Activity中移除一个Fragment,如果被移除的Fragment 没有添加到回退栈,这个Fragment实例将会被销毁
- replace() 使用另一个Fragment替换当前的,实际上就是remove() 然后add()的合体,会销毁原fragment视图
- hide() 隐藏当前的Fragment,仅仅是设为不可见,并不会销毁 
- show() 显示之前隐藏的Fragment
- detach() 会将view从UI中移除,和remove()不同,此时fragment的状态依然由 FragmentManager维护
- attach() 重建view视图,附加到UI上并显示
- addToBackStack() 将fragment添加到回退栈
-commit() 提交事务

注意:在add/replace/hide/show以后都要commit其效果才会在屏幕上显示出来

什么是Fragment的回退栈?

Fragment的回退栈是用来保存每一次Fragment事务发生的变化。
如果将Fragment任务添加到回退栈,当用户点击后退按钮时,将看到上一次的保存的Fragment。
一旦Fragment完全从回退栈中弹出,用户再次点击后退键,则退出当前Activity。

Fragment与Activity之间的通信

Activity -> Fragment
- 如果你Activity中保存了的Fragment的引用,可以通过引用直接访问所有的Fragment的public方法
- 如果Activity中未保存Fragment的引用,因为每个Fragment都有一个唯一的TAG或者ID,可以通过FragmentManager的findFragmentByTag()或者findFragmentById()获得Fragment实例,然后进行作
Fragment -> Activity
- Fragment中可以通过getActivity()得到当前绑定的Activity的实例,然后进行操作
- [推荐] 使用接口通信,Activity实现通信接口,Fragment中保存接口实例,通过接口调用实现通信,降Fragment与Activity的耦合

一个Activity托管的多个Fragment之间的通信

一个Activity托管的多个Fragment之间的通信

1. Fragment之间直接通信(不推荐)。
getActivity().getSupportFragmentManager().findFragmentById,获取需要通信的fragment。
破坏了fragment的独立性,超出了fragment的职责范围。
2. 在fragment中定义回调接口,托管activity将实现回调接口(推荐)

setArguments 使用时机

必须在fragment创建之后,添加给Activity前完成。
千万不要先调用add,再调用setArguments

屏幕旋转,Activity重启,导致fragment重叠如何解决?

在Activity.onCreate方法中判断参数savedInstanceState是否为空,空则初始化Fragment

Fragment发生重建,原本的数据如何保持?

和Activity类似,Fragment也有onSaveInstanceState的方法,在此方法中进行保存数据。
但是没有onRestoreInstanceState,所以可以在onCreate或者onCreateView或者onActivityCreated进行恢复。

在配置发生变化时,什么是最好的保存活动对象方法,比如运行中的线程,Sockets,AsyncTask。

关于保存状态的Fragment,setRetainInstance(true)

使用Fragment的setRetainInstance(true)真的是一个很好的习惯来处理旋转变化

Fragment.setRetainInstance(boolean)方法。
指示系统保留当前的fragment实例,即使是在Activity被创新创建的时候。
它主要用来处理异步请求带来的数据保存问题,尤其是异步请求未完成时,屏幕旋转。

默认情况下,配置发生变化时,Fragment会随着它们的宿主Activity被创建和销毁。
如果调用Fragment.setRetaininstance(true),则Fragment在恢复时会跳过创建(onCreate)和销毁(onDestroy),因此不能在 onCreate(中放置一些初始化逻辑)。

Fragment 1 切换到 Fragment 2时生命周期变化?

https://blog.csdn.net/LouisZhoun/article/details/84849968
https://www.jianshu.com/p/c8f34229b6dc

1、通过 add hide show 方式来切换 Fragment
    Fragment1 的生命周期变化为:onCreate()、onCreateView、onStart()、onResume() 回调 onHiddenChanged() 方法
    Fragment2 的生命周期变化为: onCreate()、onCreateView、onStart()、onResume()
    Fragment2 再次返回到 Fragment 1:不走任何生命周期方法但是回调 onHiddenChanged()方法
总结: 通过hide show 切换时,Fragment 隐藏的时候并不走 onDestroyView,所有的显示也不会走 onCreateView 方法,所有的 view 都会保存在内存
2、使用 replace 的方法进行切换时
    载入Fragment1时:Fragment1的生命周期:onCreate()、onCreateView()、onStart()、onResume()
    切换到Fragment2时:
        Fragment1的生命周期:onPause()、onStop()、onDestroyView()、onDestroy()
        Fragment2的生命周期:onCreate()、onCreateV()、onStart()、onResume()
    Fragment 2切换回Fragment 1时:
        Fragment2的生命周期:onPause()、onStop()、onDestroyView()、onDestroy()
        Fragment1的生命周期:onCreate()、onCreateV()、onStart()、onResume()
总结:通过 replace 方法进行替换的时,Fragment 都是进行了销毁,重建的过程,相当于走了一整套的生命周期
3、当使用 ViewPager 与 Fragment 进行切换时,Fragment 会进行预加载操作
所有的 Fragment 都会提前初始--->预加载;
初始化时 Fragment 们的生命周期:
Fragment1 的生命周期:onCreate()、onCreateView()
Fragment2 的生命周期:onCreate()、 onCreateView()
Fragment1 切换到 Fragment2 的生命周期:
    Fragment1 :不走任何生命周期;
    Fragment2 :走 setUserVisibleHint方法,切回去也是一样的
注意: setUserVisibleHint方法在 Fragment 1 第一次加载的时候不走,只有在切换的时候 走该方法。


从 Fragment1 进行锁屏的生命周期方法:onPause()、onSaveInstanceState()、onStop()。
从解锁 到 Fragment1 的生命周期:onStart()、 onResume()

Context 详解

说说Android中的Context是什么?哪些实现?这些实现的区别?

在这里插入图片描述

Context类本身是一个纯abstract类。,它有两个具体的实现子类,一个是ContextWrapper,一个是ContextImpl。
ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。
而ContextWrapper又有三个直接的子类,ContextThemeWrapper、Service和Application。
其中ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity。

一个应用程序有几个Context?

Context一共有Application、Activity和Service三种类型,因此一个应用程序中Context数量的计算公式就可以这样写:
Context数量 = Activity数量 + Service数量 + 1个Application
因为一个应用程序中可以有多个Activity和多个Service,但是只能有一个Application。

常说四大组件,这里怎么只有 Activity,Service持有Context,那BroadcastReceiver,ContentProvider呢?
BroadcastReceiver,ContentProvider并不是Context的子类,他们所持有的 Context都是其他地方传过去的,所以并不计入Context总数。

Context能干什么?

这个就实在是太多了,弹出Toast、启动 Activity、启动Service、发送广播、操作数据库等等都需要用到Context。

Context作用域

由于Context的具体实例是由ContextImpl类去实现 的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context 都是可以通用的。
不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。 
出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,
Activity 的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。
Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),
因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

在这里插入图片描述

上图中Application和Service所不推荐的两种使用情况。

1. 如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的 时候会报错
android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the  FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity 就找不到栈了。
解决这个问题的方法就是为待启动的Activity指定 FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。
所有这种用Application启动Activity的方式不推荐使用,Service同Application。

2.在Application和Service中去layout inflate也是合法的,但是会使用系统默认的 主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐 使用。

getApplication()和getApplicationContext()的区别?为什么要提供两个功能重复的方法呢?

在这里插入图片描述

通过上面的代码,打印得出两者的内存地址都是相同的, 看来它们是同一个对象。
返回类型不同,使用场景不同。
getApplication()返回的是Application。这个方法只有在Activity和Service中才能调用的到。
getApplicationContext()返回的是Context。BroadcastReceiver中获得Context的实例。

Broadcast

是否使用过本地广播,和全局广播有什么差别?

引入本地广播的机制是为了解决安全性的问题:
1)正在发送的本地广播不会脱离应用程序,不用担心app的数据泄露;
2)其他的程序无法发送到我的应用程序内部,不担心安全漏洞。
3)发送本地广播比发送全局的广播高效。全局广播要维护广播集合表,效率更低。全局广播,意味着可以跨进程,就需要底层的支持。
本地广播不能用静态注册。----静态注册:可以做到程序停止后还能监听。
使用:
(1)注册:LocalBroadcastManager.getInstance(this).registerReceiver(new XXXBroadCastReceiver(), new IntentFilter(action));
(2)取消注册: LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)

手机装一个程序,没有被启动过,注册的静态广播能被接收到吗

https://blog.csdn.net/a229413040/article/details/51791766

https://blog.csdn.net/CHX_W/article/details/74993305

原来安卓从Android3.1开始,新安装的程序就会被置于”stopped”状态,并且只有在至少手动启动一次后该程序才会改变状态,才能够正常接收到指定的广播消息。
Android这样做的目的是防止广播无意或者不必要地开启未启动的APP后台服务。
解决办法:
不过从Android 3.1开始,系统给Intent定义了两个新的Flag,分别为FLAG_INCLUDE_STOPPED_PACKAGES(表示包含未启动的App)和FLAG_EXCLUDE_STOPPED_PACKAGES(表示不包含未启动的App),用来控制Intent是否要对处于停止状态的App起作用
1、在静态注册广播的时候 指定exported = true
2、在发送intent的地方加上intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);

Service

IntentService 介绍

IntentService 本质上是采用Service+Handler+HandlerThread
IntentService在onCreate函数中通过HandlerThread单独开启一个线程来处理所有intent请求,
若多次启动IntentService,每个耗时操作则以队列的方式在onHandlerIntent方法中依次执行,执行完自动退出。
1.onCreate中通过HandlerThread单独开启一个线程和一个名叫ServiceHandler的Handler,同时将两者绑定
2.默认实现了onStartCommand,目的是将intent作为msg.obj,通过mServiceHandler.sendMessage,发送出去,依次插入到工作队列
3.在ServiceHandler的handlerMessage中回调onHandlerIntent()
4.在onHandlerIntent中根据intent的不同处理

注意事项:
1.工作队列依次顺序执行,后一个任务会等待前一个任务执行完,才开始执行。
2.onCreate方法只会调用一次,所以只会创建一个线程
3.多次调用startService是,onStartCommand也会调用多次
4.停止服务会清除消息队列,后续的事件不会执行

Service与IntentService的区别

1.Service用于主线程,在其中进行耗时操作会以你器ANR
2.IntentService创建一个工作线程来处理多线程任务。
3.Service需要主动调用stopSelf()来结束服务,而IntentService在执行完消息队列后,会自动关闭。

onStartCommand的返回值

https://blog.csdn.net/chdjj/article/details/50428986

返回值将影响服务异常终止情况下重启服务时的行为
默认情况下,当我们的服务因为系统内存吃紧或者其他原因被异常终止时,系统会尝试在某个时刻重新启动服务,这时,如果Service#onStartCommand方法返回
   1. START_NOT_STICKY:
    服务不会重新创建,除非你再次调用startService
    2.START_STICKY/START_STICKY_COMPATIBILITY:
    服务重新创建并启动,保留在开始状态,依次回调onCreate,onStartCommand,但是如果没有新的intent传给此service,onStartCommand接受的将是一个空的intent。
    START_STICKY_COMPATIBILITY是START_STICKY的兼容版本,2.0之下使用,它不保证一定调用onStartCommand.
    3.START_REDELIVER_INTENT:
    服务重新创建并启动,依次回调onCreate,onStartCommand,并且会把最后一次传给此服务的intent重新发给onStartCommand。

如何保障服务不被杀死

https://www.jianshu.com/p/09a2ef6e3c57

1.onStartCommand方法,返回START_STICKY

 将Service设置成START_STICKY,在运行onStartCommand后Service进程被kill,那将保留在开始状态,但是不保留那些传入的intent。不久后Service就会再次尝试重新创建,因为保留在开始状态,在创建Service后将保证调用onstartCommand。如果没有传递任何开始命令给Service,那将获取到null的intent。重传intent,保持和重启前一样。
 
2.提升Service优先级
在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = "1000"这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时适用于广播。

3.提升service进程优先级
Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收。

4.onDestroy方法里重启service
直接在onDestroy()里startService或使用service + broadcast方式,就是当service走到onDestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service;
当使用类似某某管家等第三方应用或是在setting里-应用-强制停止时,APP进程可能就直接被干掉了,onDestroy方法都进不来,所以还是无法保证。

5.监听系统广播判断Service状态
通过系统的一些广播,比如:手机重启、界面唤醒、应用状态改变等等监听并捕获到,然后判断我们的Service是否还存活,别忘记加权限。
这也能算是一种措施,不过感觉监听多了会导致Service很混乱,带来诸多不便。

6.放一个像素在前台(手机QQ)

进程的种类

前台、可见、服务、后台、空
回收顺序:空进程-->后台进程-->服务进程-->可见进程-->前台进程。

1.前台进程
是目前正在屏幕上显示的进程和一些系统进程,也就是和用户正在交互的进程。
2.可见进程
可见进程指部分程序界面能够被用户看见,却不在前台和用户交互的进程。例如在一个界面上弹出一个对话框(该对话框是一个新的Activity),那么在对话框后面的原界面是可见的,但是并没有与用户进行交互,那么原界面就是可见进程
3.服务进程
服务进程是通过startService()方法启动的进程,但是不属于前台进程和可见进程,例如在后台播放音乐和在后台下载就是服务进程,
4.后台进程
后台进程指的是目前对用户不可见的进程,例如我正在使用qq和别人聊天,这个时候qq是前台进程,但是当点击Home键让qq小时的时候,这个进程就转换成了后台进程。当内存不够的时候,它可能会将后台进程回收。
5.空进程
空进程指的是在这些进程内部,没用任何东西在运行。保留这些进程的唯一目的是作为缓存,以缩短该应用下次再其中运行组件所需的启动时间。

一个Activity中我启动一个死循环的线程后,退出后属于什么进程

后台进程??

AsyncTask

使用AsyncTask的注意事项

1.异步任务的实例必须在UI线程中创建,即AsyncTask对象必须在UI线程中创建。
2.execute(Params... params)方法必须在UI线程中调用。
3.不要手动调用onPreExecute(),doInBackground(Params... params), onProgressUpdate(Progress... values),onPostExecute(Result result)这几个方 法。
4.不能在doInBackground(Params... params)中更改UI组件的信息。 
5.一个任务实例只能执行一次,如果执行第二次将会抛出异常。

关于为什么AsyncTask一定要在主线程中创建?

AsyncTask的内部封装了两个线程池(SerialExecutor和 THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler)。
SerialExecutor线程池用于任务的排队,让需要执行的多个耗时任务,按顺序排列,
THREAD_POOL_EXECUTOR线程池才真正地执行任务,
InternalHandler 用于从工作线程切换到主线程,必须是通过主线程的Looper来创建

在API26中,InternalHandler 是通过Looper.getMainLooper()创建的,所以AsyncTask不论是在子线程还是主线程中创建都无所谓。
在API26之前,InternalHandler的创建方式如下:是通过当前线程的Looper创建的,如果当前线程是子线程,那自然是行不通的。
private static final InternalHandler sHandler = new InternalHandler();􏲪

execute方法一定要在主线程中调用?

execute方法最先调用的是onPreExecute方法,而onPreExecute方法必须运行在主线程,
如果一定要在子线程中调execute方法那就表示你不能在onPreExecute方法中做更新UI的操作,因为此时onPreExecute方法为子线程。
onProgressUpdate, onPostExecute , onCancelled 方法仍然运行在主线程(通过InternalHandler回调到主线程)。

AyscTask 实现原理

https://blog.csdn.net/caizehui/article/details/104712045
https://blog.csdn.net/zhaoyangfang/article/details/76100607

AsyncTask类对象必须在主线程创建。
execute方法必须在UI线程调用。
一个AsyncTask只能执行一次,即一次execute方法。
execute方法默认是串行执行的,如果需要并行执行需要调用executeOnExecutor方法。(曾经由于不知道这个特性犯过错误,设置网络请求超时3s,超时就不再需要这个请求,但是有时会添加多个任务时,如果那个任务7s才执行完毕,那么后边的任务也是要等待其执行完毕才开始执行的。)

第一个问题:一个AsyncTask对象只能被执行一次,即只能调用一次execute;
第二个问题:既然一个AsyncTask对象只能被执行一次,为什么AsyncTask还要用线程池;
一个进程里面所有AsyncTask对象都共享同一个SerialExecutor对象。
SerialExecutor是一个一个执行任务的,而所有的AsyncTask对象又共享同一个SerialExecutor对象(静态成员)。
public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), sThreadFactory);
    threadPoolExecutor.setRejectedExecutionHandler(sRunOnSerialPolicy);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

/**
 * An {@link Executor} that executes tasks one at a time in serial
 * order.  This serialization is global to a particular process.
 */
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

并行还是串行

Android 1.6之前的版本,AsyncTask是串行的,
在1.6至2.3的版本,改成了并行的。
在2.3之后的版本又做了修改,可以支持并行和串行,当想要串行执行时,直接执行execute()方法,如果需要并行执行,则要执行executeOnExecutor(Executor)。

AyscTask 为什么只能执行一次

https://www.cnblogs.com/wangzehuaw/p/5328254.html

https://blog.csdn.net/lintcgirl/article/details/48440543

当外界调用AsyncTask.execute的时候,最终会调用到executeOnExecutor。 
executeOnExecutor中首先对状态进行了判断,如果状态不是pending的话就会抛出异常,要么是正在执行,要么是已经被执行过的异常。
然后我将该段代码注释掉,让该AsyncTask执行两遍(调用execute两次)。发现并没有报错,但是发现doInBackground中的代码并没有被执行两遍,而是执行了一遍(第一次调用Execute被执行,第二次没有执行)。

executeOnExecutor通过exec.execute(mFuture);执行任务。
当通过ThreadPoolExecutor.execute(FutureTask)后,会调用FutureTask.run,在run中会调用Callable.call函数,执行完后会将callable清空,所以FutureTask只能被执行一次,第二次执行时会发现callable已经为空。

事件拦截/分发

https://blog.csdn.net/jinmie0193/article/details/80786797

onTouch和onTouchEvent以及onClick的顺序

onTouch和onTouchEvent以及onClick的顺序

Android中view的onTouch&onClick事件分发机制详解

Android 事件拦截/分发机制 (图解+代码)

在这里插入图片描述

顺序为:onTouch—–>onTouchEvent—>onClick
总结:

1.onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。

2.假如onTouch方法返回false会接着触发onTouchEvent,如果通过setOnClickListener()给控件注册点击事件的话,最后会在performClick()方法里回调onClick()。反之onTouchEvent方法不会被调用。

3.内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。

当onTouchEvent的type为ACTION_UP时,里面会调用performClick.

onClick是在onTouch方法之后调用的,只有onTouch返回false时,onTouchEvent才会被调用,并且在onTouchEvent的getAction为ACTION_UP时调用performClick,处理OnClickListener,回调onClick方法。

如果我们重写View.onTouchEvent返回false时,当前view的dispatchTouchEvent、onTouchEvent,以及注册的onTouch事件只会在onTouchEvent的getAction为ACTION_DOWN时被调用一次,ACTION_MOVE和ACTION_UP事件都不会被调用。

事件分发的对象是谁? 答:事件

当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件 (Touch事件)。
Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等) 被封装成MotionEvent对象
主要发生的Touch事件有如下四种:
MotionEvent.ACTION_DOWN:按下View(所有事件的开始) 
MotionEvent.ACTION_MOVE:滑动View
MotionEvent.ACTION_CANCEL:非人为原因结束本次事件 
MotionEvent.ACTION_UP:抬起View(与DOWN对应)
事件列:从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件 任何事 件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件,如下 图:

在这里插入图片描述

事件分发的本质

答:将点击事件(MotionEvent)向某个View进行传递并最终得到处理
点击事件由哪个对象发出,经过哪些对象,最终达到哪个对象并最终得到处理。

事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成。

事件在哪些对象之间进行传递?

答:Activity、ViewGroup、View,一个点击事件产生后,传递顺序是:Activity(Window) -> ViewGroup -> View

当一个点击事件发生时,调用顺序如下

Android中事件分发顺序:Activity(Window) -> ViewGroup -> View
1. 事件最先传到Activity的dispatchTouchEvent()进行事件分发
2. 调用Window类实现类PhoneWindow的superDispatchTouchEvent()
3. 调用DecorView的superDispatchTouchEvent()
4. 最终调用DecorView父类的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()。

一个LinerLayout里放一个Button,当点击Button的时候事件是怎么传递的?

点击事件用MotionEvent来表示,当点击事件发生时,
最先传递给Activity,由Activity的dispatchTouchEvent来进行事件的派发,
具体的工作是用Activity内部的Window(抽象类)来完成的。
Window将事件传递个decorview,decorview一般是当前几面的底层容器,
通过Activity.getWindow().getDecorView()可以获得。
现在,事件传递到decorView了,decorView再将事件传递给LinerLayout,LinerLayout将事件传递给Button。 
这里要补充的是:
当父容器通过 dispatchTouchEvent来传递事件时,如果他的onInterceptTouchEvent函数返回true,就不会将事件传递个子view,
当然Android的还提供了一种防止父容器阻断事件的机制:只要子View调用requestDisallowInterceptTouchEvent方法,就能保证子View一定能收到点击事件。 

View,ViewGroup事件分发

1. Touch事件分发中只有两个主角:ViewGroup和View。
    ViewGroup包含dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三个相关方法。
    View包含dispatchTouchEvent、onTouchEvent两个相关事件。
    其中ViewGroup又继承于View。
2.ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。
3.触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。
4.当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。
    ViewGroup的遍历可以看成是递归的。
    分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。
5.当某个子View.onTouchEvent返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。
    由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象。
    如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。
    当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。
6.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。
    触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。
    在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

7.onInterceptTouchEvent有两个作用:
    1.拦截Down事件的分发。
    2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

在这里插入图片描述

如何解决View的滑动冲突?

外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。onInterceptTouchEvent方法
内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。
重写子View的dispatchTouchEvent方法并设置requestDisallowInterceptTouchEvent方法,父View需要重写onInterceptTouchEvent方法

如何解决ScrollView嵌套中一个ListView的滑动冲突?

https://www.jianshu.com/p/852aad53478a

1.为什么ScrollView布局中嵌套Listview只会显示一个Itemview的第一个Item
ScrollView的重写了measureChildWithMargins方法导致它的子View的高度被强制设置成了MeasureSpec.UNSPECIFIED模式,
ListView在计测量自己的高度时对MeasureSpec.UNSPECIFIED这个模式在测量时只会返回一个List Item的高度。
2.如何解决
(外部拦截)
ListView区域的滑动事件ScrollView不要拦截。ListView区域外的由ScrollView去处理事件。
比如自定义ScrollView类,处理事件分发,覆写onInterceptTouchEvent方法,如果点击操作发生在ListView的区域的时候,
返回false让ScrollView的onTouchEvent接收不到MotionEvent,而是把Event传到下一级的控件中。
    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

RelativeLayout内部含有一个TextView

在这里插入图片描述

如果这RelativeLayout和TextView都设置了setOnClickListener,当点击TextView时,谁会收到点击事件,为什么

Textview。
事件传递顺序:外->内
事件处理顺序:内->外

点击在RelativeLayout滑动到他内部的TextView然后抬起,分析事件的传递DOWN、MOVE、UP

RelativeLayout 与 TextView默认是不可点击的。
1.两者都没有设置clickable为true或设置点击事件
    (1)点击RelativeLayout并滑动到TextView(或点击RelativeLayout)
        只有RelativeLayout收到DOWN事件。
         E/MyRelativeLayout: onTouchEvent 0 return:false isPressed:false
    (2)点击TextView并滑动到RelativeLayout(或点击TextView)
        两者都收到了DOWN事件。
        由于TextView未设置点击事件,所以其onTouchEvent返回false,此时会传递到RelativeLayout。
        E/MyTextView: onTouchEvent 0 return:false isPressed:false
        E/MyRelativeLayout: onTouchEvent 0 return:false isPressed:false
2.只设置TextView的点击事件
    (1)点击RelativeLayout并滑动到TextView(或点击RelativeLayout)
        只有RelativeLayout收到DOWN事件。
        E/MyRelativeLayout: onTouchEvent 0 return:false isPressed:false
    (2)点击TextView
        TextView收到了DOWN、MOVE、UP,并且执行了点击事件。
        因为它设置了点击事件,并且返回了true。
        E/MyTextView: setPressed true
        E/MyTextView: onTouchEvent 0 return:true isPressed:true // DOWN
        E/MyTextView: onTouchEvent 2 return:true isPressed:true // MOVE 多个
        E/MyTextView: onTouchEvent 1 return:true isPressed:true // UP
        E/YML_MainActivity: setOnClickListener textview
        E/MyTextView: setPressed false
    (3)点击TextView并滑动到RelativeLayout
        TextView收到了DOWN、MOVE、UP,但是不执行点击事件。
        因为在OnTouchEvent中,如果是收到了UP事件,会检测当前控件是不是按下状态,如果不是则忽略此次事件。而点击事件是在UP事件中,同时是按下状态才会执行performClick()方法,performClick方法中会走到点击事件。底部有onTouchEvent的源码。
        因此收不到。
        E/MyTextView: setPressed true
        E/MyTextView: onTouchEvent 0 return:true isPressed:true
        E/MyTextView: onTouchEvent 2 return:true isPressed:true
        E/MyTextView: setPressed false // 滑动出TextView,此时不再是按下状态。
        E/MyTextView: onTouchEvent 2 return:true isPressed:false
        E/MyTextView: onTouchEvent 1 return:true isPressed:false
        
3.只设置RelativeLayout的点击事件
    (1)点击RelativeLayout并滑动到TextView(或点击RelativeLayout)
        只有RelativeLayout能收到DOWN、MOVE、UP事件,并且执行了点击事件。
        当滑动到TextView是,事件确实是传递到了TextView.onTouchEvent方法中。不过这个方法会先判断当前控件是不是可以点击的,如果不是就直接返回了。
        设置控件可以点击有两个方法:
            一是xml设置android:clickable="true"
            二是设置点击事件,点击事件中会将控件设置为可以点击的状态。
        E/MyRelativeLayout: setPressed true
        E/MyTextView: setPressed true
        E/MyRelativeLayout: onTouchEvent 0 return:true isPressed:true
        E/MyRelativeLayout: onTouchEvent 2 return:true isPressed:true
        E/MyRelativeLayout: onTouchEvent 1 return:true isPressed:true
        E/YML_MainActivity: setOnClickListener layout
        E/MyRelativeLayout: setPressed false
        E/MyTextView: setPressed false
    (2)点击TextView并滑动到RelativeLayout(或点击TextView)
        只有RelativeLayout能收到DOWN、MOVE、UP事件,并且执行了点击事件。
        E/MyTextView: onTouchEvent 0 return:false isPressed:false
        E/MyRelativeLayout: setPressed true
        E/MyTextView: setPressed true
        E/MyRelativeLayout: onTouchEvent 0 return:true isPressed:true
        E/MyRelativeLayout: onTouchEvent 2 return:true isPressed:true
        E/MyRelativeLayout: onTouchEvent 1 return:true isPressed:true
        E/YML_MainActivity: setOnClickListener layout
        E/MyRelativeLayout: setPressed false
        E/MyTextView: setPressed false
4.两者都设置点击事件
    (1)点击RelativeLayout并滑动到TextView(或点击RelativeLayout)
        只有RelativeLayout能收到DOWN、MOVE、UP事件,并且执行了点击事件。
        DOWN事件只有一个,因为点击在RelativeLayout,所以只有RelativeLayout收到了。
        MOVE有多个,但是TextView不是按下状态,所以收不到。
        E/MyRelativeLayout: setPressed true
        E/MyRelativeLayout: onTouchEvent 0 return:true isPressed:true
        E/MyRelativeLayout: onTouchEvent 2 return:true isPressed:true
        E/MyRelativeLayout: onTouchEvent 1 return:true isPressed:true
        E/YML_MainActivity: setOnClickListener layout
        E/MyRelativeLayout: setPressed false
        E/MyTextView: setPressed false
    (2)点击TextView
        只有RelativeLayout能收到DOWN、MOVE、UP事件,并且执行了点击事件。
        E/MyTextView: setPressed true
        E/MyTextView: onTouchEvent 0 return:true isPressed:true
        E/MyTextView: onTouchEvent 2 return:true isPressed:true
        E/MyTextView: onTouchEvent 1 return:true isPressed:true
        E/YML_MainActivity: setOnClickListener textview
        E/MyTextView: setPressed false
    (2)点击TextView并滑动到RelativeLayout。
        只有RelativeLayout能收到DOWN、MOVE、UP事件,但是不执行点击事件。
        因为滑动出TextView时,它就不是按下状态了。
        E/MyTextView: setPressed true
        E/MyTextView: onTouchEvent 0 return:true isPressed:true
        E/MyTextView: onTouchEvent 2 return:true isPressed:true
        E/MyTextView: setPressed false // 滑动出TextView
        E/MyTextView: onTouchEvent 2 return:true isPressed:false
        E/MyTextView: onTouchEvent 1 return:true isPressed:false
    

首先RelayoutLayout和TextView需要设置clickable为true或设置点击事件,才能对MOVE,UP事件进行捕获。
不然会在View.onTouchEvent方法开始就返回false,不再进行处理了。


如果没有设置点击事件或没有设置clickable=true,无论怎么滑动两者都只能收到DOWN事件。
如果两者都设置点击事件或clickable=true,
    从RelayoutLayout中点击,并移动到TextView内部后抬起,所有的事件只有RelayoutLayout收到,且RelayoutLayout能执行点击事件。
    从TextView中点击,并移动RelayoutLayout到内部后抬起,所有的事件只有TextView收到。虽然两者都设置了点击事件,但是两者都不能执行点击事件。
    
    因为点击事件要执行的前提是,此控件属于按下状态,当从TextView中滑动出去时,TextView的按下状态会变为fasle,自然不会执行。

TextView ImageView等默认clickable=false,Button默认clickable=true。
onTouchEvent中clickable为false时,会直接返回,不进行事件分发。

对于click事件,其执行时机是在onTouchEvent的MotionEvent.ACTION_UP分支中,
其中会检测当前按钮是否属于按下状态,如果是按下状态会触发performClick()方法,
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            // 设置点击事件时,设置clickable=true
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        // 如果设置了点击事件
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            // 执行点击事件
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        // 不可点击直接返回了。
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    // (mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    final int motionClassification = event.getClassification();
                    final boolean ambiguousGesture =
                            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                    int touchSlop = mTouchSlop;
                    if (ambiguousGesture && hasPendingLongPressCallback()) {
                        final float ambiguousMultiplier =
                                ViewConfiguration.getAmbiguousGestureMultiplier();
                        if (!pointInView(x, y, touchSlop)) {
                            // The default action here is to cancel long press. But instead, we
                            // just extend the timeout here, in case the classification
                            // stays ambiguous.
                            removeLongPressCallback();
                            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                    * ambiguousMultiplier);
                            // Subtract the time already spent
                            delay -= event.getEventTime() - event.getDownTime();
                            checkForLongClick(
                                    delay,
                                    x,
                                    y,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        }
                        touchSlop *= ambiguousMultiplier;
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, touchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }

                    final boolean deepPress =
                            motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                    if (deepPress && hasPendingLongPressCallback()) {
                        // process the long click action immediately
                        removeLongPressCallback();
                        checkForLongClick(
                                0 /* send immediately */,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                    }

                    break;
            }

            return true;
        }

        return false;
    }

View

自定义View:

Android 自定义view设置xml属性

对现有控件的扩展

创建复合控件

重写View实现全新控件

View的加载流程

Activity内部有个抽象类Window,提供绘制窗口的API,它的实例为PhoneWindow。
PhoneWindow是继承于Window,内部包含了一个DecorView对象,DectorView对象是所有应用窗口(Activity界面)的根View,用于存放存放布局文件的。
DecorView继承FrameLayout,它依据theme创建不同的窗口布局,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏。
在Activity中通过setContentView所设置的布局文件其实就是被加载到【内容栏】中的,而内容栏的id是content,因此指定布局的方法叫setContentView()。
当setContentView设置显示OK以后会回调Activity的onContentChanged方法。

1.setContentView流程

在Activity中的attach()方法中,生成了PhoneWindow实例。
PhoneWindow中创建了一个DecroView, 其中创建的过程中根据Theme不同,加载不同的布局格式,例如有没有Title, 或有没有ActionBar等,然后再向mContentParent中加入子View,即Activity中设置的布局。

View的绘制流程

View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--- >ViewGroup)-->View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。

第一步:OnMeasure():测量视图大小。ViewGroup一般是先测量子View的大小,然后再确定自身的大小。
    从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。
    ViewGroup中需要重写onMeasure方法,一般是先遍历测量子view,根据子View的测量值定义自己的宽高。

第二步:OnLayout():确定View位置,进行页面布局。ViewGroup先确定自己的布局,再让子View布局。
    从顶层父View向子View的递归调用view.layout方法的过程,父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。
    ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。

第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。所有的视图最终都是调用View的draw绘制视图。
draw()方法按照以下步骤绘制:
    1.drawBackground绘制本身View背景。
    2.onDraw绘制本身View内容。默认为空实现,自定义View时要进行复写。
    3.dispatchDraw绘制子View。默认为空实现单一View中不需要实现,ViewGroup中已经实现该方法。
    4.onDrawScrollBars绘制滑动条和前景色等等
在自定义View时,不应该复写draw()方法,而是复写onDraw(Canvas)方法进行绘制。如果自定义的视图确实要复写该方法,那么需要先调用super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。

总结:
onMeasure()方法:
    View,ViewGroup都重写
    单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。
    ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
onLayout()方法:
    ViewGroup重写
    单一View,不需要实现该方法。
    ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
onDraw()方法:
    View,ViewGroup都重写
    无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法

通过setContentView()设置的界面,为什么在onResume()之后才对用户可见呢?

https://blog.csdn.net/wenyiqingnianiii/article/details/52353296

Activity成功启动后,会走到ActivityThread的handleResumeActivity方法。
handleResumeActivity分为以下几步:
    1.调用performResumeActivity(),回调Activity的OnResume。此时界面还不可见。
    2.如果这个Activity的window还没有添加到WindowManager,同时这个Activity还没有启动完成或是启动另外一个Activity。
    之后执行下面两步。
        先设置Activity的DecorView为INVISIBLE,
        然后将这个window添加到WindowManager。
    此时界面还不可见。
    如果窗口已经被添加,但在resume期间我们开始了另一个活动,此时不设置窗口可见。
    3.最后如果Activity没有被销毁,没有启动其他Activity,将设置窗口可见,调用activity.makeVisible。

Activity.makeVisible

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }
此时会先检测Window是否被添加到WindowManager。
如果没有会先获取WindowManagger,
之后调用wm.addView(mDecor, getWindow().getAttributes()),将DccorView添加到WM。
wm.addView其内部创建了一个ViewRootImpl对象,负责绘制显示各个子View。
因为WindowManager是个接口,具体是交给 WindowManagerImpl来实现的。
WindowManagerImpl内部是交给WindowManagerGlobal的addView()方法去实现。
WindowManagerGlobal的addView方法内会初始化ViewRootImpl对象,然后调用其setView()方法。
其中setView(),最终调用了performTraversals()方法。
然后依照下图流程层层调用,完成绘制,最终界面才绘制出来。
最后调用Decor.setVisibility(View.VISIBLE),设置显示,界面才会被看到。

在这里插入图片描述

Activity,View,Window三者关系

1:Activity构造的时候会初始化一个Window(抽象类),其实现类PhoneWindow。
2:这个PhoneWindow有一个“ViewRoot”,这个“ViewRoot”是一个View或者说ViewGroup,是最初始的根视图。
3:“ViewRoot”通过addView方法来一个个的添加View。比如TextView,Button等
4:这些View的事件监听,是由WindowManagerService来接受消息,并且回调Activity函数。比如onClickListener,onKeyDown等。

Activity Window DecorView ViewRoot 介绍

Activity

Activity并不负责视图控制,它只是控制生命周期和处理事件。
真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。 
Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。

Window

Window是视图的承载器,是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。
PhoneWindow内部持有一个DecorView,而这个DecorView才是view的根布局。
PhoneWindow通过创建DecorView来加载Activity中设置的布局。
Window通过WindowManager加载DecorView,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。

DecorView

DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。 
DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout。 
这个LinearLayout里面有上中下三个部分,上面是个ViewStub,根据Theme设置用于延迟加载的ActionBar,中间是标题栏(根据Theme设 置,有的布局没有),下面的是内容栏。
Activity的setContentView便是将布局设置到内容栏。

ViewRoot

ViewRootImpl源码分析事件分发
Android 源码分析四ViewRootImpl相关
所有View的绘制以及事件分发等交互都是通过它来执行或传递的。
ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制 (draw))均通过ViewRoot来完成。
ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。
ViewRoot内部持有一个Handler类,可以接收事件并分发,Android的所有触屏、按键、界面刷新等事件都是通过ViewRoot进行分发的。

事件传递流程:
硬件 -> ViewRootImpl -> DecorView -> PhoneWindow -> Activity

当用户点击屏幕产生一个触摸行为,这个触摸行为则是通过底层硬件来传递捕获,
然后交给ViewRootImpl,
接着将事件传递给DecorView,
而DecorView再交给PhoneWindow,
PhoneWindow再交给Activity,
然后接下来就是我们常见的 View事件分发了。

在这里插入图片描述

Invalidate、postInvalidate、requestLayout应用场景

一般说来需要重新布局就调用requestLayout()方法,需要重新绘制就调用invalidate()方法。

requestLayout():该方法会递归调用父窗口的requestLayout()方法,直到触发ViewRootImpl的performTraversals()方法,此时mLayoutRequested为true,会触发onMesaure()与onLayout()方法,不一定会触发onDraw()方法。
invalidate():该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会触发onMesaure()与onLayout()方法,当时会触发onDraw()方法。
postInvalidate():该方法功能和invalidate()一样,只是它可以在非UI线程中调用

Binder

跨进程通信的方式

Android中进程间通信(IPC)方式

Android中的对象序列化方法

什么是Parcel

文件共享:不适合高并发场景,并且无法做到进程间的及时通信。
Socket:网络数据交换的常用方式。
Bundle:四大组件间的进程间通信方式,简单易用,但传输的数据类型受限
Messenger:数据通过Message传输,只能传输Bundle支持的类型。
AIDL:功能强大,支持实时通信,但使用稍微复杂。
ContentProvider:android系统提供的,简单易用,但使用受限,只能根据特定规则访问数据。

为什么使用Binder,不用传统的操作系统进程间通信方式呢

Binder 是Android系统的一种进程间通信机制,它可以提供系统中任何程序都可以访问的全局服务。
1.性能方面
    Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,共享内存方式一次内 存拷贝都不需要,但实现方式又比较复杂。
2.安全方面
    Android 为每个安装好的 APP 分配了自己的 UID,故而进程的UID是鉴别进程身份的重要标志。
    Binder机制从协议本身就支持对通信双方做身份UID校检,从而大大提升了安全性。
    传统的进程通信方式对于通信双方的身份并没有做出严格的验证,比如Socket通信 的IP地址是客户端手动填入,很容易进行伪造。

IPC 原理

从进程角度来看IPC(Interprocess Communication)机制

每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。
对于用户空间,不同进程之间是不能共享的,而内核空间却是可共享的

IPC机制就是利用进程间可共享的内核内存空间来完成底层通信工作的。
Client端与Server端进程往往采用ioctl等方法与内核空间的驱动进行交互。

在这里插入图片描述

Binder架构介绍

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager 以及Binder驱动。
Binder通信的四个角色 
    Client进程: 使用服务的进程。 
    Server进程: 提供服务的进程。
    ServiceManager进程: 用于管理系统中的各种服务以及将字符形式的Binder名字转化成 Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。
    Binder驱动: 负责进程之间Binder通信的建立、Binder在进程之间的传递、Binder引用计数管理、数据包在进程之间的传递和交互等一系列底层支持。
    其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间
Binder运行机制
    1.注册服务(addService):Server进程要先注册到ServiceManager。该过 程:Server是客户端,ServiceManager是服务端。
    2.获取服务(getService):Client进程使用某个Service前,须先向ServiceManager中 获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。
    3.使用服务:Client根据得到的Service信息建立与Service所在的Server进程通信的通 路,然后就可以直接与Service交互。该过程:Client是客户端,Server是服务端。
架构图如下所示:

在这里插入图片描述

图中的Client,Server,ServiceManager之间交互都是虚线表示,
是由于它们􏰀此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信 (Interprocess Communication)方式。
其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。
Binder驱动和ServiceManager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现Client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。

Binder底层原理

动态内核可加载模块

传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。
但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?
这就得益于Linux的动态内核可加载模块的机制;
模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内 核作为内核的一部分运行。
这样,Android系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

在Android系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动 (Binder Dirver)。

内存映射

内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。
映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也 能直接反应到用户空间。
即两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。 
也正因为如此,内存映射能够提供对进程间通信的支持。
Binder机制中涉及到的内存映射通过mmap() 来实现,mmap()是操作系统中一种内存映射的方法。

Binder IPC 实现原理

Binder IPC正是基于内存映射(mmap)来实现的,但是 mmap()通常是用在有物理介质的文件系统上的。

比如进程中的用户区域是不能直接和物理设备打交道的。
如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);
通常在这种场景下 mmap() 就能发挥作用,
通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。


而 Binder 并不存在物理介质,因此Binder驱动使用mmap()并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。

一次完整的 Binder IPC 通信过程通常是这样:
    首先Binder驱动在内核空间创建一个‘内核数据接收缓存区’;
    接着在内核空间开辟一块‘内核缓存区’
    之后建立‘内核缓存区’和‘内核数据接收缓存区’之间的映射关系,
    最后建立‘内核数据接收缓存区’和‘接收进程用户空间地址’的映射关系;
    这样‘内核缓存区’和‘接收进程用户空间地址’存在间接内存映射关系;
    
    发送方进程通过系统调用copy_from_user()将数据copy到‘内核缓存区’,由于‘内核缓存区’和‘接收进程的用户空间’存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,
这样便完成了一次进程间的通信。

如下图:

在这里插入图片描述

Binder 总结

Android 系统 Binder 机制中的四个组件 Client、Server、Service Manager 和 Binder 驱动程序的关系如下图所示:

在这里插入图片描述

1. Client、Server 和 Service Manager 实现在用户空间中,Binder 驱动程序实现在内核空间中
2. Binder 驱动程序和 Service Manager 在 Android 平台中已经实现, 开发者只需要在用户空间实现自己的 Client 和 Server
3. Binder 驱动程序提供设备文件/dev/binder 与用户空间交互,Client、 Server 和 ServiceManager 通过 open 和 ioctl 文件操作函数与 Binder 驱动程序进行通信
4. Client 和 Server 之间的进程间通信通过 Binder 驱动程序间接实现
5. ServiceManager 是一个守护进程,用来管理 Server,并向 Client 提供查询 Server 接口的能力

Binder源码分析

  1. Service Manager 是如何成为一个守护进程的?即 Service Manager 是如何告知 Binder 驱动程序它是 Binder 机制的上下文管理者
  2. Server 和 Client 是如何获得 Service Manager 接口的?即 defaultServiceManager 接口是如何实现的
  3. Server 是如何把自己的服务启动起来的?Service Manager 在 Server 启动的过程中是如何为 Server 提供服务的?即 IServiceManager::addService 接口是如何实现的
  4. Service Manager 是如何为 Client 提供服务的?即 IServiceManager::getService 接口是如何实现的
  5. Android 系统进程间通信 Binder 机制在应用程序框架层的 Java 接口源代码 分析

全局异常处理

Android全局异常捕获并弹窗提示

先来张崩溃后的效果图:

(背景是另一个APP,当前的APP已崩溃并弹出该提)

在这里插入图片描述

说到全局捕获异常的UncaughtExceptionHandler,就不得不说期间遇到的各种坑:
  1. 初始化肯定在Application,网上说的Activity启各种不认同。但在Application启就存在不能弹AlertDialog的问题(目前不确定,不知道是自己哪里没处理好还是的确是这个问题,有时间再验证一下)
  2. 崩溃不一定是单次,在多层Activity中,崩溃一个顶层的Activity可能导致下层的Activity连续崩溃,所以uncaughtException可能会捕获到多次崩溃信息(具体影响后面会说到)

全局异常处理步骤

1.自定义异常处理类,实现了UncaughtExceptionHandler接口
2.重写UncaughtExceptionHandler.uncaughtException方法。在这里面自定义异常处理流程。
3.在Application中初始化,并设置监听。

实现

1.自定义异常处理类与注册监听

public class CrashHanlder implements UncaughtExceptionHandler {
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    // CrashHandler实例
    private static CrashHanlder INSTANCE = new CrashHanlder();
    // 程序的Context对象
    private Context mContext;

    private CrashHanlder() {
    }

    public static CrashHanlder getInstance() {
        return INSTANCE;
    }

    /**
     * 初始化,并设置监听
     * 
     * @param context
     */
    public void init(Context context) {
        mContext = context;
        // 获取系统默认的UncaughtException处理
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 设置该CrashHandler为程序的默认处理
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 异常捕获
     * 如果异常被捕获到并且异常信息不会NULL,
     * 处理完则跳转到CrashDialog。
     * 为什么跳Activity用Dialog样式,而不直接弹AlertDialog,
     * 是因为的确弹不出来。
     */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            //如果用户没有处理则让系统默认的异常处理器来处
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            //跳转到崩溃提示Activity
            Intent intent = new Intent(mContext, CrashDialog.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intent);
            System.exit(0);// 关闭已奔溃的app进程
        }
    }

    /**
     * 自定义错误捕获
     * 
     * @param ex
     * @return true:如果处理了该异常信息;否则返回false.
     */
    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return false;
        }

        // 收集错误信息
        getCrashInfo(ex);

        return true;
    }

    /**
     * 收集错误信息
     * 把错误信息写到了存储并在刚才的CrashDialog中读取。
     * 为什么不直接传值呢?
     * 因为刚说到的坑第2条,多次崩溃的情况下,将导致直接传值只会传最后一次崩溃信息,而最后一次崩溃信息并不是主要引发崩溃的点,收集上来的错误信息可读性不大。
     * 那为什么我不写个全局变量来存储呢?
     * 因为尝试过,不知道是机型问题(HuaweiMate7-API 23)还是全部问题,变量压根就不记录数据,最后只有将信息依次写到存储。
     * @param ex
     */
    private void getCrashInfo(Throwable ex) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        ex.printStackTrace(printWriter);
        Throwable cause = ex.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String errorMessage = writer.toString();
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String mFilePath = Environment.getExternalStorageDirectory() + "/" + App.ERROR_FILENAME;
            FileTxt.WirteTxt(mFilePath, FileTxt.ReadTxt(mFilePath) + '\n' + errorMessage);
        } else {
            Log.i(App.TAG, "哦豁,说好的SD呢...");
        }

    }

}

2.弹窗展示

public class CrashDialog extends Activity {

    private String mFilePath;
    private Button btnExit, btnRestart;
    private Boolean StorageState = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_crash);
        CrashDialog.this.setFinishOnTouchOutside(false);
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            mFilePath = Environment.getExternalStorageDirectory() + "/" + App.ERROR_FILENAME;
            StorageState = true;
        } else {
            Log.i(App.TAG, "哦豁,说好的SD呢...");
        }

        new Thread(upLog).start();
        initView();
    }

    private void initView() {
        btnExit = (Button) findViewById(R.id.cash_exit);
        btnRestart = (Button) findViewById(R.id.cash_restart);

        btnExit.setOnClickListener(mOnClick);
        btnRestart.setOnClickListener(mOnClick);

    }

    OnClickListener mOnClick = new OnClickListener() {

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.cash_exit:
                exit();
                break;
            case R.id.cash_restart:
                restart();
                break;
            default:
                break;
            }
        }
    };

    // 上传错误信息
    Runnable upLog = new Runnable() {
        @Override
        public void run() {
            try {

                String Mobile = Build.MODEL;
                String maxMemory = "" + getmem_TOLAL() / 1024 + "m";
                String nowMemory = "" + getmem_UNUSED(CrashDialog.this) / 1024 + "m";
                String eMessage = "未获取到错误信息";
                if (StorageState) {
                    eMessage = FileTxt.ReadTxt(mFilePath).replace("'", "");
                }
                Log.i(App.TAG, "Mobile:" + Mobile + " | maxMemory:" + maxMemory + " |nowMemory:" + nowMemory
                        + " |eMessage:" + eMessage);

                /**
                 * 可以在这调你自己的接口上传信息
                 */
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

    private void exit() {
        FileTxt.deleteFile(mFilePath);
        System.exit(0);
        android.os.Process.killProcess(android.os.Process.myPid());
    }

    private void restart() {
        Intent intent = getBaseContext().getPackageManager()
                .getLaunchIntentForPackage(getBaseContext().getPackageName());
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);
        exit();
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        exit();
    }

    // 获取可用内存
    public static long getmem_UNUSED(Context mContext) {
        long MEM_UNUSED;
        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
        am.getMemoryInfo(mi);

        MEM_UNUSED = mi.availMem / 1024;
        return MEM_UNUSED;
    }

    // 获取剩余内存
    public static long getmem_TOLAL() {
        long mTotal;
        String path = "/proc/meminfo";
        String content = null;
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(path), 8);
            String line;
            if ((line = br.readLine()) != null) {
                content = line;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        int begin = content.indexOf(':');
        int end = content.indexOf('k');

        content = content.substring(begin + 1, end).trim();
        mTotal = Integer.parseInt(content);
        return mTotal;
    }

}

如果把一个应用进程直接kill掉,声明周期怎么走?

Android上杀掉进程的方式有两种,分别是System.exit(0)和Process.killProcess(Process.myPid())
生命周期都不会被调用
System.exit(0)停止程序的虚拟机,只会影响当前的程序; 
Process.killProcess(Process.myPid())停止程序的虚拟机,会杀掉所有PID一样的进程。
System.exit(0)和System.exit(1)
0:代表成功的信号; 
1:代表失败的信号,常用于捕获到异常执行。

Bitmap 高效加载与三级缓存

https://www.jianshu.com/p/ee7b943a6d41

高效加载Bitmap的流程:

1将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
2从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和 outHeight参数。
3根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。 
4将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载
图片。

代码实现

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Option
        options.inJustDecodeBounds = true;
        //加载图片 BitmapFactory.decodeResource(res,resId,options); //计算缩放比
        options.inSampleSize = calculateInSampleSize(options, req Height, reqWidth);
        //重新加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    private static int calculateInSampleSize(BitmapFactory.Optio ns options, int reqHeight, int reqWidth) {
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            int halfHeight = height / 2;
            int halfWidth = width / 2;
            //计算缩放比,是2的指数 
            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize)>=reqWidth){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

什么是三级缓存

网络缓存, 不优先加载, 速度慢,浪费流量
本地缓存, 次优先加载, 速度快
内存缓存, 优先加载, 速度最快

三级缓存原理

	首次加载 Android App 时,通过网络交互来获取图片,并将图片保存至本地SD卡和内存中,之后运行App时,优先访问内存中的图片缓存,若内存中没有,则加载本地SD卡中的图片
	总之,只在初次访问新内容时,才通过网络获取图片资源

为什么要使用三级缓存

假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量。
在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的应用,其用户数量级肯定要受到影响
特别是,当我们想要重复浏览一些图片时,如果每一次浏览都需要通过网络获取,流量的浪费可想而知
所以提出三级缓存策略,通过网络、本地、内存三级缓存图片,来减少不必要的网络交互,避免浪费流量
同时在Android开发中,默认给每一个应用分配16M的内存,如果图片过多,而我们又没有对图片进行有效的缓存,就很容易导致OOM。
因此,图片的缓存是非常重要的,尤其是对图片非常多的应用。

LruCache

LRU是最近最少使用zygote的算法,它的核心思想是当缓存满时,会优先淘汰那些最近最少使用的缓存对象。
采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘 缓存,其核心思想都是LRU缓存算法。
Android中可以直接使用 LruCache实现内存缓存。而DisLruCache目前还不是Android SDK的一 部分,官方文档推荐使用DisLruCache来实现硬盘缓存。

LruCache使用
1、设置LruCache缓存的大小,一般为当前进程可用容量的1/8。 
2、重写sizeOf方法,计算出要缓存的每张图片的大小。

LruCache原理:
LruCache是个泛型类,内部维护一个缓存对象列表LinkedHashMap(LinkedHashMap是由数组+双向链表的数据结构来实现的,可以实现访问顺序和插入顺序)。
LruCache把最近使用的对象用强引用存储在 LinkedHashMap中,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。
而最近访问的对象将放在队头,最后被淘汰。
当缓存满时,会把最近最少使用的 对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。

put()方法在添加过缓存对象后,会来判断缓存是否已满,如果满了就要删除近期最少(队尾)使用的对象,直到缓存大小小于最大值。
get()方法获取集合中的缓存对象时,就代表访问了一次该元素, 会更新该元素到队头,保持整个队列是按照访问顺序排序。

ListView RecycleView

ListView如何优化

https://www.cnblogs.com/peterpan-/p/5956424.html

1.Adapter的getView方法里也提供了一个参数:convertView,这个就代表着可以复用的view对象,当然这个对象也可能为空,当它为空的时候,表示该条目view第一次创建,所以我们需要inflate一个view出来

2.建一个静态内部类viewHolder类,里面的成员变量跟view中所包含的组件个数类型相同,在convertView为null的时候,首先重新inflate出来一个view,之后创建一个viewHolder对象,然后将findviewById的结果赋值给ViewHolder中对应的成员变量。最后调用view.setTag()将holder对象与该view对象“绑”在一块。当convertView不为null时,我们让view=converView,同时取出这个view对应的holder对象,就获得了这个view对象中的TextView组件,就不需要再去findViewById了。

3.分页加载,比如说1000条新闻的List集合,我们一次加载20条,等到用户翻页到底部的时候,我们再添加下面的20条到List中,再使用Adapter刷新ListView,这样用户一次只需要等待20条数据的传输时间。

4.考虑在列表滑动的时候停止加载部分资源,例如图片,等待列表停下来再去加载这些。在OnScrollListener中去监听onScrollStateChanged的方法中判断列表是否处于滑动状态即可。预加载也就是在我将要使用的时候进行加载,也就是提前读。懒加载就是我在什么时候使用什么时候加载。

5.列表中有图片的时候可以对图片进行一些优化,例如对图片进行压缩,加载图片的时候采用三级缓存。

6.减少item布局层次,可以用ConstraintLayout 约束布局使布局只有一个层次,使用merge和include 绘制布局。

7.硬件加速,硬件加速只需要设置–android:hardwareAccelerate="true"即可。

    public View getView(int position, View convertView, ViewGroup parent) {
      View view;
      ViewHolder holder;
      // 判断convertView的状态,来达到复用效果
      if (null == convertView) {
        // 如果convertView为空,则表示第一次显示该条目,需要创建一个view
        view = View.inflate(MainActivity.this, R.layout.listview_item,null);
        holder = new ViewHolder();//新建一个viewholder对象
        holder.tvHolder = (TextView) view.findViewById(R.id.tv_item);//将findviewbyID的结果赋值给holder对应的成员变量
        view.setTag(holder);// 将holder与view进行绑定
      } else {
        // 否则表示可以复用convertView
        view = convertView;
        holder = (ViewHolder) view.getTag();
      }
      // 直接操作holder中的成员变量即可,不需要每次都findViewById
      holder.tvHolder.setText(list.get(position));
      return view;
    }

ListView如何优化,复用的原理,为什么会图片错位,如何解决,分页的思想是什么

复用的原理:listview的复用机制就是条目很多的时候,只会创建满一屏的条目,当滑动的时候不会再去创建新的view,而是复用已经滑出屏幕的view,对于view有一个缓存。
为什么会图片错位:出现该问题的条件必须是 复用+异步,缺少一个都不会造成错乱。
假设一屏上有7个条目,向上滑动,条目1会复用到条目8的位置。虽然位置不一样了,但是再内存中是同一个指向,还是同一个view。
如果网速再不好会先刷上位置1的数据,然后再刷上位置8的数据。
如何解决:给imageview设置一个tag可以是图片的url,等图片加载完后就根据tag来找到相应的imageview并显示图片。如果这个imageview已经被复用了,在listview中就找不到了,就不加载图片。这样就解决了问题。

listview的缓存机制

Android ListView 与 RecyclerView 对比浅析–缓存机制

RecycleBin缓存机制的工作原理:
ListView每当一项子view滑出界面时,RecycleBin会调用addScrapView()方法将这个废弃的子view进行缓存。
每当子view滑入界面时,RecycleBin会调用getScrapView()方法获取一个废弃已缓存的view。
ListView的Adapter的getView()中convertView就是RecycleBin缓存机制调用getScrapView()方法获取废弃已缓存的view。
当convertView为空,也就是没有废弃已缓存的view时,将调用LayoutInflater的inflate()方法加载出来布局view,这个操作是比较耗时的;
当convertView不为空时,我们就直接用convertView了,而不需要再次调用LayoutInflater的inflate()方法加载出来布局view。
所以如果我们没利用convertView,ListView的性能大就大大打折扣了

listView或RecyclerView上滑卡顿的原因?

https://www.cnblogs.com/carlo/articles/4972060.html

在构造方法中耗时会导致ListView第一次加载时比较慢, 但是如果在getView方法中耗时就会导致整个ListView卡顿
1..getView方法里面convertView没有使用setTag和getTag方式;
2.getView方法里面含有复杂的计算和耗时操作;
3.布局过于复杂或者是布局里面有大图片或者背景所致;
4.Adapter多余或者不合理的notifySetDataChanged;
5.ViewHolder初始化后的赋值或者是多个控件的显示状态和背景的显示没有优化好

RecyclerView ListView区别

1.ListView的Adapter继承的是BaseAdapter;RecyclerView的Adapter继承的是RecyclerView.Adapter

2.ListView的ViewHolder不是强制要写的, 只是不写的话容易导致OOM或者界面卡顿;RecyclerView的ViewHolder是必须要写的,是强制的,如果不写的话,就不能重写RecyclerView.Adapter中的3个方法 getItemCount()、onCreateViewHolder()、onBindViewHolder()分别表示 总共显示多少条目、创建ViewHolder、绑定数据;

3.ListView的分割线直接在布局中设置 divider,RecyclerView不支持直接在布局中添加分割线

4.Listview中的Item是只能垂直滑动的,RecyclerView可以水平滑动或者垂直滑动,RecyclerView在setAdapter之前一定要设置显示LayoutManager的样式,否则数据不能显示,如瀑布流 网格 支持多种类型

5 ListView的点击事件直接是setOnItemClickListener,RecyclerView不支持点击事件,只能用回调接口来设置点击事件

6 Listview中删除或添加item时,item是无法产生动画效果的,在RecyclerView中添加、删除或移动item时有两种默认的效果可以选择SimpleItemAnimator(简单条目动画) 和 DefaultItemAnimator(原样的条目动画)。

动画

Android 中的动画有哪几类,它们的特点和区别是什么?

https://blog.csdn.net/jie1175623465/article/details/81096991
https://blog.csdn.net/u010126792/article/details/85334839
https://blog.csdn.net/zhzh476460340/article/details/50730557

帧动画

帧动画,Frame动画,指通过指定的每一帧的图片和播放时间,有序的进行播放而形成的动画效果
1、帧动画的特性:
a. 用于生成连续的Gif效果图。
b. DrawableAnimation也是指此动画
2、帧动画的优缺点:
缺点:效果单一,逐帧播放需要很多图片,占用空间较大
优点:制作简单

在动画start()之前要先stop(),不然在第一次动画之后会停在最后一帧,这样动画就只会触发一次。
最后一点是SDK中提到的,不要在onCreate中调用start,因为AnimationDrawable还没有完全跟Window相关联,如果想要界面显示时就开始动画的话,可以在onWindowFoucsChanged()中调用start()。

stop后需要重置下,不然会停在结束的那一帧。

补间动画

视图动画,也就是所谓的补间动画。指通过指定View的初始状态、变化时间、方式、通过一系列的算法去进行图片变换,从而实现动画效果。主要有scale、alpha、Translate、Rotate四种效果。
注意:只是在视图层实现了动画效果,并没有真正改变View的属性。
补间动画
1、补间动画的特性:
a.渐变动画支持四种类型:平移(Translate)、旋转(Rotate)、缩放(Scale)、不透明度

b. 只是显示的位置变动,View的实际位置未改变,表现为View移动到其他地方,点击事件仍在原处才能响应。

c. 组合使用步骤较复杂。

d. View Animation 也是指此动画。

2、补间动画的优缺点:
缺点:当平移动画执行完停在最后的位置,结果焦点还在原来的位置(控件的属性没有真的被改变),属性动画利用属性的改变实现动画,而视图动画仅仅改变了view的大小位置,但view真正的属性没有改变。
优点:相对于逐帧动画来说,补间动画更为连贯自然

属性动画

属性动画,通过不断地改变View的属性,不断重绘而形成动画效果。相比较视图动画,View的属性是真正改变了。
注意:Android3.0(API 11)以上才支持。

1、属性动画的特性:
a.支持对所有View能更新的属性的动画(需要属性的setXxx()和getXxx())。
b. 更改的是View实际的属性,所以不会影响其在动画执行后所在位置的正常使用。
c. Android3.0(API11)及以后出现的功能,3.0之前的版本可使用github第三方开源库nineoldandroids.jar进行支持。
d.属性动画不但可以作用于View,还能作用于数值和Object。
2、属性动画的优缺点:
缺点:(3.0+API出现)向下兼容问题
优点:易定制,效果强

性能优化

1.布局优化

1删除布局中无用的控件和层次,其次有选择地使用性能比较低的ViewGroup。
(relativelayout和LinearLayout在实现效果同等情况下选择使用哪个?为什么?)
如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用 LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更 多的CPU时间
2采用标签
<include>提高布局的复用性
 <merge/>有效减少View树的层次
<ViewStub>ViewStub提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内 存,提高了程序初始化效率。
动态加载ViewStub所包含的布局文件有两种方式,方式一使用使用inflate()方法, 方式二就是使用setVisibility(View.VISIBLE)。
在运行时不止一次的显示 和隐藏某个布局,那么ViewStub是做不到的
ViewStub的另一个缺点就是目前还不支持merge标签.
3避免多度绘制
过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多 次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某 些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。 

2.绘制优化

绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方 面:
1.onDraw中不要创建新的局部对象。
2.onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,会造成View的 绘制过程不流畅。

3.内存泄露的优化分为两个方面:

1.在开发过程中避免写出有内存泄漏的代码 
2通过一些分析工具比如MAT(Memory Analyzer Tool)或者 LeakCanary来找出潜在的内存泄露,然后解决。

4.响应速度优化

响应速度优化的核心思想就是避免在主线程中做耗时操作。 
如果有耗时操作,可以开启子线程执行,即采用异步的方式来执行耗时操作。 
如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至ANR。

5.ListView/RecycleView及Bitmap优化

6.线程优化

线程优化的思想就是采用线程池,避免程序中存在大量的Thread。线程池可以重用 内部的线程,从而避免了线程的创建和销毁锁带来的性能开销,同时线程池还能有效地控制线程池的最大并发量,避免大量的线程因互相抢占系统资源从而导致阻塞 现象的发生。

其他

序列化Serializable 和Parcelable 的区别

序列化:将一个对象转换成可存储或可传输的状态,序列化后的对象可以在网络上传输,也可以存储到本地,或实现跨进程传输;

为什么要进行序列化:开发过程中,我们需要将对象的引用传给其他activity或fragment使用时,需要将这些对象放到一个Intent或Bundle中,再进行传递,而Intent或Bundle只能识别基本数据类型和被序列化的类型。

Serializable:
    Java序列化接口,使用了反射,在硬盘上读写,读写过程中有大量临时变量的生成,内部执行大量的i/o操作,效率很低。

Parcelable: 序列化接口,效率高,使用麻烦 适合在内存中读写。但它的实现原理是将一个完整的对象进行分解,分解后的每一部分都是Intent所支持的数据类型,这样实现传递对象的功能。

Parcelable实现序列化的重要方法:
    序列化功能是由writeToParcel完成,通过Parcel中的write方法来完成;
    反序列化由CREATOR完成,内部标明了如何创建序列化对象及数级,通过Parcel的read方法完成;
    内容描述功能由describeContents方法完成,一般直接返回0。

区别:
    Serializable在序列化时会产生大量临时变量,引起频繁GC。
    Serializable本质上使用了反射,序列化过程慢。
    Parcelable内存消耗小,但不适合将数据存储在磁盘上,在外界变化时,它不能很好的保证数据的持续性。

选择原则:若仅在内存中使用,如activity\service间传递对象,优先使用Parcelable,它性能高。若是持久化操作,优先使用Serializable

注意:静态成员变量属于类,不属于对象,固不会参与序列化的过程;用transient关键字编辑的成员变量不会参与序列化过程;可以通过重写writeObject()和readObject()方法来重写系统默认的序列化和反序列化。

Parcelable和Serializable的作用、效率、区别及选择

1、作用
Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、rmi以方 便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。
Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及 不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。
2、效率及选择
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输 时推荐使用Parcelable,如activity间传输数据。
Serializable可将数据持久化方便 保存,所以在需要保存或网络传输数据时选择Serializable。
因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化。
3、编程实现
对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本 id(serialVersionUID)即可。
而Parcelable则需要实现writeToParcel、 describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包 的工作自己来定义,而序列化的这些操作完全由底层实现。

谈谈对kotlin的理解

特点:
1)代码量少且代码末尾没有分号;
2)空类型安全(编译期处理了各种null情况,避免执行时异常);
3)函数式的,可使用lambda表达式;
4)可扩展方法(可扩展任意类的的属性);
5)互操作性强,可以在一个项目中使用kotlin和java两种语言混合开发;

android中的dp、px、dpi,dip相关概念

https://blog.csdn.net/csdn1125550225/article/details/80368127

px :pixel,像素Android原生API,UI设计计量单位,如获取屏幕宽高。
屏幕分辨率:指在纵向和横向方向上的像素点数,单位是px,一般显示方式是纵向像素数量*横向像素数量,如1920*1080。
屏幕尺寸:一般是屏幕对角线长度,单位是英寸,常见尺寸有3.5,4.0,4.3,4.7,5.0,6.0等。
dpi屏幕像素密度:ppi pixel per inch的缩写,每英寸屏幕上的像素数,因为屏幕尺寸是商家生产时就规定好的,屏幕尺寸一样的手机,屏幕宽高却不一定一样,所以通常取屏幕对角线像素数量和屏幕尺寸来计算屏幕像素密度,
屏幕密度= 对角线像素/对角线长度(英寸)
dp /dip :一个基于屏幕密度的抽象单位,如果一个160dpi的屏幕,1dp=1px
sp :同dp相似,但还会根据用户的字体大小偏好来缩放(建议使用sp作为文本的单位,其它用dip)
drawable-mdpi: 屏幕密度为160的手机设备( Android规定此为baseline,其他均以此为基准,在此设备上,1dp = 1px)
drawable-hdpi: 屏幕密度为240的手机设备 1dp=1.5px
drawable-xhdpi: 屏幕密度为320的手机设备 1dp=2px
drawable-xxhdpi:屏幕密度为480的手机设备 1dp=3px
drawable-xxxhdpi:屏幕密度为640的手机设备 1dp=4px

MVC、MVP、MVVM

https://blog.csdn.net/jinmie0193/article/details/81531907

数据库的操作类型有哪些,如何导入外部数据库?

1)使用数据库的方式有哪些?
    1)context.openOrCreateDatabase(String path);
    2)继承SqliteOpenHelper类对数据库及其版本进行管理(onCreate,onUpgrade)
当在程序当中调用这个类的方法getWritableDatabase()或者getReadableDatabase();的时候才会打开数据库。如果当时没有数据库文件的时候,系统就会自动生成一个数据库。
2)操作的类型:增删改查CRUD
直接操作SQL语句:SQliteDatabase.execSQL(sql);
面向对象的操作方式:SQLiteDatabase.insert(table, nullColumnHack, ContentValues);
3)如何导入外部数据库?
getDataBasePath和openOrCreateDatabase

typeArrayed.recycle

public void recycle() {
if (mRecycled) {
throw new RuntimeException(toString() + " recycled twice!");
}

mRecycled = true;

// These may have been set by the client.
/**
 * 描述:
 *  private final Resources mResources;
 *  private DisplayMetrics mMetrics;
 *  private AssetManager mAssets;
 */
mXml = null;
mTheme = null;
mAssets = null;

mResources.mTypedArrayPool.release(this);

}

SparseArray原理

https://juejin.cn/post/6844903953830789127

优点:
    节约自动装箱开销
    节约了Map.Entry<K, V>作为辅助的存储结构引入的内存开销。
缺点:
    二分查找,查找效率低
    不支持缩容
存/取
    HashMap:
        put(K key, V value)和get(Object key)
    SpraseArray:
        put(int key, E value)和get(int key)
遍历
    SparseArray执行put的时候其实是按照key的大小有序插入的。
    SparseArray维护了各个键值对的排序关系,具体的规则是以key升序排列。
    所以不同于HashMap只能通过key查找value,Sparse还能通过index查找value(或者key),方法是valueAt(int index)(或者keyAt(int index))。
    
    举例:
    put了key为100和200的两个键值对,size为2
    200-"firstValue"这对key-value对在index 0的位置
    100-"secondValue"这对键值对在index 1的位置。
    顺序是根据key的大小排的,跟put的先后顺序无关。

SparseArray 实现细节

和hashmap比较
hashmap使用key的hashcode来决定entry的存放位置,解决hash冲突使用的开散列方法。
所以hashmap的底层数据结构看起来是一个链表的数组,链表的节点是包含了key和value的Entry类。

在这里插入图片描述

SparseArray的底层数据结构只有int[] mKeys和Object[] mValues两个数组。
    
SpareArray里key和value分别存放在两个数组里,那key和value是怎么对应上的?
是根据index对应的,mKeys和mValues中index一样的key和value就是相互对应的。

在这里插入图片描述

存/取
删/gc
SparseArray删除数据是通过delete(int key)方法删除的。
在删除一条数据的时候,不是直接执行实际的删除操作,而是先把要删除的数据标记为DELETE状态。
在需要获取size、扩容、取数据的时候,会执行gc,一次性真正把前面标记的所有删除数据删掉。
扩容/缩容

###总结

建议使用SparseArray替换HashMap是因为得益于下面几点,SparseArray可能比HashMap更节省内存,而某些android设备上内存是紧缺资源:

    避免了Integer的自动装箱
    基于index建立的key和value的映射关系相比Map基于Entry结构建立key和value的映射关系节约内存
    某些场景如hash冲突下访问速度可能优于hashmap;不适合数据集比较大的场景。


SparseArray没有缩容机制。某些场景下不适合使用,比如:大量地put后大量地delete,然后长久持有SparseArray,导致大量的空位置没法被虚拟机gc,浪费内存
SparseArray一般来说比Hashmap慢,因为二分查找比较慢,而且插入删除数据涉及数组的copy。在数据集不大时不明显
SparseArray每次插入删除数据都保证了所有存储数据的排列有序。
SparseArray可以通过index定位数据,Hashmap不行。

context.getCacheDir context.getExternalFilesDir Environment.getExternalStorageDirectory() 区别

https://blog.csdn.net/u010015108/article/details/74179182

方法返回值
getCacheDir()/data/data/pkg/cache
getFilesDir()/data/data/pkg/files
getExternalCacheDir/storage/emulated/0/Android/data/pkg/cache
getExternalFilesDir/storage/emulated/0/Android/data/pkg/file
Environment.getExternalStorageDirectory()/storage/emulated/0

第三方库

okHttp源码

https://www.jianshu.com/p/e3a182bbe7f7

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amarao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值