Android 基础
Activity
Activity 启动模式
- standard : 标准启动模式,每次启动Activity 都会重新创建一个Actvity
- singleTop : 栈顶复用,当启动的Activity正在栈顶就会直接复用当前Activity,然后Intent传递的数据可以通过onNewIntent方法接收,如果不是栈顶就会重新创建Activity
- singleTask :栈内复用,当启动的Activty上方有Acitity时,会先让上方Activity出栈,然后在复用当前启动Activity
- singleInstace:单例模式,创建一个新的任务栈,当前活动实例独处在这个活动栈中
Activity 中的onStart和onResume的区别,onPuse和onStop的区别
首先Activity可以分成三类:
- 前台Activity:活跃的Activity ,可以跟用户交互的Activity
- 可见但非前台Activity:常见于栈顶背景透明的Acitivty,处于其下方的Activity就是可见Activity但是不可以和用户交互
- 后台Activity:以及背暂停的Activity,比如已经执行Stop的Activity
所以onStart和onStop通常指的是当前活动是否位于前台这个角度,而onResume 和onPause从是否可见的角度来讲的
如何保持Activity 的状态
Activity的状态通常情况下系统会自动保存,只有当我们需要保存额外的数据才需要使用到这样的功能
- 一般来说,调用onPause和onStop方法后的Activity的实例还存在内存中,Activity所有的信息和状态数据是不回消失的,当Activity重新回到前台的之后,所有改变的都会得到保留
- 但是系统内存不足的时,调用onPuse和onStop有可能会被系统回收销毁,此时内存中就不回存在Activity的实例对象了,如果之后Activity回到前台,之前所有的改变就回消失,为了避免这样的情况,我们可一复写onSaveInstanceState()方法。onSaceInstanceState()方法回接受一个Bundle类型的参数,开发者可以将数据存储到这个Bundle对象中,这样即使Activity被销毁,当用户重新启动这个Activity而调用他的onCreate方法时,上述的Bundle对象会作为实参传递给onCreate方法,开发者可以重这个bundle对象中取出数据,然后利用这这些数据将Activty恢复当摧毁前的状态
- 需要注意的是onSaveInstaceState()方法不一定会被调用的,因为有些场景是不需要保存这些状态数据的,比如用户按下BACK键退出Activity时,用户显然是想关闭这个Activity此时是有没有必要保存数据以供恢复的,也就就是onSacveInstanceState()方法不会被调用,如果调用onSaveInstanceState()方法将发生在onPause和onStop之前
横竖切换时Activity的生命周期
此时的生命周期跟清单文件配置有关系
- 不设置Activity的android:configChanges时,切屏就会重新调用各个生命周期默认首先销毁当前的Activity在重新加载
- 设置Activity的adnroid:configChanges=”orientation|keyboardHidden|sereenSize”时切屏不会重新调用各个生命周期,只会执行onConfigChanged方法
两个Activity 之间跳转时必然会执行哪几个方法
比如有两个Activity分别是A和B
通常当A启动B时,A会调用onPuse()方法然后B调用onCreate()、onStart()、onResume()在A调用onStop方法
但是如果B是个透明的或者是对话框的样式,就不掉调用A的onStop方法
如何设置一个Activity为窗口样式
设置android:them=”@adnroid:style/Theme.Dialog”
Activity、Window、View 三者之间的关系
- Activity:Android 四大组件之一,负责界面展示,用户交互与业务逻辑处理
- Window: 是负责界面展示以及交互的职能部门,就相当于Activity的下属,Activity的生命周期负责业务处理
- View:是放在Window容器的元素、Window是View的载体、View是Window具体的展示
Activity 启动流程
- 点击桌面APP图标,Launcher进程采用Binder IPC向sysetm_server进程发起startActivity请求
- system_server进程接收到请求后,通过Socket想zygote进程发送创建进程请求
- zygote进程for出新的子进程,即APP进程
- APP进程通过Binder IPC向system_server进程发生attchApplication请求
- system_server进程接收到请求后,进行一系列的准备工做后,再通过binder IPC向APP发生ScheduleLaunchActivity请求
- APP进程 的binder线程在接收到请求后,通过Handelr向主席线程发送LAUNCH_ACTIVITY消息
- 主线程在接收到Message消息后,通过反射机制创建目标Acitivity,并且调用Acivity.onCreate()等方法
Fragment
如何切换 Fragement(不重新实例化)
正确切换方式add(),切换是先调用hide()然后调用add()添加一个Fragment,再次切换时、只需要hide当前,在调用show()另外一个,这个样就可以做到多个Framgnet切换而不重新创建事例
Fragment的优点
- Fragment 可以使你能够将 activity 分离成多个可重用的组件,每个都有它自己的生命周期和UI
- Fragment 可以轻松得创建动态灵活的 UI 设计,可以适应于不同的屏幕尺寸。从手机到平板电脑
- Fragment 是一个独立的模块,紧紧地与 activity 绑定在一起。可以运行中动态地移除、加入、交换等
- Fragment 提供一个新的方式让你在不同的安卓设备上统一你的 UI
- Fragment 解决 Activity 间的切换不流畅,轻量切换
- Fragment 替代 TabActivity 做导航,性能更好
Fragment的replace和add方法的区别
- add不会重新初始化fragment,replace每次都会。所以如果在fragment生命周期内获取获取数据,使用replace会重复获取;
- 添加相同的fragment时,replace不会有任何变化,add会报IllegalStateException异常;
- replace先remove掉相同id的所有fragment,然后在add当前的这个fragment,而add是覆盖前一个fragment。所以如果使用add一般会伴随hide()和show(),避免布局重叠;
- 使用add,如果应用放在后台,或以其他方式被系统销毁,再打开时,hide()中引用的fragment会销毁,所以依然会出现布局重叠bug,可以使用replace或使用add时,判断是否已经添加过。
Fragment 与Activity 之间是如何传值的
- Activity向Fragment传值
将要传的值,放到bundle对象里;在Activity中创建该Fragment的对象framgent,通过调用fragment.setArgunments()传递到fragment中
在该Fragment中通过调用getArguments()得到hundle对象,就能得到里面的值
- Fragment向Activity传值
在Activity中调用getFragmentManager()得到fragmentManager,调用findFragmentByTag()或者通过findFragmentById()获取Fragment对象
通过接口回调的方式
FragmentPageAdapter 与FragmentStatePageAdapter 区别
- FragmentStatePageAdapter
FragmentStatePageAdapter会销毁不需要的Fragment,一般来说,ViewHolder会保存正在显示的Fragment和它左右两边的Fragment,分别是A、B、C、那么当显示的Fragment变成C时,保存的Fragment就会变成B、C、D了,而A此时就会呗笑回,但是需要注意的是,此时A销毁的时候,回通过onSaveInstanceState方法来保存Fragemnt中的Bundle信息,当再次切换回了的时候,就可以利用保存的信息来恢复到原来的状态
- FragmentPageAdapter
FragmentPageAdapter会调用事物的Detach方法处理,而不是使用remove方法。因此FragmentPageAdapter只是销毁Fragment的视图,实例还是保存在FragmentManager中
getFragmentManager、getSupportFragmentManager 、getChildFragmentManager之间的区别?
Activity: 是通过getFragmentManager 获取FragmentManager的
FragmnetActivity:是通过getSupterFragmentManager 获取FragmentManmger
Fragment : 是通过getChildFragmentManager 获取FragmentManager
Service
Activity 怎么和 Service 绑定,怎么在 Activity 中启动自己对应的Service?
Activity 通过 bindService(Intent service, ServiceConnection conn, int flags)跟 Service 进行绑定,当绑定成功的时候 Service 会将代理对象通过回调的形式传给 conn,这样我们就拿到了Service 提供的服务代理对象
在 Activity 中可以通过 startService 和 bindService 方法启动 Service。一般情况下如果想获取Service 的服务对象那么肯定需要通过 bindService()方法,比如音乐播放器,第三方支付等。如果仅仅只是为了开启一个后台任务那么可以使用 startService()方法
描述一下 Service 的生命周期
Service 有绑定模式和非绑定模式,以及这两种模式的混合使用方式。不同的使用方法生命周期方法也不同
- 非绑定模式:当第一次调用 startService 的时候执行的方法依次为 onCreate() 、onStartCommand(),当Service 关闭的时候调用 onDestory 方法
- 绑定模式:第一次 bindService()的时候,执行的方法为 onCreate()、onBind()解除绑定的时候会执行onUnbind()、onDestory()
上面的两种生命周期是在相对单纯的模式下的情形。我们在开发的过程中还必须注意 Service 实例只会有一个,也就是说如果当前要启动的 Service 已经存在了那么就不会再次创建该 Service 当然也不会调用 onCreate()方法
一个 Service 可以被多个客户进行绑定,只有所有的绑定对象都执行了 onBind()方法后该Service 才会销毁,不过如果有一个客户执行了 onStart()方法,那么这个时候如果所有的 bind 客户都执行了 unBind()该 Service 也不会销毁
Activity、Intent、Service 是什么关系
他们都是 Android 开发中使用频率最高的类。其中 Activity 和 Service 都是 Android 四大组件之一。他俩都是 Context 类的子类ContextWrapper 的子类, 因此他俩可以算是兄弟关系
- Activity 负责用户界面的显示和交互
- Service 负责后台任务的处理
Activity 和 Service 之间可 以通过 Intent 传递数据,因此可以把 Intent 看作是通信使者
Service 是否在 main thread 中执行, service 里面是否能执行耗时的操作?
默认情况,如果没有显示的指 servic 所运行的进程, Service 和 activity 是运行在当前 app 所在进程的 main thread(UI 主线程)里面。service 里面不能执行耗时的操作(网络请求,拷贝数据库,大文件)
Service 和 Activity 在同一个线程吗
对于同一 app 来说默认情况下是在同一个线程中的,main Thread (UI Thread)
Service 里面可以弹吐司么?
可以的。弹吐司有个条件就是得有一个 Context 上下文,而 Service 本身就是 Context 的子类,因此在 Service 里面弹吐司是完全可以的。比如我们在 Service 中完成下载任务后可以弹一个吐司通知用户
在 service 的生命周期方法 onstartConmand()可不可以执行网络操作?
可以直接在 Service 中执行网络操作,在 onStartCommand()方法中可以执行网络操作
什么是 IntentService?有何优点?
IntentService 是 Service 的子类,比普通的 Service 增加了额外的功能
先看 Service 本身存在两个问题:
- Service 不会专门启动一条单独的进程,Service 与它所在应用位于同一个进程中
- Service 也不是专门一条新线程,因此不应该在 Service 中直接处理耗时的任务
IntentService 特征
- 会创建独立的 worker 线程来处理所有的 Intent 请求
- 会创建独立的 worker 线程来处理 onHandleIntent()方法实现的代码,无需处理多线程问题
- 所有请求处理完成后,IntentService 会自动停止,无需调用 stopSelf()方法停止 Service
- 为Service 的 onBind()提供默认实现,返回 null
- 为Service 的 onStartCommand 提供默认实现,将请求 Intent 添加到队列中
Service 与 Thread 的区别
服务仅仅是一个组件,即使用户不再与你的应用程序发生交互,它仍然能在后台运行。因此,应该只在需要时才创建一个服务
如果你需要在主线程之外执行一些工作,但仅当用户与你的应用程序交互时才会用到,那你应该创建一个新的线程而不是创建服务。 比如,如果你需要播放一些音乐,但只是当你的activity在运行时才需要播放,你可以在onCreate()中创建一个线程,在onStart()中开始运行,然后在onStop()中终止运行。还可以考虑使用AsyncTask或HandlerThread来取代传统的Thread类
由于无法在不同的 Activity 中对同一 Thread 进行控制,这个时候就要考虑用服务实现。如果你使用了服务,它默认就运行于应用程序的主线程中。因此,如果服务执行密集计算或者阻塞操作,你仍然应该在服务中创建一个新的线程来完成(避免ANR)
BroadcastReceiver
BroadcastReceiver 与 LocalBroadcastReceiver 有什么区别
BroadcastReceiver(广播接收者):是跨应用广播,利用Binder机制实现
静态注册:
(1)在清单文件中,通过标签声明;
(2)在Android3.1开始,对于接收系统广播的BroadcastReceiver,App进程退出后,无法接收到广播;对于自定义的广播,可以通过重写flag的值,使得即使App进程退出,仍然可以接收到广播。
(3)静态注册的广播是由PackageManagerService负责。
动态注册:1.在代码中注册,程序运行的时候才能进行;2.跟随组件的生命周期;3.动态注册的广播是由AMS(ActivityManagerService)负责的。
注意:对于动态注册,最好在Activity的onResume()中注册,在onPause()中注销。在系统内存不足时,onStop()、onDestory()可能不会执行App就被销毁,onPause()在App销毁前一定会被执行,保证广播在App销毁前注销。
LocalBroadcastManager实现原理:是应用内广播,利用Handler实现
1.使用了单例模式,并且将外部传入的Context转换成了Application的Context,避免造成内存泄露。
2.在构造方法中创建了Handler,实质是通过Handler进行发送和接受消息的。
3.创建Handler时,传入了主线程的Looper,说明这个Handler是在主线程创建的,即广播接收者是在主线程接收消息的,所以不能在onReceiver()中做耗时操作。
注意:对于LocalBroadcastManager发送的广播,只能通过LocalBroadcastManager动态注册,不能静态注册。
特别注意:
1.如果BroadcastReceiver在onReceiver()方法中在10秒内没有执行完成,会造成ANR异常。
2.对于不同注册方式的广播接收者回调方法onReceive()返回的Context是不一样的。
静态注册:context为ReceiverRestrictedContext。
动态注册:context为Activity的Context。
LocalBroadcastManager的动态注册:context为Application的Context。
Android消息机制
Handler被设计出来的原因?有什么用?
一种东西被设计出来肯定就有它存在的意义,而Handler的意义就是切换线程
作为Android消息机制的主要成员,它管理着所有与界面有关的消息事件,常见的使用场景有:
- 跨进程之后的界面消息处理
比如Activity的启动,就是AMS在进行进程间通信的时候,通过Binder线程 将消息发送给ApplicationThread的消息处理者Handler,然后再将消息分发给主线程中去执行
- 网络交互后切换到主线程进行UI更新
当子线程网络操作之后,需要切换到主线程进行UI更新
总之一句话,Hanlder的存在就是为了解决在子线程中无法访问UI的问题
为什么建议子线程不访问(更新)UI?
因为Android中的UI控件不是线程安全的,如果多线程访问UI控件那还不乱套了
那为什么不加锁呢?
会降低UI访问的效率。本身UI控件就是离用户比较近的一个组件,加锁之后自然会发生阻塞,那么UI访问的效率会降低,最终反应到用户端就是这个手机有点卡太复杂了。本身UI访问时一个比较简单的操作逻辑,直接创建UI,修改UI即可。如果加锁之后就让这个UI访问的逻辑变得很复杂,没必要
所以,Android设计出了 单线程模型 来处理UI操作,再搭配上Handler,是一个比较合适的解决方案
子线程访问UI的 崩溃原因 和 解决办法?
崩溃发生在ViewRootImpl类的checkThread方法中:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");
}
}
复制代码
其实就是判断了当前线程 是否是 ViewRootImpl创建时候的线程,如果不是,就会崩溃
而ViewRootImpl创建的时机就是界面被绘制的时候,也就是onResume之后,所以如果在子线程进行UI更新,就会发现当前线程(子线程)和View创建的线程(主线程)不是同一个线程,发生崩溃
解决办法有三种:
- 在新建视图的线程进行这个视图的UI更新,主线程创建View,主线程更新View
- 在
ViewRootImpl创建之前进行子线程的UI更新,比如onCreate方法中进行子线程更新UI - 子线程切换到主线程进行UI更新,比如
Handler、view.post方法
MessageQueue是干嘛呢?用的什么数据结构来存储数据?
看名字应该是个队列结构,队列的特点是什么?先进先出,一般在队尾增加数据,在队首进行取数据或者删除数据。
那Hanlder中的消息似乎也满足这样的特点,先发的消息肯定就会先被处理。但是,Handler中还有比较特殊的情况,比如延时消息。
延时消息的存在就让这个队列有些特殊性了,并不能完全保证先进先出,而是需要根据时间来判断,所以Android中采用了链表的形式来实现这个队列,也方便了数据的插入。
来一起看看消息的发送过程,无论是哪种方法发送消息,都会走到sendMessageDelayed方法
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
return enqueueMessage(queue, msg, uptimeMillis);
}
复制代码
sendMessageDelayed方法主要计算了消息需要被处理的时间,如果delayMillis为0,那么消息的处理时间就是当前时间。
然后就是关键方法enqueueMessage。
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
复制代码
不懂得地方先不看,只看我们想看的:
- 首先设置了
Message的when字段,也就是代表了这个消息的处理时间 - 然后判断当前队列是不是为空,是不是即时消息,是不是执行时间when大于表头的消息时间,满足任意一个,就把当前消息msg插入到表头。
- 否则,就需要遍历这个队列,也就是
链表,找出when小于某个节点的when,找到后插入。
好了,其他内容暂且不看,总之,插入消息就是通过消息的执行时间,也就是when字段,来找到合适的位置插入链表。
具体方法就是通过死循环,使用快慢指针p和prev,每次向后移动一格,直到找到某个节点p的when大于我们要插入消息的when字段,则插入到p和prev之间。或者遍历到链表结束,插入到链表结尾。
所以,MessageQueue就是一个用于存储消息、用链表实现的特殊队列结构。
延迟消息是怎么实现的?
总结上述内容,延迟消息的实现主要跟消息的统一存储方法有关,也就是上文说过的enqueueMessage方法
无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的when字段进程赋值
然后在MessageQueue中找到合适的位置(安排when小到大排列),并将消息插入到MessageQueue中
这样,MessageQueue就是一个按照消息时间排列的一个链表结构
MessageQueue的消息怎么被取出来的?
刚才说过了消息的存储,接下来看看消息的取出,也就是queue.next方法
Message next() {
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
}
}
}
复制代码
奇怪,为什么取消息也是用的死循环呢?
其实死循环就是为了保证一定要返回一条消息,如果没有可用消息,那么就阻塞在这里,一直到有新消息的到来
其中,nativePollOnce方法就是阻塞方法,nextPollTimeoutMillis参数就是阻塞的时间
那什么时候会阻塞呢?两种情况:
- 1、有消息,但是当前时间小于消息执行时间,也就是代码中的这一句:
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}
复制代码
这时候阻塞时间就是消息时间减去当前时间,然后进入下一次循环,阻塞
- 2、没有消息的时候,也就是上述代码的最后一句:
if (msg != null) {}
else {
// No more messages.
nextPollTimeoutMillis = -1;
}
复制代码
-1就代表一直阻塞
MessageQueue没有消息时候会怎样?阻塞之后怎么唤醒呢?说说pipe/epoll机制?
接着上文的逻辑,当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制
epoll机制是一种IO多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒
- 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过
epoll机制进入阻塞状态 - 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程
同步屏障和异步消息是怎么实现的?
其实在Handler机制中,有三种消息类型:
同步消息:也就是普通的消息。异步消息:通过setAsynchronous(true)设置的消息。同步屏障消息:通过postSyncBarrier方法添加的消息,特点是target为空,也就是没有对应的handler。
这三者之间的关系如何呢?
- 正常情况下,同步消息和异步消息都是正常被处理,也就是根据时间when来取消息,处理消息。
- 当遇到同步屏障消息的时候,就开始从消息队列里面去找异步消息,找到了再根据时间决定阻塞还是返回消息。
也就是说同步屏障消息不会被返回,他只是一个标志,一个工具,遇到它就代表要去先行处理异步消息了。
所以同步屏障和异步消息的存在的意义就在于有些消息需要“加急处理”
同步屏障和异步消息有具体的使用场景吗?
使用场景就很多了,比如绘制方法scheduleTraversals。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 同步屏障,阻塞所有的同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 通过 Choreographer 发送绘制任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
复制代码
在该方法中加入了同步屏障,后续加入一个异步消息MSG_DO_SCHEDULE_CALLBACK,最后会执行到FrameDisplayEventReceiver,用于申请VSYNC信号
Message消息被分发之后会怎么处理?消息怎么复用的?
再看看loop方法,在消息被分发之后,也就是执行了dispatchMessage方法之后,还偷偷做了一个操作——recycleUnchecked
public static void loop() {
for (;;) {
Message msg = queue.next(); // might block
try {
msg.target.dispatchMessage(msg);
}
msg.recycleUnchecked();
}
}
//Message.java
private static Message sPool;
private static final int MAX_POOL_SIZE = 50;
void recycleUnchecked() {
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
复制代码
在recycleUnchecked方法中,释放了所有资源,然后将当前的空消息插入到sPool表头
这里的sPool就是一个消息对象池,它也是一个链表结构的消息,最大长度为50
那么Message又是怎么复用的呢?在Message的实例化方法obtain中:
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
复制代码
直接复用消息池sPool中的第一条消息,然后sPool指向下一个节点,消息池数量减一
Looper是干嘛呢?怎么获取当前线程的Looper?为什么不直接用Map存储线程和对象呢
在Handler发送消息之后,消息就被存储到MessageQueue中,而Looper就是一个管理消息队列的角色。Looper会从MessageQueue中不断的查找消息,也就是loop方法,并将消息交回给Handler进行处理
而Looper的获取就是通过ThreadLocal机制:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
复制代码
通过prepare方法创建Looper并且加入到sThreadLocal中,通过myLooper方法从sThreadLocal中获取Looper
ThreadLocal运行机制?这种机制设计的好处?
下面就具体说说ThreadLocal运行机制。
//ThreadLocal.java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
复制代码
从ThreadLocal类中的get和set方法可以大致看出来,有一个ThreadLocalMap变量,这个变量存储着键值对形式的数据。
key为this,也就是当前ThreadLocal变量。value为T,也就是要存储的值。
然后继续看看ThreadLocalMap哪来的,也就是getMap方法:
//ThreadLocal.java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码
原来这个ThreadLocalMap变量是存储在线程类Thread中的
所以ThreadLocal的基本机制就搞清楚了:
在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象
这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样
挺神奇的是不是,其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样
这样做有什么好处呢?为什么不直接用Map存储线程和对象呢?
打个比方:
ThreadLocal就是老师Thread就是同学Looper(需要的值)就是铅笔
现在老师买了一批铅笔,然后想把这些铅笔发给同学们,怎么发呢?两种办法:
- 1、老师把每个铅笔上写好每个同学的名字,放到一个大盒子里面去(map),用的时候就让同学们自己来找。
这种做法就是Map里面存储的是同学和铅笔,然后用的时候通过同学来从这个Map里找铅笔
这种做法就有点像使用一个Map,存储所有的线程和对象,不好的地方就在于会很混乱,每个线程之间有了联系,也容易造成内存泄漏
- 2、老师把每个铅笔直接发给每个同学,放到同学的口袋里(map),用的时候每个同学从口袋里面拿出铅笔就可以了
这种做法就是Map里面存储的是老师和铅笔,然后用的时候老师说一声,同学只需要从口袋里拿出来就行了
很明显这种做法更科学,这也就是ThreadLocal的做法,因为铅笔本身就是同学自己在用,所以一开始就把铅笔交给同学自己保管是最好的,每个同学之间进行隔离
可以多次创建Looper吗?
Looper的创建是通过Looper.prepare方法实现的,而在prepare方法中就判断了,当前线程是否存在Looper对象,如果有,就会直接抛出异常:
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
复制代码
所以同一个线程,只能创建一个Looper,多次创建会报错
Looper中的quitAllowed字段是啥?有什么用?
按照字面意思就是是否允许退出,我们看看他都在哪些地方用到了:
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
}
}
复制代码
哦,就是这个quit方法用到了,如果这个字段为false,代表不允许退出,就会报错
但是这个quit方法又是干嘛的呢?从来没用过呢。还有这个safe又是啥呢?
其实看名字就差不多能了解了,quit方法就是退出消息队列,终止消息循环
- 首先设置了
mQuitting字段为true - 然后判断是否安全退出,如果安全退出,就执行
removeAllFutureMessagesLocked方法,它内部的逻辑是清空所有的延迟消息,之前没处理的非延迟消息还是需要取处理,然后设置非延迟消息的下一个节点为空(p.next=null) - 如果不是安全退出,就执行
removeAllMessagesLocked方法,直接清空所有的消息,然后设置消息队列指向空(mMessages = null)
然后看看当调用quit方法之后,消息的发送和处理:
//消息发送
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
}
复制代码
当调用了quit方法之后,mQuitting为true,消息就发不出去了,会报错
再看看消息的处理,loop和next方法:
Message next() {
for (;;) {
synchronized (this) {
if (mQuitting) {
dispose();
return null;
}
}
}
}
public static void loop() {
for (;;) {
Message msg = queue.next();
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
}
}
复制代码
很明显,当mQuitting为true的时候,next方法返回null,那么loop方法中就会退出死循环
那么这个quit方法一般是什么时候使用呢?
- 主线程中,一般情况下肯定不能退出,因为退出后主线程就停止了。所以是当APP需要退出的时候,就会调用quit方法,涉及到的消息是EXIT_APPLICATION,大家可以搜索下。
- 子线程中,如果消息都处理完了,就需要调用quit方法停止消息循环
Looper.loop方法是死循环,为什么不会卡死(ANR)?
- 1、主线程本身就是需要一只运行的,因为要处理各个View,界面变化。所以需要这个死循环来保证主线程一直执行下去,不会被退出。
- 2、真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身
- 3、在主线程以外,会有其他的线程来处理接受其他进程的事件,比如
Binder线程(ApplicationThread),会接受AMS发送来的事件 - 4、在收到跨进程消息后,会交给主线程的
Hanlder再进行消息分发。所以Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到onCreate方法 - 5、当没有消息的时候,会阻塞在loop的
queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗CPU资源
Message是怎么找到它所属的Handler然后进行分发的?
在loop方法中,找到要处理的Message,然后调用了这么一句代码处理消息:
msg.target.dispatchMessage(msg);
复制代码
所以是将消息交给了msg.target来处理,那么这个target是啥呢?
找找它的来头:
//Handler
private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {
msg.target = this;
return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码
在使用Hanlder发送消息的时候,会设置msg.target = this,所以target就是当初把消息加到消息队列的那个Handler
Handler 的 post(Runnable) 与 sendMessage 有什么区别
Hanlder中主要的发送消息可以分为两种:
- post(Runnable)
- sendMessage
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
复制代码
通过post的源码可知,其实post和sendMessage的区别就在于:
post方法给Message设置了一个callback。
那么这个callback有什么用呢?我们再转到消息处理的方法dispatchMessage中看看:
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
复制代码
这段代码可以分为三部分看:
- 1、如果
msg.callback不为空,也就是通过post方法发送消息的时候,会把消息交给这个msg.callback进行处理,然后就没有后续了 - 2、如果
msg.callback为空,也就是通过sendMessage发送消息的时候,会判断Handler当前的mCallback是否为空,如果不为空就交给Handler.Callback.handleMessage处理 - 3、如果
mCallback.handleMessage返回true,则无后续了 - 4、如果
mCallback.handleMessage返回false,则调用handler类重写的handleMessage方法
所以post(Runnable) 与 sendMessage的区别就在于后续消息的处理方式,是交给msg.callback还是 Handler.Callback或者Handler.handleMessage
Handler.Callback.handleMessage 和 Handler.handleMessage 有什么不一样?为什么这么设计?
接着上面的代码说,这两个处理方法的区别在于Handler.Callback.handleMessage方法是否返回true:
- 如果为
true,则不再执行Handler.handleMessage - 如果为
false,则两个方法都要执行
那么什么时候有Callback,什么时候没有呢?这涉及到两种Hanlder的 创建方式:
val handler1= object : Handler(){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
}
}
val handler2 = Handler(object : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
return true
}
})
复制代码
常用的方法就是第1种,派生一个Handler的子类并重写handleMessage方法。而第2种就是系统给我们提供了一种不需要派生子类的使用方法,只需要传入一个Callback即可
Handler、Looper、MessageQueue、线程是一一对应关系吗?
- 一个线程只会有一个
Looper对象,所以线程和Looper是一一对应的。 MessageQueue对象是在new Looper的时候创建的,所以Looper和MessageQueue是一一对应的。Handler的作用只是将消息加到MessageQueue中,并后续取出消息后,根据消息的target字段分发给当初的那个handler,所以Handler对于Looper是可以多对一的,也就是多个Hanlder对象都可以用同一个线程、同一个Looper、同一个MessageQueue。
总结:Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的
ActivityThread中做了哪些关于Handler的工作?(为什么主线程不需要单独创建Looper)
主要做了两件事:
- 1、在main方法中,创建了主线程的
Looper和MessageQueue,并且调用loop方法开启了主线程的消息循环。
public static void main(String[] args) {
Looper.prepareMainLooper();
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
复制代码
- 2、创建了一个Handler来进行四大组件的启动停止等事件处理
final H mH = new H();
class H extends Handler {
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
public static final int RECEIVER = 113;
public static final int CREATE_SERVICE = 114;
public static final int STOP_SERVICE = 116;
public static final int BIND_SERVICE = 121;
复制代码
IdleHandler是啥?有什么使用场景?
之前说过,当MessageQueue没有消息的时候,就会阻塞在next方法中,其实在阻塞之前,MessageQueue还会做一件事,就是检查是否存在IdleHandler,如果有,就会去执行它的queueIdle方法。
private IdleHandler[] mPendingIdleHandlers;
Message next() {
int pendingIdleHandlerCount = -1;
for (;;) {
synchronized (this) {
//当消息执行完毕,就设置pendingIdleHandlerCount
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
//初始化mPendingIdleHandlers
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
//mIdleHandlers转为数组
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 遍历数组,处理每个IdleHandler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
//如果queueIdle方法返回false,则处理完就删除这个IdleHandler
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
}
}
复制代码
当没有消息处理的时候,就会去处理这个mIdleHandlers集合里面的每个IdleHandler对象,并调用其queueIdle方法。最后根据queueIdle返回值判断是否用完删除当前的IdleHandler。
然后看看IdleHandler是怎么加进去的:
Looper.myQueue().addIdleHandler(new IdleHandler() {
@Override
public boolean queueIdle() {
//做事情
return false;
}
});
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
复制代码
ok,综上所述,IdleHandler就是当消息队列里面没有当前要处理的消息了,需要堵塞之前,可以做一些空闲任务的处理。
常见的使用场景有:启动优化
我们一般会把一些事件(比如界面view的绘制、赋值)放到onCreate方法或者onResume方法中。但是这两个方法其实都是在界面绘制之前调用的,也就是说一定程度上这两个方法的耗时会影响到启动时间
所以我们可以把一些操作放到IdleHandler中,也就是界面绘制完成之后才去调用,这样就能减少启动时间了。
但是,这里需要注意下可能会有坑
如果使用不当,IdleHandler会一直不执行,比如在View的onDraw方法里面无限制的直接或者间接调用View的invalidate方法
其原因就在于onDraw方法中执行invalidate,会添加一个同步屏障消息,在等到异步消息之前,会阻塞在next方法,而等到FrameDisplayEventReceiver异步任务之后又会执行onDraw方法,从而无限循环
HandlerThread是啥?有什么使用场景?
直接看源码:
public class HandlerThread extends Thread {
@Override
public void run() {
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
}
复制代码
哦,原来如此。HandlerThread就是一个封装了Looper的Thread类
就是为了让我们在子线程里面更方便的使用Handler
这里的加锁就是为了保证线程安全,获取当前线程的Looper对象,获取成功之后再通过notifyAll方法唤醒其他线程,那哪里调用了wait方法呢?
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
复制代码
就是getLooper方法,所以wait的意思就是等待Looper创建好,那边创建好之后再通知这边正确返回Looper
IntentService是啥?有什么使用场景?
老规矩,直接看源码:
public abstract class IntentService extends Service {
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
复制代码
理一下这个源码:
- 首先,这是一个
Service - 并且内部维护了一个
HandlerThread,也就是有完整的Looper在运行 - 还维护了一个子线程的
ServiceHandler - 启动Service后,会通过Handler执行
onHandleIntent方法 - 完成任务后,会自动执行
stopSelf停止当前Service
所以,这就是一个可以在子线程进行耗时任务,并且在任务执行后自动停止的Service
Handler内存泄露的原因是什么?
Handler导致内存泄漏一般发生在发送延迟消息的时候,当Activity关闭之后,延迟消息还没发出,那么主线程中的MessageQueue就会持有这个消息的引用,而这个消息是持有Handler的引用,而handler作为匿名内部类持有了Activity的引用,所以就有了以下的一条引用链
主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity
其根本原因是因为这条引用链的头头,也就是主线程,是不会被回收的,所以导致Activity无法被回收,出现内存泄漏,其中Handler只能算是导火索
而我们平时用到的子线程通过Handler更新UI,其原因是因为运行中的子线程不会被回收,而子线程持有了Actiivty的引用(不然也无法调用Activity的Handler),所以就导致内存泄漏了,但是这个情况的主要原因还是在于子线程本身
所以综合两种情况,在发生内存泄漏的情况中,Handler都不能算是罪魁祸首,罪魁祸首(根本原因)都是他们的头头——线程
View绘制
View绘制流程
View的绘制流程是从ViewRoot的performTraversals开始的,它经过measure,layout,draw三个过程最终将View绘制出来。performTraversals会依次调用performMeasure,performLayout,performDraw三个方法,他们会依次调用measure,layout,draw方法,然后又调用了onMeasure,onLayout,dispatchDraw。
- measure :
对于自定义的单一view的测量,只需要根据父 view 传递的MeasureSpec进行计算大小。
对于ViewGroup的测量,一般要重写onMeasure方法,在onMeasure方法中,父容器会对所有的子View进行Measure,子元素又会作为父容器,重复对它自己的子元素进行Measure,这样Measure过程就从DecorView一级一级传递下去了,也就是要遍历所有子View的的尺寸,最终得出出总的viewGroup的尺寸。Layout和Draw方法也是如此。
- layout :根据
measure子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
对于自定义的单一view,计算本身的位置即可。
对于ViewGroup来说,需要重写onlayout方法。除了计算自己View的位置,还需要确定每一个子View在父容器的位置以及子view的宽高(getMeasuredWidth和getMeasuredHeight),最后调用所有子view的layout方法来设定子view的位置。
- draw :把 View 对象绘制到屏幕上。
draw()会依次调用四个方法:
1)drawBackground(),根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界。2)onDraw(),绘制View本身的内容,一般自定义单一view会重写这个方法,实现一些绘制逻辑。3) dispatchDraw(),绘制子View 4)onDrawScrollBars(canvas),绘制装饰,如 滚动指示器、滚动条、和前景
说说你理解的MeasureSpec
MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求,这个测量要求就是MeasureSpec。
- 首先,
MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size
// 获取测量模式
int specMode = MeasureSpec.getMode(measureSpec)
// 获取测量大小
int specSize = MeasureSpec.getSize(measureSpec)
// 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
复制代码
- 其次,每个子View的
MeasureSpec值根据子View的布局参数和父容器的MeasureSpec值计算得来的,所以就有一个父布局测量模式,子视图布局参数,以及子view本身的MeasureSpec关系图:

其实也就是getChildMeasureSpec方法的源码逻辑,会根据子View的布局参数和父容器的MeasureSpec计算出来单个子view的MeasureSpec。
- 最后是实际应用时:
对于自定义的单一view,一般可以不处理onMeasure方法,如果要对宽高进行自定义,就重写onMeasure方法,并将算好的宽高通过setMeasuredDimension方法传进去。对于自定义的ViewGroup,一般需要重写onMeasure方法,并且调用measureChildren方法遍历所有子View并进行测量(measureChild方法是测量具体某一个view的宽高),然后可以通过getMeasuredWidth/getMeasuredHeight获取宽高,最后通过setMeasuredDimension方法存储本身的总宽高
View的三种测量模式
MeasureSpec.EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值(例如layout_width=”100dp”)、match_parent时,父容器会将其设置为EXACTLY;
MeasureSpec.AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,父容器会将其设置为AT_MOST;
MeasureSpec.UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见
Scroller是怎么实现View的弹性滑动?
- 在
MotionEvent.ACTION_UP事件触发时调用startScroll()方法,该方法并没有进行实际的滑动操作,而是记录滑动相关量(滑动距离、滑动时间) - 接着调用
invalidate/postInvalidate()方法,请求View重绘,导致View.draw方法被执行 - 当View重绘后会在draw方法中调用
computeScroll方法,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,和之前流程一样,如此反复导致View不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过成结束。
mScroller = new Scroller(context);
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// 滚动开始时X的坐标,滚动开始时Y的坐标,横向滚动的距离,纵向滚动的距离
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
// 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
复制代码
Window和DecorView是什么?DecorView又是如何和Window建立联系的
DecorView的作用
- DecorView是顶级View,本质就是一个FrameLayout
- 包含了两个部分,标题栏和内容栏
- 内容栏id是content,也就是activity中setContentView所设置的部分,最终将布局添加到id为content的FrameLayout中
- 获取content:
ViewGroup content = findViewById(R.android.id.content) - 获取设置的View:
content.getChidlAt(0)
Window是什么?
- 表示一个窗口的概念,是所有
View的直接管理者,任何视图都通过Window呈现(单击事件由Window->DecorView->View; Activity的setContentView底层通过Window完成) Window是一个抽象类,具体实现是PhoneWindow- 创建
Window需要通过WindowManager创建 WindowManager是外界访问Window的入口Window具体实现位于WindowManagerService中WindowManager和WindowManagerService的交互是通过IPC完成
DecorView又是如何和Window建立联系的?
- 在Activity的启动流程中,处理onResume()的相关方法中,将DecorView作为Window的成员变量保存到Window内部
- DecorView与Window建立联系又有什么用呢?例如Activity的onSaveInstanceState()进行数据保存时,就通过window内部的DecorView触发整个View树进行状态保存
//ActivityThread.java
final void handleResumeActivity(IBinder token, ...) {
//1. 创建DecorView,设置为不可见INVISIBLE
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//2. 获取到WindowManager, addView方法将DecorView添加到Window中
ViewManager wm = a.getWindowManager();
wm.addView(decor, l);
//3. 将DecorView设置为visible
r.activity.makeVisible();
}
复制代码
LinearLayout, FrameLayout, RelativeLayout 哪个效率高, 为什么
对于比较三者的效率那肯定是要在相同布局条件下比较绘制的流畅度及绘制过程,在这里流畅度不好表达,并且受其他外部因素干扰比较多,比如CPU、GPU等等,我说下在绘制过程中的比较,1、Fragment是从上到下的一个堆叠的方式布局的,那当然是绘制速度最快,只需要将本身绘制出来即可,但是由于它的绘制方式导致在复杂场景中直接是不能使用的,所以工作效率来说Fragment仅使用于单一场景,2、LinearLayout 在两个方向上绘制的布局,在工作中使用页比较多,绘制的时候只需要按照指定的方向绘制,绘制效率比Fragment要慢,但使用场景比较多,3、RelativeLayout 它的没个子控件都是需要相对的其他控件来计算,按照View树的绘制流程、在不同的分支上要进行计算相对应的位置,绘制效率最低,但是一般工作中的布局使用较多,所以说这三者之间效率分开来讲个有优势、不足,那一起来讲也是有优势、不足,所以不能绝对的区分三者的效率,好马用好铵 那需求来说
RelativeLayout 为什么调用两次measure

invalidate()、postInvalidate()、requestLayout() 区别
invalidate方法会执行draw过程,重绘View树。
当改变view的显隐性、背景、状态(focus/enable)等,这些都属于appearance范畴,都会引起invalidate操作。需要更新界面显示,就可以直接调用invalidate方法。
postInvalidate在子线程中被调用,刷新UI,效果跟invalidate一样
requestLayout() 当View的宽高,发生了变化,不再适合现在的区域,调用requestLayout方法重新对View布局。
当View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用
自定义Viewwrap_content不起作用的原因
- 在
onMeasure()中的getDefaultSize()的默认实现中,当View的测量模式是AT_MOST或EXACTLY时,View的大小都会被设置成子View MeasureSpec的specSize。 - 因为AT_MOST对应
wrap_content;EXACTLY对应match_parent,所以,默认情况下,wrap_content和match_parent是具有相同的效果的。 - 因为在计算子View MeasureSpec的
getChildMeasureSpec()中,子View MeasureSpec在属性被设置为wrap_content或match_parent情况下,子View MeasureSpec的specSize被设置成parenSize = 父容器当前剩余空间大小
所以:wrap_content起到了和match_parent相同的作用:等于父容器当前剩余空间大小
在Activity中获取某个View的宽高有几种方法
- onWindowFocusChanged: View已经初始化完毕,宽高已经有了,需要注意onWindowFocusChanged会被调用多次,Activity得到焦点和失去焦点都会执行这个回调
- view.post(runnable): 通过post可以将一个runnable投递到消息队列的尾部,等待Looper调用此runnable的时候,View也已经初始化好了
- ViewTreeObserver: 使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,OnGlobalLayout方法将会被回调,这是获取View宽高很好的一个时机,需要注意的是,伴随着View树的状态改变,OnGlobalLayout会被调用多次
- view.measure(int widthMeasureSpec, int heightMeasureSpec): 通过手动对View进行measure来得到View的宽高
为什么onCreate获取不到View的宽高
因为View的测绘流程是在perfromTraversals()才开始的,而这个方法的调用是在onResume方法之后调用的
View#post与Handler#post的区别
-
Handler.post,它的执行时间基本是等同于onCreate里那行代码触达的时间;
-
View.post,则不同,它说白了执行时间一定是在
Act#onResume发生后才开始算的;或者换句话说它的效果相当于你上面的View.post方法是写在Act#onResume里面的(但只执行一次,因为onCreate不像onResume会被多次触发); -
当然,虽然这里说的是
post方法,但对应的postDelayed方法区别也是类似的
为什么使用SurfaceView
我们知道View是通过刷新来重绘视图,系统通过发出VSSYNC信号来进行屏幕的重绘,刷新的时间间隔是16ms,如果我们可以在16ms以内将绘制工作完成,则没有任何问题,如果我们绘制过程逻辑很复杂,并且我们的界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此Android提供了SurfaceView来解决这一问题
View和SurfaceView的区别
- View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面
- View在主线程中对页面进行刷新,而SurfaceView则开启一个子线程来对页面进行刷新
- View在绘图时没有实现双缓冲机制,SurfaceView在底层机制中就实现了双缓冲机制
屏幕刷新丢帧一般是什么原因引起的
- 一般是主线程执行耗时操作引起的
- 布局Layout过于复杂 ,无法在16ms内完成渲染
- 同一时间动画执行的次数过多,导致CPU和GPU负载过重
- View的过度绘制,导致某些像素在同一帧时间内绘制多次
- GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间
Android 刷新频率60帧/秒,每隔16ms调用onDraw绘制一次
不一定每次Vsync就会调用一次onDraw绘制,是需要应用端主动发起重绘,这样才会向SurfaceFingle请求接收vsync信号,这样在下次vsync信号就会执行onDraw绘制
Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思?是指每隔 16.6ms 调用 onDraw() 绘制一次么?如果界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?
我们常说的 Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面,而这个每一帧的画面数据就是我们 app 在接收到屏幕刷新信号之后去执行遍历绘制 View 树工作所计算出来的屏幕数据。而 app 并不是每隔 16.6ms 的屏幕刷新信号都可以接收到,只有当 app 向底层注册监听下一个屏幕刷新信号之后,才能接收到下一个屏幕刷新信号到来的通知。而只有当某个 View 发起了刷新请求时,app 才会去向底层注册监听下一个屏幕刷新信号。
也就是说,只有当界面有刷新的需要时,我们 app 才会在下一个屏幕刷新信号来时,遍历绘制 View 树来重新计算屏幕数据。如果界面没有刷新的需要,一直保持不变时,我们 app 就不会去接收每隔 16.6ms 的屏幕刷新信号事件了,但底层仍然会以这个固定频率来切换每一帧的画面,只是后面这些帧的画面都是相同的而已。
界面的显示其实就是一个 Activity 的 View 树里所有的 View 都进行测量、布局、绘制操作之后的结果呈现,那么如果这部分工作都完成后,屏幕会马上就刷新么?
我们 app 只负责计算屏幕数据而已,接收到屏幕刷新信号就去计算,计算完毕就计算完毕了。至于屏幕的刷新,这些是由底层以固定的频率来切换屏幕每一帧的画面。所以即使屏幕数据都计算完毕,屏幕会不会马上刷新就取决于底层是否到了要切换下一帧画面的时机了
getWidth()和getMeasuredWidth()的区别
getMeasuredWidth()获取的是view原始的大小,也就是这个view在XML文件中配置或者是代码中设置的大小。getWidth()获取的是这个view最终显示的大小,这个大小有可能等于原始的大小也有可能不等于原始大小。
Requestlayout,onlayout,onDraw,DrawChild区别与联系
requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。 说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制 任何视图包括该调用者本身。
onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局)
调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
drawChild()去重新回调每个子视图的draw()方法
自定义view注意事项
1.让view支持wrap_content
这是因为直接继承view或者viewgroup的控件,如果不在onMeasure中对wrap_content做特殊处理的,那么当外借在不居中使用wrap_content时就无法达到预期效果,而是和设置match_parent的效果是一样的
2.如果要让view支持padding
这是因为直接继承view的控件,如果不在draw方法中处理padding,那么padding属性将无法生效.另外,直接继承viewGroup的控件,如果不在onMeasure和onLayout方法中处理padding和子view之间的margin的影响的话,那么设置padding和子view之间的margin将无效
3.尽量不要在view中使用handler
可以使用view.post系列的方法替代handler的作用,除非必须使用handler来传递消息
4.view中如果有线程或动画的话,需要及时停止,避免内存泄露
view中如果有线程或动画的话,可以在onDetachedFromWindow()这个时机停止,这个方法会在activity退出和view被remove的时候调用.还有在view变得不可见的时候也需要停止线程或动画.
5.view带有嵌套滑动时,要处理好滑动冲突
View事件分发
View的坐标参数 主要有哪些?分别有什么注意的要点
Left,Right,top,Bottom 注意这4个值其实就是 view 和 他的父控件的 相对坐标值。 并非是距离屏幕左上角的绝对值,这点要注意。
此外,X和Y 其实也是相对于父控件的坐标值。 TranslationX,TranslationY 这2个值 默认都为0,是相对于父控件的左上角的偏移量。
换算关系:
x=left+tranX,y=top+tranY.
很多人不理解,为什么事这样,其实就是View 如果有移动的话,比如平移这种,你们就要注意了,top和left 这种值 是不会变化的。
无论你把view怎么拖动,但是 x,y,tranX,tranY 的值是随着拖动平移 而变化的。想明白这点 就行了
onTouchEvent和GestureDetector 在什么时候用哪个比较好?
只有滑动需求的时候 就用前者,如果有双击等这种行为的时候 就用后者
Scroller 用来解决什么问题?
view的scrollTo和scrollBy 滑动效果太差了,是瞬间完成。而scroller可以配合view的computeScroll 来完成 渐变的滑动效果。体验更好
ScrollTo和ScrollBy 有什么需要注意的
前者是绝对滑动,后者是相对滑动。滑动的是view的内容 而不是view本身。这很重要。比如textview 调用这2个方法 滑动的就是显示出来的字的内容
使用动画来实现view的滑动 有什么后果?
实际上view动画 是对view的表面ui 也就是给用户呈现出的视觉效果 来做的移动,动画本身并不能移动view的真正位置。属性动画除外。动画播放结束以后,view最终还是会回到自己的位置的,。当然了你可以设置fillafter 属性 来让动画播放结束以后 view表象停留在 变化以后的位置。所以这会带来一个很严重的后果。比如你的button在屏幕的左边,你现在用个动画 并且设置了fillafter属性让他去了右边。你会发现 点击右边的button 没有click事件触发,但是点击左边的 却可以触发,原因就是右边的button 只是view的表象,真正的button 还在左边没有动过。你一定要这么做的话 可以提前在右边button移动后的位置放一个新的button,当你动画执行结束以后 把右边的enable 左边的让他gone就可以了
让view滑动总共有几种方式,分别要注意什么?都适用于那些场景?
a:scrollto,scrollby。这种是最简单的,但是只能滑动view的内容 不可以滑动view本身
b:动画。动画可以滑动view内容,但是注意非属性动画 就如我们问题5说的内容 会影响到交互,使用的时候要多注意。不过多数复杂的滑动效果都是属性动画来完成的,属于大杀器级别
c:改变布局参数。这种最好理解了,无非是动态的通过java代码来修改 margin等view的参数罢了。不过用的比较少。我本人不怎么用这种方法
view的事件传递机制 如何用伪代码来表示
对于一个root viewgroup来说,如果接受了一个点击事件,那么首先会调用他的dispatchTouchEvent方法。 如果这个viewgroup的onInterceptTouchEvent 返回true,那就代表要拦截这个事件。接下来这个事件就给viewgroup自己处理了,从而viewgroup的onTouchEvent方法就会被调用。如果如果这个viewgroup的onInterceptTouchEvent返回false就代表我不拦截这个事件,然后就把这个事件传递给自己的子元素,然后子元素的dispatchTouchEvent就会被调用,就是这样一个循环直到 事件被处理
view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者优先级如何?
onTouchListener优先级最高,如果onTouch方法返回 false ,那onTouchEvent就被调用了,返回true 就不会被调用。至于onClick 优先级最低
如果某个view 处理事件的时候 没有消耗down事件 会有什么结果?
假如一个view,在down事件来的时候 他的onTouchEvent返回false, 那么这个down事件 所属的事件序列 就是他后续的move 和up 都不会给他处理了,全部都给他的父view处理
如果view 不消耗move或者up事件 会有什么结果?
那这个事件所属的事件序列就消失了,父view也不会处理的,最终都给activity 去处理了
一旦有事件传递给view,view的onTouchEvent一定会被调用吗
是的,因为view 本身没有onInterceptTouchEvent方法,所以只要事件来到view这里 就一定会走onTouchEvent方法。
并且默认都是消耗掉,返回true的。除非这个view是不可点击的,所谓不可点击就是clickable和longgclikable同时为fale
Button的clickable就是true 但是textview是false
enable是否影响view的onTouchEvent返回值?
不影响,只要clickable和longClickable有一个为真,那么onTouchEvent就返回true
requestDisallowInterceptTouchEvent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗
肯定可以,但是down事件干扰不了
MotionEvent.Action_Cancel什么时候发生
step1: 父View收到ACTION_DOWN,如果没有拦截事件,则ACTION_DOWN前驱事件被子视图接收,父视图后续事件会发送到子View。
step2: 此时如果在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件
事件是先到DecorView还是先到Window
ViewRootImpl——>DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup
点击事件被拦截,但是想传到下面的View,如何操作
子View调用父View的requestDisallowInterceptTouchEvent()方法设置为true,就不会执行父View的onInterceptTouchEvent(),即可将点击事件传到下面的View
在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件,ACTION_UP事件是怎么传递
ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。不会再往下传递
动画
动画的类型
视图动画(View Animation)\
- 补间动画\
- 逐帧动画\
属性动画
间动画和属性动画主要区别
- 作用对象不同,补间动画只能作用在view上,属性动画可以作用在所有对象上。
- 属性变化不同,补间动画只是改变显示效果,不会改变view的属性,比如位置、宽高等,而属性动画实际改变对象的属性。
- 动画效果不同,补间动画只能实现位移、缩放、旋转和透明度四种动画操作,而属性动画还能实现补间动画所有效果及其他更多动画效果。
ValueAnimator类 & ObjectAnimator 类的区别
- 对比
ValueAnimator类 &ObjectAnimator类,其实二者都属于属性动画,本质上都是一致的:先改变值,然后 赋值 给对象的属性从而实现动画效果。 - 但二者的区别在于:
ValueAnimator类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;
ValueAnimator类本质上是一种 改变 值 的操作机制
ObjectAnimator类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;
可以理解为:
ObjectAnimator更加智能、自动化程度更高
插值器与估值器的区别

