为了面试,为了高工资,废话不多说,不定期更新。
1. Activity正常和异常情况下的生命周期分析。
Activity生命周期是一个老生常谈的问题,但是有些刁钻的问题也会一时半会的影响大家的思路,我们还是耐着性子来分析下吧。
I.正常生命周期
- onCreate
Avtivity启动时调用的第一个方法,表示Activity正在被创建。应该在此初始化Activity的必需组件,最重要的是必需在该方法中调用setContentView方法来设置Activity的布局。
不应在此接口中执行耗时操作,否则会影响Activity的显示。
- onStart
在Activity即将对用户可见之前调用(该接口与onResume对应,onResume代表已经可见了),可以理解为已经在后台准备就绪了。
- onResume
在Activity即将开始与用户进行交互前调用,此时Activity处于Activity堆栈的顶层,并具有用户输入焦点。
- onPause
当系统开始显示另外一个Activity(Dialog不算,Dialog形式的Activity算)时调用,通常用于确认对持久性数据的保存更改,停止动画及其他可能消耗CPU的内容。它应该快速执行所需操作,因为它返回后,下一个Activity才能继续执行。
此时Activity对于用户来讲是部分可见的,例如弹出一个Dialog形式的Activity。
- onStop
在Activity对用户完全不可见时调用,可以认为处于后台。
- onDestory
在Activity被销毁前调用,这是Activity将收到的最后的调用。
一般会在此销毁启动的线程,或者处理一些可能导致内存泄露的问题。
- onRestart
系统由不可见变为可见时调用,调用顺序:onRestart-onstart-onResume.
Note: 除非程序在onCreate()方法里面就调用了finish()方法,系统通常是在执行了onPause()与onStop() 之后再调用onDestroy() 。在某些情况下,例如我们的activity只是做了一个临时的逻辑跳转的功能,它只是用来决定跳转到哪一个activity,这样的话,需要在onCreate里面调用finish方法,这样系统会直接调用onDestory,跳过生命周期中的其他方法。
II.异常生命周期
- 资源配置发生变更导致的Activity杀死重建
当系统配置发生变化后,例如屏幕方向切换、系统语言切换,生命周期方法调用如下
onPause---onSaveInstanceState---onStop---onDestory---onCreate---onRestoreInstanceState--onStart--onResume
在Activity异常终止时,系统会调用onSaveInstanceState来保存一些状态信息(在onStop方法之前调用,但是和onPause方法并无明确的调用顺序)。
重建Activity时,onSaveInstanceState保存的信息Bundle会以参数的形式传入onCreate和onRestoreInstanceState(在onStart方法之后调用)方法中,所以我们可以在这两个方法中进行一些状态恢复。
Note:onCreate方法的Bundle参数可能为空,所以我们一般配对使用onSaveInstanceState和onRestoreInstanceState。
保存数据的思想:
Activity被意外终止时,Activity会调用onSavedInstanceState去保存数据,然后Activity会委托Window去保存数据,接着Window再委托它上面的顶级容器去保存数据,顶层容器是一个ViewGroup,一般来说它很可能是一个DecorView.最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了,可以发现,这是一种典型的委托思想,上层委托下层,父容器委托子容器去处理一些事情。
- 资源或者内存不足导致被杀死
对于Activity我们分为三种:
a.前台Activity(高)
b.可见非前台Activity(中)
c.后台Activity(低)
当系统发现内存不足时,会按照上面的优先级选择杀死Activity所在的进程,并在后续(需要再显示的时候)进行恢复。
Note:系统只恢复那些被开发者指定过id的控件,如果没有为控件指定id,则系统就无法恢复了。
2. Android启动模式分析
2.1 Application&Task&Process
Application
通俗的讲,Application就是一组组件的集合,每一个应用都是一个Application。
Task
Task是在程序运行时,只针对Activity的概念,Task是一组相互关联的Activity的集合,它存在framework层的一个概念,控制界面的跳转和返回。
Process
进程是操作系统内核的一个概念,表示接受内核调度的执行单位。
在默认情况下,一个应用程序的所有组件运行在同一个进程中。
2.2 启动模式
Activity存在四种启动模式:standard,singleTop,singleTask,singleInstance,使用时需要在AndroidManifest文件中配置。
standard:标准
标准模式,也是系统默认的Activity启动模式。
Activity可以多次实例化,而且每个实例可以属于不同的任务,并且一个任务可以拥有多个实例。
谁启动的Activity,Activity就和它处于同一个任务栈中。
在使用ApplicationContext或者Service中的Context启动Activity时,需要添加FLAG_ACTIVITY_NEW_TASK的标志才能正常启动Activity;因为非Activity的Context没有所谓的任务栈。
singleTop:栈顶复用
栈顶复用模式。
如果当前任务栈顶已经存在Activity的一个实例,系统会调用该实例的onNewIntent方法向其传递Intent,而不会创建Activity的新实例。(该Activity的onCreate、onStart方法不会被系统调用,因为它并没有发生改变)
Activity可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例(但前提是位于返回栈顶部的 Activity并不是Activity的现有实例)。
例如,假设任务返回栈包含Activity A 以及 Activity B、C 和位于顶部的 D(堆栈是 A-B-C-D;D 位于顶部)。
收到针对 D 类 Activity 的 Intent。
如果 D 具有默认的 "standard" 启动模式,则会创建该类的新实例,且堆栈会变成 A-B-C-D-D。
但是,如果 D 的启动模式是 "singleTop",则 D 的现有实例会通过 onNewIntent() 接收 Intent,因为它位于堆栈的顶部;而堆栈仍为 A-B-C-D。
但是,如果收到针对 B 类 Activity 的 Intent,则会向堆栈添加B新实例,即便其启动模式为 "singleTop" 也是如此。
singleTask:栈内复用
栈内复用模式。
启动一个singleTask的Activity实例时,如果任务栈中已经存在这样一个实例,就会将这个实例调度到任务的栈顶,并清除它当前所在任务中位于它上面的所有的activity,不会重新实例化该Activity;如果任务栈中不存在该Activity实例,则创建实例后入栈。
和SingleTop一样,系统会回调它的oneNewInent方法。
singleInstance:单例
单例模式。
总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其他activity会自动运行于另一个任务中。当再次启动该activity的实例时,会重用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。
同一时刻在系统中只会存在一个这样的Activity实例。
taskAffinity
taskAffinity表示Activity所处的任务栈,默认情况下一个应用中所有的Activity具有相同的taskAffinity(处于同一个任务中),默认的taskAffinity为应用的包名。
可以指定taskAffinity为空字符串,代表Activity不属于任何任务。
taskActivity主要结合singleTask启动模式或者allowTaskReparenting属性配对使用,在其他情况下无意义。
面试的时候,给面试官举例说明,更加简洁明了,省的表述不清。
启动模式指定
启动模式的指定存在两种方式:
- launchMode指定:无法指定FLAG_ACTIVITY_CLEAR_TOP.
- Intent Flag指定(优先级高):无法指定singleInstance.
FLAG
FLAG_ACTIVITY_SINGLE_TOP
相当于singleTop
FLAG_ACTIVITY_NEW_TASK
没有对应的启动模式,它的部分特性与singleTask相同。
默认的跳转类型,会重新创建一个新的Activity,比方说Task1中有A,B,C三个Activity,此时在C中启动D的话,如果在Manifest.xml文件中给D添加了Affinity的值和Task中的不一样的话,则会在新标记的Affinity所存在的Task中压入这个Activity。如果是默认的或者指定的Affinity和Task一样的话,就和标准模式一样了启动一个新的Activity.
FLAG_ACTIVITY_CLEAR_TOP
当Intent对象包含这个标记时,如果在栈中发现存在Activity实例,则清空这个实例之上的Activity,使其处于栈顶。
在使用默认的“standard”启动模式下,如果没有在Intent使用到FLAG_ACTIVITY_SINGLE_TOP标记,那么它将关闭后重建,如果使用了这个FLAG_ACTIVITY_SINGLE_TOP标记,则会使用已存在的实例;对于其他启动模式,无需再使用FLAG_ACTIVITY_SINGLE_TOP,它都将使用已存在的实例,Intent会被传递到这个实例的onNewIntent()中。
3. 进程保活
参考链接:Android 进程保活招式大全
进程保活包括两个方面:
- 提升进程优先级,降低进程被杀死的概率。
- 进程被杀死后,进行拉活。
进程优先级
Android系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,依此类推,以回收系统资源。
按照进程的优先级,进程分为以下五类:
- 前台进程
- 可见进程
- 服务进程
- 后台进程
- 空进程
前台进程
用户操作所必需的进程,如果一个进程满足以下任何一个条件,即为前台进程:
- 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
- 托管某个Service,后者绑定到用户正在交互的Activity
- 托管正在“前台”运行的Service(服务已调用 startForeground())
- 托管正执行一个生命周期回调的Service(onCreate()、onStart() 或 onDestroy())
- 托管正执行其onReceive()方法的 BroadcastReceiver
通常,在任意给定时间前台进程都为数不多。
只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。
此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。
可见进程
没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。
如果一个进程满足以下任一条件,即视为可见进程:
- 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。
例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。 - 托管绑定到可见(或前台)Activity 的 Service。
可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
服务进程
尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
正在运行 startService() 方法启动的服务,且不属于上述两个更高类别进程的进程。
后台进程
包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。
这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。
通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。
如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。
空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
进程被杀死的情况
综上,可以得出减少进程被杀死概率无非就是想办法提高进程优先级,减少进程在内存不足等情况下被杀死的概率。
提升进程优先级方案
1. 利用Activity提升
方案设计思想:监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。注意该 Activity 需设计成用户无感知。
通过该方案,可以使进程的优先级在屏幕锁屏时间由4提升为最高优先级1。
方案适用范围:
适用场景:本方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。
适用版本:适用于所有的 Android 版本。
方案具体实现:
首先定义 Activity,并设置 Activity 的大小为1像素。
其次,从 AndroidManifest 中通过如下属性,排除 Activity 在 RecentTask 中的显示。
excludeFromRecents="true"
exported="false"
finishOnTaskLaunch="false"
最后,设置Activity为透明主题。
2. 利用Notification提升
方案设计思想:Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。
方案实现挑战:从 Android2.3 开始调用 setForeground 将后台 Service 设置为前台 Service 时,必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。
对于不需要常驻通知栏的应用来说,该方案虽好,但却是用户感知的,无法直接使用。
方案挑战应对措施:通过实现一个内部 Service,在 LiveService 和其内部 Service 中同时发送具有相同 ID 的 Notification,然后将内部 Service 结束掉。随着内部 Service 的结束,Notification 将会消失,但系统优先级依然保持为2。
方案适用范围:适用于目前已知所有版本。
拉起进程
1. 利用系统广播拉活
方案设计思想:在发生特定系统事件时,系统会发出响应的广播,通过在 AndroidManifest 中“静态”注册对应的广播监听器,即可在发生响应事件时拉活。
方案适用范围:适用于全部 Android 平台。但存在如下几个缺点:
- 广播接收器被管理软件、系统软件通过“自启管理”等功能禁用的场景无法接收到广播,从而无法自启。
- 系统广播事件不可控,只能保证发生事件时拉活进程,但无法保证进程挂掉后立即拉活。
因此,该方案主要作为备用手段。
2. 利用第三方广播拉活
方案设计思想:该方案总的设计思想与接收系统广播类似,不同的是该方案为接收第三方 Top 应用广播。
通过反编译第三方 Top 应用,如:手机QQ、微信、支付宝、UC浏览器等,以及友盟、信鸽、个推等 SDK,找出它们外发的广播,在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。
方案适用范围:该方案的有效程度除与系统广播一样的因素外,主要受如下因素限制:
- 反编译分析过的第三方应用的多少
- 第三方应用的广播属于应用私有,当前版本中有效的广播,在后续版本随时就可能被移除或被改为不外发。
这些因素都影响了拉活的效果。
3. 利用系统Service机制拉活
方案设计思想:将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活。
方案适用范围:如下两种情况无法拉活
Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
4 利用Native进程拉活
方案设计思想:
主要思想:利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。
主要原理:在 Android 中所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理。而且,通过 Linux 的 fork 机制创建的进程为纯 Linux 进程,其生命周期不受 Android 的管理。
感知进程死亡
要在 Native 进程中感知主进程是否存活有两种实现方式:
在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,当主进程不存活时进行拉活。该方案的很大缺点是不停的轮询执行判断逻辑,非常耗电。
在主进程中创建一个监控文件,并且在主进程中持有文件锁。在拉活进程启动后申请文件锁将会被堵塞,一旦可以成功获取到锁,说明主进程挂掉,即可进行拉活。由于 Android 中的应用都运行于虚拟机之上,Java 层的文件锁与 Linux 层的文件锁是不同的,要实现该功能需要封装 Linux 层的文件锁供上层调用。
拉活主进程
通过 Native 进程拉活主进程的部分代码如下,即通过 am 命令进行拉活。通过指定“--include-stopped-packages”参数来拉活主进程处于 forestop 状态的情况。
如何保证 Native 进程的唯一
从可扩展性和进程唯一等多方面考虑,将 Native 进程设计成 C/S 结构模式,主进程与 Native 进程通过 Localsocket 进行通信。在Native进程中利用 Localsocket 保证 Native 进程的唯一性,不至于出现创建多个 Native 进程以及 Native 进程变成僵尸进程等问题。
方案适用范围:该方案主要适用于 Android5.0 以下版本手机。
该方案不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。
对于 Android5.0 以上手机,系统虽然会将native进程内的所有进程都杀死,这里其实就是系统“依次”杀死进程时间与拉活逻辑执行时间赛跑的问题,如果可以跑的比系统逻辑快,依然可以有效拉起。记得网上有人做过实验,该结论是成立的,在某些 Android 5.0 以上机型有效。
5. 利用 JobScheduler 机制拉活
方案设计思想:Android5.0 以后系统对 Native 进程等加强了管理,Native 拉活方式失效。系统在 Android5.0 以上版本提供了 JobScheduler 接口,系统会定时调用该进程以使应用进行一些逻辑操作。
在本项目中,我对 JobScheduler 进行了进一步封装,兼容 Android5.0 以下版本。
方案适用范围:该方案主要适用于 Android5.0 以上版本手机。
该方案在 Android5.0 以上版本中不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以上版本拉活效果非常好。
仅在小米手机可能会出现有时无法拉活的问题。
6. 利用账号同步机制拉活
方案设计思想:Android 系统的账号同步机制会定期同步账号进行,该方案目的在于利用同步机制进行进程的拉活。
方案适用范围:该方案适用于所有的 Android 版本,包括被 forestop 掉的进程也可以进行拉活。
最新 Android 版本(Android N)中系统好像对账户同步这里做了变动,该方法不再有效。
其他
其他还有一些技术之外的措施,比如说应用内 Push 通道的选择:
- 国外版应用:接入 Google 的 GCM。
- 国内版应用:根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送;其他手机可以考虑接入腾讯信鸽或极光推送与小米推送做 A/B Test。
4. IntentService与Service的区别?IntentService的实现原理。
本人已经在另外一篇文章分析过,麻烦移步。[IntentService-你可能需要知道这些](http://www.jianshu.com/p/6b9358cbfc26)
5. 优雅的展示Bitmap大图
郭霖大神的文章,移步。[优雅的展示BitMap大图](http://blog.csdn.net/guolin_blog/article/details/9316683)
6. Retrofit使用的注解是哪种注解?以及,注解的底层实现是怎样的。
Retrofit采用的是运行时注解,下面上实锤(@GET):
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
/**
* A relative or absolute path, or full URL of the endpoint. This value is optional if the first
* parameter of the method is annotated with {@link Url @Url}.
* <p>
* See {@linkplain retrofit2.Retrofit.Builder#baseUrl(HttpUrl) base URL} for details of how
* this is resolved against a base URL to create the full endpoint URL.
*/
String value() default "";
}
我们可以看到@GET的@Retention(RUNTIME),为运行时注解无疑。
关于注解,本人写过一篇文章分析,麻烦移步。注解-你可能需要知道这些
7. Thread和HandlerThread
简单的总结下,两者的区别:
- HandlerThread是Thread的子类。
- HandlerThread内部持有一个Looper,可以使用MessageQueue重复使用当前线程,节省系统开销。
- HandlerThread一般结合Handler使用,按顺序处理任务。
HandlerThread的源码很简单,我们来简单分析下。
public class HandlerThread extends Thread {
int mPriority;
int mTid = -1;
Looper mLooper;
public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
}
/**
* Constructs a HandlerThread.
* @param name
* @param priority The priority to run the thread at. The value supplied must be from
* {@link android.os.Process} and not from java.lang.Thread.
*/
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}
/**
* Call back method that can be explicitly overridden if needed to execute some
* setup before Looper loops.
*/
protected void onLooperPrepared() {
}
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
/**
* This method returns the Looper associated with this thread. If this thread not been started
* or for any reason is isAlive() returns false, this method will return null. If this thread
* has been started, this method will block until the looper has been initialized.
* @return The looper.
*/
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;
}
/**
* Quits the handler thread's looper.
* <p>
* Causes the handler thread's looper to terminate without processing any
* more messages in the message queue.
* </p><p>
* Any attempt to post messages to the queue after the looper is asked to quit will fail.
* For example, the {@link Handler#sendMessage(Message)} method will return false.
* </p><p class="note">
* Using this method may be unsafe because some messages may not be delivered
* before the looper terminates. Consider using {@link #quitSafely} instead to ensure
* that all pending work is completed in an orderly manner.
* </p>
*
* @return True if the looper looper has been asked to quit or false if the
* thread had not yet started running.
*
* @see #quitSafely
*/
public boolean quit() {
Looper looper = getLooper();
if (looper != null) {
looper.quit();
return true;
}
return false;
}
/**
* Quits the handler thread's looper safely.
* <p>
* Causes the handler thread's looper to terminate as soon as all remaining messages
* in the message queue that are already due to be delivered have been handled.
* Pending delayed messages with due times in the future will not be delivered.
* </p><p>
* Any attempt to post messages to the queue after the looper is asked to quit will fail.
* For example, the {@link Handler#sendMessage(Message)} method will return false.
* </p><p>
* If the thread has not been started or has finished (that is if
* {@link #getLooper} returns null), then false is returned.
* Otherwise the looper is asked to quit and true is returned.
* </p>
*
* @return True if the looper looper has been asked to quit or false if the
* thread had not yet started running.
*/
public boolean quitSafely() {
Looper looper = getLooper();
if (looper != null) {
looper.quitSafely();
return true;
}
return false;
}
/**
* Returns the identifier of this thread. See Process.myTid().
*/
public int getThreadId() {
return mTid;
}
}
从源码中,我们可以看到,HandlerThread继承于Thread。
重点来看下run方法:
@Override
public void run() {
mTid = Process.myTid();
// 为当前线程设置Looper
Looper.prepare();
synchronized (this) {
// getLooper()返回的就是mLooper
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
// 开始处理消息队列
Looper.loop();
mTid = -1;
}
Handler+Thread的方式,在开发中我们经常使用到,我们来对比下HandlerThread+Handler和Handler+Thread的实现方式。
- Handler+Thread
private MyHandler mHandler;
public void buildHandler() {
new Thread(new Runnable() {
@Override
public void run() {
// 为当前线程设置Looper
Looper.prepare();
// 使用当前线程的Looper构造Handler
mHandler = new MyHandler(Looper.myLooper());
// 开始处理MessageQueue
Looper.loop();
}
}).start();
}
class MyHandler extends Handler {
MyHandler(Looper looper){
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
- HandlerThread+Handler
private MyHandler mHandler;
public void buildHandler() {
// 构造HandlerThread
HandlerThread handlerThread = new HandlerThread("WorkThread");
handlerThread.start();
// 直接使用HandlerThread的looper创建Handler
mHandler = new MyHandler(handlerThread.getLooper());
}
class MyHandler extends Handler {
MyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
结合HandlerThread的源码和两种方式的对比,验证了我们在开头的总结。
- HandlerThread是Thread的子类。
2.HandlerThread内部持有一个Looper,可以使用MessageQueue重复使用当前线程,节省系统开销。 - HandlerThread一般结合Handler使用,按顺序处理任务。
8. Java是值传递还是引用传递
Java中方法参数传递方式是按值传递。
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
9. final和static的区别
static
static变量
按照是否静态的对类成员变量进行分类可分两种:
- 一种是被static修饰的变量,叫静态变量或类变量。
- 另一种是没有被static修饰的变量,叫实例变量。
两者的区别是:
静态变量在内存中只有一个拷贝(节省内存),JVM只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
实例变量属于某个确定的实例,每当创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。
static代码块
static代码块是类加载时,初始化自动执行的。如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
static方法
static方法是被static修饰的方法,可以通过类名直接调用,因此static方法不能使用this和super关键字(实例),不能访问类实例变量和方法,只能访问静态变量和静态方法;因为其属于类而独立于任何实例,所以必须为非抽象。
因为方法属于类,所以子类和父类可以存在同名的方法,但是不涉及重载(不能被继承)。
final
final变量
声明 final 字段有助于编译器作出更好的优化决定,因为如果编译器知道字段的值不会更改,那么它能安全地在寄存器中高速缓存该值。
final 字段还通过让编译器强制该字段为只读来提供额外的安全级别。
其初始化可以在两个地方:
- 一是其定义处,也就是说在final变量定义时直接给其赋值
- 二是在构造函数中。
这两个地方只能选其一,要么在定义时给值,要么在构造函数中给值,不能同时既在定义时给了值,又在构造函数中给另外的值。不能通过调用方法来赋值。
一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。
final方法
- 把方法锁定,防止任何继承类修改它的意义和实现。
- 高效。编译器在遇到调用final方法时候会转入内嵌inline机制,大大提高执行效率。
final类
final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。
在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类
Note:同时被static和final修饰的变量,必须在声明时立即赋值。
可以同时使用static和final来修饰方法,但是意义不大。因为被static修饰的方法本身就是无
法被继承的。
10. HashMap的实现原理
本人已经分析了HashMap的实现原理,请移步。HashMap-你可能需要知道这些
11. HashMap和HashSet的区别
HashMap | HashSet |
---|---|
HashMap实现了Map接口 | HashSet实现了Set接口 |
HashMap存储键值对 | HashSet存储对象 |
使用put方法添加值 | 使用add方法添加值 |
HashMap中使用键对象来计算hashcode值 | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
HashMap比较快,因为是使用唯一的键来获取对象 | HashSet较HashMap来说比较慢 |
HashSet取值的时候部分情况会比HashMap快,因为HashSet不允许重复值,hash到那个地方直接取值了,而HashMap有可能有下拉链表,这样就要再遍历链表了;算法两者用的都是hash算法,而hashMap可能多一步链表遍历,所以肯定是HashSet部分情况比hashMap快。
HashSet不允许存在重复的值。(hashCode和equals来判断)
12. 浅拷贝&深拷贝区别
浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值,这个方式称为深拷贝。
也就是说浅拷贝只复制一个对象,传递引用,不能复制实例。而深拷贝对对象内部的引用均复制,它是创建一个新的实例,并且复制实例。
深拷贝实现方式:
- 对其引用变量逐个深拷贝并复制给新对象。
- 实现序列化接口,并结合流进行写入和读出。(ByteArrayOutputStream&ObjectInputStream)。
public Object deepClone() throws Exception{
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
13. 静态代理&动态代理
移步本人的文章,注解-你可能需要知道这些.
14. LayoutInflater.inflate有几种使用方式
LayoutInflater的获取:
- LayoutInflater.from(context)
- LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
第一种方式是封装了第二种方式。
inflate方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
总结的话有三种:
- resource!=null,root==null,attachToRott=false,返回解析到的resource的布局,布局参数无效。
- resource!=null,root!=null,attachToRott=false,返回解析到的resource的布局,布局参数有效。
- resource!=null,root!=null,attachToRott=true,返回root,resource被添加到root中,布局参数有效。
15.MVC与MVP的区别
MVC:
- Activity不仅要显示UI,还担任了一部分Controller的职责
- 请求的业务代码往往被丢到了Activity里面,布局文件只能提供默认的UI设置,所以开发中视图层的变化也被丢到了Activity里面。
- 再加上Activity本身承担着控制层的责任。所以Activity达成了MVC集合的成就,实现了代码的耦合,最终我们的Activity就变得越来越难看,从几百行变成了几千行。维护的成本也越来越高。
MVP:
MVP与MVC最大的不同,其实是Activity职责的变化,由原来的C (控制层) 变成了 V(视图层),不再管控制层的问题,只管如何去显示。
控制层的角色就由我们的新人 Presenter来担当,这种架构就解决了Activity过度耦合控制层和视图层的问题。
16. Activity有没有事件分发机制?
有,必须有。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity是事件分发的起点,只有Activity不拦截处理事件,事件才会分发至Window--DecorView--View。
17. 引用的分类和区别
引用主要分为四种类型:
强引用
Object object = new Object(),object就是一个强引用了。
当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。软引用
只有内存不够时才回收,常用于缓存;当内存达到一个阀值,GC就会去回收它;弱引用
弱引用的对象拥有更短暂的生命周期。
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。虚引用
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
18. Synchronized
作用位置:
- synchronized 方法
- synchronized static 方法
- 代码块
synchronized方法:
- synchronized方法控制对类成员变量的访问.
- 每个类实例对应一把锁,每个synchronized方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态.
- 同一时刻对于每一个类实例,其所有声明为 synchronized 的成员方法中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized.
synchronized static 方法:
某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
Class Foo {
// 同步的static 函数
public synchronized static void methodAAA() {
//….
}
public void methodBBB() {
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
synchronized代码块:
- 一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
- 也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
19. 解耦的本质
设计模式中的几大原则:
- SRP:单一职责原则,一个类应该仅有一个引起它变化的原因。
- OCP:开闭原则,对于扩展开发,对于修改封闭。
- 里氏替换原则,所有使用基类的地方必须能透明的使用其子类对象。
- DIP:依赖倒置原则。指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次模块的实现细节,依赖模块被颠倒了。
a. 高层模块不应该依赖于低层次模块,两者都应该依赖于抽象。
b. 抽象不应该依赖于细节,细节应该依赖于抽象。
总结:模块间的依赖通过抽象发生,实现类不应该发生直接的依赖关系,依赖关系是通过接口或者抽象实现。 - ISP:接口隔离原则,类之间的依赖关系应该建立在最小的接口之上。
- LOD:迪米特原则,一个对象应该对其他对象有最少的了解。
那么很明确了,解耦的本质就是DIP原则。
20. Android运行时权限
权限分类
危险权限
phone
读写外部存储
camera
如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。
弹出的权限请求dialog描述的是一组权限,无法定制
正常权限
获取网络状态
如何检查和申请
检查
checkSlefPermission
deny
grant
请求
requestPermissions
异步方法
处理的回调方法:onRequestPermissionsResult
解释
shouldShowRequestPermissionRationale
只有在第一次用户已经拒绝情况下,该方法才会返回true
开源框架
MPermissions
21. Activity-Window-View
- 每个Activity在创建的时候,都会产生一个Window对象,这个Window对象实际为PhoneWindow。
- PhoneWindow对象包含一个根View:DecorView,Activity要显示的内容就包含在DecorView中。
setContentView的显示流程
Activity setContentView->PhoneWindow setContentView ->mLayoutInflater.inflate(layoutResID, mContentParent) -> onResume时将DecorView添加至WindowManager ->WindowManagerImpl.addView--ViewRootImpl.setView ->1. ViewRootImpl.requestLayout -> 2. 向WMS发起显示Window的请求
22. Activity与Fragment通信方式
- Handler
- 接口
- EventBus
- 广播
23. Fragment的坑
getActivity为空
在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些)
Can not perform this action after onSaveInstanceState
在重新回到该Activity的时候(onResumeFragments()或onPostResume()),再执行该事务!
Fragment重叠异常
在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),导致重复加载了同一个Fragment导致重叠。
24. 在向Fragment传递参数时,为什么不采用构造方法中传递?
- 如果通过构造参数进行传递,那么在Fragment销毁重建时参数就无法保持了。
- 一般通过setArguments传递参数,从而在Fragment销毁重建时保持数据。
那么参数是如何保持的呢?
Activity--onSaveInstanceState
protected void onSaveInstanceState(Bundle outState) {
// 保存Activity的视图状态
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
// 获取了所有Fragment的状态并保存到了outState中
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
getApplication().dispatchActivitySaveInstanceState(this, outState);
}
Activity--onCreate
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
if (mLastNonConfigurationInstances != null) {
mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
}
if (mActivityInfo.parentActivityName != null) {
if (mActionBar == null) {
mEnableDefaultActionBarUp = true;
} else {
mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
}
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
// 恢复保存的Fragment状态
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
}
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
if (mVoiceInteractor != null) {
mVoiceInteractor.attachActivity(this);
}
mCalled = true;
}
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (mWindow != null) {
// 恢复视图状态
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
mWindow.restoreHierarchyState(windowState);
}
}
}
从上面两个接口我们可以看到,在Activity销毁重建时会保存恢复Fragment的状态,但是我们还是没有看到setArguments的参数保持啊,接着往下看。
public FragmentState(Fragment frag) {
mClassName = frag.getClass().getName();
mIndex = frag.mIndex;
mFromLayout = frag.mFromLayout;
mFragmentId = frag.mFragmentId;
mContainerId = frag.mContainerId;
mTag = frag.mTag;
mRetainInstance = frag.mRetainInstance;
mDetached = frag.mDetached;
mArguments = frag.mArguments;
mHidden = frag.mHidden;
}
Parcelable p = mFragments.saveAllState();
保存Fragment信息:
public Parcelable saveAllState() {
return mHost.mFragmentManager.saveAllState();
}
// FragmentManager->saveAllState
Parcelable saveAllState() {
// Make sure all pending operations have now been executed to get
// our state update-to-date.
execPendingActions();
mStateSaved = true;
if (mActive == null || mActive.size() <= 0) {
return null;
}
// First collect all active fragments.
int N = mActive.size();
FragmentState[] active = new FragmentState[N];
boolean haveFragments = false;
for (int i=0; i<N; i++) {
Fragment f = mActive.get(i);
if (f != null) {
if (f.mIndex < 0) {
throwException(new IllegalStateException(
"Failure saving state: active " + f
+ " has cleared index: " + f.mIndex));
}
haveFragments = true;
// 构造待保存Fragment的状态信息(变量)
// FragmentState会记录Fragment的mArguments
FragmentState fs = new FragmentState(f);
active[i] = fs;
if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
// 保存fragment的一些视图状态信息
fs.mSavedFragmentState = saveFragmentBasicState(f);
if (f.mTarget != null) {
if (f.mTarget.mIndex < 0) {
throwException(new IllegalStateException(
"Failure saving state: " + f
+ " has target not in fragment manager: " + f.mTarget));
}
if (fs.mSavedFragmentState == null) {
fs.mSavedFragmentState = new Bundle();
}
putFragment(fs.mSavedFragmentState,
FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget);
if (f.mTargetRequestCode != 0) {
fs.mSavedFragmentState.putInt(
FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG,
f.mTargetRequestCode);
}
}
} else {
fs.mSavedFragmentState = f.mSavedFragmentState;
}
if (DEBUG) Log.v(TAG, "Saved state of " + f + ": "
+ fs.mSavedFragmentState);
}
}
if (!haveFragments) {
if (DEBUG) Log.v(TAG, "saveAllState: no fragments!");
return null;
}
int[] added = null;
BackStackState[] backStack = null;
// Build list of currently added fragments.
if (mAdded != null) {
N = mAdded.size();
if (N > 0) {
added = new int[N];
for (int i=0; i<N; i++) {
added[i] = mAdded.get(i).mIndex;
if (added[i] < 0) {
throwException(new IllegalStateException(
"Failure saving state: active " + mAdded.get(i)
+ " has cleared index: " + added[i]));
}
if (DEBUG) Log.v(TAG, "saveAllState: adding fragment #" + i
+ ": " + mAdded.get(i));
}
}
}
// Now save back stack.
if (mBackStack != null) {
N = mBackStack.size();
if (N > 0) {
backStack = new BackStackState[N];
for (int i=0; i<N; i++) {
backStack[i] = new BackStackState(this, mBackStack.get(i));
if (DEBUG) Log.v(TAG, "saveAllState: adding back stack #" + i
+ ": " + mBackStack.get(i));
}
}
}
FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
return fms;
}
恢复信息:
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
// 检查保存的状态信息
if (state == null) return;
FragmentManagerState fms = (FragmentManagerState)state;
if (fms.mActive == null) return;
List<FragmentManagerNonConfig> childNonConfigs = null;
// First re-attach any non-config instances we are retaining back
// to their saved state, so we don't try to instantiate them again.
if (nonConfig != null) {
List<Fragment> nonConfigFragments = nonConfig.getFragments();
childNonConfigs = nonConfig.getChildNonConfigs();
final int count = nonConfigFragments != null ? nonConfigFragments.size() : 0;
for (int i = 0; i < count; i++) {
Fragment f = nonConfigFragments.get(i);
if (DEBUG) Log.v(TAG, "restoreAllState: re-attaching retained " + f);
FragmentState fs = fms.mActive[f.mIndex];
fs.mInstance = f;
f.mSavedViewState = null;
f.mBackStackNesting = 0;
f.mInLayout = false;
f.mAdded = false;
f.mTarget = null;
if (fs.mSavedFragmentState != null) {
fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader());
f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(
FragmentManagerImpl.VIEW_STATE_TAG);
f.mSavedFragmentState = fs.mSavedFragmentState;
}
}
}
// Build the full list of active fragments, instantiating them from
// their saved state.
mActive = new ArrayList<>(fms.mActive.length);
if (mAvailIndices != null) {
mAvailIndices.clear();
}
for (int i=0; i<fms.mActive.length; i++) {
FragmentState fs = fms.mActive[i];
if (fs != null) {
FragmentManagerNonConfig childNonConfig = null;
if (childNonConfigs != null && i < childNonConfigs.size()) {
childNonConfig = childNonConfigs.get(i);
}
// 恢复Fragment
// 通过FragmentState来生成新的Fragment,之前保存到恶状态信息得以恢复
Fragment f = fs.instantiate(mHost, mParent, childNonConfig);
if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
mActive.add(f);
fs.mInstance = null;
} else {
mActive.add(null);
if (mAvailIndices == null) {
mAvailIndices = new ArrayList<>();
}
if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i);
mAvailIndices.add(i);
}
}
// Update the target of all retained fragments.
if (nonConfig != null) {
List<Fragment> nonConfigFragments = nonConfig.getFragments();
final int count = nonConfigFragments != null ? nonConfigFragments.size() : 0;
for (int i = 0; i < count; i++) {
Fragment f = nonConfigFragments.get(i);
if (f.mTargetIndex >= 0) {
if (f.mTargetIndex < mActive.size()) {
f.mTarget = mActive.get(f.mTargetIndex);
} else {
Log.w(TAG, "Re-attaching retained fragment " + f
+ " target no longer exists: " + f.mTargetIndex);
f.mTarget = null;
}
}
}
}
// Build the list of currently added fragments.
if (fms.mAdded != null) {
mAdded = new ArrayList<Fragment>(fms.mAdded.length);
for (int i=0; i<fms.mAdded.length; i++) {
Fragment f = mActive.get(fms.mAdded[i]);
if (f == null) {
throwException(new IllegalStateException(
"No instantiated fragment for index #" + fms.mAdded[i]));
}
f.mAdded = true;
if (DEBUG) Log.v(TAG, "restoreAllState: added #" + i + ": " + f);
if (mAdded.contains(f)) {
throw new IllegalStateException("Already added!");
}
mAdded.add(f);
}
} else {
mAdded = null;
}
// Build the back stack.
if (fms.mBackStack != null) {
mBackStack = new ArrayList<BackStackRecord>(fms.mBackStack.length);
for (int i=0; i<fms.mBackStack.length; i++) {
BackStackRecord bse = fms.mBackStack[i].instantiate(this);
if (DEBUG) {
Log.v(TAG, "restoreAllState: back stack #" + i
+ " (index " + bse.mIndex + "): " + bse);
LogWriter logw = new LogWriter(Log.VERBOSE, TAG);
PrintWriter pw = new FastPrintWriter(logw, false, 1024);
bse.dump(" ", pw, false);
pw.flush();
}
mBackStack.add(bse);
if (bse.mIndex >= 0) {
setBackStackIndex(bse.mIndex, bse);
}
}
} else {
mBackStack = null;
}
}
FragmentState.instantiate
public Fragment instantiate(FragmentHostCallback host, Fragment parent,
FragmentManagerNonConfig childNonConfig) {
if (mInstance == null) {
final Context context = host.getContext();
if (mArguments != null) {
mArguments.setClassLoader(context.getClassLoader());
}
mInstance = Fragment.instantiate(context, mClassName, mArguments);
if (mSavedFragmentState != null) {
mSavedFragmentState.setClassLoader(context.getClassLoader());
mInstance.mSavedFragmentState = mSavedFragmentState;
}
mInstance.setIndex(mIndex, parent);
mInstance.mFromLayout = mFromLayout;
mInstance.mRestored = true;
mInstance.mFragmentId = mFragmentId;
mInstance.mContainerId = mContainerId;
mInstance.mTag = mTag;
mInstance.mRetainInstance = mRetainInstance;
mInstance.mDetached = mDetached;
mInstance.mHidden = mHidden;
mInstance.mFragmentManager = host.mFragmentManager;
if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG,
"Instantiated fragment " + mInstance);
}
mInstance.mChildNonConfig = childNonConfig;
return mInstance;
}
25. RecyclerView
- 为什么要使用RecyclerView?
===提供了插拔式体验,高度解耦
===内部实现了ViewHolder机制,因此使用上更方便
- 你想要控制其显示的方式,请通过布局管理器LayoutManager
- 你想要控制Item间的间隔(可绘制),请通过ItemDecoration
- 你想要控制Item增删的动画,请通过ItemAnimator
- 你想要控制点击、长按事件,请自己写(擦,这点尼玛。)
Adapter的几个主要方法:
- public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
- public abstract void onBindViewHolder(VH holder, int position);