十八:注解是什么?有哪些元注解
注解,在我看来它是一种信息描述,不影响代码执行,但是可以用来配置一些代码或者功能。常见的注解比如@Override,代表重写方法,看看它是怎么生成的:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
可以看到Override被@interface所修饰,代表注解,同时上方还有两个注解@Target和@Retention,这种修饰注解的注解叫做元注解,很好理解吧,就是最基本的注解呗。java中一共有四个元注解:
- @Target:表示注解对象的作用范围。
- @Retention:表示注解保留的生命周期
- @Inherited:表示注解类型能被类自动继承。
- @Documented:表示含有该注解类型的元素(带有注释的)会通过javadoc或类似工具进行文档化。
具体说下这几个元注解都是怎么用的
- @Target
target,表示注解对象的作用范围,比如Override注解所标示的就是ElementType.METHOD,即所作用的范围是方法范围,也就是只能在方法头上加这个注解。另外还有以下几个修饰范围参数:
- TYPE:类、接口、枚举、注解类型。
- FIELD:类成员(构造方法、方法、成员变量)。
- METHOD:方法。
- PARAMETER:参数。
- CONSTRUCTOR:构造器。
- LOCAL_VARIABLE:局部变量。
- ANNOTATION_TYPE:注解。
- PACKAGE:包声明。
- TYPE_PARAMETER:类型参数。
- TYPE_USE:类型使用声明。
比如ANNOTATION_TYPE就是表示该注解的作用范围就是注解,哈哈,有点绕吧,看看Target注解的代码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
带了一个ElementType类型的参数,也就是上面说到的作用范围参数,另外还被Target注解修饰了,传的参数就是ANNOTATION_TYPE,也就是我注解我自己,我设置我自己的作用范围是注解。大家自己绕一下。。
- @Retention
表示注解保留的生命周期,或者说表示该注解所保留的时长,主要有以下几个可选参数:
- SOURCE:仅存在Java源文件,经过编译器后便丢弃相应的注解。适用于一些检查性的操作,比如@Override。
- CLASS:编译class文件时生效,存在Java源文件,以及经编译器后生成的Class字节码文件,但在运行时VM不再保留注释。这个也是默认的参数。适用于在编译时进行一些预处理操作,比如ButterKnife的@BindView,可以在编译时生成一些辅助的代码或者完成一些功能。
- RUNTIME:存在源文件、编译生成的Class字节码文件,以及保留在运行时VM中,可通过反射性地读取注解。适用于一些需要运行时动态获取注解信息,类似反射获取注解等。
- @Inherited
表示注解类型能被类自动继承。这里需要注意两点:
- 类。也就是说只有在类集成关系中,子类才会集成父类使用的注解中被@Inherited所修饰的那个注解。其他的接口集成关系,类实现接口关系中,都不会存在自动继承注解。
- 自动继承。也就是说如果父类有@Inherited所修饰的那个注解,那么子类不需要去写这个注解,就会自动有了这个注解。
还是看个例子:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface MyInheritedAnnotation {
//注解1,有Inherited注解修饰
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
//注解2,没有Inherited注解修饰
}
@MyInheritedAnnotation
@MyAnnotation
public class BaseClass {
//父类,有以上两个注解
}
public class ExtendClass extends BaseClass {
//子类会继承父类的MyInheritedAnnotation注解,
//而不会继承MyAnnotation注解
}
- @Documented
表示拥有该注解的元素可通过javadoc此类的工具进行文档化,也就是说生成JavaAPI文档的时候会被写进文档中。
注解可以用来做什么
主要有以下几个用处:
- 降低项目的耦合度。
- 自动完成一些规律性的代码。
- 自动生成java代码,减轻开发者的工作量。
十九:Activity从创建到我们看到界面,发生了哪些事
首先是通过setContentView加载布局,这其中创建了一个DecorView,然后根据activity设置的主题(theme)或者特征(Feature)加载不同的根布局文件,最后再通过inflate方法加载layoutResID资源文件,其实就是解析了xml文件,根据节点生成了View对象。流程图:
其次就是进行view绘制到界面上,这个过程发生在handleResumeActivity方法中,也就是触发onResume的方法。在这里会创建一个ViewRootImpl对象,作为DecorView的parent然后对DecorView进行测量布局和绘制三大流程。流程图:
二十:Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?
- PhoneWindow是Window 的唯一子类,每个Activity都会创建一个PhoneWindow对象,你可以理解它为一个窗口,但不是真正的可视窗口,而是一个管理类,是Activity和整个View系统交互的接口,是Activity和View交互系统的中间层。
- DecorView是PhoneWindow的一个内部类,是整个View层级的最顶层,一般包括标题栏和内容栏两部分,会根据不同的主题特性调整不同的布局。它是在setContentView方法中被创建,具体点来说是在PhoneWindow的installDecor方法中被创建。
- ViewRootImpl是DecorView的parent,用来控制View的各种事件,在handleResumeActivity方法中被创建。
二十一:requestLayout和invalidate
requestLayout方法是用来触发绘制流程,他会一层层调用 parent 的requestLayout,一直到最上层也就是ViewRootImpl的requestLayout,这里也就是判断线程的地方了,最后会执行到performMeasure -> performLayout -> performDraw 三个绘制流程,也就是测量——布局——绘制。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();//执行绘制流程
}
}
其中performMeasure方法会执行到View的measure方法,用来测量大小。performLayout方法会执行到view的layout方法,用来计算位置。performDraw方法需要注意下,他会执行到view的draw方法,但是并不一定会进行绘制,只有 flag 被设置为 PFLAG_DIRTY_OPAQUE 才会进行绘制。
invalidate方法也是用来触发绘制流程,主要表现就是会调用draw()方法。虽然他也会走到scheduleTraversals方法,也就是会走到三大流程,但是View会通过mPrivateFlags来判断是否进行onMeasure和onLayout操作。而在用invalidate方法时,更新了mPrivateFlags,所以不会进行measure和layout。同时他也会设置Flag为PFLAG_DIRTY_OPAQUE,所以肯定会执行onDraw方法。
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
//...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();//执行绘制流程
}
}
最后看一下scheduleTraversals方法中三大绘制流程逻辑,是不是我们之前说的那样,FORCE_LAYOUT标志才会onMeasure和onLayout,PFLAG_DIRTY_OPAQUE标志才会onDraw:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 只有mPrivateFlags为PFLAG_FORCE_LAYOUT的时候才会进行onMeasure方法
if (forceLayout || needsLayout) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// 设置 LAYOUT_REQUIRED flag
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
public void layout(int l, int t, int r, int b) {
...
//判断标记位为PFLAG_LAYOUT_REQUIRED的时候才进行onLayout方法
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
// flag 是 PFLAG_DIRTY_OPAQUE 则需要绘制
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
if (!dirtyOpaque) {
drawBackground(canvas);
}
if (!dirtyOpaque) onDraw(canvas);
// 绘制 Child
dispatchDraw(canvas);
// foreground 不管 dirtyOpaque 标志,每次都会绘制
onDrawForeground(canvas);
参考文章中有一段总结挺好的:
虽然两者都是用来触发绘制流程,但是在measure和layout过程中,只会对 flag 设置为 FORCE_LAYOUT 的情况进行重新测量和布局,而draw方法中只会重绘flag为 dirty 的区域。requestLayout 是用来设置FORCE_LAYOUT标志,invalidate 用来设置 dirty 标志。所以 requestLayout 只会触发 measure 和 layout,invalidate 只会触发 draw。
二十二:系统为什么提供Handler
这点大家应该都知道一些,就是为了切换线程,主要就是为了解决在子线程无法访问UI的问题。
那么为什么系统不允许在子线程中访问UI呢?
- 因为Android的UI控件不是线程安全的,所以采用单线程模型来处理UI操作,通过Handler切换UI访问的线程即可。
那么为什么不给UI控件加锁呢?
- 因为加锁会让UI访问的逻辑变得复杂,而且会降低UI访问的效率,阻塞线程执行。
Handler是怎么获取到当前线程的Looper的?
- 大家应该都知道Looper是绑定到线程上的,他的作用域就是线程,而且不同线程具有不同的Looper,也就是要从不同的线程取出线程中的Looper对象,这里用到的就是ThreadLocal。
假设我们不知道有这个类,如果要完成这样一个需求,从不同的线程获取线程中的Looper,是不是可以采用一个全局对象,比如hashmap,用来存储线程和对应的Looper?
所以需要一个管理Looper的类,但是,线程中并不止这一个要存储和获取的数据,还有可能有其他的需求,也是跟线程所绑定的。所以,我们的系统就设计出了ThreadLocal这种工具类。
ThreadLocal的工作流程是这样的:我们从不同的线程可以访问同一个ThreadLocal的get方法,然后ThreadLocal会从各自的线程中取出一个数组,然后再数组中通过ThreadLocal的索引找出对应的value值。
具体逻辑呢,我们还是看看代码,分别是ThreadLocal的get方法和set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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();
}
set方法,获取到当前线程,然后取出线程中的threadLocals变量,是一个ThreadLocalMap类,然后将当前的ThreadLocal作为key,要设置的值作为value存到这个map中。
get方法就同理了,还是获取到当前线程,然后取出线程中的ThreadLocalMap实例,然后从中取到当前ThreadLocal对应的值。
其实可以看到,操作的对象都是线程中的ThreadLocalMap实例,也就是读写操作都只限制在线程内部,这也就是ThreadLocal故意设计的精妙之处了,他可以在不同的线程进行读写数据而且线程之间互不干扰。
画个图方便理解记忆:
当MessageQueue 没有消息的时候,在干什么,会占用CPU资源吗。
- MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 方法这里。具体就是会调用到nativePollOnce方法里,最终调用到epoll_wait()进行阻塞等待。
这时,主线程会进行休眠状态,也就不会消耗CPU资源。当下个消息到达的时候,就会通过pipe管道写入数据然后唤醒主线程进行工作。
这里涉及到阻塞和唤醒的机制叫做 epoll 机制。
先说说文件描述符和I/O多路复用:
在Linux操作系统中,可以将一切都看作是文件,而文件描述符简称fd,当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符,可以理解为一个索引值。
I/O多路复用是一种机制,让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
所以I/O多路复用其实就是一种监听读写的通知机制,而Linux提供的三种 IO 复用方式分别是:select、poll 和 epoll 。而这其中epoll是性能最好的多路I/O就绪通知方法。
所以,这里用到的epoll其实就是一种I/O多路复用方式,用来监控多个文件描述符的I/O事件。通过epoll_wait方法等待I/O事件,如果当前没有可用的事件则阻塞调用线程。
二十三:Binder通信过程和原理
首先,还是看一张图,原图也是出自神书中:
首先要明确的是客户端进程是无法直接操作服务端中的类和方法的,因为不同进程直接是不共享资源的。所以客户端这边操作的只是服务端进程的一个代理对象,也就是一个服务端的类引用,也就是Binder引用。
总体通信流程就是:
- 客户端通过代理对象向服务器发送请求。
- 代理对象通过Binder驱动发送到服务器进程
- 服务器进程处理请求,并通过Binder驱动返回处理结果给代理对象
- 代理对象将结果返回给客户端。
再看看在我们应用中常常用到的工作模型,上图:
这就是在应用层面我们常用的工作模型,通过ServiceManager去获取各种系统进程服务。这里的通信过程如下:
- 服务端跨进程的类都要继承Binder类,所以也就是服务端对应的Binder实体。这个类并不是实际真实的远程Binder对象,而是一个Binder引用(即服务端的类引用),会在Binder驱动里还要做一次映射。
- 客户端要调用远程对象函数时,只需把数据写入到Parcel,在调用所持有的Binder引用的transact()函数
- transact函数执行过程中会把参数、标识符(标记远程对象及其函数)等数据放入到Client的共享内存,Binder驱动从Client的共享内存中读取数据,根据这些数据找到对应的远程进程的共享内存。
- 然后把数据拷贝到远程进程的共享内存中,并通知远程进程执行onTransact()函数,这个函数也是属于Binder类。
- 远程进程Binder对象执行完成后,将得到的写入自己的共享内存中,Binder驱动再将远程进程的共享内存数据拷贝到客户端的共享内存,并唤醒客户端线程。
所以通信过程中比较重要的就是这个服务端的Binder引用,通过它来找到服务端并与之完成通信。
看到这里可能有的人疑惑了,图中线程池怎么没用到啊?
- 可以从第一张图中看出,Binder线程池位于服务端,它的主要作用就是将每个业务模块的Binder请求统一转发到远程Servie中去执行,从而避免了重复创建Service的过程。也就是服务端只有一个,但是可以处理多个不同客户端的Binder请求。
二十四:Binder在Android中的应用
Binder在Android中的应用除了刚才的ServiceManager,你还想到了什么呢?
- 系统服务是用过getSystemService获取的服务,内部也就是通过ServiceManager。例如四大组件的启动调度等工作,就是通过Binder机制传递给ActivityManagerService,再反馈给Zygote。而我们自己平时应用中获取服务也是通过getSystemService(getApplication().WINDOW_SERVICE)代码获取。
- AIDL(Android Interface definition language)。例如我们定义一个IServer.aidl文件,aidl工具会自动生成一个IServer.java的java接口类(包含Stub,Proxy等内部类)。
- 前台进程通过bindService绑定后台服务进程时,onServiceConnected(ComponentName name, IBinder service)传回IBinder对象,并且可以通过IServer.Stub.asInterface(service)获取IServer的内部类Proxy的对象,其实现了IServer接口。
二十五:Binder优势
在Linux中,进程通信的方式肯定不止Binder这一种,还有以下这些:
管道(Pipe) 信号(Signal) 消息队列(Message) 共享内存(Share Memory) 套接字(Socket) Binder
而Binder在这之后主要有以下优点:
- 性能高,效率高:传统的IPC(套接字、管道、消息队列)需要拷贝两次内存、Binder只需要拷贝一次内存、共享内存不需要拷贝内存。
- 安全性好:接收方可以从数据包中获取发送发的进程Id和用户Id,方便验证发送方的身份,其他IPC想要实验只能够主动存入,但是这有可能在发送的过程中被修改。
熟悉Zygote的朋友可能知道,在fork()进程的时候,也就是向Zygote进程发出创建进程的消息的时候,用到的进程间通信方式就不是Binder了,而换成了Socket,这主要是因为fork不允许存在多线程,Binder通讯偏偏就是多线程。
所以具体的情况还是要去具体选择合适的IPC方式。