使用动画的注意事项
OOM问题:这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现OOM,这个在实际开发中要尤其注意,尽量避免使用帧动画。内存泄露:在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄露,通过验证后发现View动画并不存在此问题。兼容性问题:动画在3.0以下的系统有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配工作。View动画的问题:View动画是对View的影像做动画,并不是真正改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.GOEN)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决问题。不要使用px:在进行动画的过程中,要尽量使用dp,使用px会导致在不用的设备上有不用的效果。动画元素的交互:从3.0开始,将view移动(平移)后,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。在Android3.0以前的系统中,不管是View动画还是属性动画,新位置都无法触发单击事件同时,老位置仍然能触发单击事件(因为属性动画在Android3.0以前是没有的,是通过兼容包实现的,底层也是调用View动画)。硬件加速:使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性。
关于硬件加速,直观上说就是依赖GPU实现图形绘制加速,同软硬件加速的区别主要是图形的绘制究竟是GPU来处理还是CPU,如果是GPU,就认为是硬件加速绘制,反之,软件绘制
Bitmap
Bitmap是什么,怎么存储图片
Bitmap,位图,本质上是一张图片的内容在内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值,每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来。其中,A代表透明度,RGB代表红绿蓝三种颜色通道值
Bitmap内存如何计算
Bitmap一直都是Android中的内存大户,计算大小的方式有三种:
getRowBytes()这个在API Level 1添加的,返回的是bitmap一行所占的大小,需要乘以bitmap的高,才能得出btimap的大小getByteCount()这个是在API Level 12添加的,其实是对getRowBytes()乘以高的封装getAllocationByteCount()这个是在API Level 19添加的
这里我将一张图片放到项目的drawable-xxhdpi文件夹中,然后通过方法获取图片所占的内存大小:
var bitmap = BitmapFactory.decodeResource(resources, R.drawable.test)
img.setImageBitmap(bitmap)
Log.e(TAG,"dpi = ${resources.displayMetrics.densityDpi}")
Log.e(TAG,"size = ${bitmap.allocationByteCount}")
复制代码
打印出来的结果是
size=1960000
复制代码
具体是怎么计算的呢?
图片内存=宽 * 高 * 每个像素所占字节。
这个像素所占字节又和Bitmap.Config有关,Bitmap.Config是个枚举类,用于描述每个像素点的信息,比如:
ARGB_8888。常用类型,总共32位,4个字节,分别表示透明度和RGB通道。RGB_565。16位,2个字节,只能描述RGB通道。
所以我们这里的图片内存计算就得出:
宽700 * 高700 * 每个像素4字节=1960000
Bitmap内存 和drawable目录的关系
首先放一张drawable目录对应的屏幕密度对照表,来自郭霖的博客:

