具体说下这几个元注解都是怎么用的
- @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代码
,减轻开发者的工作量。
序列化指的是什么?有什么用
序列化指的是讲对象变成有序的字节流
,变成字节流之后才能进行传输存储等一系列操作。反序列化就是序列化的相反操作
,也就是把序列化生成的字节流转为我们内存的对象。
介绍下Android中两种序列化接口
- Serializable
是Java
提供的一个序列化接口,是一个空接口,专门为对象提供序列化和反序列化操作。具体使用如下:
public class User implements Serializable { private static final long serialVersionUID=519067123721561165l; private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } }
实现Serializable
接口,声明一个serialVersionUID
。
到这里可能有人就问了,不对啊,平时没有这个serialVersionUID
啊。没错,serialVersionUID
不是必须的,因为不写的话,系统会自动生成这个变量。它有什么用呢?当序列化的时候,系统会把当前类的serialVersionUID
写入序列化的文件中,当反序列化的时候会去检测这个serialVersionUID
,看他是否和当前类的serialVersionUID
一致,一样则可以正常反序列化,如果不一样就会报错了。
所以这个serialVersionUID
就是序列化和反序列化过程中的一个标识,代表一致性。不加的话会有什么影响?如果我们序列化后,改动了这个类的某些成员变量,那么serialVersionUID
就会改变,这时候再拿之前序列化的数据来反序列化就会报错。所以如果我们手动指定serialVersionUID
就能保证最大限度来恢复数据。
- Parcelable
Android自带的接口,使用起来要麻烦很多:需要实现Parcelable接口,重写describeContents(),writeToParcel(Parcel dest, @WriteFlags int flags)
,并添加一个静态成员变量CREATOR
并实现Parcelable.Creator
接口
public class User implements Parcelable { private int id; protected User(Parcel in) { id = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); } @Override public int describeContents() { return 0; } public static final Creator<User> CREATOR = new Creator<User>() { @Override public User createFromParcel(Parcel in) { return new User(in); } @Override public User[] newArray(int size) { return new User[size]; } }; public int getId() { return id; } public void setId(int id) { this.id = id; } }
createFromParcel
,User(Parcel in) ,代表从序列化的对象中创建原始对象newArray
,代表创建指定长度的原始对象数组writeToParcel
,代表将当前对象写入到序列化结构中。describeContents
,代表返回当前对象的内容描述。如果还有文件描述符,返回1,否则返回0。
两者有什么区别,该怎么使用选择
Serializable
是Java提供的序列化接口,使用简单但是开销很大,序列化和反序列化过程都需要大量I/O
操作。Parcelable
是Android中提供的,也是Android
中推荐的序列化方式。虽然使用麻烦,但是效率很高。
所以,如果是内存序列化层面,那么还是建议Parcelable
,因为他效率会比较高。如果是网络传输和存储磁盘情况,就推荐Serializable
,因为序列化方式比较简单,而且Parcelable不能保证,当外部条件发生变化时数据的连续性。
- Serializable
Serializable
的实质其实是是把Java对象序列化为二进制文件,然后就能在进程之间传递,并且用于网络传输或者本地存储等一系列操作,因为他的本质就存储了文件。可以看看源码:
private void writeObject0(Object obj, boolean unshared) throws IOException { ... try { Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; desc = ObjectStreamClass.lookup(cl, true); if (obj instanceof Class) { writeClass((Class) obj, unshared); } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); // END Android-changed: Make Class and ObjectStreamClass replaceable. } else if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } } ... } private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { ... try { desc.checkSerialize(); //写入二进制文件,普通对象开头的魔数0x73 bout.writeByte(TC_OBJECT); //写入对应的类的描述符,见底下源码 writeClassDesc(desc, false); handles.assign(unshared ? null : obj); if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } public long getSerialVersionUID() { // 如果没有定义serialVersionUID,序列化机制就会调用一个函数根据类内部的属性等计算出一个hash值 if (suid == null) { suid = AccessController.doPrivileged( new PrivilegedAction<Long>() { public Long run() { return computeDefaultSUID(cl); } } ); } return suid.longValue(); }
可以看到是通过反射获取对象以及对象属性的相关信息,然后将数据写到了一个二进制文件
,并且写入了序列化协议版本等等。而获取·serialVersionUID·的逻辑也体现出来,如果id为空则会生成计算一个hash值。
- Parcelable
Parcelable
的存储是通过Parcel
存储到内存的,简单地说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel
可以从这块共享内存中读出字节流,并反序列化成对象。
这其中实际又是通过native
方法实现的。具体逻辑我就没有去分析了,如果有大神朋友可以在评论区解析下。
当然,Parcelable
也是可以持久化的,涉及到Parcel中的unmarshall
和marshall
方法。这里简单贴一下代码:
protected void saveParce() { FileOutputStream fos; try { fos = getApplicationContext().openFileOutput(TAG, Context.MODE_PRIVATE); BufferedOutputStream bos = new BufferedOutputStream(fos); Parcel parcel = Parcel.obtain(); parcel.writeParcelable(new ParceData(), 0); bos.write(parcel.marshall()); bos.flush(); bos.close(); fos.flush(); fos.close(); } catch (Exception e) { e.printStackTrace(); } } protected void loadParce() { FileInputStream fis; try { fis = getApplicationContext().openFileInput(TAG); byte[] bytes = new byte[fis.available()]; fis.read(bytes); Parcel parcel = Parcel.obtain(); parcel.unmarshall(bytes, 0, bytes.length); parcel.setDataPosition(0); ParceData data = parcel.readParcelable(ParceData.class.getClassLoader()); fis.close(); } catch (Exception e) { e.printStackTrace(); } }
序列化总结
1)对于内存序列化方面建议用Parcelable
,为什么呢?
- 因为
Serializable
是存储了一个二进制文件,所以会有频繁的IO操作,消耗也比较大,而且用到了大量反射,反射操作也是耗时的。相比之下Parcelable
就要效率高很多。
2)对于数据持久化还是建议用Serializable
,为什么呢?
- 首先,
Serializable
本身就是存储到二进制文件,所以用于持久化比较方便。而Parcelable
序列化是在内存中操作,如果进程关闭或者重启的时候,内存中的数据就会消失,那么Parcelable
序列化用来持久化就有可能会失败,也就是数据不会连续完整。而且Parcelable
还有一个问题是兼容性,每个Android版本可能内部实现都不一样,知识用于内存中也就是传递数据的话是不影响的,但是如果持久化可能就会有问题了,低版本的数据拿到高版本可能会出现兼容性问题。所以还是建议用Serializable
进行持久化。
3)Parcelable一定比Serializable快吗?
- 有个比较有趣的例子是:当序列化一个超级大的对象图表(表示通过一个对象,拥有通过某路径能访问到其他很多的对象),并且每个对象有10个以上属性时,并且
Serializable
实现了writeObject()
以及readObject()
,在平均每台安卓设备上,Serializable
序列化速度大于Parcelable
3.6倍,反序列化速度大于1.6倍.
具体原因就是因为Serilazable
的实现方式中,是有缓存的概念的,当一个对象被解析过后,将会缓存在HandleTable
中,当下一次解析到同一种类型的对象后,便可以向二进制流中,写入对应的缓存索引即可。但是对于Parcel
来说,没有这种概念,每一次的序列化都是独立的,每一个对象,都当作一种新的对象以及新的类型的方式来处理。
LruCache介绍
LruCache
是Android3.1提供的一个缓存类,用于数据缓存,一般用于图片的内存缓存。Lru的英文是Least Recently Used
,也就是近期最少使用算法,核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。
当我们进行网络加载图片的时候,肯定要对图片进行缓存,这样下次加载图片就可以直接从缓存中取。三级缓存
大家应该都比较熟悉,内存,硬盘和网络。所以一般要进行内存缓存和硬盘缓存,其中内存缓存就是用的LruCache。
LruCache使用
public class MyImageLoader { private LruCache<String, Bitmap> mLruCache; public MyImageLoader() { int maxMemory = (int) (Runtime.getRuntime().maxMemory())/1024; int cacheSize = maxMemory / 8; mLruCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight()/1024; } }; } /** * 添加图片缓存 */ public void addBitmap(String key, Bitmap bitmap) { mLruCache.put(key, bitmap); } /** * 从缓存中获取图片 * */ public Bitmap getBitmap(String key) { return mLruCache.get(key); } }
使用方法如上,只需要提供缓存的总容量大小并重写sizeOf
方法计算缓存对象大小即可。这里总容量的大小也是通用方法,即进程可用内存的1/8,单位kb。然后就可以使用put方法来添加缓存对象,get方法来获取缓存对象。
LruCache原理
原理其实也很简单,就是用到了LRU
算法,内部使用LinkedHashMap
进行存储。在缓存满了之后,会将最近最少使用的元素移除。怎么保证找到这个最近最少的元素呢?就是每次使用get方法访问了元素或者增加了一个元素,就把元素移动到LinkedHashMap
的尾部,这样第一个元素就是最不经常使用的元素,在容量满了之后就可以将它移除。
简单看看源码:
public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); } public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; //查找是否已经存在key对应的元素 synchronized (this) { putCount++; //计算entry的大小 size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { //如果之前存在,这先减去之前那个entry所占用的内存大小 size -= safeSizeOf(key, previous); } } if (previous != null) { //如果之前存在则调用entryRemoved回调子类重写的此方法,做一些处理 entryRemoved(false, key, previous, value); } //根据最大的容量,计算是否需要淘汰掉最不常使用的entry trimToSize(maxSize); return previous; } public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; //根据key来查询符合条件的etnry synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. */ V createdValue = create(key); if (createdValue == null) { return null; } synchronized (this) { createCount++; //mapValue返回的是已经存在相同key的entry mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } }
其实可以看到,LruCache类本身做的事情不多,限定了缓存map的大小,然后利用了LinkHashMap完成了LRU的缓存策略。所以主要的实现LRU逻辑部分还是在LinkHashMap中。LinkedHashMap是hashmap和链表的结合体,通过链表来记录元素的顺序和链接关系,通过HashMap来存储数据,它可以控制元素的被遍历时候输出的顺序。他是一个双向链表,上面说过他会把最近访问的元素放到队列的尾部,有兴趣的可以看看LinkHashMap的源码。
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方法中被创建。ViewRootImp
l是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。