对照表
刚才的案例,我们是把图片放到drawable-xxhdpi文件夹,而drawable-xxhdpi文件夹对应的dpi就是我们测试手机的dpi—480。所以图片的内存就是我们所计算的宽 * 高 * 每个像素所占字节。
如果我们把图片放到其他的文件夹,比如drawable-hdpi文件夹(对应的dpi是240),会发生什么呢?
再次打印结果:
size = 7840000
复制代码
这是因为一张图片的实际占用内存大小计算公式是:
占用内存 = 宽 * 缩放比例 * 高 * 缩放比例 * 每个像素所占字节
这个缩放比例就跟屏幕密度DPI有关了:
缩放比例 = 设备dpi/图片所在目录的dpi
所以我们这张图片的实际占用内存位:
宽700 * (480/240) * 高700 * (480/240) * 每个像素4字节 = 7840000
Bitmap加载优化?不改变图片质量的情况下怎么优化?
常用的优化方式是两种:
- 修改Bitmap.Config
这一点刚才也说过,不同的Conifg代表每个像素不同的占用空间,所以如果我们把默认的ARGB_8888改成RGB_565,那么每个像素占用空间就会由4字节变成2字节了,那么图片所占内存就会减半了。
可能一定程度上会降低图片质量,但是我实际测试看不出什么变化。
- 修改inSampleSize
inSampleSize,采样率,这个参数是用于图片尺寸压缩的,他会在宽高的维度上每隔inSampleSize个像素进行一次采集,从而达到缩放图片的效果。这种方法只会改变图片大小,不会影响图片质量。
val options=BitmapFactory.Options()
options.inSampleSize=2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test2,options)
img.setImageBitmap(bitmap)
复制代码
实际项目中,我们可以设置一个与目标图像大小相近的inSampleSize,来减少实际使用的内存:
fun getImage(): Bitmap {
var options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.test2, options)
// 计算最佳采样率
options.inSampleSize = getImageSampleSize(options.outWidth, options.outHeight)
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(resources, R.drawable.test2, options)
}
复制代码
inJustDecodeBounds是什么?
上面的例子大家应该发现了,其中有个inJustDecodeBounds,又设置为true,又设置成false的,总感觉多此一举,那么他到底是干嘛呢?
因为我们要获取图片本身的大小,如果直接decodeResource加载一遍的话,那么就会增加内存了,所以官方提供了这样一个参数inJustDecodeBounds。如果inJustDecodeBounds为ture,那么decode的bitmap为null,也就是不返回实际的bitmap,只把图片的大小信息放到了options的值中。
所以这个参数就是用来获取图片的大小信息的同时不占用内存
Bitmap内存复用怎么实现?
如果有个需求,是在同一个imageview中可以加载不同的图片,那我们需要每次都去新建一个Bitmap对象,占用新的内存空间吗?如果我们这样写的话:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.actvitiy_bitmap)
btn1.setOnClickListener {
img.setImageBitmap(getBitmap(R.drawable.test))
}
btn2.setOnClickListener {
img.setImageBitmap(getBitmap(R.drawable.test2))
}
}
fun getBitmap(resId: Int): Bitmap {
var options = BitmapFactory.Options()
return BitmapFactory.decodeResource(resources, resId, options)
}
复制代码
这样就会Bitmap就会频繁去申请内存,释放内存,从而导致大量GC,内存抖动。
为了防止这种情况呢,我们就可以用到inBitmap参数,用于Bitmap的内存复用。这样同一块内存空间就可以被多个Bitmap对象复用,从而减少了频繁的GC。
val options by lazy {
BitmapFactory.Options()
}
val reuseBitmap by lazy {
options.inMutable = true
BitmapFactory.decodeResource(resources, R.drawable.test, options)
}
fun getBitmap(resId: Int): Bitmap {
options.inMutable = true
options.inBitmap = reuseBitmap
return BitmapFactory.decodeResource(resources, resId, options)
}
复制代码
这里有几个要注意的点
inBitmap要和inMutable属性配套使用,否则将无法复用。- 在
Android 4.4之前,只能重用相同大小的Bitmap内存区域;4.4之后只要复用内存空间的Bitmap对象大小比inBitmap指向的内存空间要小即可。
所以一般在复用之前,还要判断下,新的Bitmap内存是不是小于可以复用的Bitmap内存,然后才能进行复用。
高清大图加载该怎么处理?
如果是高清大图,那就说明不允许进行图片压缩,比如微博长图,清明上河图。
所以我们就要对图片进行局部显示,这就用到BitmapRegionDecoder属性,主要用于显示图片的某一块矩形区域。
比如我要显示左上角的100 * 100区域:
fun setImagePart() {
val inputStream: InputStream = assets.open("test.jpg")
val bitmapRegionDecoder: BitmapRegionDecoder =
BitmapRegionDecoder.newInstance(inputStream, false)
val options = BitmapFactory.Options()
val bitmap = bitmapRegionDecoder.decodeRegion(
Rect(0, 0, 100, 100), options)
image.setImageBitmap(bitmap)
}
复制代码
实际项目使用中,我们可以根据手势滑动,然后不断更新我们的Rect参数来实现具体的功能即可
如何跨进程传递大图?
Bundle直接传递。bundle最常用于Activity间传递,也属于跨进程的一种方式,但是传递的大小有限制,一般为1M。
//intent.put的putExtra方法实质也是通过bundle
intent.putExtra("image",bitmap);
bundle.putParcelable("image",bitmap)
复制代码
Bitmap之所以可以直接传递,是因为其实现了Parcelable接口进行了序列化。而Parcelable的传递原理是利用了Binder机制,将Parcel序列化的数据写入到一个共享内存(缓冲区)中,读取的时候也会从这个缓冲区中去读取字节流,然后再反序列化成对象使用。这个共享内存也就是缓存区有一个大小限制—1M,而且是公用的。所以传图片的话很容易就容易超过这个大小然后报错TransactionTooLargeException。
所以这个方案不可靠。
文件传输。
将图片保存到文件,然后只传输文件路径,这样肯定是可以的,但是不高效。
putBinder
这个就是考点了。通过传递binder的方式传递bitmap。
//传递binder
val bundle = Bundle()
bundle.putBinder("bitmap", BitmapBinder(mBitmap))
//接收binder中的bitmap
val imageBinder: BitmapBinder = bundle.getBinder("bitmap") as BitmapBinder
val bitmap: Bitmap? = imageBinder.getBitmap()
//Binder子类
class BitmapBinder :Binder(){
private var bitmap: Bitmap? = null
fun ImageBinder(bitmap: Bitmap?) {
this.bitmap = bitmap
}
fun getBitmap(): Bitmap? {
return bitmap
}
}
复制代码
为什么用putBinder就没有大小限制了呢?
- 因为
putBinder中传递的其实是一个文件描述符fd,文件本身被放到一个共享内存中,然后获取到这个fd之后,只需要从共享内存中取出Bitmap数据即可,这样传输就很高效了。 - 而用
Intent/bundle直接传输的时候,会禁用文件描述符fd,只能在parcel的缓存区中分配空间来保存数据,所以无法突破1M的大小限制。
文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。第一个打开的文件是0,第二个是1,依此类推
mvc&mvp&mvvm
MVP,MVVM,MVC 区别
MVC
- 架构介绍
Model:数据模型,比如我们从数据库或者网络获取数据View:视图,也就是我们的xml布局文件Controller:控制器,也就是我们的Activity
- 模型联系
View --> Controller,也就是反应View的一些用户事件(点击触摸事件)到Activity上。Controller --> Model, 也就是Activity去读写一些我们需要的数据。Controller --> View, 也就是Activity在获取数据之后,将更新内容反映到View上。
这样一个完整的项目架构就出来了,也是我们早期进行开发比较常用的项目架构。
- 优缺点
这种缺点还是比较明显的,主要表现就是我们的Activity太重了,经常一写就是几百上千行了。造成这种问题的原因就是Controller层和View层的关系太过紧密,也就是Activity中有太多操作View的代码了。
但是!但是!其实Android这种并称不上传统的MVC结构,因为Activity又可以叫View层又可以叫Controller层,所以我觉得这种Android默认的开发结构,其实称不上什么MVC项目架构,因为他本身就是Android一开始默认的开发形式,所有东西都往Activity中丢,然后能封装的封装一下,根本分不出来这些层级。当然这是我个人看法,可以都来讨论下。
MVP
- 架构介绍
之前不就是因为Activity中有操作view,又做Controller工作吗。所以其实MVP架构就是从原来的Activity层把view和Controller区分开,单独抽出来一层Presenter作为原来Controller的职位。然后最后演化成,将View层写成接口的形式,然后Activity去实现View接口,最后在Presenter类中去实现方法。
Model:数据模型,比如我们从数据库或者网络获取数据。View:视图,也就是我们的xml布局文件和Activity。Presenter:主持人,单独的类,只做调度工作。
- 模型联系
View --> Presenter,反应View的一些用户事件到Presenter上。Presenter --> Model, Presenter去读写操作一些我们需要的数据。Controller --> View, Presenter在获取数据之后,将更新内容反馈给Activity,进行view更新。
- 优缺点
这种的优点就是确实大大减少了Activity的负担,让Activity主要承担一个更新View的工作,然后把跟Model交互的工作转移给了Presenter,从而由Presenter方来控制和交互Model方以及View方。所以让项目更加明确简单,顺序性思维开发。
缺点也很明显:首先就是代码量大大增加了,每个页面或者说功能点,都要专门写一个Presenter类,并且由于是面向接口编程,需要增加大量接口,会有大量繁琐的回调。其次,由于Presenter里持有了Activity对象,所以可能会导致内存泄漏或者view空指针,这也是需要注意的地方。
MVVM
- 架构介绍
MVVM的特点就是双向绑定,并且有Google官方加持,更新了Jetpack中很多架构组件,比如ViewModel,Livedata,DataBinding等等,所以这个是现在的主流框架和官方推崇的框架。
Model:数据模型,比如我们从数据库或者网络获取数据。View:视图,也就是我们的xml布局文件和Activity。ViewModel:关联层,将Model和View绑定,使他们之间可以相互绑定实时更新
- 模型联系
View --> ViewModel -->View,双向绑定,数据改动可以反映到界面,界面的修改可以反映到数据。ViewModel --> Model, 操作一些我们需要的数据。
- 优缺点
优点就是官方大力支持,所以也更新了很多相关库,让MVVM架构更强更好用,而且双向绑定的特点可以让我们省去很多View和Model的交互。也基本解决了上面两个架构的问题。
具体说说你理解的MVVM
先说说MVVM是怎么解决了其他两个架构所在的缺陷和问题:
解决了各个层级之间耦合度太高的问题,也就是更好的完成了解耦。MVP层中,Presenter还是会持有View的引用,但是在MVVM中,View和Model进行双向绑定,从而使viewModel基本只需要处理业务逻辑,无需关系界面相关的元素了。解决了代码量太多,或者模式化代码太多的问题。由于双向绑定,所以UI相关的代码就少了很多,这也是代码量少的关键。而这其中起到比较关键的组件就是DataBinding,使所有的UI变动都交给了被观察的数据模型。解决了可能会有的内存泄漏问题。MVVM架构组件中有一个组件是LiveData,它具有生命周期感知能力,可以感知到Activity等的生命周期,所以就可以在其关联的生命周期遭到销毁后自行清理,就大大减少了内存泄漏问题。解决了因为Activity停止而导致的View空指针问题。在MVVM中使用了LiveData,那么在需要更新View的时候,如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。也就是他会保证在界面可见的时候才会进行响应,这样就解决了空指针问题。解决了生命周期管理问题。这主要得益于Lifecycle组件,它使得一些控件可以对生命周期进行观察,就能随时随地进行生命周期事件。
2)再说说响应式编程
响应式编程,说白了就是我先构建好事物之间的关系,然后就可以不用管了。他们之间会因为这层关系而互相驱动。其实也就是我们常说的观察者模式,或者说订阅发布模式。
为什么说这个呢,因为MVVM的本质思想就是类似这种。不管是双向绑定,还是生命周期感知,其实都是一种观察者模式,使所有事物变得可观察,那么我们只需要把这种观察关系给稳定住,那么项目也就稳健了。
3)最后再说说MVVM为什么这么强大?
我个人觉得,MVVM强大不是因为这个架构本身,而是因为这种响应式编程的优势比较大,再加上Google官方的大力支持,出了这么多支持的组件,来维系MVVM架构,其实也是官方想进行项目架构的统一。
优秀的架构思想+官方支持=强大
ViewModel 是什么,说说你所理解的ViewModel?
如果看过我上一篇文章的小伙伴应该都有所了解,ViewModel是MVVM架构的一个层级,用来联系View和model之间的关系。而我们今天要说的就是官方出的一个框架——ViewModel。
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据
官方是这么介绍的,这里面有两个信息:
- 注重生命周期的方式。由于
ViewModel的生命周期是作用于整个Activity的,所以就节省了一些关于状态维护的工作,最明显的就是对于屏幕旋转这种情况,以前对数据进行保存读取,而ViewModel则不需要,他可以自动保留数据。
其次,由于ViewModel在生命周期内会保持局部单例,所以可以更方便Activity的多个Fragment之间通信,因为他们能获取到同一个ViewModel实例,也就是数据状态可以共享了。
- 存储和管理界面相关的数据。
ViewModel层的根本职责,就是负责维护界面上UI的状态,其实就是维护对应的数据,因为数据会最终体现到UI界面上。所以ViewModel层其实就是对界面相关的数据进行管理,存储等操作。
ViewModel 为什么被设计出来,解决了什么问题?
- 在
ViewModel组件被设计出来之前,MVVM又是怎么实现ViewModel这一层级的呢?
其实就是自己编写类,然后通过接口,内部依赖实现View和数据的双向绑定。所以Google出这个ViewModel组件,无非就是为了规范MVVM架构的实现,并尽量让ViewModel这一层级只触及到业务代码,不去关心VIew层级的引用等。然后配合其他的组件,包括livedata,databindingrang等让MVVM架构更加完善,规范,健硕。
- 解决了什么问题呢?
其实上面已经说过一些了,比如:
1)不会因为屏幕旋转而销毁,减少了维护状态的工作 2)由于在作用域内单一实例的特性,使得多个fragment之间可以方便通信,并且维护同一个数据状态。3)完善了MVVM架构,使得解耦更加纯粹。
说说ViewModel原理。
- 首先说说是怎么保存生命周期
ViewModel2.0之前呢,其实原理是在Activity上add一个HolderFragment,然后设置setRetainInstance(true)方法就能让这个Fragment在Activity重建时存活下来,也就保证了ViewModel的状态不会随Activity的状态所改变。
2.0之后,其实是用到了Activity的onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()这两个方法,相当于在横竖屏切的时候会保存ViewModel的实例,然后恢复,所以也就保证了ViewModel的数据。
- 再说说怎么保证作用域内唯一实例
首先,ViewModel的实例是通过反射获取的,反射的时候带上application的上下文,这样就保证了不会持有Activity或者Fragment等View的引用。然后实例创建出来会保存到一个ViewModelStore容器里面,其实也就是一个集合类,这个ViewModelStore 类其实就是保存在界面上的那个实例,而我们的ViewModel就是里面的一个集合类的子元素。
所以我们每次获取的时候,首先看看这个集合里面有无我们的ViewModel,如果没有就去实例化,如果有就直接拿到实例使用,这样就保证了唯一实例。最后在界面销毁的时候,会去执行ViewModelStore的clear方法,去清除集合里面的ViewModel数据。一小段代码说明下:
public <T extends ViewModel> T get(Class<T> modelClass) {
// 先从ViewModelStore容器中去找是否存在ViewModel的实例
ViewModel viewModel = mViewModelStore.get(key);
// 若ViewModel已经存在,就直接返回
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
// 若不存在,再通过反射的方式实例化ViewModel,并存储进ViewModelStore
viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.onCleared();
}
mMap.clear();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}
}
复制代码
ViewModel怎么实现自动处理生命周期?为什么在旋转屏幕后不会丢失状态?为什么ViewModel可以跟随Activity/Fragment的生命周期而又不会造成内存泄漏呢?
这三个问题很类似,都是关于生命周期的问题,其实也就是问为什么ViewModel能管理生命周期,并且不会因为重建等情况造成影响。
- ViewModel2.0之前
利用一个无view 的HolderFragment来维持它的生命周期,我们知道ViewModel实例是存储到一个ViewModelStore容器里的,那么这个空的fragment就可以用来管理这个容器,只要Activity处于活动状态,HolderFragment也就不会被销毁,就保证了ViewModel的生命周期。
而且设置setRetainInstance(true)方法可以保证configchange时的生命周期不被改变,让这个Fragment在Activity重建时存活下来。总结来说就是用一个空的fragment来管理维护ViewModelStore,然后对应的activity销毁的时候就去把viewmodel的映射删除。就让ViewModel的生命周期保持和Activity一样了。这也是很多三方库用到的巧妙方法,比如Glide,也是建立空的Fragment来管理。
- 2.0之后,有了androidx支持
其实是用到了Activity的一个子类ComponentActivity,然后重写了onRetainNonConfigurationInstance()方法保存ViewModelStore,并在需要的时候,也就是重建的Activity中去通过getLastNonConfigurationInstance()方法获取到ViewModelStore实例。这样也就保证了ViewModelStore中的ViewModel不会随Activity的重建而改变。
同时由于实现了LifecycleOwner接口,所以能利用Lifecycles组件组件感知每个页面的生命周期,就可以通过它来订阅当Activity销毁时,且不是因为配置导致的destory情况下,去清除ViewModel,也就是调用ViewModelStore的clear方法。
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// 判断是否因为配置更改导致的destroy
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
复制代码
这里的onRetainNonConfigurationInstance方法再说下,是会在Activity因为配置改变而被销毁时被调用,跟onSaveInstanceState方法调用时机比较相像,不同的是onSaveInstanceState保存的是Bundle,Bundle是有类型限制和大小限制的,而且需要在主线程进行序列号。而onRetainNonConfigurationInstance方法都没有限制,所以更倾向于用它。
所以,到这里,第三个问题应该也可以回答了,2.0之前呢,都是通过他们创建了一个空的fragment,然后跟随这个fragment的生命周期。2.0之后呢,是因为不管是Activity或者Fragment,都实现了LifecycleOwner接口,所以ViewModel是可以通过Lifecycles感知到他们的生命周期,从而进行实例管理的
ViewModelScope了解吗
这里主要就是考ViewModel和其他一些组件的关系了。关于协程,之前也专门说过一篇,主要用作线程切换。如果在多个协程中,需要停止某些任务,就必须对这些协程进行管理,一般是加入一个CoroutineScope,如果需要取消协程,就可以去取消这个CoroutineScope,他所跟踪的所有协程都会被取消。
GlobalScope.launch {
longRunningFunction()
anotherLongRunningFunction()
}
复制代码
但是这种全局使用方法,是不被推荐使用的,如果要限定作用域的时候,一般推荐viewModelScope。
viewModelScope 是一个 ViewModel 的 Kotlin 扩展属性。它能在ViewModel销毁时 (onCleared() 方法调用时) 退出。所以只要使用了 ViewModel,就可以使用 viewModelScope在 ViewModel 中启动各种协程,而不用担心任务泄漏。
class MyViewModel() : ViewModel() {
fun initialize() {
viewModelScope.launch {
processBitmap()
}
}
suspend fun processBitmap() = withContext(Dispatchers.Default) {
// 在这里做耗时操作
}
}
复制代码
LiveData 是什么?
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
官方介绍如下,其实说的比较清楚了,主要作用在两点:
数据存储器类。也就是一个用来存储数据的类。可观察。这个数据存储类是可以观察的,也就是比一般的数据存储类多了这么一个功能,对于数据的变动能进行响应。
主要思想就是用到了观察者模式思想,让观察者和被观察者解耦,同时还能感知到数据的变化,所以一般被用到ViewModel中,ViewModel负责触发数据的更新,更新会通知到LiveData,然后LiveData再通知活跃状态的观察者。
var liveData = MutableLiveData<String>()
liveData.observe(this, object : Observer<String> {
override fun onChanged(t: String?) {
}
})
liveData.setVaile("xixi")
//子线程调用
liveData.postValue("test")
复制代码
LiveData 为什么被设计出来,解决了什么问题?
LiveData作为一种观察者模式设计思想,常常被和Rxjava一起比较,观察者模式的最大好处就是事件发射的上游 和 接收事件的下游 互不干涉,大幅降低了互相持有的依赖关系所带来的强耦合性。
其次,LiveData还能无缝衔接到MVVM架构中,主要体现在其可以感知到Activity等生命周期,这样就带来了很多好处:
- 不会发生内存泄漏 观察者会绑定到
Lifecycle对象,并在其关联的生命周期遭到销毁后进行自我清理。 - 不会因 Activity 停止而导致崩溃 如果观察者的生命周期处于
非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。 - 自动判断生命周期并回调方法 如果观察者的生命周期处于
STARTED或RESUMED状态,则 LiveData 会认为该观察者处于活跃状态,就会调用onActive方法,否则,如果 LiveData 对象没有任何活跃观察者时,会调用onInactive()方法
说说LiveData原理。
说到原理,其实就是两个方法:
- 订阅方法,也就是
observe方法。通过该方法把订阅者和被观察者关联起来,形成观察者模式。
简单看看源码:
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
//...
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
public V putIfAbsent(@NonNull K key, @NonNull V v) {
Entry<K, V> entry = get(key);
if (entry != null) {
return entry.mValue;
}
put(key, v);
return null;
}
复制代码
这里putIfAbsent方法是讲生命周期相关的wrapper和观察者observer作为key和value存到了mObservers中。
- 回调方法,也就是
onChanged方法。通过改变存储值,来通知到观察者也就是调用onChanged方法。从改变存储值方法setValue看起:
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
private void dispatchingValue(@Nullable ObserverWrapper initiator) {
//...
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}
复制代码
这一套下来逻辑还是比较简单的,遍历刚才的map——mObservers,然后找到观察者observer,如果观察者不在活跃状态(活跃状态,也就是可见状态,处于 STARTED 或 RESUMED状态),则直接返回,不去通知。否则正常通知到观察者的onChanged方法。
当然,如果想任何时候都能监听到,都能获取回调,调用observeForever方法即可



















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)


![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)