使用起来也是得心顺畅,舒服。
其他
类型完备,使用时智能提示,方便快捷。
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。
//具体原型类
class Realizetype implements Cloneable {
Realizetype() {
System.out.println("具体原型创建成功!");
}
public Object clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Realizetype) super.clone();
}
}
//原型模式的测试类
public class PrototypeTest {
public static void main(String[] args) throws CloneNotSupportedException {
Realizetype obj1 = new Realizetype();
Realizetype obj2 = (Realizetype) obj1.clone();
System.out.println("obj1==obj2?" + (obj1 == obj2));
}
}
输出
具体原型创建成功!
具体原型复制成功!
obj1==obj2?false
和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。
不过换了一种表现形式:
out
来支持协变,等同于 Java 中的上界通配符 ? extends
。in
来支持逆变,等同于 Java 中的下界通配符 ? super
。val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>
复制代码
它们完全等价于:
Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;
复制代码
换了个写法,但作用是完全一样的。out
表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in
就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
上面讲的都是在使用的时候再对泛型进行限制,我们称之为「上界通配符」和「下界通配符」。那我们可以在函数设计的时候,就设置这个限制么?
可以的可以的。
比如:
open class Animal
class PetShop<T : Animal?>(val t: T)
复制代码
等同于 Java 的:
class PetShop<T extends Animal> {
private T t;
PetShop(T t) {
this.t = t;
}
}
复制代码
这样,我们在设计宠物店类 PetShop
就给支持的泛型设置了上界约束,支持的泛型类型必须是 Animal
的子类。所以我们使用的话:
class Cat : Animal()
val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
// 👆 报错:Type mismatch. Required: Animal? Found: Apple
复制代码
很明显,Apple
并不是 Animal
的子类,当然不满足 PetShop
泛型类型的上界约束。
那....可以设置多个上界约束么?
当然可以,在 Java 中,给一个泛型参数声明多个约束的方式是,使用 &
:
class PetShop<T extends Animal & Serializable> {
// 👆 通过 & 实现了两个上界,必须是 Animal 和 Serializable 的子类或实现类
private T t;
PetShop(T t) {
this.t = t;
}
}
复制代码
而在 Kotlin 中舍弃了 &
这种方式,而是增加了 where
关键字:
open class Animal
class PetShop<T>(val t: T) where T : Animal?, T : Serializable
复制代码
通过上面的方式,就实现了多个上界的约束。
前面我们说的泛型类型都是在我们需要知道参数类型是什么类型的,那如果我们对泛型参数的类型不感兴趣,有没有一种方式处理这个情况呢?
有的有的。
在 Kotlin 中,可以用通配符 *
来替代泛型参数。比如:
val list: MutableList<*> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 👆 报错:Type mismatch. Required: Nothing Found: String
复制代码
这个报错确实让人匪夷所思,上面用通配符代表了 MutableList
的泛型参数类型。初始化里面也加入了 String
类型,但在新 add
字符串的时候,却发生了编译错误。
而如果是这样的代码:
val list: MutableList<Any> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 👆 不再报错
复制代码
看来,所谓的通配符作为泛型参数并不等价于 Any
作为泛型参数。MutableList<*>
和 MutableList<Any>
并不是同一种列表,后者的类型是确定的,而前者的类型并不确定,编译器并不能知道这是一种什么类型。所以它不被允许添加元素,因为会导致类型不安全。
不过细心的同学肯定发现了,这个和前面泛型的协变非常类似。其实通配符 *
不过是一种语法糖,背后也是用协变来实现的。所以:MutableList<*>
等价于 MutableList<out Any?>
,使用通配符与协变有着一样的特性。
在 Java 中,也有一样意义的通配符,不过使用的是 ?
作为通配。
List<?> list = new ArrayList<Apple>();
复制代码
Java 中的通配符 ?
也等价于 ? extends Object
。
那可以声明多个泛型么?
可以的可以的。
HashMap
不就是一个典型的例子么?
class HashMap<K,V>
复制代码
多个泛型,可以通过 ,
进行分割,多个声明,上面是两个,实际上多个都是可以的。
class HashMap<K: Animal, V, T, M, Z : Serializable>
复制代码
上面讲的都是都是在类上声明泛型类型,那可以声明在方法上么?
可以的可以的。
如果你是一名 Android 开发,View
的 findViewById
不就是最好的例子么?
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
复制代码
很明显,View
是没有泛型参数类型的,但其 findViewById
就是典型的泛型方法,泛型声明就在方法上。
上述写法改写成 Kotlin 也非常简单:
fun <T : View?> findViewById(@IdRes id: Int): T? {
return if (id == View.NO_ID) {
null
} else findViewTraversal(id)
}
复制代码
前面有说到,由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。比如你不能检查一个对象是否为泛型类型 T
的实例:
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof
}
}
复制代码
Kotlin 里同样也不行:
fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 IDE 会提示错误,Cannot check for instance of erased type: T
println(item)
}
}
复制代码
这个问题,在 Java 中的解决方案通常是额外传递一个 Class<T>
类型的参数,然后通过 Class#isInstance
方法来检查:
👇
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
👆
}
}
复制代码
Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified
配合 inline
来解决:
👇 👇
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了
}
}
复制代码
上面的 Gson 解析的时候用的非常广泛,比如咱们项目里就有这样的扩展方法:
inline fun <reified T> String?.toObject(type: Type? = null): T? {
return if (type != null) {
GsonFactory.GSON.fromJson(this, type)
} else {
GsonFactory.GSON.fromJson(this, T::class.java)
}
}
复制代码
本文花了非常大的篇幅来讲 Kotlin 的泛型和 Java 的泛型,现在再回过头去回答文首的几个问题,同学你有谱了吗?如果还是感觉一知半解,不妨多看几遍。
个人说明:拿过国内某算法大赛全国三等。。。(渣渣)
它用一组连续的内存空间,来存储一组具有相同类型的数据。
优点:查找速度快,可以快速随机访问
缺点:删除,插入效率低,大小固定,不支持动态扩展,要求内存空间必须连续
数组是一种线性表结构
如果要想在任意位置插入元素,那么必须要将这个位置后的其他元素后移。
那么其最好的时间复杂度O(1) 最差的时间复杂度为O(n)
同样的删除数据需要前移
注意事项:
如果插入的数据多的情况下,那么数组的容量是固定的,那么就存在扩容的操作。
同样的,如果删除数据多的情况下,也会出现缩容的操作
例:java的arraylist
首先,先看看ArrayList的初始化,源码如下:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//更根据初始值大小创建数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//默认无规定初始值大小时,会创建一个空数组
//private static final Object[] EMPTY_ELEMENTDATA = {};
//待通过add方法时创建初始容量为10的数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
add(E e)方法的源码解析
public boolean add(E e) {
//检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//添加新元素
elementData[size++] = e;
return true;
}
//传入数组最小所需要的长度
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//检查原先数组是否为空数组,返回数组所需最小长度
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断数组是否为空。
//private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//private static final int DEFAULT_CAPACITY = 10;
//数组为空时,返回DEFAULT_CAPACITY与minCapacity中大的数,减少扩容次数
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//直接返回数组所需的最小长度size+1
return minCapacity;
}
//判断当前数组长度是否超过添加元素所需的最小长度
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//如果所需最小长度大于当前数组长度
if (minCapacity - elementData.length > 0)
//进行扩容
grow(minCapacity);
//否则,不做任何处理
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//新数组长度等于旧长度+1/2旧长度
int newCapacity = oldCapacity + (oldCapacity >> 1);
//计算newCapacity的大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//拷贝到新数组,数组长度为newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
ArrayList缩容
ArrayList没有自动缩容机制。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。ArrayList的缩容,需要我们自己手动去调用trimToSize()方法,达到缩容的目的。
/**
* Trims the capacity of this <tt>ArrayList</tt> instance to be the
* list's current size. An application can use this operation to minimize
* the storage of an <tt>ArrayList</tt> instance.
*/
public void trimToSize() {
modCount++;
//判断当前容量与数组长度的大小关系
if (size < elementData.length) {
//如果size小于elementData.length,则将数组拷贝到长度为size的数组中,如果size==0,则将elementData 置为空数组,{}
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
以下算法只给思路(代码写一写就知道了)
并没什么可注意的,哈哈哈哈
有一点就是:数组根据下标进行访问时,时间复杂度为O(1),进行插入和删除操作时,时间复杂度为O(n)(主要就是结构可以让他这么屌)
leetcode等级困难
时间要求O(n)
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
思路:
java中Hashset数组是个没有重复元素的集合,那么可以将所有数据存入到set中,之后进行顺序删除,把数组进行遍历,把每个遍历的元素进行删除,删除的最大长度则为数组最大序列长度。
public int longestConsecutive(int[] nums) {
if(nums.length<=0){
return 0;
}
Set<Integer> set = new HashSet<>();
for(int n : nums){
set.add(n);
}
int maxv = 1;
for(int n : nums){
if(!set.remove(n)){
continue;
}
int vb = 1;
int va = 1;
while (set.remove(n-vb)){ vb++; }
while (set.remove(n+va)){ va++; }
maxv = Math.max(maxv,vb + va -1);
}
return maxv;
}
时间复杂度为O(n)空间复杂度为O(n) remove 的次数最多为n
给定一个整数数组,请调整数组中数的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
要求时间复杂度为O(n)
思路:
双指针法,可以两个指针分别从前往后,进行交换。
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
思路:
双指针法
收起阅读 »
平时在聊天谈论算法时候,发现很多人并不清楚算法的时间复杂度怎么计算,一些稍微复杂的算法时间复杂度问题,就无法算出时间复杂度。那么我在今天的文章里去解答这些问题
执行这个算法所需要的计算工作量
随着输入数据的规模,时间也不断增长,那么时间复杂度就是一个算法性能可观的数据。
由于实际算法的时间会根据机器的性能,软件的环境导致不同的结果,那么通常都是预估这个时间。
算法花费时间与执行语句次数成正比,执行次数称为语句频度或时间频度。记为T(n)
在时间频度中,n称为问题的规模,当n不断变化时,它所呈现出来的规律,称之为时间复杂度
说明:
例如:T(n)=2n+4+n^2
T(n)=n+8 + 4n^2
很明显时间频度不同,但是他们时间复杂度是相同的 都为O(n^2)
原则:
例1:
for(i in 0..n){
a++
}
for(i in 0..n){
for(i in 0..n){
a++
}
}
在看这个算法时候,很明显时间 复杂度为O(n+n^2) 但要留下最高阶 也就是O(n^2)
例2:
for(i in 0..n){
a++
b++
c++
}
在看这个算法时候,很明显时间 复杂度为O(3n) 但要抹掉常数3 也就是O(n)
例3:
var i = 1
while (i < n) {
a++
b++
c++
i *= 2
}
在看这个算法时候,很明显时间 复杂度为O(3logn) 但要抹掉常数3 也就是O(logn)
例4:
a++
b++
c++
在看这个算法时候,很明显时间 复杂度为O(3) 常数级别的算法都为O(1)
为啥我要加个重点标识呢?因为很多人都不晓得平衡二叉搜索树的O(logn)是咋计算出来的,今天我给大家掰扯掰扯
假设生成高度树h的节点数是n(h) ,高度为h-1的节点数为n(h-1) 常见的时问复杂度如表所 为什么要引入这4个概念? 如何分析平均、均摊时间复杂度? 执行这个算法所需要的内存空间 例 空间复杂度为 O(n)
他们之间的关系为 n(h) = n(h-1)+n(h-1)+1
n(h) = 2n(h-1)+1
n(h) 约等于 2n(h-1)
注意的一点 n(h-1) 几乎是一半的节点数目
基准点 h(0) = 1 h(1) = 3
依次类推
n(h) 约等于 h^2
那么
n(h) = h^2
那么
h = log2n
则平衡二叉搜索树的时间复杂度 O(logn)
时间复杂度比较
常用的时间复杂度所耗费的时间从小到大依次是:
复杂度在1s内,能处理的数据量大小
复杂度分析的4个概念
代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示。
两个条件满足时使用:
1)代码在绝大多数情况下是低级别复杂度,只有极少数情况是高级别复杂度;
2)低级别和高级别复杂度出现具有时序规律。均摊结果一般都等于低级别复杂度。
最坏情况与平均情况
我们查找一个有n 个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数字就在最后一个位置上待着,那么算法的时间复杂度就是O(n),这是最坏的一种情况了。
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。 在应用中,这是一种最重要的需求, 通常, 除非特别指定, 我们提到的运行时间都是最坏情况的运行时间。
而平均运行时间也就是从概率的角度看, 这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。也就是说,我们运行一段程序代码时,是希望看到平均运行时间的。可现实中,平均运行时间很难通过分析得到,一般都是通过运行一定数量的实验数据后估算出来的。一般在没有特殊说明的情况下,都是指最坏时间复杂度。
空间复杂度
var m = arrayOfNulls<Int>(n)
for(i in 0..n){
a++
m[i] = a
}
常用算法的时间复杂度和空间复杂度
个人说明:拿过国内某算法大赛全国三等。。。(渣渣)
链表是计算机数据结构中比较重要的一个,也是最基础之一。在开发过程中,有些时候会采用这种结构。链表可以说是一种动态的数据结构。
链表是一种物理存储上非连续的存储结构,数据的顺序与关联通过链表中的节点指针来实现。结点可以动态变更,那也就说明链表这种结构可快速添加数据。
链表是一种线性表结构
链表中的元素的指向只能指向链表中的下一个元素或者为空,元素之间不能相互指向,是一种线性链表
在单向链表和双向链表的基础上,将两种链表的最后一个结点指向第一个结点从而实现循环
每个节点既有指向下一个元素的指针,又有指向前一个元素的指针,其中每个结点都有两种指针
节点
包含下一节点(上一节点)指针与数据
就拿单链表作为示例(其他链表操作没太大区别)
删除头节点:
直接让root节点指向头节点的下一个节点
删除中间节点:
直接让中间节点的前一个节点指向当前节点的下一个节点
删除尾节点:
直接让当前尾节点的上一个节点指向null
增加的新节点会指向当前节点的下一个,那么当前节点的下一个又会指向新的节点。很快就增加了一个新节点。
添加节点如果在头部的话,root指到新节点,新节点的下一个节点指向旧root
添加尾节点的话,就让尾节点的下一个节点指向新节点
例:java的LinkedList(本文针对的是1.7的源码)
首先,先看看LinkedList的初始化,源码如下:
LinkedList包含3个全局参数,
size存放当前链表有多少个节点。
first为指向链表的第一个节点的引用。
last为指向链表的最后一个节点的引用。
// 什么都没做,是一个空实现
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
// 检查传入的索引值是否在合理范围内
checkPositionIndex(index);
// 将给定的Collection对象转为Object数组
Object[] a = c.toArray();
int numNew = a.length;
// 数组为空的话,直接返回false
if (numNew == 0)
return false;
// 数组不为空
Node<E> pred, succ;
if (index == size) {
// 构造方法调用的时候,index = size = 0,进入这个条件。
succ = null;
pred = last;
} else {
// 链表非空时调用,node方法返回给定索引位置的节点对象
succ = node(index);
pred = succ.prev;
}
// 遍历数组,将数组的对象插入到节点中
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred; // 将当前链表最后一个节点赋值给last
} else {
// 链表非空时,将断开的部分连接上
pred.next = succ;
succ.prev = pred;
}
// 记录当前节点个数
size += numNew;
modCount++;
return true;
}
Node节点类:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
addFirst/addLast
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f); // 创建新的节点,新节点的后继指向原来的头节点,即将原头节点向后移一位,新节点代替头结点的位置。
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
getFirst/getLast
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
get方法,在操作时候会判断索引位置,来从后或从前找,提升查找效率
public E get(int index) {
// 校验给定的索引值是否在合理范围内
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
//如果索引小于size的一半则从前往后找,如果大于则从后往前找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
removeFirst/removeLast
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
//摘掉头结点,将原来的第二个节点变为头结点,改变frist的指向
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
以下算法只给思路(代码写一写就知道了)
并没什么可注意的,哈哈哈哈
有一点就是:链表的插入和删除操作O(1)的复杂度(主要就是结构可以让他这么屌)
这是个比较简单的算法
思路:由于单链表的结构无法从链表尾节点来倒序查找,那么你可以两个节点相差k-1来计算倒数k个节点,如果节点1的next是空,那么节点2就是你要找的。
首先创建两个指针1和2,同时指向这个链表的头节点。让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。
将两个链表的节点分别存在两个栈中,
题目:
有100人排排坐,从第一个人开始,喊号(1,2,3)。第一个人喊1,第二个人喊2,第三个人喊3,这时候第三个人就出队,第四个人开始喊1,如此循环,最后剩下哪个人?
思路:
循环链表,把100个人放到循环链表里,循环每次到三时候,把三这个删除节点,继续循环,直到一个节点的next指针指到自己,结束循环。
收起阅读 »
Lifecycle是具备宿主生命周期感知能力的组件。它能持有组件(如Activity或Fragment)生命周期状态的信息,并且允许其他观察者监听宿主的状态。它也是Jetpack组件库的核心基础,包括我们就会讲到的LiveData,ViewModel组件等也都是基于它来实现的。
再也不用手动分发宿主生命周期,再也不用手动反注册了
使用Lifecycle前需要先添加依赖:
//通常情况下,只需要添加appcompat就可以了
api 'androidx.appcompat:appcompat:1.1.0'
//如果想单独使用,可引入下面这个依赖
api 'androidx.lifecycle:lifecycle-common:2.1.0'
复制代码
通过实现LifecycleOwner接口
androidx中的ComponentActivity和Fragment自生都已经实现了LifecycleOwner。
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner {}
//androidx.fragment.app.Fragment
public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner,
ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner {}
复制代码
如果要自己实现可以参考androidx的Activity和Fragment的实现。
方式一:LifecycleObserver配合注解
//1. 自定义的LifecycleObserver观察者,用注解声明每一个观察者的宿主状态
class LocationObserver implements LifecycleObserver{
//宿主执行了onstart时,会分发该事件
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart(@NotNull LifecycleOwner owner){
//开启定位
}
//宿主执行了onstop时 会分发该事件
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop(@NotNull LifecycleOwner owner){
//停止定位
}
}
//2. 注册观察者,观察宿主生命周期状态变化
class MyFragment extends Fragment{
public void onCreate(Bundle bundle){
LocationObserver observer =new LocationObserver()
getLifecycle().addObserver(observer);
}
}
复制代码
方式二:实现FullLifecyclerObserver接口
class LocationObserver implements FullLifecycleObserver{
void onCreate(LifecycleOwner owner);
void onStart(LifecycleOwner owner);
void onResume(LifecycleOwner owner);
void onPause(LifecycleOwner owner);
void onStop(LifecycleOwner owner);
void onDestroy(LifecycleOwner owner);
}
复制代码
方式三:LifecycleEventObserver宿主生命周期事件封装成Lifecycle.Event
//1.源码
public interface LifecycleEventObserver extends LifecycleObserver {
void onStateChanged(LifecycleOwner source, Lifecycle.Event event);
}
//2.用法
class LocationObserver extends LifecycleEventObserver{
@override
void onStateChanged(LifecycleOwner source, Lifecycle.Event event){
//需要自行判断life-event是onstart, 还是onstop
}
}
复制代码
上面的这两种Lifecycle写法比较推荐第二种和第三种,因为第一种你虽然用注解很爽,但是如果没有添加lifecycle-compiler这个注解处理器的话,运行时会使用反射的形式回调到对应的方法上。
//被观察者.addObserver(观察者)
getLifecycle().addObserver(presenter);
复制代码
引入的库
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.0.0"
annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.0.0"
}
复制代码
事件在分发宿主生命期事件的流程中涉及到三个类:
具体的类关系请看下面的Fragment的基本实现。
使用Fragment实现Lifecycle需要在各个生命周期方法内里用LifecycleRegistry分发相应的事件给每个观察者,以实现生命周期观察的能力:
public class Fragment implements LifecycleOwner {
LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
@Override
public Lifecycle getLifecycle() {
//复写自LifecycleOwner,所以必须new LifecycleRegistry对象返回
return mLifecycleRegistry;
}
void performCreate(){
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
void performStart(){
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
}
.....
void performResume(){
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
}
}
复制代码
Activity实现Lifecycle需要借助于ReportFragment往Activity上添加一个fragment用以报告生命周期的变化。目的是为了兼容不是集成自
AppCompactActivity的场景,同时也支持我们自定义LifecycleOwener的场景。
public class ComponentActivity extends Activity implements LifecycleOwner{
private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
protected void onCreate(Bundle bundle) {
super.onCreate(savedInstanceState);
//往Activity上添加一个fragment,用以报告生命周期的变化
//目的是为了兼顾不是继承自AppCompactActivity的场景.
ReportFragment.injectIfNeededIn(this);
}
复制代码
注意:ComponentActivity的部分生命周期是自己实现的,并不是全部都是ReportFragment实现的。
这里的实现其实跟Fragment中的源码是一样的,在各个生命周期方法内利用LifecycleRegistry派发相应的Lifecycle.Event事件给每个观察者。
注意:在具体的生命周期的调用上有所差异:
SDK>=29:用LifecycleCallbacks进行回调
SDK<29:用ReportFragment的生命周期中调用
public class ReportFragment extends Fragment{
public static void injectIfNeededIn(Activity activity) {
//注意:在29以后对Activity注册了一个LifecycleCallbacks,这里的dispatch的调用时机有所差异
if (Build.VERSION.SDK_INT >= 29) {
activity.registerActivityLifecycleCallbacks(
new LifecycleCallbacks());
}
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
manager.executePendingTransactions();
}
}
@Override
public void onStart() {
super.onStart();
dispatch(Lifecycle.Event.ON_START);
}
@Override
public void onResume() {
super.onResume();
dispatch(Lifecycle.Event.ON_RESUME);
}
@Override
public void onPause() {
super.onPause();
dispatch(Lifecycle.Event.ON_PAUSE);
}
@Override
public void onDestroy() {
super.onDestroy();
dispatch(Lifecycle.Event.ON_DESTROY);
}
//只要<29才借助的ReportFragment的生命周期
private void dispatch(@NonNull Lifecycle.Event event) {
if (Build.VERSION.SDK_INT < 29) {
dispatch(getActivity(), event);
}
//29之后的另外处理
}
//最终收拢在这里处理
static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
//兼容Activity自己实现的LifecycleRegistryOwner
if (activity instanceof LifecycleRegistryOwner) {
((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
return;
}
if (activity instanceof LifecycleOwner) {
Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
}
}
}
}
复制代码
SDK29以后的实现调用
//ReportFragment的静态内部类
static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(@NonNull Activity activity,
@Nullable Bundle bundle) {
}
@Override
public void onActivityPostCreated(@NonNull Activity activity,
@Nullable Bundle savedInstanceState) {
//调用的是静态方法
dispatch(activity, Lifecycle.Event.ON_CREATE);
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityPostStarted(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_START);
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPostResumed(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_RESUME);
}
@Override
public void onActivityPrePaused(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_PAUSE);
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityPreStopped(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_STOP);
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity,
@NonNull Bundle bundle) {
}
@Override
public void onActivityPreDestroyed(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_DESTROY);
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
}
复制代码
在Activity中的生命周期对进行回调
//Activity
private void dispatchActivityCreated(@Nullable Bundle savedInstanceState) {
getApplication().dispatchActivityCreated(this, savedInstanceState);
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i = 0; i < callbacks.length; i++) {
((Application.ActivityLifecycleCallbacks) callbacks[i]).onActivityCreated(this,
savedInstanceState);
}
}
}
private void dispatchActivityPostCreated(@Nullable Bundle savedInstanceState) {
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i = 0; i < callbacks.length; i++) {
//回调监听
((Application.ActivityLifecycleCallbacks) callbacks[i]).onActivityPostCreated(this,
savedInstanceState);
}
}
getApplication().dispatchActivityPostCreated(this, savedInstanceState);
}
复制代码
而dispatch()方法则会判断Activity是否实现了LifecycleOwner接口,如果实现了该接口就调用LifecycleRegister的handleLifecycleEvent()
这样生命周期的状态就会借由LifecycleRegistry通知给各个LifecycleObserver从而调用其中对应Lifecycle.Event的方法。这种通过Fragment来感知Activity生命周期的方法其实在Glide的中也是有体现的。
LifecycleRegistry在分发事件的时候会涉及到两个概念:
从下面这张图不难看出宿主生命周期与宿主状态的对应关系分裂为onCreate-Created、onStart-Started、onResume-Resumed、onPause-Started、onStop-Created、onDestroy-Destroyed,这里不用全部记住有个印象即可。
添加observer时,完整的生命周期事件分发
基于Lifecycle的特性我们在任意生命周期方法内注册观察者都能接受到完整的生命周期事件,比如在onResume中注册一个观察者,它会依次收到:
LifecycleEvent.onCreate -> LifecycleEvent.onStart -> LifecycleEvent.onResume
复制代码
这一点需要掌握,面试中是肯定会考察的。但是如果没有看过源码是回答不上来的:
public void addObserver(@NonNull LifecycleObserver observer) {
//1.初始状态。添加新的Observer时,会首先根据宿主的状态计算出它的初始状态,只要不是在onDestroy中注册的,它的初始状态都是INITIALIZED
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
//2.将观察者和状态封装。
//会把observer包装成ObserverWithState,这个类主要是包含了观察者及其状态。每个事件都会经由这个对象类转发,这个类后面会来分析
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
//3.添加到集合,如果之前已经添加过了,则return
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
if (previous != null) {
return;
}
State targetState = calculateTargetState(observer);
//4.同步状态
//这里的while循环,是实现上图状态同步与事件分发的主要逻辑
//拿观察者的状态和宿主当前状态做比较,如果小于0,说明两者状态还没有对齐。
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
//接着就会分发一次相应的事件,于此同时statefulObserver的mState对象也会被升级
//假设是在宿主的onresume方法内注册的该观察者
//第一次:分发on_Create事件,观察者状态INIT->CREATED
//第二次:分发on_Start事件,观察者状态CREATED->STARTED
//第三次:分发on_Resume事件,观察者状态STARTED->RESUMED
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
//再一次计算观察者应该到达的状态,在下一轮循环中和宿主状态在做比较,知道两者状态对齐,退出循环。
targetState = calculateTargetState(observer);
}
}
复制代码
这一点了解即可,面试中也不会考这一部分的内容:
//宿主生命周期变化时触发
public void handleLifecycleEvent(@NonNull Lifecycle.Event event){
//宿主的每个生命周期的变化都会分发一个对应的Lifecycle.Event,走到这里
//此时会根据需要分发的事件反推出宿主当前的状态
State next = getStateAfter(event);
// moveToState方法只是将传入的宿主新的state和前持有宿主状态作比对,然后保存一下。
moveToState(next);
}
private void moveToState(State next) {
if (mState == next) {
return;
}
mState = next;
if (mHandlingEvent || mAddingObserverCounter != 0) {
mNewEventOccurred = true;
// we will figure out what to do on upper level.
return;
}
mHandlingEvent = true;
sync();
mHandlingEvent = false;
}
//如果宿主状态有变动,则调用sync方法来完成事件的分发和观察者状态的同步
private void sync() {
while (!isSynced()) {
//如果宿主当前转态 小于 mObserverMap集合中最先添加的那个观察者的状态
//则说明宿主可能发生了状态回退,比如当前是RESUMED状态,执行了onPause则回退到STARTED状态
//此时调用backwardPass把集合中的每个一观察者分发一个on_pause事件,并同步它的状态。
if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
backwardPass(lifecycleOwner);
}
//如果宿主当前转态 大于 mObserverMap集合中最先添加的那个观察者的状态
//则说明宿主可能发生了状态前进,比如当前是STARTED状态,执行了onResume则前进到RESUMED状态
//此时调用forwardPass把集合中的每个一观察者分发一个on_resume事件,并同步它的状态。
Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
if (!mNewEventOccurred && newest != null
&& mState.compareTo(newest.getValue().mState) > 0) {
forwardPass(lifecycleOwner);
}
}
}
复制代码
ObserverWithState:持有观察者及其状态的内部类
把传入的LifecycleObserver适配成LifecycleEventObserver,目的是为了统一事件的分发形式。
持有观察者的状态,方便与宿主状态做比对同步:
static class ObserverWithState {
State mState;
LifecycleEventObserver mLifecycleObserver;
ObserverWithState(LifecycleObserver observer, State initialState) {
//把传入的LifecycleObserver适配成LifecycleEventObserver,目的是为了统一事件的分发形式
//因为我们前面提到观察者有三种类型,每种类型接收事件的形式并不一样,如果在分发的时候不统一事件分发的形式,将会变得很麻烦
//至于是如何适配转换的,由于不是本文重点,所以不再详细展开
//但核心思想这里说明一下,同学们自行看下就能明白
//它会判断传入的observer是前面提到的那一种类型,进而转换成对应的适配器类,适配器类会对onStateChanged方法进行适配,并以相应的方式(反射、中转、)把事件转发到我们的observer上
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
mState = initialState;
}
void dispatchEvent(LifecycleOwner owner, Event event) {
//再一次根据需要分发的事件类型反推出该观察者的状态,这样的好处是事件 & 状态 一一对应,不会出现跳跃。但阅读上可能会稍微有点绕
State newState = getStateAfter(event);
mState = min(mState, newState);
//把事件分发给被包装的对象,完成本次流程。
mLifecycleObserver.onStateChanged(owner, event);
mState = newState;
}
}
复制代码
本篇从三种用法+分发原理+面试考点 三个维度展开对Lifecycle组件的介绍,现在相信同学们已经掌握了Lifecycle的核心了。Lifecycle组件是Jetpack组件库的核心,一旦跟宿主生命周期挂钩,那可以做很多文章,后面讲到的LiveData、ViewModel都是基于它来实现的。
基于Lifecycle实现APP前后台切换事件观察的能力。
参考:使用 ProcessLifecycle 优雅地监听应用前后台切换
class AppLifecycleOwner implements LifecycleOwner{
LifecycleRegistry registry = new LifecycleRegistry(this)
@override
Lifecycle getLifecycle(){
return registry
}
void init(Application application){
//利用application的 ActivityLifecycleCallbacks 去监听每一个 Activity的onstart,onStop事件。
//计算出可见的Activity数量,从而计算出当前处于前台还是后台。然后分发给每个观察者
}
}
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
使用抽象工厂模式一般要满足以下条件。
抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
抽象工厂模式的主要角色如下。
interface IProduct{
fun setPingPai(string: String)
fun showName() :String
}
interface ITools{
fun setPingPai(string: String)
fun showName() :String
}
class Dog : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "dog"
}
class DogTools : ITools{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "DogTools"
}
class Cat : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "cat"
}
class CatTools : ITools{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "CatTools"
}
interface IFactory{
fun getPinPai():String
fun createProduct(type:Int):IProduct
fun createProductTools(type:Int):ITools
}
class ABCFactory():IFactory{
override fun getPinPai() = "ABC"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
override fun createProductTools(type: Int): ITools {
return when(type){
1-> DogTools().apply { setPingPai(getPinPai()) }
2-> CatTools().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
}
class CBDFactory():IFactory{
override fun getPinPai() = "CBD"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
override fun createProductTools(type: Int): ITools {
return when(type){
1-> DogTools().apply { setPingPai(getPinPai()) }
2-> CatTools().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
}
iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是从iOS 9起可以使用Universal Links技术进行跳转页面,这是一种体验更加完美的解决方案
什么是Universal Link(通用链接)
Universal Link是Apple在iOS 9推出的一种能够方便的通过传统HTTPS链接来启动APP的功能。如果你的应用支持Universal Link,当用户点击一个链接时可以跳转到你的网站并获得无缝重定向到对应的APP,且不需要通过Safari浏览器。如果你的应用不支持的话,则会在Safari中打开该链接
支持Universal Link(通用链接)
先决条件:必须有一个支持HTTPS的域名,并且拥有该域名下上传到根目录的权限(为了上传Apple指定文件)
集成步骤
1、开发者中心配置
找到对应的App ID,在Application Services列表里有Associated Domains一条,把它变为Enabled就可以了
2、工程配置
targets->Capabilites->Associated Domains,在其中的Domains中填入你想支持的域名,必须以applinks:为前缀,如:applinks:domain
3、配置指定文件
创建一个内容为json格式的文件,苹果将会在合适的时候,从我们在项目中填入的域名请求这个文件。这个文件名必须为apple-app-site-association,切记没有后缀名,文件内容大概是这样子:
{ 4、上传该文件 5、代码中的相关支持 Universal Link(通用链接)注意点 Universal Link跨域 当我们的App在设备上第一次运行时,如果支持Associated Domains功能,那么iOS会自动去GET定义的Domain下的apple-app-site-association文件 服务器上apple-app-site-association的更新不会让iOS本地的apple-app-site-association同步更新,即iOS只会在App第一次启动时请求一次,以后除非App更新或重新安装,否则不会在每次打开时请求apple-app-site-association Universal Link的好处 之前的Custom URL scheme是自定义的协议,因此在没有安装该app的情况下是无法直接打开的。而Universal Links本身就是一个能够指向web页面或者app内容页的标准web link,因此能够很好的兼容其他情况 作者:72行代码
“applinks”: {
“apps”: [],
“details”: [
{
“appID”: “9JA89QQLNQ.com.apple.wwdc”,
“paths”: [ “/wwdc/news/“, “/videos/wwdc/2015/“]
},
{
“appID”: “ABCD1234.com.apple.wwdc”,
“paths”: [ ““ ]
}
]
}
}
复制代码appID:组成方式是TeamID.BundleID。如上面的9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID
paths:设定你的app支持的路径列表,只有这些指定路径的链接,才能被app所处理。*的写法代表了可识别域名下所有链接
上传该文件到你的域名所对应的根目录或者.well-known目录下,这是为了苹果能获取到你上传的文件。上传完后,先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载apple-app-site-association文件
当点击某个链接,可以直接进我们的app,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容,我们需要在工程里实现AppDelegate对应的方法:
// NSUserActivityTypeBrowsingWeb 由Universal Links唤醒的APP
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]){
} NSURL *webpageURL = userActivity.webpageURL;
NSString *host = webpageURL.host;
if ([host isEqualToString:@"api.r2games.com.cn"]){
//进行我们的处理
NSLog(@"TODO....");
}else{
NSLog(@"openurl");
[[UIApplication sharedApplication] openURL:webpageURL options:nil completionHandler:nil];
// [[UIApplication sharedApplication] openURL:webpageURL];
}
return YES;
}
复制代码苹果为了方便开发者,提供了一个网页验证我们编写的这个apple-app-site-association是否合法有效
Universal Link有跨域问题,Universal Link必须要求跨域,如果不跨域,就不会跳转(iOS 9.2之后的改动)
假如当前网页的域名是A,当前网页发起跳转的域名是B,必须要求B和A是不同域名才会触发Universal Link,如果B和A是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App
Universal Link请求apple-app-site-association时机
Universal links是从服务器上查询是哪个app需要被打开,因此不存在Custom URL scheme那样名字被抢占、冲突的情况
Universal links支持从其他app中的UIWebView中跳转到目标app
提供Universal link给别的app进行app间的交流时,对方并不能够用这个方法去检测你的app是否被安装(之前的custom scheme URL的canOpenURL方法可以)
链接:https://juejin.cn/post/6844903988526055437
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »
Moshi 是Square公司在2015年6月开源的一个 Json 解析库,相对于Gson,FastJson等老牌解析库而言,Moshi不仅支持对Kotlin的解析,并且提供了Reflection跟Annotion两种解析Kotlin的方法,除此之外,Moshi最大的改变在于支持自定义JsonAdapter,能够将Json的Value转换成任意你需要的类型。
implementation 'com.squareup.moshi:moshi:1.7.0'
复制代码
String json = ...;
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Bean> jsonAdapter = moshi.adapter(Bean.class);
//Deserialize
Bean bean = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.toJson(bean);
复制代码
Moshi moshi = new Moshi.Builder().build();
Type listOfCardsType = Types.newParameterizedType(List.class, Bean.class);
JsonAdapter<List<Bean>> jsonAdapter = moshi.adapter(listOfCardsType);
//Deserialize
List<Bean> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);
复制代码
Moshi moshi = new Moshi.Builder().build();
ParameterizedType newMapType = Types.newParameterizedType(Map.class, String.class, Integer.class);
JsonAdapter<Map<String,Integer>> jsonAdapter = moshi.adapter(newMapType);
//Deserialize
Map<String,Integer> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);
复制代码
public final class Bean {
@Json(name = "lucky number") int luckyNumber;
@Json(name = "objec") int data;
@Json(name = "toatl_price") String totolPrice;
private transient int total;//jump the field
}
复制代码
相对于 Java 只能通过反射进行解析,针对Kotlin,Moshi提供了两种解析方式,一种是通过 Reflection ,一种是通过 Annotation ,你可以采用其中的一种,也可以两种都使用,下面分别介绍下这两种解析方式
implementation 'com.squareup.moshi:moshi-kotlin:1.7.0'
复制代码
data class ConfigBean(
var isGood: Boolean = false,
var title: String = "",
var type: CustomType = CustomType.DEFAULT
)
复制代码
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
复制代码
这种方式会引入Kotlin-Reflect的Jar包,大概有2.5M。
上面提到了Reflection,会导致APK体积增大,所以Moshi还提供了另外一种解析方式,就是注解,Moshi的官方叫法叫做Codegen,因为是采用注解生成的,所以除了添加Moshi的Kotlin依赖之外,还需要加上kapt
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.7.0'
复制代码
给我们的数据类增加JsonClass注解
@JsonClass(generateAdapter = true)
data class ConfigBean(
var isGood: Boolean = false,
var title: String = "",
var type: CustomType = CustomType.DEFAULT
)
复制代码
这样的话,Moshi会在编译期生成我们需要的JsonAdapter,然后通过JsonReader遍历的方式去解析Json数据,这种方式不仅仅不依赖于反射,而且速度快于Kotlin。
JsonAdapter是Moshi有别于Gson,FastJson的最大特点,顾名思义,这是一个Json的转换器,他的主要作用在于将拿到的Json数据转换成任意你想要的类型,Moshi内置了很多JsonAdapter,有如下这些:
对于一些比较简单规范的数据,使用Moshi内置的JsonAdapter已经完全能够Cover住,但是由于Json只支持基本数据类型传输,所以很多时候不能满足业务上需要,举个例子:
{
"type": 2,
"isGood": 1
"title": "TW9zaGkgaXMgZmxleGlibGU="
}
复制代码
这是一个很普通的Json,包含了5个字段,我们如果按照服务端返回的字段来定义解析的Bean,显然是可以完全解析的,但是我们在实际调用的时候,这些数据并不是很干净,我们还需要处理一下:
对于客户端的同学来说,好像没毛病,以前都是这么干的,如果这种 不干净 的Json少点还好,多了之后就很头疼,每个在用的时候都需要转一遍,很多时候我这么干的时候都觉得浪费时间,而今天有了Moshi之后,我们只需要针对需要转换的类型定义对应的JsonAdapter,达到 一次定义,一劳永逸 的效果,Moshi针对常见的数据类型已经定义了Adapter,但是内置的Adapter现在已经不能满足我们的需求了,所以我们需要自定义JsonAdapter。
class ConfigBean {
public CustomType type;
public Boolean isGood;
public String title;
}
复制代码
此处我们定义的数据类型不是根据 服务器 返回的Json数据,而是定义的我们业务需要的格式,那么最终是通过JsonAdapter转换器来完成这个转换,下面开始自定义JsonAdapter。
enum CustomType {
DEFAULT(0, "DEFAULT"), BAD(1, "BAD"), NORMAL(2, "NORMAL"), GOOD(3, "NORMAL");
public int type;
public String content;
CustomType(int type, String content) {
this.type = type;
this.content = content;
}
}
复制代码
定义一个TypeAdapter继承自JsonAdapter,传入对应的泛型,会自动帮我们复写fromJson跟toJson两个方法
public class TypeAdapter {
@FromJson
public CustomType fromJson(int value) throws IOException {
CustomType type = CustomType.DEFAULT;
switch (value) {
case 1:
type = CustomType.BAD;
break;
case 2:
type = CustomType.NORMAL;
break;
case 3:
type = CustomType.GOOD;
break;
}
return type;
}
@ToJson
public Integer toJson(CustomType value) {
return value != null ? value.type : 0;
}
}
复制代码
至此已经完成Type的转换,接下来我们再以title举个例子,别的基本上都是照葫芦画瓢,没什么难度
public class TitleAdapter {
@FromJson
public String fromJson(String value) {
byte[] decode = Base64.getDecoder().decode(value);
return new String(decode);
}
@ToJson
public String toJson(String value) {
return new String(Base64.getEncoder().encode(value.getBytes()));
}
}
复制代码
public class BooleanAdapter {
@FromJson
public Boolean fromJson(int value) {
return value == 1;
}
@ToJson
public Integer toJson(Boolean value) {
return value ? 1 : 0;
}
}
复制代码
下面我们来测试一下
String json = "{\n" + "\"type\": 2,\n" + "\"isGood\": 1,\n"
+ "\"title\": \"TW9zaGkgaXMgZmxleGlibGU=\"\n"+ "}";
Moshi moshi = new Moshi.Builder()
.add(new TypeAdapter())
.add(new TitleAdapter())
.add(new BooleanAdapter())
.build();
JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
ConfigBean cofig = jsonAdapter.fromJson(json);
System.out.println("=========Deserialize ========");
System.out.println(cofig);
String cofigJson = jsonAdapter.toJson(cofig);
System.out.println("=========serialize ========");
System.out.println(cofigJson);
复制代码
打印Log
=========Deserialize ========
ConfigBean{type=CustomType{type=2, content='NORMAL'}, isGood=true, title='Moshi is flexible'}
=========serialize ========
{"isGood":1,"title":"TW9zaGkgaXMgZmxleGlibGU=","type":2}
复制代码
符合我们预期的结果,并且我们在开发的时候,只需要将Moshi设置成单例的,一次性将所有的Adapter全部add进去,就可以一劳永逸,然后愉快地进行开发了。
Moshi底层采用了 Okio 进行优化,但是上层的JsonReader,JsonWriter等代码是直接从Gson借鉴过来的,所以不再过多分析,主要是就Moshi的两大创新点 JsonAdapter 以及Kotlin的 Codegen 解析重点分析一下。
Moshi moshi = new Moshi.Builder().add(new BooleanAdapter()).build();
复制代码
Moshi是通过Builder模式进行构建的,支持添加多个JsonAdapter,下面先看看Builder源码
public static final class Builder {
//存储所有Adapter的创建方式,如果没有添加自定义Adapter,则为空
final List<JsonAdapter.Factory> factories = new ArrayList<>();
//添加自定义Adapter,并返回自身
public Builder add(Object adapter) {
return add(AdapterMethodsFactory.get(adapter));
}
//添加JsonAdapter的创建方法到factories里,并返回自身
public Builder add(JsonAdapter.Factory factory) {
factories.add(factory);
return this;
}
//添加JsonAdapter的创建方法集合到factories里,并返回自身
public Builder addAll(List<JsonAdapter.Factory> factories) {
this.factories.addAll(factories);
return this;
}
//通过Type添加Adapter的创建方法,并返回自身
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
return add(new JsonAdapter.Factory() {
@Override
public @Nullable JsonAdapter<?> create(
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) { return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
}
});
}
//创建一个Moshi的实例
public Moshi build() {
return new Moshi(this);
}
}
复制代码
通过源码发现Builder保存了所有自定义Adapter的创建方式,然后调用Builder的build方式创建了一个Moshi的实例,下面看一下Moshi的源码。
Moshi(Builder builder) {
List<JsonAdapter.Factory> factories = new ArrayList<>(
builder.factories.size() + BUILT_IN_FACTORIES.size());
factories.addAll(builder.factories);
factories.addAll(BUILT_IN_FACTORIES);
this.factories = Collections.unmodifiableList(factories);
}
复制代码
构造方法里面创建了factories,然后加入了Builder中的factories,然后又增加了一个BUILT_IN_FACTORIES,我们应该也能猜到这个就是Moshi内置的JsonAdapter,点进去看一下
static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
static {
BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
}
复制代码
BUILT_IN_FACTORIES这里面提前用一个静态代码块加入了所有内置的JsonAdapter
JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
复制代码
不管是我们自定义的JsonAdapter还是Moshi内置的JsonAdapter,最终都是为我们的解析服务的,所以最终所有的JsonAdapter最终汇聚成JsonAdapter,我们看看是怎么生成的,跟一下Moshi的adapter方法,发现最终调用的是下面的方法
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
@Nullable String fieldName) {
type = canonicalize(type);
// 如果有对应的缓存,那么直接返回缓存
Object cacheKey = cacheKey(type, annotations);
synchronized (adapterCache) {
JsonAdapter<?> result = adapterCache.get(cacheKey);
if (result != null) return (JsonAdapter<T>) result;
}
boolean success = false;
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
try {
if (adapterFromCall != null)
return adapterFromCall;
// 遍历Factories,直到命中泛型T的Adapter
for (int i = 0, size = factories.size(); i < size; i++) {
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
if (result == null) continue;
lookupChain.adapterFound(result);
success = true;
return result;
}
}
}
复制代码
最开始看到这里,我比较奇怪,不太确定我的Config命中了哪一个JsonAdapter,最终通过断点追踪,发现了是命中了 ClassJsonAdapter ,既然命中了他,那么我们就看一下他的具体实现
构造方法
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
//省略了很多异常判断代码
Class<?> rawType = Types.getRawType(type);
//获取Class的所有类型
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
Map<String, FieldBinding<?>> fields = new TreeMap<>();
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
//创建Moshi跟Filed的绑定关系,便于解析后赋值
createFieldBindings(moshi, t, fields);
}
return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
}
}
复制代码
当我们拿到一个JsonAdapter的时候,基本上所有的构建都已经完成,此时可以进行Deserialize 或者Serialize 操作,先看下Deserialize 也就是fromjson方法
对于Java的解析,Moshi并没有在传输效率上进行显著的提升,只是底层的IO操作采用的是Okio,Moshi的创新在于灵活性上面,也就是JsonAdapter,而且Moshi的官方文档上面也提到了
Moshi uses the same streaming and binding mechanisms as Gson . If you’re a Gson user you’ll find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without much violence!
所以这里的JsonReader跟JsonWriter说白了都是从Gson那里直接借鉴过来的,就是这么坦诚。
ConfigBean cofig = jsonAdapter.fromJson(json);
复制代码
这个方法先是调用了父类JsonAdapter的fromJson方法
public abstract T fromJson(JsonReader reader) throws IOException;
public final T fromJson(BufferedSource source) throws IOException {
return fromJson(JsonReader.of(source));
}
public final T fromJson(String string) throws IOException {
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
T result = fromJson(reader);
return result;
复制代码
我们发现fromJson是个重载方法,既可以传String也可以传BufferedSource,不过最终调用的都是fromJson(JsonReader reader)这个方法,BufferedSource是Okio的一个类,因为Moshi底层的IO采用的是Okio,但是我们发现参数为JsonReader的这个方法是抽象方法,所以具体的实现是是在ClassJsonAdapter里面,。
@Override public T fromJson(JsonReader reader) throws IOException {
T result = classFactory.newInstance();
try {
reader.beginObject();
while (reader.hasNext()) {
int index = reader.selectName(options);
//如果不是Key,直接跳过
if (index == -1) {
reader.skipName();
reader.skipValue();
continue;
}
//解析赋值
fieldsArray[index].read(reader, result);
}
reader.endObject();
return result;
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
//递归调用,直到最后
void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
T fieldValue = adapter.fromJson(reader);
field.set(value, fieldValue);
}
复制代码
String cofigJson = jsonAdapter.toJson(cofig);
复制代码
跟fromJson一样,先是调用的JsonAdapter的toJson方法
public abstract void toJson(JsonWriter writer, T value) throws IOException;
public final void toJson(BufferedSink sink, T value) throws IOException {
JsonWriter writer = JsonWriter.of(sink);
toJson(writer, value);
}
public final String toJson( T value) {
Buffer buffer = new Buffer();
try {
toJson(buffer, value);
} catch (IOException e) {
throw new AssertionError(e); // No I/O writing to a Buffer.
}
return buffer.readUtf8();
}
复制代码
不管传入的是泛型T还是BufferedSink,最终调用的toJson(JsonWriter writer),然后返回了buffer.readUtf8()。我们继续看一下子类的具体实现
@Override public void toJson(JsonWriter writer, T value) throws IOException {
try {
writer.beginObject();
for (FieldBinding<?> fieldBinding : fieldsArray) {
writer.name(fieldBinding.name);
//将fieldsArray的值依次写入writer里面
fieldBinding.write(writer, value);
}
writer.endObject();
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
复制代码
Moshi’s Kotlin codegen support is an annotation processor. It generates a small and fast adapter for each of your Kotlin classes at compile time. Enable it by annotating each class that you want to encode as JSON:
所谓Codegen,也就是我们上文提到的Annotation,在编译期间生成对应的JsonAdapter,我们看一下先加一下注解,看看Kotlin帮我们自动生成的注解跟我们自定义的注解有什么区别,rebuild一下项目:
@JsonClass(generateAdapter = true)
data class CustomType(var type: Int, var content: String)
复制代码
我们来看一下对应生成的JsonAdapter
这个类方法很多,我们重点看一下formJson跟toJson
override fun fromJson(reader: JsonReader): CustomType {
var type: Int? = null
var content: String? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.selectName(options)) {
//按照变量的定义顺序依次赋值
0 -> type = intAdapter.fromJson(reader)
1 -> content = stringAdapter.fromJson(reader)
-1 -> {
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
//不通过反射,直接创建对象,传入解析的Value
var result = CustomType(type = type ,content = content )
return result
}
override fun toJson(writer: JsonWriter, value: CustomType?) {
writer.beginObject()
writer.name("type")//写入type
intAdapter.toJson(writer, value.type)
writer.name("content")//写入content
stringAdapter.toJson(writer, value.content)
writer.endObject()
}
复制代码
@JsonClass(generateAdapter = true)
data class ConfigBean(var isGood: Boolean ,var title: String ,var type: CustomType)
复制代码
override fun fromJson(reader: JsonReader): ConfigBean {
var isGood: Boolean? = null
var title: String? = null
var type: CustomType? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.selectName(options)) {
0 -> isGood = booleanAdapter.fromJson(reader)
1 -> title = stringAdapter.fromJson(reader)
2 -> type = customTypeAdapter.fromJson(reader)
-1 -> {
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
var result = ConfigBean(isGood = isGood ,title = title ,type = type
return result
}
override fun toJson(writer: JsonWriter, value: ConfigBean?) {
writer.beginObject()
writer.name("isGood")
booleanAdapter.toJson(writer, value.isGood)
writer.name("title")
stringAdapter.toJson(writer, value.title)
writer.name("type")
customTypeAdapter.toJson(writer, value.type)
writer.endObject()
}
复制代码
通过查看生成的CustomTypeJsonAdapter以及ConfigBeanJsonAdapter,我们发现通过Codegen生成也就是注解的方式,跟反射对比一下,会发现有如下优点:
在进行kotlin解析的时候不管是采用Reflect还是Codegen,都必须保证类型一致,也就是父类跟子类必须是Java或者kotlin,因为两种解析方式,最终都是通过ClassType来进行解析的,同时在使用Codegen解析的时候必须保证Koltin的类型是 internal
或者 public
的。
Moshi整个用法跟源码看下来,其实并不是很复杂,但是针对Java跟Kotlin的解析增加了JsonAdapter的转换,以及针对Kotlin的Data类的解析提供了Codegen这种方式,真的让人耳目一新,以前遇到这种业务调用的时候需要二次转换的时候,都是去写 工具 类或者用的时候直接转换。不过Moshi也有些缺点,对于Kotlin的Null类型的支持并不友好,这样会在Kotlin解析的时候如果对于一个不可空的字段变成了Null就会直接抛异常,感觉不太友好,应该给个默认值比较好一些,还有就是对默认值的支持,如果Json出现了Null类型,那么解析到对应的字段依然会被赋值成Null,跟之前的Gson一样,希望以后可以进行完善,毕竟瑕不掩瑜。
本文来源:码农网
本文链接:https://www.codercto.com/a/36525.html
Activity 的启动模式本身是一个挺难理解的知识点,大多数开发者对这个概念的了解可能只限于四种 launchMode 属性值,但启动模式其实还需要受 Intent flag 的影响。而且 Activity 启动模式并不只是单纯地用来启动一个 Activity,实际上还会直接影响到用户的直观感受和使用体验,因为启动模式直接就决定了应用的任务栈和返回栈,这都是用户能直接接触到的
本篇文章就来简单介绍下 Activity 的启动模式,希望对你有所帮助 😇😇
任务栈是指用户在执行某项工作时与之互动的一系列 Activity 的集合,这些 Activity 按照打开的顺序排列在一个后进先出的栈中。例如,电子邮件应用包含一个 Activity 来显示邮件列表,当用户选择一封邮件时就会打开一个新的 Activity 来显示邮件详情,这个新的 Activity 就会添加到任务栈中,被推送到栈顶部并获得焦点,从而获得了与用户进行交互的机会。当用户按返回键时,邮件详情 Activity 就会从任务栈中退出并被销毁,邮件列表 Activity 就会成为新的栈顶并重新获得焦点
任务栈代表的是一个整体,本身包含了多个 Activity,当任务栈中的所有 Activity 都被弹出后,任务栈也就随之就被回收了。就像下图所示,三个 Activity 通过相继启动组成了一个任务栈,Activity 1 是整个任务栈的根 Activity,当用户不断按返回键,Activity 就会依次被弹出
返回栈是从用户使用的角度来进行定义的,返回栈中包含一个或多个任务栈,但同时只会有一个任务栈够处于前台,只有处于前台任务栈的 Activity 才能与用户交互
例如,用户先启动了应用 A,先后打开了 Activity 1 和 Activity 2,此时 Task A 是前台任务栈。之后用户又点击 Home 键回到了桌面,启动了应用 B,又先后打开了 的 Activity 3 和 Activity 4,此时 Task B 就成为了前台任务栈,Task A 成了后台任务栈。用户点击返回键的过程中依次展现的页面就会是 Activity 4 -> Activity 3 -> 桌面
而如果用户在打开应用 B 时并没有回到桌面,而是直接通过应用 A 启动了应用 B 的话,用户点击返回键的过程中依次展现的页面就会是 Activity 4 -> Activity 3 -> Activity 2 -> Activity 1 -> 桌面
返回栈所表示的就是当用户不断回退页面时所能看到的一系列 Activity 的集合,而这些页面可能是处于多个不同的任务栈中。在第一种情况中,返回栈只包含 Task B 一个任务栈,所以当 Task B 被清空后就会直接回到桌面。在第二种情况中,返回栈中包含 Task A 和 Task B 两个任务栈,所以当 Task B 被清空后也会先切回到 Task A,等到 Task A 也被清空后才会回到桌面
需要注意的是,返回栈中包含的多个任务栈之间并没有强制的先后顺序,多个任务栈之间的叠加关系可以随时发现变化。例如,当应用 A 启动了应用 B 后,Task B 是处于 Task A 之上,但之后如果应用 B 又反向启动了应用 A 的话,Task A 就会重新成为前台 Task 并覆盖在 Task B 之上
返回栈这个概念对应的就是 taskAffinity,是 Activity 在 AndroidManifest 文件中声明的一个属性值。taskAffinity 翻译为“亲和性”,用于表示特定 Activity 倾向于将自身存放在哪个任务栈中
在默认情况下,同一应用中的所有 Activity 会具有相同的亲和性,所有 Activity 默认会以当前应用的 applicationId 作为自己的 taskAffinity 属性值。我们可以手动为应用内的部分 Activity 指定特定的 taskAffinity,从而将这部分 Activity 进行分组
<activity
android:name=".StandardActivity"
android:launchMode="standard"
android:taskAffinity="task.test1" />
<activity
android:name=".SingleTopActivity"
android:launchMode="singleTop"
android:taskAffinity="task.test2" />
<activity
android:name=".SingleTaskActivity"
android:launchMode="singleTask"
android:taskAffinity="task.test3" />
<activity
android:name=".SingleInstanceActivity"
android:launchMode="singleInstance"
android:taskAffinity="task.test4" />
复制代码
从概念上讲,具有相同 taskAffinity 的 Activity 归属于同一任务栈(实际上并不一定)。从用户的角度来看则是归属于同一“应用”,因为每种 taskAffinity 在最近任务列表中会各自独占一个列表项,看起来就像一个个单独的应用,而实际上这些列表项可能是来自于同个应用
Activity 的启动模式是一个很复杂的难点,其决定了要启动的 Activity 和任务栈还有返回栈之间的关联关系,直接影响到了用户的直观感受
启动模式就由 launchMode 和 Intent flag 这两者来共同决定,我们可以通过两种方式来进行定义:
startActivity(Intent)
启动 Activity 时,向 Intent
添加或设置 flag 标记位,通过该 flag 来定义启动模式如果只看四个 launchMode 的话其实并不难理解,可是再考虑多应用交互还有 Intent flag 的话,情况就会变得复杂很多,其复杂性和难点主要就在于:单个任务栈包含的 Activity 可以是来自于不同的应用、单个应用也可以包含多个任务栈、返回栈包含的多个任务栈之间也可以进行顺序切换、甚至任务栈中的 Activity 也可以被迁移到另外一个任务栈、Intent flag 可以多个组合使用
有些启动模式可通过 launchMode 来定义,但不能通过 Intent flag 定义,同样,有些启动模式可通过 Intent flag 定义,却不能在 launchMode 中定义。两者互相补充,但不能完全互相替代,且 Intent flag 的优先级会更高一些
launchMode 一共包含以下四种属性值:
onNewIntent()
方法来将 Intent 转送给该实例并进行复用,否则会创建一个目标 Activity 的新实例。目标 Activity 可以多次实例化,不同实例可以属于不同的任务栈,一个任务栈可以拥有多个实例(此时多个实例不会连续叠放在一起)onNewIntent()
方法将 Intent 转送给该现有实例,而不会创建新实例,并同时弹出该目标 Activity 之上的所有其它实例,使目标 Activity 成为栈顶。如果系统当前包含目标任务栈,但该任务栈不包含目标 Activity 实例,则会实例化目标 Activity 并将其入栈。因此,系统全局一次只能有一个目标 Activity 实例存在singleTask
相似,唯一不同的是通过 singleInstance 启动的 Activity 会独占一个任务栈,系统不会将其和其它 Activity 放置到同个任务栈中,由该 Activity 启动的任何 Activity 都会在其它的任务栈中打开四种 launchMode 还是很好理解的,当中比较特殊的应该属 singleTask,使用 singleTask 标记的 Activity 会有将自己存放在特定任务栈的倾向。如果目标任务栈和目标 Activity 都已经存在,则会进行复用,否则才会创建目标任务栈和目标 Activity。singleInstance 则是在 singleTask 的基础上多了一个“独占任务栈”的特性
采用 singleTask
启动的 Activity 添加到返回栈的过程就如下图所示。一开始返回栈中只包含 Activity 1 和 Activity 2 组成的任务栈,当 Activity 2 启动了处于后台的 Activity Y 时,Activity Y 和 Activity X 组成的任务栈就会被转到前台,覆盖住当前任务栈。最终返回栈中就变成了四个 Activity
再来写个 Demo 来验证下这四种 launchMode 的效果
声明四种不同 launchMode 的 Activity,每个 Activity 均声明了不同的 taskAffinity
<activity
android:name=".StandardActivity"
android:launchMode="standard"
android:taskAffinity="task.a" />
<activity
android:name=".SingleTopActivity"
android:launchMode="singleTop"
android:taskAffinity="task.b" />
<activity
android:name=".SingleTaskActivity"
android:launchMode="singleTask"
android:taskAffinity="task.c" />
<activity
android:name=".SingleInstanceActivity"
android:launchMode="singleInstance"
android:taskAffinity="task.d" />
复制代码
通过打印 Activity 的 hashCode()
方法返回值来判断 Activity 的实例是否被复用了,再通过 getTaskId()
方法来判断 Activity 处于哪个任务栈中
/**
* @Author: leavesC
* @Date: 2021/4/16 16:38
* @Desc:
* @Github:https://github.com/leavesC
*/
abstract class BaseLaunchModeActivity : BaseActivity() {
override val bind by getBind<ActivityBaseLaunchModeBinding>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind.tvTips.text =
getTip() + "\n" + "hashCode: " + hashCode() + "\n" + "taskId: " + taskId
bind.btnStartStandardActivity.setOnClickListener {
startActivity(StandardActivity::class.java)
}
bind.btnStartSingleTopActivity.setOnClickListener {
startActivity(SingleTopActivity::class.java)
}
bind.btnStartSingleTaskActivity.setOnClickListener {
startActivity(SingleTaskActivity::class.java)
}
bind.btnStartSingleInstanceActivity.setOnClickListener {
startActivity(SingleInstanceActivity::class.java)
}
log("onCreate")
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
log("onNewIntent")
}
override fun onDestroy() {
super.onDestroy()
log("onDestroy")
}
abstract fun getTip(): String
private fun log(log: String) {
Log.e(getTip(), log + " " + "hashCode: " + hashCode() + " " + "taskId: " + taskId)
}
}
class StandardActivity : BaseLaunchModeActivity() {
override fun getTip(): String {
return "StandardActivity"
}
}
复制代码
四个 Activity 相继互相启动,查看输出的日志,可以看出 SingleTaskActivity 和 SingleInstanceActivity 均处于独立的任务栈中,而 StandardActivity 和 SingleTopActivity 处于同个任务栈中。说明 taskAffinity 对于 standard 和 singleTop 这两种模式不起作用
E/StandardActivity: onCreate hashCode: 31933912 taskId: 37
E/SingleTopActivity: onCreate hashCode: 95410735 taskId: 37
E/SingleTaskActivity: onCreate hashCode: 255733510 taskId: 38
E/SingleInstanceActivity: onCreate hashCode: 20352185 taskId: 39
复制代码
再依次启动 SingleTaskActivity 和 SingleTopActivity。可以看到 SingleTaskActivity 被复用了,且在 38 这个任务栈上启动了一个新的 SingleTopActivity 实例。之所以没有复用 SingleTopActivity,是因为之前的 SingleTopActivity 是在 37 任务栈中,并非当前任务栈
E/SingleTaskActivity: onNewIntent hashCode: 255733510 taskId: 38
E/SingleTopActivity: onCreate hashCode: 20652250 taskId: 38
复制代码
再启动一次 SingleTopActivity,两次 StandardActivity。可以看到 SingleTopActivity 的确在当前任务栈中被复用了,并均创建了两个新的 StandardActivity 实例。说明 singleTop 想要被复用需要当前任务栈的栈顶就是目标 Activity,而 standard 模式每次均会创建新实例
E/SingleTopActivity: onNewIntent hashCode: 20652250 taskId: 38
E/StandardActivity: onCreate hashCode: 252563788 taskId: 38
E/StandardActivity: onCreate hashCode: 25716630 taskId: 38
复制代码
再依次启动 SingleTaskActivity 和 SingleInstanceActivity。可以看到 SingleTaskActivity 和 SingleInstanceActivity 均被复用了,且 SingleTaskActivity 之上的三个 Activity 均从任务栈中被弹出销毁了,SingleTaskActivity 成为了 task 38 新的栈顶 Activity
E/StandardActivity: onDestroy hashCode: 252563788 taskId: 38
E/SingleTopActivity: onDestroy hashCode: 20652250 taskId: 38
E/SingleTaskActivity: onNewIntent hashCode: 255733510 taskId: 38
E/StandardActivity: onDestroy hashCode: 25716630 taskId: 38
E/SingleInstanceActivity: onNewIntent hashCode: 20352185 taskId: 39
复制代码
再依次启动 StandardActivity 和 SingleTopActivity。可以看到创建了一个新的任务栈,且启动的是两个新的 Activity 实例。由于 SingleInstanceActivity 所在的任务栈只会由其自身所独占,所以 StandardActivity 启动时就需要创建一个新的任务栈用来容纳自身
E/StandardActivity: onCreate hashCode: 89641200 taskId: 40
E/SingleTopActivity: onCreate hashCode: 254021317 taskId: 40
复制代码
可以做个总结:
需要注意的是,以上结论只适用于没有主动添加 Intent flag 的情况,如果同时添加了 Intent flag 的话就会出现很多奇奇怪怪的现象了
在启动 Activity 时,我们可以通过在传送给 startActivity(Intent)
方法的 Intent 中设置多个相应的 flag 来修改 Activity 与其任务栈的默认关联,即 Intent flag 的优先级会比 launchMode 高
Intent 提供的设置 flag 的方法有以下两个,一个是覆盖设置,一个是增量添加
private int mFlags;
public @NonNull Intent setFlags(@Flags int flags) {
mFlags = flags;
return this;
}
public @NonNull Intent addFlags(@Flags int flags) {
mFlags |= flags;
return this;
}
复制代码
通过如下方式来添加 flag 并启动 Activity
val intent = Intent(this, StandardActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
复制代码
如果 Activity 的启动模式只由 launchMode 定义的话,那么在运行时 Activity 的启动模式就再也无法改变了,相当于被写死了,所以 launchMode 适合于那些具有固定情景的业务。而 Intent flag 存在的意义就是为了改变或者补充 launchMode,适合于那些大部分情况下固定,少数情况下需要动态进行变化的场景,例如在某些情况下不希望 singleInstance 模式的 Activity 被重用,此时就可以通过 Intent flag 来动态实现
而这也造成了 Intent flag 很难理清楚逻辑,因为 Intent flag 往往需要组合使用,且还需要考虑和 launchMode 的各种组合配置,两者并不是简单的进行替换
Intent flag 有很多个,比较常见的有四个,这里就简单介绍下这几种 Intent flag
FLAG_ACTIVITY_NEW_TASK 应该是大多数开发者最熟悉的一个 flag,比较常用的一个场景就是用于在非 ActivityContext 环境下启动 Activity。Android 系统默认情况下是会将待启动的 Activity 加入到启动者所在的任务栈,而如果启动 Activity 的是 ServiceContext 的话,此时系统就不确定该如何存放目标 Activity 了,此时就会抛出一个 RuntimeException
java.lang.RuntimeException: Unable to start service github.leavesc.launchmode.MyService@e3183b7 with Intent { cmp=github.leavesc.demo/github.leavesc.launchmode.MyService }: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
复制代码
从异常信息可以看出此时 Intent 需要添加一个 FLAG_ACTIVITY_NEW_TASK 才行,添加后 Activity 就可以正常启动了
val intent = Intent(this, StandardActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
复制代码
FLAG_ACTIVITY_NEW_TASK 也有一个隐含的知识点,上文有讲到 standard 和 singleTop 这两种模式下 taskAffinity 属性均不会生效,但这个结论也只适用于没有主动添加 Intent flag 的情况
FLAG_ACTIVITY_NEW_TASK 和 standard 模式的组合情况可以总结为:
onNewIntent
方法,甚至也不管该 Activity 实例是否处于栈顶,总之只要存在相同实例就不做任何响应。。。。。。。可以看到,FLAG_ACTIVITY_NEW_TASK 的语义还是有点费解的,该标记位可以使得 taskAffinity 生效,创建或者复用任务栈并将其转到前台,但并不要求必须创建一个新的 Activity 实例,而是只要 Activity 实例有存在即可,而且也无需该 Activity 实例就在栈顶
FLAG_ACTIVITY_SINGLE_TOP 这个 flag 看名字就很容易和 singleTop 联系在一起,实际上该 flag 也的确起到了和 singleTop 相同的作用
只要待启动的 Activity 添加了该标记位,且当前任务栈的栈顶就是目标 Activity,那么该 Activity 实例就会被复用,并且回调其onNewIntent
方法,即使该 Activity 声明了 standard 模式,这相当于将 Activity 的 launchMode 覆盖为了 singleTop
FLAG_ACTIVITY_CLEAR_TOP 这个 flag 则是起到了清除目标 Activity 之上所有 Activity 的作用。例如,假设当前要启动的 Activity 已经在目标任务栈中了,那么设置该 flag 后系统就会清除目标 Activity 之上的所有其它 Activity,但系统最终并不一定会复用现有的目标 Activity 实例,有可能是销毁后再次创建一个新的实例
看个例子。先以不携带 flag 的方式启动 StandardActivity 和 SingleTopActivity,此时日志信息如下
E/StandardActivity: onCreate hashCode: 76763823 taskId: 39
E/SingleTopActivity: onCreate hashCode: 217068130 taskId: 39
复制代码
再启动一次 StandardActivity,此时就带上 FLAG_ACTIVITY_CLEAR_TOP。此时就会看到最开始启动的两个 Activity 都会销毁了,并且再次新建了一个 StandardActivity 实例入栈
E/StandardActivity: onDestroy hashCode: 76763823 taskId: 39
E/StandardActivity: onCreate hashCode: 51163106 taskId: 39
E/SingleTopActivity: onDestroy hashCode: 217068130 taskId: 39
复制代码
而如果同时加上 FLAG_ACTIVITY_SINGLE_TOP 和 FLAG_ACTIVITY_CLEAR_TOP 两个 flag 的话,那么 SingleTopActivity 就会被弹出,StandardActivity 会被复用,并且回调其onNewIntent
方法,两个 flag 相当于组合出了 singleTask 的效果。这一个效果读者可以自行验证
FLAG_ACTIVITY_CLEAR_TASK 的源码注释标明了该 flag 必须和 FLAG_ACTIVITY_NEW_TASK 组合使用,它起到的作用就是将目标任务栈中的所有 Activity 情空,然后新建一个目标 Activity 实例入栈,该 flag 的优先级很高,即使是 singleInstance 类型的 Activity 也会被销毁
看个例子。先启动一个 SingleInstanceActivity,然后以添加了 NEW_TASK 和 CLEAR_TASK 两个 flag 的方式再次启动 SingleInstanceActivity,可以看到旧的 Activity 实例被销毁了,并重建了一个新实例入栈,但比较奇怪的一点就是:旧的 Activity 实例的 onNewIntent
方法同时也被调用了
E/SingleInstanceActivity: onCreate hashCode: 144724929 taskId: 47
E/SingleInstanceActivity: onNewIntent hashCode: 144724929 taskId: 47
E/SingleInstanceActivity: onCreate hashCode: 106721743 taskId: 47
E/SingleInstanceActivity: onDestroy hashCode: 144724929 taskId: 47
复制代码
关于 Activity 的启动模式的讲解到这里就结束了,最后再强调一遍,launchMode 和 Intent flag 的各种组合效果还是有点过于难理解了,使得我很难全面地进行描述,再加上似乎还存在版本兼容性问题,使用起来就更加麻烦了,所以我觉得开发者只需要有个大致的印象即可,当真正要使用的时候再来亲自测试验证效果就好,不必强行记忆
以上各个示例 Demo 点这里:AndroidOpenSourceDemo
本文不去阐述 Okio 产生原因,也不去对比 Okio 与 Java 原生 IO 的优劣性,单纯分析 Okio 的实现,对每个关键点分析透彻,并配上精美图解。
Okio 整个框架的代码量并不大,体现了高内聚的设计,类框架大概如下:
图中体现了框架内部类之间的关系,整个框架做的事情大致就是:
Source 与 Sink 是 Okio 中所有输入输出流的基类接口,类似 Java IO 中 InputStream,OutputStream。
Source,源的意思,也就是说我是数据源,你们要的数据都从我这来。
Sink,往下沉,往外运输的意思,从 Source 中读到数据后,通过我传出去。
分别定义了数据流的读写接口:
public interface Source extends Closeable {
/**
* Removes at least 1, and up to {@code byteCount} bytes from this and appends
* them to {@code sink}. Returns the number of bytes read, or -1 if this
* source is exhausted.
*/
long read(Buffer sink, long byteCount) throws IOException;
/** Returns the timeout for this source. */
Timeout timeout();
/**
* Closes this source and releases the resources held by this source. It is an
* error to read a closed source. It is safe to close a source more than once.
*/
@Override void close() throws IOException;
}
复制代码
public interface Sink extends Closeable, Flushable {
/** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
void write(Buffer source, long byteCount) throws IOException;
/** Pushes all buffered bytes to their final destination. */
@Override void flush() throws IOException;
/** Returns the timeout for this sink. */
Timeout timeout();
/**
* Pushes all buffered bytes to their final destination and releases the
* resources held by this sink. It is an error to write a closed sink. It is
* safe to close a sink more than once.
*/
@Override void close() throws IOException;
}
复制代码
在 Okio 中 Source,Sink 也有几个实现类,我们只看 RealBufferedSource,RealBufferedSink。
RealBufferedSource
从名字中看出,带有缓冲的数据源(既然是源,那么你们想要的数据都从我这拿),但是它不是真正的数据源,真正的数据源是成员变量 source,它只是包装装饰了一下,
当 read 开头的方法被调用时,都是从成员变量 source 中读取,读到数据后,先存入成员变量 buffer 中,然后再从 buffer 中读数据。
来看 read 方法:
--> RealBufferedSource.java
final class RealBufferedSource implements BufferedSource {
// 缓冲,数据先放到 buffer 中,别人来读的时候先从 buffer 中读,没有的话再从 source 中读
public final Buffer buffer = new Buffer();
// 真正数据源,也是此 RealBufferedSource 的上游数据源,当 buffer 中没数据时,从 source 中读
public final Source source;
boolean closed;
...
@Override public long read(Buffer sink, long byteCount) throws IOException {
// 该方法作用:从当前 Source 中 读取 byteCount 个字节存到 sink 中
if (sink == null) throw new IllegalArgumentException("sink == null");
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
// 若当前 buffer 中没数据,就从成员变量 source 读,读到了再存入 buffer 中
// 若当前 source 中也没数据,则返回-1
// 若当前 buffer 中有数据,则跳过这里,直接从 buffer 中读
if (buffer.size == 0) {
long read = source.read(buffer, Segment.SIZE);
if (read == -1) return -1;
}
// 到这里 buffer 中肯定是有数据的,取小者,因为 buffer 中的数据量可能不足 byteCount 个,谁小就用谁
long toRead = Math.min(byteCount, buffer.size);
// 将 buffer 中数据读到 sink 中
return buffer.read(sink, toRead);
}
}
复制代码
RealBufferedSink
从名字中看出,带有缓冲的输出流(既然是输出流,那么你们想要往外写的数据都通过我来写),但它不是真正的输出流,真正的输出流是 成员变量 sink,它只是包装装饰了一下,当 write 开头的方法被调用时,都是先将数据写到成员变量 buffer 中,然后再通过成员变量 sink 往外写。
来看 write 方法:
final class RealBufferedSink implements BufferedSink {
// 缓冲,当需要通过我进行数据输出时,数据会先存到 buffer 中,再通过 sink 输出
public final Buffer buffer = new Buffer();
// 真正输出流,也是此 RealBufferedSink 的下游,当需要进行数据输出时,通过 sink 输出
public final Sink sink;
boolean closed;
...
@Override public void write(Buffer source, long byteCount)
throws IOException {
if (closed) throw new IllegalStateException("closed");
// 将 source 中的数据写到 buffer 中
buffer.write(source, byteCount);
// 将 buffer 中的数据通过 sink 写出去,内部具体如何写的,在后续 Buffer 章节中详细分析
emitCompleteSegments();
}
复制代码
整个输入流到输出流可用如下图表示:
缓冲的意思,Buffer 是 Okio 中的核心类,整个数据中转靠的就是它。
那么请问,我们为什么要 Buffer 这个东西?我们之前用 Java IO 时不用带 Buffer 的 InputStream 不也照样可以读么?
我们举个通俗的例子解答这个问题,假如,我们在果园里有一颗苹果树,想吃的时候,去摘一个,什么时候再想吃了,再去树上摘一个,那么,这样跑来跑去的不累么?每次还得跑到园子里。那我们何不先摘个十个八个的,放到箩筐里面带回家,想吃的时候,直接从箩筐里拿,就不必跑那么远到树上去摘了。
Buffer 就是扮演的上面的箩筐的角色,所以 Buffer 的存在是非常关键的,可以做到省时省力。
看下 Okio 中 Buffer:
--> Buffer.java
public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
// Buffer 实现了 BufferedSource,BufferedSink,也就是说,Buffer 既可以
// 作为 Source 的缓冲,也可以作为 Sink 的缓冲,
private static final byte[] DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
static final int REPLACEMENT_CHARACTER = '\ufffd';
// 关键点,存储数据链表头
@Nullable Segment head;
// 当前 buffer 中的字节数量
long size;
...
public Buffer() {
}
}
复制代码
Buffer 是用来缓冲数据的,要缓冲数据,就需要容器,那么它内部靠什么来存储数据?是 Segment。Buffer 中主要就是依靠 Segment 来存储数据,将 Segment 组成循环链表的结构,可用下图表示:
那我们就来看看 Segment 是什么,注意,Buffer 还没说完,等 Segment 分析完后,会再通过分析具体方法,来看看 Buffer 与 Segment 是如何配合完成工作的。
Segment 片段的意思,先看下类源码:
--> Segment.java
final class Segment {
// 每个 Segment 最大容量,也就是最多容纳 8192 个字节
static final int SIZE = 8192;
// 在 Segment 分割的场景(后面讲),如果要分割出去的字节数大于 1024 这个临界值,
// 那么直接共享 data,不要去做数组拷贝了
static final int SHARE_MINIMUM = 1024;
// 真正存放数据的地方
final byte[] data;
// data 中第一个可读位置,比如当前 data 中有 1024 个字节,pos = 0,被读取了一个字节后
// pos = 1,下次再读的话,就要从 1 开始读了
int pos;
// data 中第一个可写的位置,比如当前 data 中有 1024 个字节,那么第一个可写的位置是 1025
int limit;
// 此 Segment 是否在与别的 Segment 或者 ByteString 共享 data
boolean shared;
// 该 Segment 是否独享 data,独享时,可向 data 写入数据
boolean owner;
// 循环链表下一个节点
Segment next;
// 循环链表前一个节点
Segment prev;
Segment() {
this.data = new byte[SIZE];
this.owner = true;
this.shared = false;
}
Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {
this.data = data;
this.pos = pos;
this.limit = limit;
this.shared = shared;
this.owner = owner;
}
...
}
复制代码
看到 next,prev,就知道 Segment 会组成双向链表结构,只不过 Okio 中是双向循环链表,Segment 可用如下图表示:
Segment 内部定义了操作数据、操作链表的方法,我们着重分析以下几个方法:
// 往当前链表中添加一个节点,放在被调用 Segment 之后
public final Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
复制代码
// 从当前链表中删除该节点
public final @Nullable Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
复制代码
上面两个方法是简单的链表增删操作,不需要注释,只需要注意以下几点:
push:
1、先将被push进来的节点的 prev、next 安顿好
2、再将与新节点相邻的前一个节点的 next 安顿好
3、最后将与新节点相邻的后一个节点的 prev 安顿好
pop:
1、先安顿前邻居的 next
2、再安顿后邻居的 prev
3、最后安顿自己的 prev、next
总之,在一个双向链表中增删操作,无非就是关心三个节点:自己、前邻居、后邻居,自己要解决prev next,前邻居解决next,后邻居解决 prev
public final Segment split(int byteCount) {
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
Segment prefix;
// We have two competing performance goals:
// 我们有两个竞争的性能目标
// - Avoid copying data. We accomplish this by sharing segments.
// - 避免数据的拷贝。我们通过共享 Segment 来达成这一目标
// - Avoid short shared segments. These are bad for performance because they are readonly and
// may lead to long chains of short segments.
// - 避免短的共享 Segment,这样会导致性能差,因为共享后,Segment 只能读不能写,并且
// 可能导致链表中短 Segment 链变的很长
// To balance these goals we only share segments when the copy will be large.
// 为了平衡这些目标,我们只在需要拷贝的数据量比较大时,才会采用共享 Segment 的方式
if (byteCount >= SHARE_MINIMUM) {
// 当要分割出去的数据大于SHARE_MINIMUM(1024)时,会采用共享 Segment 的方式创建新的Segment,
// 注意,共享的是 Segment 里面的 data,还是会创建一个新的 Segment,只不过 data 是同一个
prefix = sharedCopy();
} else {
// 当要分割的数据小于SHARE_MINIMUM(1024)时,那么直接 copy 吧,反正顶天不会超过 1024 个字节
prefix = SegmentPool.take();
System.arraycopy(data, pos, prefix.data, 0, byteCount);
}
// 到这里,我们分割出来的 Segment 就创建好了,但是我们要始终注意的是:分割的是
// Segment 里的 data,data 被一刀两断了,那么两个 Segment 是不是要把 pos,limit 都调整下
// 调整新创建的 Segment 的 limit 为 pos + byteCount
prefix.limit = prefix.pos + byteCount;
// 调整原 Segment 的 pos 为 pos + byteCount
pos += byteCount;
// 加到该 Segment 的前节点后面,其实在 Okio 中,这个 prev 就是 tail,
// 你可能会问,这个方法哪个 Segment 对象都可以调用,这个 prev 是调用这个方法
// 的 Segment 的前节点,为啥会是 tail 节点?其实在 Okio 框架内部只有一个地方
// 调用了,就是在 Buffer 的 write(Buffer source, long byteCount) 里,
// 里面是用 head 去调用的,而 head 的 prev 是 tail
// 你可能又有疑问了,这方法是 public 的,外部都可以访问啊,但请看 Segment ,这家伙
// 不是 public 的,包内访问限制,外部无法使用,只能在 Okio 内使用,so...
prev.push(prefix);
return prefix;
}
复制代码
split 方法的过程可用下图表示:
split 过程完成后,Buffer 中 链表变化如下图:
那么这个 split 方法有何用呢? 上面注释中提到过,split 方法在 Okio 中唯一调用处在 Buffer 的 write(Buffer source, long byteCount),这个方法中的最后一段注释这样写道:
/**
* Occasionally we write only part of a source buffer to a sink buffer. For
* example, given a sink [51%, 91%], we may want to write the first 30% of
* a source [92%, 82%] to it. To simplify, we first transform the source to
* an equivalent buffer [30%, 62%, 82%] and then move the head segment,
* yielding sink [51%, 91%, 30%] and source [62%, 82%].
*/
复制代码
意思是:在某些场景下,我们只需要从 Source 中 写一部分数据 到 Sink 中,例如我们现在有一个 Sink,未读数据占比是[51%, 91%](注意,代表这个 Buffer 有两个 Segment,未读数据占比),我们可能想从一个[92%, 82%]的 Source 中往 Sink 中写30%的数据。为了简便,我们首先把 Source 转化为一个同等的 Buffer [30%, 62%, 82%],然后把头 Segment 移动到 Sink 中去(注意是移动,不是拷贝,这样就是一个指针操作),那么此时 Source 中只剩[62%, 82%]。
不难理解,无非就是把 Source 的两个 Segment 拆成三个,然后把其中一个移到 Sink 中,这样就避免了比较大的数据量的拷贝,只是移动了指针,在 split 中,当需要写的数据小于1024,才会有拷贝操作,大于1024时,直接共享数据,所以这里是一个性能提升的地方。
// 把当前 Segment 数据写到 sink 中去
public final void writeTo(Segment sink, int byteCount) {
// 当 sink 不独享数据时,直接抛异常,因为不独享时,只可读,不可写
if (!sink.owner) throw new IllegalArgumentException();
// 若 sink 的可写空间不足了
if (sink.limit + byteCount > SIZE) {
// We can't fit byteCount bytes at the sink's current position. Shift sink first.
// 若 sink 与别的 Segment 共享数据,只读不写,直接抛异常
if (sink.shared) throw new IllegalArgumentException();
// 若将已读空间重复利用,sink空间还不够,抛异常
if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
sink.limit -= sink.pos;
sink.pos = 0;
}
System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
sink.limit += byteCount;
pos += byteCount;
}
}
复制代码
writeTo 过程可用如下图表示:
// 压缩,将 tail 中数据往 prev 中转移
public final void compact() {
// 若前节点就是自己,那么此时链表中只有一个节点
if (prev == this) throw new IllegalStateException();
// 若前节点非独享,也就是不可写,直接return
if (!prev.owner) return; // Cannot compact: prev isn't writable.
// 当前节点的未读数据大小
int byteCount = limit - pos;
// 前节点的最大可写空间,记得我们上面的 writeTo方法吗,最大可写空间=可写空间+已读空间
int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
// 若前节点的最大可写空间容不下即将要写入的数据,直接return
if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
// 到这里肯定是可以写入的,可不还是调用 writeTo 么
writeTo(prev, byteCount);
// 当前节点从链表中断开
pop();
// 从链表中断开了别扔了,回收,下次利用
SegmentPool.recycle(this);
}
复制代码
compact 是压缩的意思,在有的情况下,链表中的 Segment 利用率不高(可写的空间还有很多),这个时候我们能不能把后面一个节点的数据往这个节点里面压一压呢?以提高利用率,同时可以回收后一个节点,减小链表长度,一举两得。要注意的是,这个方法是 tail 节点调用。
compact过程可由下图表示:
Segment 提供了这些原子方法,让他人去调用吧。
之前在 Buffer 小节,还未说完,我们现在通过 Buffer 的 write 和 read 方法来具体分析从一个缓存到另一个缓存的读写过程。
// 从 source 中移动 byteCount 个字节到当前Buffer(去掉许多注释)
@Override public void write(Buffer source, long byteCount) {
// Move bytes from the head of the source buffer to the tail of this buffer
// 从 source 的 head 移动数据到当前Buffer的 tail中
if (source == null) throw new IllegalArgumentException("source == null");
if (source == this) throw new IllegalArgumentException("source == this");
// 检查偏移和移动数量,防止越界
checkOffsetAndCount(source.size, 0, byteCount);
// 为什么是while循环,因为byteCount可能很大,而Segment移动数据有限
while (byteCount > 0) {
// Is a prefix of the source's head segment all that we need to move?
// 如果source的头里面的未读数据就比byteCount大
if (byteCount < (source.head.limit - source.head.pos)) {
// 因为写都是往尾部写入,这里先找到当前Buffer的tail节点
Segment tail = head != null ? head.prev : null;
if (tail != null && tail.owner
&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
// Our existing segments are sufficient. Move bytes from source's head to our tail.
// 当前Buffer的tail节点的可写空间就够了,直接将数据写入tail就行,这里使用的是拷贝
source.head.writeTo(tail, (int) byteCount);
source.size -= byteCount;
size += byteCount;
return;
} else {
// We're going to need another segment. Split the source's head
// segment in two, then move the first of those two to this buffer.
// 将source的head一分为二,因为当前Buffer的tail容不下byteCount个字节了
// 当然,你可能会问,如果tail为null或者不满足上面的if里的任何一个条件都会走
// 这里的else,别忘了,这个else始终被外层的if条件约束着
source.head = source.head.split((int) byteCount);
}
}
// Remove the source's head segment and append it to our tail.
// 如过走了上面的if,那么这里的source.head已经是分割过的,如果没走if,什么都没干
Segment segmentToMove = source.head;
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
// 将segmentToMove从source中移除,因为它将要加入到当前的Buffer中
source.head = segmentToMove.pop();
if (head == null) {
head = segmentToMove;
head.next = head.prev = head;
} else {
Segment tail = head.prev;
// 加入到当前Buffer中
tail = tail.push(segmentToMove);
// 再压缩一下,尽量让segment满载,提高利用率
tail.compact();
}
// 数据移动完了,设置source与当前Buffer的变量,source数据少了,这边多了
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
//如果一趟没将byteCount个字节的数据移动完,再进行下一次循环
}
}
复制代码
public long read(Buffer sink, long byteCount) {
if (sink == null) throw new IllegalArgumentException("sink == null");
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (size == 0) return -1L;
if (byteCount > size) byteCount = size;
// 其实就是将当前Buffer中的数据写到sink中,这里好像与我们看到这个方法的第一反应
// 有点不同,我们可能想的是:既然是read,就是要从外头读数据,但是这里是往外头读
// 我们还是要理解source与sink的概念,从source读,往sink中写
sink.write(this, byteCount);
return byteCount;
}
是一个请求有多个对象来处理,这些对象是一条链,但具体由哪个对象来处理,根据条件判断来确定,如果不能处理会传递给该链中的下一个对象,直到有对象处理它为止。
/**
* 抽象请假审批类
*/
public abstract class Handler {
public int maxDay;
private Handler nextHandler;
public Handler(int maxDay) {
this.maxDay = maxDay;
}
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
public void handlerRequest(int day){
if(day <= maxDay){
reply(day);
} else{
if(nextHandler != null){
nextHandler.handlerRequest(day);
}else {
System.out.println("没有跟高的领导审批了");
}
}
}
protected abstract void reply(int day);
}
/**
* 技术主管审批类
*/
public class Handler1 extends Handler {
public Handler1() {
super(10);
}
@Override
protected void reply(int day) {
System.out.print(day+"天请假,技术主管直接通过");
}
}
* 项目经理审批类
*/
public class Handler2 extends Handler {
public Handler2() {
super(5);
}
@Override
protected void reply(int day) {
System.out.print(day+"天请假,项目经历直接通过");
}
}
//代码运行
Handler2 handler2 = new Handler2();
handler2.setNextHandler(new Handler1());
handler2.handlerRequest(3);
在java中的实际运用,try-cache语句
在android 中发布有序广播 ordered broadcast,viewGroup/view 事件传递
责任链模式是一种对象行为型模式,其主要优点如下。
其主要缺点如下。
1)主要概念
MotionEvent 事件分发的基本操作,所有事件都是MotionEvent,对MotionEvent操作。
主要有三种操作 ACTION_DOWN/ACTION_MOVE/ACTION_UP
三个方法
dispatchTouchEvent 当有事件传递给view的时候,view就会调用该方法。这个返回值为boolean,返回告诉系统是否消耗了该事件。true为消耗,不会继续向下分发
onIntereptTouchEvent 拦截事件,只存在于viewgroup,view(存在于view树的最底层,没必要拦截了)中没有拦截事件。如果当前view 成功拦截这个事件,则要返回true,默认为false
onTouchEvent 具体处理整个事件逻辑的,对于他的返回结果就是是否消耗当前事件,如果不消耗当前事件的话,对于同一个事件,当前view就不会再接到这个事件。
总结:如果当前view可以处理就拦截处理,如果不可以,就交给子view
事件处理顺序
Activity -> PhoneWindow ->RootView ->ViewGroup ->View
2)viewgroup的事件分发
流程:
3)view的事件分发
view为啥会有dispatchTouchEvent方法?view可以注册很多事件监听器,可以进行事件分发
事件调用顺序:onTouchListener>onTouchEvent>onLongClickListener>onClickListener
收起阅读 »在对象之间定义了一对多的依赖,使得么当一个对象状态发生改变,其相关依赖对象会收到通知并自动更新。
实现观察者代码:
/**
*
* 创建观察者抽象类
*/
public interface Observer {
//更新方法
void update(String newStatus);
}
/**
* 创建观察者实现类
*/
public class ConcreteObserver implements Observer {
/**
* 观察者状态
*/
private String observerState;
@Override
public void update(String newStatus) {
observerState = newStatus;
System.out.println(newStatus);
}
}
/**
* 创建抽象目标者
* Created by shidawei on 2019/5/23.
*/
public abstract class Subject {
private List<Observer> mObservers = new ArrayList<>();
/**
* 注册观察
* @param observer
*/
public void attach(Observer observer){
mObservers.add(observer);
System.out.println("注册观察");
}
/**
* 移除观察者
* @param observer
*/
public void detach(Observer observer){
mObservers.remove(observer);
}
/**
* 通知观察者
* @param newStatus
*/
public void notifyObsercers(String newStatus){
for(Observer observer:mObservers){
observer.update(newStatus);
}
}
}
/**
* 实现被观察者
*/
public class ConcreteSubject extends Subject {
private String state;
public String getState() {
return state;
}
public void change(String newState){
state = newState;
System.out.println(newState);
notifyObsercers(newState);
}
}
//测试代码
ConcreteSubject concreteSubject = new ConcreteSubject();
concreteSubject.attach(new ConcreteObsercer());
concreteSubject.attach(new ConcreteObsercer());
concreteSubject.attach(new ConcreteObsercer());
concreteSubject.change("123");
java内部的接口实现:
/**
* Observable 是被观察者对象接口,是对被观察者的实现
*/
public class TargetObervable extends Observable {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
//被观察者数据发生改变的时候通过如下两行代码通知所有观察者
this.setChanged();
this.notifyObservers();
}
}
/**
* Observer 对象是观察者,实现Observer的对象就是实现观察者对象
*/
public class TargetOberver implements Observer {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println(name + "收到数据变更" + ((TargetObervable) o).getMessage());
}
}
//测试代码
TargetObervable targetObervable = new TargetObervable();
targetObervable.addObserver(new TargetOberver());
targetObervable.addObserver(new TargetOberver());
targetObervable.addObserver(new TargetOberver());
targetObervable.setMessage("1234");
两种观察者对比:
通过读源码Observable.class我们得知对Observer的集合为Vector
Vector为线程安全的,会保证线程安全,但是性能差。可以采用CopyOnWriteArrayList来代替Vector。
回调模式:一对一的模式
实现了抽象类/接口的实例实现了负累的提供的抽象方法,然后将该方法还给父类来处理。
Fragment与activity通信的代码实例:
/**
*回调接口,与activity通信
**/
public interface ISwitchCaoZuoRecordFragment {
void toSwitch(CaiZuoRecordFragFragment fragment, CaiZuoRecordFragPresenter presenterDecorator);
}
/**
* activity实现该接口
**/
public class CaoZuoRecordActivity extends BaseActivity<CaoZuoRecordView, CaoZuoRecordPresenter> implements CaoZuoRecordView ,CaiZuoRecordFragFragment.ISwitchCaoZuoRecordFragment{
@Override
public void toSwitch(CaiZuoRecordFragFragment fragment, CaiZuoRecordFragPresenter presenterDecorator) {
mPresenterDecorator = presenterDecorator;
if(presenterDecorator.studentName!=null&&!presenterDecorator.studentName.equals("")){
searchListTitleBar.getSearch().setText(presenterDecorator.studentName);
searchListTitleBar.getClear().setVisibility(View.VISIBLE);
searchListTitleBar.getSearch_layout().setVisibility(View.VISIBLE);
}else{
searchListTitleBar.getSearch().setText("");
searchListTitleBar.getClear().setVisibility(View.GONE);
searchListTitleBar.getSearch_layout().setVisibility(View.GONE);
}
}
/**
*fragment注册和取消
*/
@Override
public void onDetach() {
super.onDetach();
iSwitchCaoZuoRecordFragment = null;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof ISwitchCaoZuoRecordFragment) {
iSwitchCaoZuoRecordFragment = (ISwitchCaoZuoRecordFragment) context;
}else{
throw new RuntimeException(context.toString()
+ " 必须实现 ISwitchCaoZuoRecordFragment");
}
}
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。通俗的说的就是有很多相同的步骤的,在某一些地方可能有一些差别适合于这种模式,如大话设计模式中说到的考试场景中,每个人的试卷都是一样的,只有答案不一样。这种场景就适合于模板方法模式。
现在我有一个需求,就是要建立一套上课业务规则
/**
* 班级规则
*/
public abstract class ClassShangke {
public void shangke(){
}
public abstract void kaoqing();
public void xiake(){
}
//注意模版规则 final 不可被重写
public final void guize(){
shangke();
kaoqing();
xiake();
}
}
/**
* A班级上课
*/
public class AClassKaoQing extends ClassShangke {
@Override
public void kaoqing() {
System.out.println("AClassKaoQing kaoqing");
}
}
/**
* B班级上课
*/
public class BClassKaoQing extends ClassShangke {
@Override
public void kaoqing() {
System.out.println("BClassKaoQing kaoqing");
}
}
//运行
ClassShangke classShangkea = new AClassKaoQing();
classShangkea.guize();
ClassShangke classShangkeb = new BClassKaoQing();
classShangkeb.guize();
我在项目中对网络请求返回数据进行模版方法定义
public abstract class BaseRxNetworkResponseObserver<T extends SModel> extends BaseRxNetworkObserver<T> {
@Override
public final void onNext(T t) {
onBeforeResponseOperation();
try{
onResponse(t);
}catch (Exception e){
ULog.e(e,e.getMessage());
ULog.e(e);
TipToast.shortTip(e.getMessage());
onResponseFail(new CustomarilyException(e.getMessage()));
}finally {
onNextFinally();
}
}
protected void onNextFinally() {
}
/**
* 返回值
* @param t
*/
public abstract void onResponse(T t);
/**
* 错误
* @param e
*/
public abstract void onResponseFail(Exception e);
/**
* 执行一些起始操作
*/
protected abstract void onBeforeResponseOperation();
}
注意模版方法的final 为了制定一些规则必须不可以被重写
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换,策略模式让算法独立于使用它的客户而独立变化。
策略模式使这些算法在客户端调用它们的时候能够互不影响地变化。
一个类定义了多种行为,并且这个行为在这个类的方法中以多个条件语句形式出现,那么可以使用策略模式避免在类中使用大量的条件语句。
为了更清晰的展示出策略模式的优点,我在此写一套不用策略模式实现的代码。如下:
/**
* 有如下几个超市价格规则 1:五月一日 价格统统8折 2:十月一日 价格统统7折 3:十一月一日 价格统统半价
* Created by on 2019/4/30.
*/
public class Price {
public double jisuanPrice(double price,String s){
double p = price;
System.out.println(s);
switch (s){
case "五月一日":
p = price*0.8;
break;
case "十月一日":
p = price*0.7;
break;
case "十一月一日":
p = price*0.5;
break;
}
return p;
}
}
//运行
Price p = new Price();
System.out.println(p.jisuanPrice(10),"五月一日");
你会发现,也很清晰表示了判断不同的日期来计算不同的价格,不同日期用switch来判断,但是呢,如果日期很多,而且规则又有了对特定商品的价格规则,那么这个类的负担是否有些复杂了呢?这个类不是单一职责。还根据swith来计算规则。
于是我写下了如下的策略方法:
/**
* 半价计算策略
*/
public class Banjia implements IPrice {
@Override
public double jisuanPrice(double price) {
return price*0.5;
}
}
/**
* 8折计算策略
*/
public class BaZhe implements IPrice {
@Override
public double jisuanPrice(double price) {
return price*0.8;
}
}
/**
* 正常计算策略
*/
public class NorPrice implements IPrice {
@Override
public double jisuanPrice(double price) {
return price;
}
}
/**
* 7折计算
*/
public class Qizhe implements IPrice {
@Override
public double jisuanPrice(double price) {
return price*0.7;
}
}
//作为价格管理器一定要持有IPrice的引用
public class Price{
private IPrice iPrice;
public void setiPrice(IPrice iPrice) {
this.iPrice = iPrice;
}
public double jisuanPrice(double price) {
return iPrice.jisuanPrice(price);
}
}
//运行
//1.创建具体测策略实现
IPrice iprice = new BaZhe();
//2.在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中
Price p = new Price();
p.setiPrice(iprice);
//3.调用上下文对象的方法来完成对具体策略实现的回调
System.out.println(p.jisuanPrice(10));
这个能够明显是对价格做了分类,假如说十月一日要进行半价打折,那你是不是很容易就改变了策略呢?不需要动其他代码。
1、上下文Context 和具体策略器(oncreteStrategy)是松耦合关系。
2、满足开-闭原则。增加新的具体策略不需要修改context。
开闭原则:
策略和上下文的关系:
如下例子来自 https://www.cnblogs.com/lewis0077/p/5133812.html 非常好的一篇文章
**下面我们演示这种情况:**
在跨国公司中,一般都会在各个国家和地区设置分支机构,聘用当地人为员工,这样就有这样一个需要:每月发工资的时候,中国国籍的员工要发人民币,美国国籍的员工要发美元,英国国籍的要发英镑。
public interface PayStrategy {
//在支付策略接口的支付方法中含有支付上下文作为参数,以便在具体的支付策略中回调上下文中的方法获取数据
public void pay(PayContext ctx);
}
//人民币支付策略
public class RMBPay implements PayStrategy {
@Override
public void pay(PayContext ctx) {
System.out.println("现在给:"+ctx.getUsername()+" 人民币支付 "+ctx.getMoney()+"元!");
}
}
//美金支付策略
public class DollarPay implements PayStrategy {
@Override
public void pay(PayContext ctx) {
System.out.println("现在给:"+ctx.getUsername()+" 美金支付 "+ctx.getMoney()+"dollar !");
}
}
//支付上下文,含有多个算法的公有数据
public class PayContext {
//员工姓名
private String username;
//员工的工资
private double money;
//支付策略
private PayStrategy payStrategy;
public void pay(){
//调用具体的支付策略来进行支付
payStrategy.pay(this);
}
public PayContext(String username, double money, PayStrategy payStrategy) {
this.username = username;
this.money = money;
this.payStrategy = payStrategy;
}
public String getUsername() {
return username;
}
public double getMoney() {
return money;
}
}
//外部客户端
public class Client {
public static void main(String[] args) {
//创建具体的支付策略
PayStrategy rmbStrategy = new RMBPay();
PayStrategy dollarStrategy = new DollarPay();
//准备小王的支付上下文
PayContext ctx = new PayContext("小王",30000,rmbStrategy);
//向小王支付工资
ctx.pay();
//准备Jack的支付上下文
ctx = new PayContext("jack",10000,dollarStrategy);
//向Jack支付工资
ctx.pay();
}
}
控制台输出:
现在给:小王 人民币支付 30000.0元!
现在给:jack 美金支付 10000.0dollar !
那现在我们要新增一个银行账户的支付策略,该怎么办呢?
显然我们应该新增一个支付找银行账户的策略实现,由于需要从上下文中获取数据,为了不修改已有的上下文,我们可以通过继承已有的上下文来扩展一个新的带有银行账户的上下文,然后再客户端中使用新的策略实现和带有银行账户的上下文,这样之前已有的实现完全不需要改动,遵守了开闭原则。
//银行账户支付
public class AccountPay implements PayStrategy {
@Override
public void pay(PayContext ctx) {
PayContextWithAccount ctxAccount = (PayContextWithAccount) ctx;
System.out.println("现在给:"+ctxAccount.getUsername()+"的账户:"+ctxAccount.getAccount()+" 支付工资:"+ctxAccount.getMoney()+" 元!");
}
}
//带银行账户的支付上下文
public class PayContextWithAccount extends PayContext {
//银行账户
private String account;
public PayContextWithAccount(String username, double money, PayStrategy payStrategy,String account) {
super(username, money, payStrategy);
this.account = account;
}
public String getAccount() {
return account;
}
}
//外部客户端
public class Client {
public static void main(String[] args) {
//创建具体的支付策略
PayStrategy rmbStrategy = new RMBPay();
PayStrategy dollarStrategy = new DollarPay();
//准备小王的支付上下文
PayContext ctx = new PayContext("小王",30000,rmbStrategy);
//向小王支付工资
ctx.pay();
//准备Jack的支付上下文
ctx = new PayContext("jack",10000,dollarStrategy);
//向Jack支付工资
ctx.pay();
//创建支付到银行账户的支付策略
PayStrategy accountStrategy = new AccountPay();
//准备带有银行账户的上下文
ctx = new PayContextWithAccount("小张",40000,accountStrategy,"1234567890");
//向小张的账户支付
ctx.pay();
}
}
控制台输出:
现在给:小王 人民币支付 30000.0元!
现在给:jack 美金支付 10000.0dollar !
现在给:小张的账户:1234567890 支付工资:40000.0 元!
除了上面的方法,还有其他的实现方式吗?
当然有了,上面的实现方式是策略实现所需要的数据都是从上下文中获取,因此扩展了上下文;现在我们可以不扩展上下文,直接从策略实现内部来获取数据,看下面的实现:
//支付到银行账户的策略
public class AccountPay2 implements PayStrategy {
//银行账户
private String account;
public AccountPay2(String account) {
this.account = account;
}
@Override
public void pay(PayContext ctx) {
System.out.println("现在给:"+ctx.getUsername()+"的账户:"+getAccount()+" 支付工资:"+ctx.getMoney()+" 元!");
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
}
//外部客户端
public class Client {
public static void main(String[] args) {
//创建具体的支付策略
PayStrategy rmbStrategy = new RMBPay();
PayStrategy dollarStrategy = new DollarPay();
//准备小王的支付上下文
PayContext ctx = new PayContext("小王",30000,rmbStrategy);
//向小王支付工资
ctx.pay();
//准备Jack的支付上下文
ctx = new PayContext("jack",10000,dollarStrategy);
//向Jack支付工资
ctx.pay();
//创建支付到银行账户的支付策略
PayStrategy accountStrategy = new AccountPay2("1234567890");
//准备上下文
ctx = new PayContext("小张",40000,accountStrategy);
//向小张的账户支付
ctx.pay();
}
}
控制台输出:
现在给:小王 人民币支付 30000.0元!
现在给:jack 美金支付 10000.0dollar !
现在给:小张的账户:1234567890 支付工资:40000.0 元!
那我们来比较一下上面两种实现方式:
扩展上下文的实现:
优点:具体的策略实现风格很是统一,策略实现所需要的数据都是从上下文中获取的,在上下文中添加的数据,可以视为公共的数据,其他的策略实现也可以使用。
缺点:很明显如果某些数据只是特定的策略实现需要,大部分的策略实现不需要,那这些数据有“浪费”之嫌,另外如果每次添加算法数据都扩展上下文,很容易导致上下文的层级很是复杂。
在具体的策略实现上添加所需要的数据的实现:
优点:容易想到,实现简单
缺点:与其他的策略实现风格不一致,其他的策略实现所需数据都是来自上下文,而这个策略实现一部分数据来自于自身,一部分数据来自于上下文;外部在使用这个策略实现的时候也和其他的策略实现不一致了,难以以一个统一的方式动态的切换策略实现。
Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据.也可以追踪程序运行的过程,这样instrument就可以帮助我们了解用户的应用程序和操作系统的行为。
总结一下instrument能做的事情:
1. Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;instrument还可以:
2.Instruments支持多线程的调试;
3.可以用Instruments去录制和回放,图形用户界面的操作过程
4.可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
1.追踪代码中的(甚至是那些难以复制的)问题;
2.分析程序的性能;
3.实现程序的自动化测试;
4.部分实现程序的压力测试;
5.执行系统级别的通用问题追踪调试;
6.使你对程序的内部运行过程更加了解。
打开方式:Xcode -> Open Developer Tool -> Instruments
其中比较常用的有四种:
1.Allocations:用来检查内存分配,跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史
2.Leaks:一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录
3.Time Profiler:分析代码的执行时间,执行对系统的CPU上运行的进程低负载时间为基础采样
4.Zombies:检查是否访问了僵尸对象
其他的:
Blank:创建一个空的模板,可以从Library库中添加其他模板
Activity Monitor:显示器处理的CPU、内存和网络使用情况统计
Automation:用JavaScript语言编写,主要用于分析应用的性能和用户行为,模仿/击发被请求的事件,利用它可以完成对被测应用的简单的UI测试及相关功能测试
Cocoa Layout:观察约束变化,找出布局代码的问题所在。
Core Animation:用来检测Core Animation性能的,给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画,界面滑动FPS可以进行测试
Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要
Energy Diagnostic :用于Xcode下的Instruments来分析手机电量消耗的。(必须是真机才有电量)
GPU Driver :可以测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animtaion那样显示FPS的工具。
Network:分析应用程序如何使用TCP / IP和UDP / IP连接使用连接仪器。就是检查手机网速的。(这个最好是真机)
Leaked memory:泄漏的内存,如为对象A申请了内存空间,之后再也没用到A,也没有释放A导致内存泄漏(野指针。。。)
Abandoned memory:被遗弃的内存,如循环引用,递归不断申请内存而导致的内存泄漏
Cached memory:缓存的内存
其中内存泄漏我们可以用Leaks
,野指针可以用Zombies
(僵尸对象),而在这里我们就可以用Allocations
来检测Abandoned memory
的内存。
即我们采用Generational Analysis的方法来分析,反复进入退出某一场景,查看内存的分配与释放情况,以定位哪些对象是属于Abandoned Memory的范畴。
在Allocations工具中,有专门的Generational Analysis设置,如下:
我们可以在程序运行时,在进入某个模块前标记一个Generation,这样会生成一个快照。然后进入、退出,再标记一个Generation,如下图:
在详情面板中我们可以看到两个Generation间内存的增长情况,其中就可能存在潜在的被遗弃的对象,如下图:
其中growth就是我们增长的内存,GenerationA是程序启动到进入该场景增长的内存,GenerationB就是第二次进入该场景所增长的内存,查看子类可以发现有两个管理类造成了Abandoned memory
。
使用instrument测试内存泄露 工具 Allocations 测试是否内存泄露 使用标记,可以更省事省力的测试页面是否有内存泄露
1)设置Generations
2)选择mark generation
3)使用方法 在进入测试页面之前,mark一下----->进入页面----->退出----->mark------>进入------->退出------->mark------>进入如此往复5、6次,就可以看到如下结果
这种情况下是内存有泄露,看到每次的增量都是好几百K或者上M的,都是属于内存有泄露的,这时候就需要检测下代码一般情况
100K以下都属于正常范围,growth表示距离你上次mark的增量
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
在前面的ALLcations里面我们提到过内存泄漏就是应该释放而没有释放的内存。而内存泄漏分为两种:Leaked Memory 和 Abandoned Memory。前面我们讲到了如何找到Abandoned Memory被遗忘的内存,现在我们研究的就是Leaked Memory。
以发生的方式来分类,内存泄漏可以分为4类:
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
影响:从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
下边我们介绍Instruments里面的Leaked的用法,首先打开Leaked,跑起工程来,点击要测试的页面,如果有内存泄漏,会出现下图中的红色的❌。然后按照后边的步骤进行修复即可
上面的旧版的样式,下面的是新版的样式,基本操作差不多
在详情面板选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标进行修改。
下图是对Leaked页面进一步的理解:
内存泄漏动态分析技巧:
1.在 Display Settings 界面建议把 Snapshot Interval (snapʃɒt, 数据快照)间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks 会自动进行内存捕捉分析。(新版本直接在底部修改)
2.熟练使用 Leaks 后会对内存泄漏判断更准确,在可能导致泄漏的操作里,在你怀疑有内存泄漏的操作前和操作后,可以点击 Snapshot Now 进行手动捕捉。
3.开始时如果设备性能较好,可以把自动捕捉间隔设置为 5 秒钟。
4.使用ARC的项目,一般内存泄漏都是 malloc、自定义结构、资源引起的,多注意这些地方进行分析。
5.开启ARC后,内存泄漏的原因,开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。
注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的
这里对 Display Settings中 的 Call tree
选项做一下说明 [官方user guide翻译]:
Separate By Thread:线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
Invert Call Tree:从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中花费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面.反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
Hide Missing Symbols:如果dSYM无法找到你的APP或者调用系统框架的话,那么表中将看到调用方法名只能看到16进制的数值,勾选这个选项则可以隐藏这些符号,便于简化分析数据.
Hide System Libraries:表示隐藏系统的函数,调用这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
Show Obj-C Only:只显示oc代码 ,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的
Flatten Recursion:递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
Top Functions:找到最耗时的函数或方法。 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B.花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。
用来检测app中每个方法所用的时间,并且可以排序,并查找出哪些函数占用了大量时间。
使用Time Profile前有两点需要注意的地方:
1、一定要使用真机调试
在开始进行应用程序性能分析的时候,一定要使用真机。因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候,这就导致模拟器性能数据和用户真机使用性能数据相去甚远
2、应用程序一定要使用发布配置
在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。另iOS引入一种"Watch Dog"[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能,如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用"Watch Dog"
我们将僵尸对象“复活”的目的:僵尸对象就是让已经释放了的对象重新复活,便于调试;是为了让已经释放了的对象在被再次访问时能够输出一些错误信息。其实这里的“复活”并不是真的复活,而是强行不死:这么说吧 相当于 他的RC=0的时候 系统再强行让他RC=1,顺便打上一个标记 zoom,等到你去掉那个沟以后 系统会把带有标记zoom的对象RC=0。
C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指 指向1块随机的内存空间。
OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.
僵尸对象: 1个已经被释放的对象 就叫做僵尸对象.
使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题.
当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候, - 这个时候其实是可以访问的.
因为对象的数据还在.
当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
所以,你不要通过1个野指针去访问1个僵尸对象.
虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.
僵尸对象检测.
默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.
可以开启Xcode的僵尸对象检测.
那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.
为什么不默认开启僵尸对象检测呢?
因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
那么这样的话 就影响效率了.
如何避免僵尸对象报错.
当1个指针变为野指针以后. 就把这个指针的值设置为nil
僵尸对象无法复活.
当1个对象的引用计数器变为0以后 这个对象就被释放了.
就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.
摘自:https://blog.csdn.net/weixin_41963895/article/details/107231347
收起阅读 »如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!
注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.触摸点是否在自己身上
3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
4.如果没有符合条件的子控件,那么就认为自己最合适处理
详述:
1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
两个重要的方法:
hitTest:withEvent:方法
pointInside方法
3.3.1.1.hitTest:withEvent:方法
什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
作用
寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意
:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
1.正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
2.不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
3.通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
事件传递给谁,就会调用谁的hitTest:withEvent:方法。注 意
:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
所以事件的传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
例如:whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!
// 这里redView是whiteView的第0个子控件
#import "redView.h"
@implementation redView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"red-touch");
}@end
// 或者
#import "whiteView.h"
@implementation whiteView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self.subviews[0];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"white-touch");
}
@end
特殊情况:
谁都不能处理事件,窗口也不能处理。
重写window的hitTest:withEvent:方法return nil
只能有窗口处理事件。
控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self
return nil的含义:
hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。
寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法
#import "WYWindow.h"
@implementation WYWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。
3.3.1.2.pointInside:withEvent:方法
pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
3.3.2.练习
屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *view = [super hitTest:point withEvent:event];
if (view == self) {
return nil;
}
return view;
}
1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
响应者链条:
在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示
响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
如何判断上一个响应者
1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者
2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链的事件传递过程:
1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4>如果UIApplication也不能处理该事件或消息,则将其丢弃
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
4.最合适的view会调用自己的touches方法处理事件
5.touches默认做法是把事件顺着响应者链条向上抛。
touches的默认做法:#import "WYView.h"
@implementation WYView
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件
}
@end
事件的传递与响应:
1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃
3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
按照时间顺序,事件的生命周期:
事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)
重点和难点是:
1.如何寻找最合适的view
2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)
iOS中的事件可以分为3大类型:
1.触摸事件
2.加速计事件
3.远程控制事件
这里我们只讨论iOS中的触摸事件。
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
UIApplication
UIWindows
UIViewController
UIView
因为UIResponder中提供了以下4个对象方法来处理触摸事件。UIResponder内部提供了以下方法来处理事件触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象
需要注意的是:以上四个方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。
1.如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
2.如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
3.重写以上四个方法,如果是处理UIView的触摸事件。必须要自定义UIView子类继承自UIView。因为苹果不开源,没有把UIView的.m文件提 供给我们。我们只能通过子类继承父类,重写子类方法的方式处理UIView的触摸事件(注意:我说的是UIView触摸事件而不是说的 UIViewController的触摸事件)。
4.如果是处理UIViewController的触摸事件,那么在控制器的.m文件中直接重写那四个方法即可!
/************************自定义UIView的.h.m文件************************/
#import
@interface WYView : UIView
@end
#import "WYView.h"
@implementation WYView
// 开始触摸时就会调用一次这个方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"摸我干啥!");
}
// 手指移动就会调用这个方法
// 这个方法调用非常频繁
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"移动过程中持续调用!");
}
// 手指离开屏幕时就会调用一次这个方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"手放开还能继续玩耍!");
}
@end
/**************************控制器的.m文件*************************/
#import "ViewController.h"
#import "WYView.h"
@interface ViewController ()
@end@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建自定义view
WYView *touchView = [[WYView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
// 背景颜色
touchView.backgroundColor = [UIColor redColor];
// 添加到父控件
[self.view addSubview:touchView];
}
@end
注 意:有人认为,我要是处理控制器的自带的view的事件就不需要自定义UIView子类继承于UIView,因为可以在viewController.m 文件中重写touchBegan:withEvent:方法,但是,我们此处讨论的是处理UIView的触摸事件,而不是处理 UIViewController的触摸事件。你如果是在viewController.m文件中重写touchBegan:withEvent:方法,相当于处理的是viewController的触摸事件,因为viewController也是继承自UIResponder,所以会给人一种错觉。
所以,还是那句话,想处理UIView的触摸事件,必须自定义UIView子类继承自UIView。
那么,如何实现UIView的拖拽呢?也就是让UIView随着手指的移动而移动。
- 重写touchsMoved:withEvent:方法
此时需要用到参数touches,下面是UITouch的属性和方法:
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase phase;
@property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray *gestureRecognizers NS_AVAILABLE_IOS(3_2);
- (CGPoint)locationInView:(nullable UIView *)view;
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象
一根手指对应一个UITouch对象
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象
提 示:iPhone开发中,要避免使用双击事件!
2.1.1.2.UITouch的属性
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view
;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
2.1.1.3.UITouch的方法
(CGPoint)locationInView:(UIView *)view;
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
(CGPoint)previousLocationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);}
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
UIView不能接收触摸事件的三种情况:
1.不允许交互:userInteractionEnabled = NO
2.隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
3.透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注 意
:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。
1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
摘自链接:https://blog.csdn.net/wywinstonwy/article/details/105293525
简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。
工厂方法模式的主要角色如下。
具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
kotlin代码实现
//抽象产品:提供了产品的接口
interface IProduct{
fun setPingPai(string: String)
fun showName() :String
}
//具体产品1:实现抽象产品中的抽象方法
class Dog : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "dog"
}
//具体产品2:实现抽象产品中的抽象方法
class Cat : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "cat"
}
//抽象工厂:提供了厂品的生成方法
interface IFactory{
fun getPinPai():String
fun createProduct(type:Int):IProduct
}
//具体工厂1:实现了厂品的生成方法
class ABCFactory():IFactory{
override fun getPinPai() = "ABC"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
}
//具体工厂2:实现了厂品的生成方法
class CBDFactory():IFactory{
override fun getPinPai() = "CBD"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
}
//抽象产品
public interface Runnable {
public abstract void run();
}
//抽象工厂
public interface ThreadFactory {
Thread newThread(Runnable r);
}
具体的实现
//实现1 TaskThreadFactory
class TaskThreadFactory(var name: String) : ThreadFactory {
private val mThreadNumber = AtomicInteger(1)
override fun newThread(r: Runnable): Thread {
return Thread(r, name + "#" + mThreadNumber.getAndIncrement())
}
}
//实现2 DiskLruCacheThreadFactory
private static final class DiskLruCacheThreadFactory implements ThreadFactory {
@Override
public synchronized Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, "glide-disk-lru-cache-thread");
result.setPriority(Thread.MIN_PRIORITY);
return result;
}
}
解释:
参数Runnable r,我们可以创建很多此类线程的产品类,我们还可以创建工厂来创造某类专用线程
在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 contents 属性,完成显示。
这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。
那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?
答案是可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础。
UIView 调用 setNeedsDisplay 方法其实是调用其 layer 属性的同名方法,这时 layer 并不会立刻调用 display 方法,而是要等到当前 runloop 即将结束的时候调用 display,进入到绘制流程。在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并没有实现 displayLayer: 方法,所以进入系统的绘制流程,我们可以通过实现 displayLayer: 方法来进行异步绘制。
有了上面的异步绘制原理流程图,我们可以得到一个实现异步绘制的初步思路:
在“异步绘制入口”去开辟子线程,然后在子线程中实现和系统类似的绘制流程。
要实现异步绘制,我们首先要了解系统的绘制流程,看下面一张流程图:
我们看一幅时序图
#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncDrawLabel
- (void)setText:(NSString *)text {
_text = text;
[self.layer setNeedsDisplay];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self.layer setNeedsDisplay];
}
- (void)displayLayer:(CALayer *)layer {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
});
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)image.CGImage;
});
});
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
AsyncDrawLabel 是一个继承 UIView 的类,其 Label 的文本绘制功能需要我们自己实现。
我们在 - (void)displayLayer:(CALayer *)layer 方法中异步在全局队列中创建上下文环境然后使用 - (void)draw:(CGContextRef)context size:(CGSize)size 方法进行文本的简单绘制,再回到主线程为 self.layer.contents 赋值。从而完成了一个简单的异步绘制。
当然这样的绘制的问题是,如果绘制数量较多,绘制频繁,会阻塞全局队列,因为全局队列中还有一些系统提交的任务需要执行,可能会对其造成影响。
YYAsyncLayer
我们需要更加优化的方式去管理异步绘制的线程和执行流程,使用 YYAsyncLayer 可以让我们把注意力放在具体的绘制(需要我们做的是上面代码中 - draw: size: 做的事情),而不需要考虑线程的管理,绘制的时机等,大大提高绘制的效率以及我们编程的速度。
YYAsyncLayer 的主要流程如下
在主线程的 RunLoop 中注册一个 observer,它的优先级要比系统的 CATransaction 低,保证系统先做完必须的工作。
把需要异步绘制的操作集中起来。比如设置字体、颜色、背景色等,不是设置一个就绘制一个,而是把它们集中起来,RunLoop 会在 observer 需要的时机通知统一处理。
处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给 layer.contents。
流程图如下:
使用 YYAsyncLayer 的代码:
#import "AsyncDrawLabel.h"
#import <YYAsyncLayer.h>
#import <CoreText/CoreText.h>
@interface AsyncDrawLabel ()<YYAsyncLayerDelegate>
@end
@implementation AsyncDrawLabel
+ (Class)layerClass {
return YYAsyncLayer.class;
}
- (void)setText:(NSString *)text {
_text = text.copy;
[self commitTransaction];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self commitTransaction];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self commitTransaction];
}
- (void)contentsNeedUpdated {
[self.layer setNeedsDisplay];
}
- (void)commitTransaction {
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
// 在这里创建异步绘制的任务
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer * _Nonnull layer) {
};
task.display = ^(CGContextRef _Nonnull context, CGSize size, BOOL (^ _Nonnull isCancelled)(void)) {
if (isCancelled() || self.text.length == 0) {
return;
}
// 在这里进行异步绘制
[self draw:context size:size];
};
task.didDisplay = ^(CALayer * _Nonnull layer, BOOL finished) {
if (finished) {
} else {
}
};
return task;
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
原文链接:https://blog.csdn.net/wywinstonwy/article/details/105660643
收起阅读 »Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。
所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果。
1、调用时机:loadView ->ViewDidload ->drawRect:
2、如果在UIView初始化时没有设置rect大小,将直接导致drawRect:不被自动调用。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame
的时候自动调用drawRect:
。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是:view当前的rect不能为nil
5、该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
这里简单说一下sizeToFit和sizeThatFit:
sizeToFit:会计算出最优的 size 而且会改变自己的size
sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size
这个方法是用来对subviews重新布局
,默认没有做任何事情,需要子类进行重写。
当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。
这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
标记
,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
。如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]
这里有必要描述下三者之间的关系:
在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。setNeedLayout
就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。layoutIfNeed
就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
而layoutSubviews
则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed
,告诉系统是否立即执行重新布局的操作。
1、init初始化不会触发layoutSubviews。这里需要补充一点:
2、addSubview会触发layoutSubviews。(当然这里frame为0,是不会调用的,同上面的drawrect:一样)
3、设置view的Frame会触发layoutSubviews,(当然前提是frame的值设置前后发生了变化。)
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转屏幕会触发父UIView上的layoutSubviews事件。(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview中做相应的操作)
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。(Apple是不建议这么做的)
layoutSubview是布局相关,而drawRect则是负责绘制。因此从调用时序上来讲,layoutSubviews要早于drawRect:函数。
前言
Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。
我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。
我们在Category中,可以直接添加方法,而且我们也都知道,添加的方法会合并到本类当中,同时我们也可以声明属性,但是此时的属性没有功能,也就是不能存值,这就类似于Swift中的计算属性,如果我们想让这个属性可以储存值,就要用runtime的方式,动态的添加。
探究
1. Category为什么能添加方法不能添加成员变量
首先我们先创建一个Person类,然后创建一个Person+Run的Category,并在Person+Run中实现-run方法。
我们可以使用命令行对Person+Run.m进行编译
xcrun -sdk iphonesimulator clang -rewrite-objc Person+Run.m
得到一个Person+Run.cpp文件,在文件的底部,可以找到这样一个结构体
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
这些字段几乎都是见名知意了。
每一个Category都会编译然后存储在一个_category_t类型的变量中
static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
0,
0,
0,
};
因为我们的Person+Run里面只有一个实例方法,从上述代码中来看,也只有对应的位置传值了。
通过这个_category_t的结构结构我们也可以看出,属性存储在_prop_list_t,这里并没有类中的objc_ivar_list结构体,所以Category的_category_t结构体中根本没有储存ivar的地方,所以不能添加成员变量。
如果我们在分类中手动为成员变量添加了set和get方法之后,也可以调用,但实际上是没有内存来储值的,这就好像Swift中的计算属性,只起到了计算的作用,就相当于是两个方法(set和get),但是并不能拥有真用的内存来存储值。
举个例子
@property (copy, nonatomic) NSString * name;
下面这个声明如果实在类中,系统会默认帮我们声明一个成员变量_name, 在.h中声明setName和name两个方法,并提供setName和name方法的默认实现。
如果是在Category中,只相当于声明setName和name两个方法,没有实现也没有_name。
2. Category的方法是何时合并到类中的
大家都知道Category分类肯定是我们的应用启动是,通过运行时特性加载的,但是这个加载过程具体的细节就要结合runtime的源码来分析了。
runtime源码太多了,我们先通过大概浏览代码来定位实现功能的相关位置。
我从objc-runtime-new.mm中找到了下面这个方法。
void attachLists(List* const * addedLists, uint32_t addedCount)
而且他的注释一些的很清楚,修复类的方法,协议和变量列表,关联还未关联的分类。
然后我们继续找,就找到了我们需要的这个方法。
void attachLists(List* const * addedLists, uint32_t addedCount)
我们从其中摘出一段代码来分析就可以解决我们的问题了。
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
在调用此方法之前我们所有的分类会被方法一个list里面(每一个分类都是一个元素),然后再调用attachLists方法,我们可以看到,在realloc的时候传进一个newCount,这是因为要增加分类中的方法,所以需要对之前的数组扩容,在扩容结束后先调用了memmove方法,在调用memcopy,大家可以上网查一下这两个方法具体的区别,这里简单一说,其实完成的效果都是把后面的内存的内容拷贝到前面内存中去,但是memmove可以处理内存重叠的问题。
其实也就是首先将原来数组中的每个元素先往后移动(我们要添加几个元素,就移动几位),因为移动后的位置,其实也是数组自己的内存空间,所以存在重叠问题,直接移动会导致元素丢失的问题,所以用memmove(会检测是否有内存重叠)。
移动完之后,把我们储存分类中方法的list中的元素移动到数组前面位置。
过程就是这样子了,其实我们第三个问题就顺便解决完了。
3. Category方法和类中方法的执行顺序
上面其实说到了,类中原来的方法是要往后面移动的,分类的方法添加到前面的位置,而且调用方法的时候是在list中遍历查找,所以我们调用方法的时候,肯定会先调用到Category中的方法,但是这并不是覆盖,因为我们的原方法还在,只是这中机制保证了如果分类中有重写类的方法,会被优先查找到。
4. +load和+initialize的区别
对于这个问题我们从两个角度出发分析,调用方式和调用时刻。
+load
简单的举一个例子,我们创建一个Person类,然后重写+load方法,然后为Person新建两个Category,都分别实现+load。
@implementation Person
+ (void)load {
NSLog(@"Person - load");
}
@end
@implementation Person (Test1)
+ (void)load {
NSLog(@"Person Test1 - load");
}
@end
@implementation Person (Test2)
+ (void)load {
NSLog(@"Person Test2 - load");
}
@end
当我们进行项目的时候,会得到下面的打印结果。
2020-09-14 09:34:41.900161+0800 Category[4533:53426] Person - load
2020-09-14 09:34:41.900629+0800 Category[4533:53426] Person Test1 - load
2020-09-14 09:34:41.900700+0800 Category[4533:53426] Person Test2 - load
我们并没有使用这个Person类和他的Category,所以应该是项目运行后,runtime在加载类和分类的时候,就会调用+load方法。
我们从源码中找到下面这个方法
void load_images(const char *path __unused, const struct mach_header *mh)
方法中的最后一行调用了call_load_methods(),这个call_load_methods()中就是实现了+load的调用方式。
下面是call_load_methods()函数的实现 ,大家简单浏览一遍
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
从源码中很清楚的可以看到, 先调用call_class_loads(), 再调用call_category_loads(),这就说明了在调用所有的+load方法时,实现调用了所有类的+load方法,再去调用分类中的+load方法。
然后我们在进入到call_class_loads()函数中
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
从中间的循环中可以看出,是取到了每个类的+load函数的指针,直接通过指针调用了这个函数。 call_category_loads()函数中体现出来的Category的+load方法的调用,也是同理。
同时这也解答了我们的另一个疑惑,那就是为什么总是先调用类的+load,在调用Category的+load。
思考:如果存在继承的情况,+load又会是怎样的调用顺序呢?
从上面call_class_loads()函数中可以看到有一个list:loadable_classes,我们猜测这里面应该就是存放着我们所有的类,因为下面的循环是从0开始循环,所以我们要研究所有的类的+load方法的执行顺序,就要看这个list中的类的顺序是怎么样的。
我们从个源码中可以找到这样一个方法,prepare_load_methods,在其实现中调用了schedule_class_load方法,我们看一下schedule_class_load的源码
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
从源码中schedule_class_load(cls->superclass);这一句中可以看出,递归调用自己本身,并且传入自己的父类,结果递归之后,才调用add_class_to_loadable_list,这就说明父类总是在子类前面加入到list当中,所有在调用一个类的+load方法之前,肯定要先调用其父类的+load方法。
那如果是其他没有继承关系的类呢,这就跟编译顺序有关系了,大家可以自己尝试验证一下。
小结:
+load方法会在runtime加载类和分类时调用
每个类和分类的+load方法之后调用一次
调用顺序:先调用类的+load
+initialize
+initialize的调用是不同的,如果某一个类我们没有使用过,他的+initialize方法是不会调用的,到我们使用这个类(调用了类的某个方法)的时候,才会触发+initialize方法的调用。
@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end
@implementation Person (Test1)
+ (void)initialize {
NSLog(@"Person Test1 - initialize");
}
@end
@implementation Person (Test2)
+ (void)initialize {
NSLog(@"Person Test2 - initialize");
}
@end
当我们执行[Person alloc];的时候,才会走+initialize方法,而且执行的Category中的+initialize:
2020-09-14 10:40:23.579623+0800 Category[9134:94173] Person Test2 - initialize
这个我们之前已经说过了,Category的方法会添加list的前面,所以会先被找到并且执行,所以我们猜测+initialize的执行是走的正常的消息机制,objc_msgSend。
由于objc_msgSend实现并没有完全开源,都是汇编代码,所以我们需要换一个思路来研究源码。
objc_msgSend本质是什么?以调用实例方法为例,其实就是通过isa指针找到该类,然后寻找方法,找到之后调用。如果没有找到则通过superClass找到父类,继续查找方法。上面的例子中,我们仅仅是调用了一个alloc方法,但是也执行了+initialize方法,所以我们猜测+initialize会在查找方法的时候调用到。通过这个思路,我们定位到了class_getInstanceMethod()函数(class_getInstanceMethod函数就是在类中查找某个sel时候调用的),在这个函数中,又调用了IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
在该函数中我们可以找到下面这段代码
if ((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
可以看出如果类还没有执行+initialize 就会先执行,我们再看一下if语句中的initializeNonMetaClass函数,他会先拿到superClass,执行superClass的+initialize
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
这就是存在继承的情况,为什么会先执行父类的+initialize。
大总结
调用方式:
load是根据函数地址直接调用
initialize是通过消息机制objc_msgSend调用
调用时刻:
load是在runtime加载类和分类时调用(只会调用一次)
initialize是在类第一次收到消息时调用个,默认没有继承的情况下每个类只会initialize一次(父类的initialize可能会被执行多次)
调用顺序
load
先调用类的load:先编译的类先调用,子类调用之前,先调用父类的
在调用Category的load:先编译的先调用
initialize
先初始化父类
在初始化子类(初始化子类可能调用父类的initialize)
补充
上面总结的时候说到父类的initialize会被执行多次,什么情况下会被执行多次,为什么?举个例子:
@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end
@implementation Student
@end
Student类继承Person类,并且只有父类Person中实现了+initialize,Student类中并没有实现
此时我们调用[Student alloc];, 会得到如下的打印。
2020-09-14 11:31:55.377569+0800 Category[11483:125034] Person - initialize
2020-09-14 11:31:55.377659+0800 Category[11483:125034] Person - initialize
Person的+initialize被执行了两次,但这两个意义是不同的,第一次执行是因为在调用子类的+initialize方法之前必须先执行父类的了+initialize,所以会打印一次。当要执行子类的+initialize时,通过消息机制,student类中并没有找到实现的+initialize的实现,所以要通过superClass指针去到父类中继续查找,因为父类中实现了+initialize,所以才会有了第二次的打印。
结尾
本文的篇幅略长,笔者按照自己的思路和想法写完了此文,陈述过程不一定那么调理和完善,大家在阅读过程中发现问题,可以留言交流。
感谢阅读。
转自:https://www.jianshu.com/p/141b04e376d4
收起阅读 »在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:
export function injectMiniAppScript() {
if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
const s = document.createElement('script');
s.src = 'https://appx/web-view.min.js';
s.onload = () => {
// 加载完成时触发自定义事件
const customEvent = new CustomEvent('myLoad', { detail:'' });
document.dispatchEvent(customEvent);
};
s.onerror = (e) => {
// 加载失败时上传日志
uploadLog({
tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
});
};
document.body.insertBefore(s, document.body.firstChild);
}
}
加载脚本完成后,我们就可以调用my.postMessage
和my.onMessage
进行通信(统一约定h5发送消息给小程序时,必须带action
,小程序根据action
处理业务逻辑,同时小程序处理完成的结果必须带type
,h5在不同的业务场景下通过my.onMessage
处理不同type的响应),比如典型的,h5调用小程序签到:
h5部分代码如下:
// 处理扫脸签到逻辑
const faceVerify = (): Promise => {
return new Promise((resolve) => {
const handle = () => {
window.my.onMessage = (result: AlipaySignResult) => {
if (result.type === 'FACE_VERIFY_TIMEOUT' ||
result.type === 'DO_SIGN' ||
result.type === 'FACE_VERIFY' ||
result.type === 'LOCATION' ||
result.type === 'LOCATION_UNBELIEVABLE' ||
result.type === 'NOT_IN_ALIPAY') {
resolve(result);
}
};
window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
};
if (window.my) {
handle();
} else {
// 先记录错误日志
sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
// 监听load事件
document.addEventListener('myLoad', handle);
}
});
};
实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus
,先来看看怎么用,还是上面的场景
// 处理扫脸签到逻辑
const faceVerify = (): Promise => {
miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
return miniAppBus.subscribeAsync([
'FACE_VERIFY_TIMEOUT',
'DO_SIGN',
'FACE_VERIFY',
'LOCATION',
'LOCATION_UNBELIEVABLE',
'NOT_IN_ALIPAY',
])
};
可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。
为了满足不同场景和使用的方便,公开暴露的interface如下:
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}
subscribe
:函数接收两个参数,
type:需要订阅的type,可以是字符串,也可以是数组。
callback:回调函数。subscribeAsync
:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。unsubscribe
:取消订阅。postMessage
:postMessage替代,无需关注环境变量。
完整代码:
import { injectMiniAppScript } from './tools';
/**
* @description 小程序返回结果
* @export
* @interface MiniAppMessage
*/
interface MiniAppMessageBase {
type: string;
}
type MiniAppMessage = MiniAppMessageBase & {
[P in keyof T]: T[P]
}
/**
* @description 小程序接收消息
* @export
* @interface MessageToMiniApp
*/
export interface MessageToMiniApp {
action: string;
[x: string]: unknown
}
interface MiniAppMessageSubscriber {
(params: MiniAppMessage): void
}
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}
class MiniAppEventBus implements MiniAppEventBus{
/**
* @description: 监听函数
* @type {Map}
* @memberof MiniAppEventBus
*/
listeners: Map;
constructor() {
this.listeners = new Map>>();
this.init();
}
/**
* @description 初始化
* @private
* @memberof MiniAppEventBus
*/
private init() {
if (!window.my) {
// 引入脚本
injectMiniAppScript();
}
this.startListen();
}
/**
* @description 保证my变量存在的时候执行函数func
* @private
* @param {Function} func
* @returns
* @memberof MiniAppEventBus
*/
private async ensureEnv(func: Function) {
return new Promise((resolve) => {
const promiseResolve = () => {
resolve(func.call(this));
};
// 全局变量
if (window.my) {
promiseResolve();
}
document.addEventListener('myLoad', promiseResolve);
});
}
/**
* @description 监听小程序消息
* @private
* @memberof MiniAppEventBus
*/
private listen() {
window.my.onMessage = (msg: MiniAppMessage) => {
this.dispatch(msg.type, msg);
};
}
private async startListen() {
return this.ensureEnv(this.listen);
}
/**
* @description 发送消息,必须包含action
* @param {MessageToMiniApp} msg
* @returns
* @memberof MiniAppEventBus
*/
public postMessage(msg: MessageToMiniApp) {
return new Promise((resolve) => {
const realPost = () => {
resolve(window.my.postMessage(msg));
};
resolve(this.ensureEnv(realPost));
});
}
/**
* @description 订阅消息,支持单个或多个
* @template T
* @param {(string|string[])} type
* @param {MiniAppMessageSubscriber} callback
* @returns
* @memberof MiniAppEventBus
*/
public subscribe(type: string | string[], callback: MiniAppMessageSubscriber) {
const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber) => {
let listeners = this.listeners.get(type) || [];
listeners.push(cb);
this.listeners.set(type, listeners);
};
this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
}
private forEach(type:string | string[],cb:(type:string)=>void){
if (typeof type === 'string') {
return cb(type);
}
for (const key in type) {
if (Object.prototype.hasOwnProperty.call(type, key)) {
const element = type[key];
cb(element);
}
}
}
/**
* @description 异步订阅
* @template T
* @param {(string|string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
public async subscribeAsync(type: string | string[]): Promise> {
return new Promise((resolve, _reject) => {
this.subscribe(type, resolve);
});
}
/**
* @description 触发事件
* @param {string} type
* @param {MiniAppMessage} msg
* @memberof MiniAppEventBus
*/
public async dispatch(type: string, msg: MiniAppMessage) {
let listeners = this.listeners.get(type) || [];
listeners.map(i => {
if (typeof i === 'function') {
i(msg);
}
});
}
public async unSubscribe(type:string | string[]){
const unsubscribeSingle = (type: string) => {
this.listeners.set(type, []);
};
this.forEach(type,(type:string)=>unsubscribeSingle(type));
}
}
export default new MiniAppEventBus();
class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。
定义action handle,通过策略模式解耦:
const actionHandles = {
async FACE_VERIFY(){},
async GET_STEP(){},
async UPLOAD_HASH(){},
async GET_AUTH_CODE(){},
...// 其他action
}
....
// 在webview的消息监听函数中
async startProcess(e) {
const data = e.detail;
// 根据不同的action调用不同的handle处理
const handle = actionHandles[data.action];
if (handle) {
return actionHandles[data.action](this, data)
}
return uploadLogsExtend({
tip: STRING_CONTANT.UNKNOWN_ACTIONS,
data
})
}
使用起来也是得心顺畅,舒服。
类型完备,使用时智能提示,方便快捷。
定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。
简单工厂通常为静态方法,因此又叫静态工厂方法模式
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
简单工厂模式的主要角色如下:
其结构图如下图所示。
kotlin代码实现
简单工厂模式在Android中的实际应用
有时候,为了简化简单工厂模式,我们可以将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中。Fragment的创建使用简单工厂方法没有抽象产品类,所以工厂类放到了实现产品类中。
优点
看构造函数可知,无法new出bitmap,那么怎么创建bitmap对象呢?
内部源码
看下BitmapFactory的注释我们可以看到,这个工厂支持从不同的资源创建Bitmap对象,包括files, streams, 和byte-arrays,但是调用关系都大同小异。
收起阅读 »近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。
上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。
如何将操作路径还原这个问题,首选官方提供的 SDK: miniprogram-automator
。
小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:
上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档,当然如果之前用过 puppeteer ,也可以快速上手,api 基本一致。下面简单介绍下 SDK 的使用方式。
// 引入sdk
const automator = require('miniprogram-automator')
// 启动微信开发者工具
automator.launch({
// 微信开发者工具安装路径下的 cli 工具
// Windows下为安装路径下的 cli.bat
// MacOS下为安装路径下的 cli
cliPath: 'path/to/cli',
// 项目地址,即要运行的小程序的路径
projectPath: 'path/to/project',
}).then(async miniProgram => { // miniProgram 为 IDE 启动后的实例
// 启动小程序里的 index 页面
const page = await miniProgram.reLaunch('/page/index/index')
// 等待 500 ms
await page.waitFor(500)
// 获取页面元素
const element = await page.$('.main-btn')
// 点击元素
await element.tap()
// 关闭 IDE
await miniProgram.close()
})
有个地方需要提醒一下:使用 SDK 之前需要开启开发者工具的服务端口,要不然会启动失败。
有了还原操作路径的办法,接下来就要解决记录操作路径的难题了。
在小程序中,并不能像 web 中通过事件冒泡的方式在 window 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 Page
、Component
方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 event
对象,以此来捕获所有的事件。
// 暂存原生方法
const originPage = Page
const originComponent = Component
// 改写 Page
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
originPage(params)
}
// 改写 Component
Component = (params) => {
if (params.methods) {
const { methods } = params
const names = Object.keys(methods)
for (const name of names) {
// 进行方法拦截
if (typeof methods[name] === 'function') {
methods[name] = hookMethod(name, methods[name], true)
}
}
}
originComponent(params)
}
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (evt && evt.target && evt.type) {
// 记录用户行为
}
return method.apply(this, args)
}
}
这里的代码只是代理了所有的事件方法,并不能用来还原用户的行为,要还原用户行为还必须知道该事件类型是否是需要的,比如点击、长按、输入。
const evtTypes = [
'tap', // 点击
'input', // 输入
'confirm', // 回车
'longpress' // 长按
]
const hookMethod = (name, method) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
// 记录用户行为
}
return method.apply(this, args)
}
}
确定事件类型之后,还需要明确点击的元素到底是哪个,但是小程序里面比较坑的地方就是,event 对象的 target 属性中,并没有元素的类名,但是可以获取元素的 dataset。
为了准确的获取元素,我们需要在构建中增加一个步骤,修改 wxml 文件,将所有元素的 class
属性复制一份到
<!-- 构建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn"></view>
<view class="{{mainClassName}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"{{mainClassName}}"></view>
但是获取到 class 之后,又会有另一个坑,小程序的自动化测试工具并不能直接获取页面里自定义组件中的元素,必须先获取自定义组件。
<!-- Page -->
<toast text="loading" show="{{showToast}}" />
<!-- Component -->
<view class="toast" wx:if="{{show}}">
<text class="toast-text">{{text}}</text>
<view class="toast-close" />
</view>
// 如果直接查找 .toast-close 会得到 null
const element = await page.$('.toast-close')
element.tap() // Error!
// 必须先通过自定义组件的 tagName 找到自定义组件
// 再从自定义组件中通过 className 查找对应元素
const element = await page.$('toast .toast-close')
element.tap()
所以我们在构建操作的时候,还需要为元素插入 tagName。
<!-- 构建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"view" />
<toast text="loading" show="{{showToast}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"toast" />
现在我们可以继续愉快的记录用户行为了。
// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
actions.push({
time: Date.now(),
type,
query,
value
})
}
// 代理事件方法
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
const { type, target, detail } = evt
const { id, dataset = {} } = target
const { className = '' } = dataset
const { value = '' } = detail // input事件触发时,输入框的值
// 记录用户行为
let query = ''
if (isComponent) {
// 如果是组件内的方法,需要获取当前组件的 tagName
query = `${this.dataset.tagName} `
}
if (id) {
// id 存在,则直接通过 id 查找元素
query += id
} else {
// id 不存在,才通过 className 查找元素
query += className
}
addAction(type, query, value)
}
return method.apply(this, args)
}
}
到这里已经记录了用户所有的点击、输入、回车相关的操作。但是还有滚动屏幕的操作没有记录,我们可以直接代理 Page 的 onPageScroll
方法。
// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
if (type === 'scroll' || type === 'input') {
// 如果上一次行为也是滚动或输入,则重置 value 即可
const last = this.actions[this.actions.length - 1]
if (last && last.type === type) {
last.value = value
last.time = Date.now()
return
}
}
actions.push({
time: Date.now(),
type,
query,
value
})
}
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
const { onPageScroll } = params
// 拦截滚动事件
params.onPageScroll = function (...args) {
const [evt] = args
const { scrollTop } = evt
addAction('scroll', '', scrollTop)
onPageScroll.apply(this, args)
}
originPage(params)
}
这里有个优化点,就是滚动操作记录的时候,可以判断一下上次操作是否也为滚动操作,如果是同一个操作,则只需要修改一下滚动距离即可,因为两次滚动可以一步到位。同理,输入事件也是,输入的值也可以一步到位。
用户操作完毕后,可以在控制台输出用户行为的 json 文本,把 json 文本复制出来后,就可以通过自动化工具运行了。
// 引入sdk
const automator = require('miniprogram-automator')
// 用户操作行为
const actions = [
{ type: 'tap', query: 'goods .title', value: '', time: 1596965650000 },
{ type: 'scroll', query: '', value: 560, time: 1596965710680 },
{ type: 'tap', query: 'gotoTop', value: '', time: 1596965770000 }
]
// 启动微信开发者工具
automator.launch({
projectPath: 'path/to/project',
}).then(async miniProgram => {
let page = await miniProgram.reLaunch('/page/index/index')
let prevTime
for (const action of actions) {
const { type, query, value, time } = action
if (prevTime) {
// 计算两次操作之间的等待时间
await page.waitFor(time - prevTime)
}
// 重置上次操作时间
prevTime = time
// 获取当前页面实例
page = await miniProgram.currentPage()
switch (type) {
case 'tap':
const element = await page.$(query)
await element.tap()
break;
case 'input':
const element = await page.$(query)
await element.input(value)
break;
case 'confirm':
const element = await page.$(query)
await element.trigger('confirm', { value });
break;
case 'scroll':
await miniProgram.pageScrollTo(value)
break;
}
// 每次操作结束后,等待 5s,防止页面跳转过程中,后面的操作找不到页面
await page.waitFor(5000)
}
// 关闭 IDE
await miniProgram.close()
})
这里只是简单的还原了用户的操作行为,实际运行过程中,还会涉及到网络请求和 localstorage 的 mock,这里不再展开讲述。同时,我们还可以接入 jest 工具,更加方便用例的编写。
看似很难的需求,只要用心去发掘,总能找到对应的解决办法。另外微信小程序的自动化工具真的有很多坑,遇到问题可以先到小程序社区去找找,大部分坑都有前人踩过,还有一些一时无法解决的问题只能想其他办法来规避。最后祝愿天下无 bug。
原文链接:https://segmentfault.com/a/1190000023555693
在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。
在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。
从小程序的 1.0 版本开始,就支持为 input 组件设置 type,不同的 type 会显示不同的手机键盘。默认情况下,显示的是 text 文本输入键盘,这个键盘的特点是显示所有的内容,可以适用于所有的场景。
但,适用于所有场景也就意味着不适用于所有场景,总会在每一个场景中有着种种不便,因此,在实际的开发中,为了获得更佳的体验,你可以通过设置不同的 Type 来控制实际的键盘显示情况。
除了默认的 text 类以外,你还可以使用 number
(数字输入键盘)、idcard
身份证输入键盘和 digit
带小数点的数字键盘。
你可以根据自己的实际使用场景来设置不同的类型,比如说
text
类型的键盘,显然不如给一个 number
类型的键盘更合适。number
类型的键盘,来优化用户输入时的体验。这里的思路是类似的,当你预期用户输入的内容只有数字,就可以考虑 number
、digit
、idcard
等类型,来优化你的小程序的实际使用体验。
## 总结
input 组件默认提供的 四种 type ,可以通过选择不同的类型,从而获得不同的体验效果,从而对于你的小程序体验进行优化和推进。
原文链接:https://segmentfault.com/a/1190000025160488
收起阅读 »我们需要在选择图片后
对图片做一次安全校验
现在我们需要一个 后端接口 来实现图片的 安全校验 功能
这时候临时搭个Node服务好像不太现实
又不是什么正经项目
于是就想到了微信的云开发功能
用起来真实方便快捷
至于图片的校验方法
直接用云函数调用 security.imgSecCheck 接口就好了
chooseImage() {
/// 用户选择图片
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async res => {
if (res.errMsg === 'chooseImage:ok') {
wx.showLoading({ title: '图片加载中' })
// 获取图片临时地址
const path = res.tempFilePaths[0]
// 将图片地址实例化为图片
const image = await loadImage(path, this.canvas)
// 压缩图片
const filePath = await compress.call(this, image, 'canvas_compress')
// 校验图片合法性
const imgValid = await checkImage(filePath)
wx.hideLoading()
if (!imgValid) return
// 图片安全检测通过,执行后续操作
...
}
})
}
基本逻辑就是
超出尺寸的图片等比例缩小就好了
我们先要有一个canvas元素
用来处理需要压缩的图片
<template>
<view class="menu-background">
<view class="item replace" bindtap="chooseImage">
<i class="iconfont icon-image"></i>
<text class="title">图片</text>
<text class="sub-title">图片仅供本地使用</text>
</view>
//
// canvas
//
<canvas
type="2d"
id="canvas_compress"
class="canvas-compress"
style="width: {{canvasCompress.width}}px; height: {{canvasCompress.height}}px"
/>
</view>
</template>
将canvas移到视野不可见到位置
.canvas-compress
position absolute
left 0
top 1000px
图片进行压缩处理
/**
* 压缩图片
* 将尺寸超过规范的图片最小限度压缩
* @param {Image} image 需要压缩的图片实例
* @param {String} canvasId 用来处理压缩图片的canvas对应的canvasId
* @param {Object} config 压缩的图片规范 -> { maxWidth 最大宽度, maxHeight 最小宽度 }
* @return {Promise} promise返回 压缩后的 图片路径
*/
export default function (image, canvasId, config = { maxWidth: 750, maxHeight: 1334 }) {
// 引用的组件传入的this作用域
const _this = this
return new Promise((resolve, reject) => {
// 获取图片原始宽高
let width = image.width
let height = image.height
// 宽度 > 最大限宽 -> 重置尺寸
if (width > config.maxWidth) {
const ratio = width / config.maxWidth
width = config.maxWidth
height = height / ratio
}
// 高度 > 最大限高度 -> 重置尺寸
if (height > config.maxHeight) {
const ratio = height / config.maxHeight
height = config.maxHeight
width = width / ratio
}
// 设置canvas的css宽高
_this.canvasCompress.width = width
_this.canvasCompress.height = height
const query = this.createSelectorQuery()
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec(async res => {
// 获取 canvas 实例
const canvas = res[0].node
// 获取 canvas 绘图上下文
const ctx = canvas.getContext('2d')
// 根据设备dpr处理尺寸
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 将图片绘制到 canvas
ctx.drawImage(image, 0, 0, width, height)
// 将canvas图片上传到微信临时文件
wx.canvasToTempFilePath({
canvas,
x: 0,
y: 0,
destWidth: width,
destHeight: height,
complete (res) {
if (res.errMsg === 'canvasToTempFilePath:ok') {
// 返回临时文件路径
resolve(res.tempFilePath)
}
},
fail(err) {
reject(err)
}
})
})
})
}
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
/**
* 校验图片合法性
* @param {*} event.fileID 微信云存储的图片ID
* @return {Number} 0:校验失败;1:校验通过
*/
exports.main = async (event, context) => {
const contentType = 'image/png'
const fileID = event.fileID
try {
// 根据fileID下载图片
const file = await cloud.downloadFile({
fileID
})
const value = file.fileContent
// 调用 imgSecCheck 借口,校验不通过接口会抛错
// 必要参数 media { contentType, value }
const result = await cloud.openapi.security.imgSecCheck({
media: {
contentType,
value
}
})
return 1
} catch (err) {
return 0
}
}
/**
* 校验图片是否存在敏感信息
* @param { String } filePath
* @return { Promise } promise返回校验结果
*/
export default function (filePath) {
return new Promise((resolve, reject) => {
// 先将图片上传到云开发存储
wx.cloud.uploadFile({
cloudPath: `${new Date().getTime()}.png`,
filePath,
success (res) {
// 调用云函数-checkImage
wx.cloud.callFunction({
name: 'checkImage',
data: {
fileID: res.fileID
},
success (res) {
// res.result -> 0:存在敏感信息;1:校验通过
resolve(res.result)
if (!res.result) {
wx.showToast({
title: '图片可能含有敏感信息, 请重新选择',
icon: 'none'
})
}
},
fail (err) {
reject(err)
}
})
},
fail (err) {
reject(err)
}
})
})
}
原文链接:https://segmentfault.com/a/1190000038685508
最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。
微信提供了一个接口,可以生成如 weixin://dl/business/?t= *TICKET*
的 URL Scheme。你可以在系统自带的浏览器,比如 Safari 中访问这个地址,自动跳转到你自己的微信小程序中。
URL Scheme 的用途最大自然是各种营销用途,比如短信营销。不过,如果我们发散思维,就可以知道,URL Scheme 可以有更多的用途。
URL Scheme 在 iOS 系统应用中是比较多的,不少 iOS 的 Power User 都会借助 URL Scheme 来自定义自己的手机中的一些操作,实现特别的操作。我们可以参考 iOS 的 Power User 的用法,理解微信的 URL Scheme 的用法
如果我们将这些能力迁移到微信生态中,就可以发现,这里我们同样可以实现:
不仅如此,因为目前微信的安装率远高于普通 App,因此,你在进行营销的时候,就再也无需担心用户没有安装自己的 App,大可以先让用户进入到小程序,成为用户后,再引导用户下载 App,提升产品体验。
虽然很好,不过 URL Scheme 目前还有一些问题,比如只限于国内非个人主体小程序,对于个人开发者来说就无法使用了。
URL Scheme 的开放,对于微信生态来说,是一个很有力的工具,开发者可以借助与 URL Scheme 来完成自己在微信生态中的推广。在未来,我们可以看到,越来越多的开发者借助于 URL Scheme ,来实现一些很有意思的营销方式。
让我们拭目以待。
原文链接:https://segmentfault.com/a/1190000038919562
一只小奶狗会有名字、品种以及一堆可爱的特点作为其属性。如果将其建模为一个类,并且只用来保存这些属性数据,那么您应当使用数据类。在使用数据类时,编译器会为您自动生成 toString()
、equals()
与 hashCode()
函数,并提供开箱即用的 解构 与拷贝功能,从而帮您简化工作,使您可以专注于那些需要展示的数据。接下来本文将会带您了解数据类的其他好处、限制以及其实现的内部原理。
声明一个数据类,需要使用 data 修饰符并在其构造函数中以 val 或 var 参数的形式指定其属性。您可以为数据类的构造函数提供默认参数,就像其他函数与构造函数一样;您也可以直接访问和修改属性,以及在类中定义函数。
但相比于普通类,您可以获得以下几个好处:
toString()
、equals()
与 hashCode()
函数 ,从而避免了一系列人工操作可能造成的小错误,例如: 忘记在每次新增或更新属性后更新这些函数、实现 hashCode
时出现逻辑错误,或是在实现 equals
后忘记实现 hashCode
等;/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
data class Puppy(
val name: String,
val breed: String,
var cuteness: Int = 11
)
// 创建新的实例
val tofuPuppy = Puppy(name = "Tofu", breed = "Corgi", cuteness = Int.MAX_VALUE)
val tacoPuppy = Puppy(name = "Taco", breed = "Cockapoo")
// 访问和修改属性
val breed = tofuPuppy.breed
tofuPuppy.cuteness++
// 解构
val (name, breed, cuteness) = tofuPuppy
println(name) // prints: "Tofu"
// 拷贝:使用与 tofuPuppy 相同的品种和可爱度创建一个小狗,但名字不同
val tacoPuppy = tofuPuppy.copy(name = "Taco")
数据类有着一系列的限制。
数据类是作为数据持有者被创建的。为了强制执行这一角色,您必须至少传入一个参数到它的主构造函数,而且参数必须是 val 或 var 属性。尝试添加不带 val 或 var 的参数将会导致编译错误。
作为最佳实践,请考虑使用 val
而不是 var
,来提升不可变性,否则可能会出现一些细微的问题。如使用数据类作为 HashMap
对象的键时,容器可能会因为其 var
值的改变而获取出无效的结果。
同样,尝试在主构造函数中添加 vararg
参数也会导致编译错误:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
data class Puppy constructor(
val name: String,
val breed: String,
var cuteness: Int = 11,
// 错误:数据类的的主构造函数中只能包含属性 (val 或 var) 参数
playful: Boolean,
// 错误:数据类型的主构造函数已禁用 vararg 参数
vararg friends: Puppy
)
vararg
不被允许是由于 JVM 中数组和集合的 equals()
的实现方法不同。Andrey Breslav 的解释是:
集合的 equals() 进行的是结构化比较,而数组不是,数组使用
equals()
等效于判断其引用是否相等: this===
other。
*阅读更多: blog.jetbrains.com/kotlin/2015…
数据类可以继承于接口、抽象类或者普通类,但是不能继承其他数据类。数据类也不能被标记为 open
。添加 open
修饰符会导致错误: Modifier ‘open’ is incompatible with ‘data’ (‘open’ 修饰符不兼容 ‘data’)
。
为了理解这些功能为何能够实现,我们来检查下 Kotlin 究竟生成了什么。为了做到这点,我们需要查看反编译后的 Java 代码: Tools -> Kotlin -> Show Kotlin Bytecode
,然后点击 Decompile
按钮。
就像普通的类一样,Puppy
是一个公共 final
类,包含了我们定义的属性以及它们的 getter 和 setter:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class Puppy {
@NotNull
private final String name;
@NotNull
private final String breed;
private int cuteness;
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getBreed() {
return this.breed;
}
public final int getCuteness() {
return this.cuteness;
}
public final void setCuteness(int var1) {
this.cuteness = var1;
}
...
}
复制代码
我们定义的构造函数是由编译器生成的。由于我们在构造函数中使用了默认参数,所以我们也得到了第二个合成构造函数。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public Puppy(@NotNull String name, @NotNull String breed, int cuteness) {
...
this.name = name;
this.breed = breed;
this.cuteness = cuteness;
}
// $FF: synthetic method
public Puppy(String var1, String var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 4) != 0) {
var3 = 11;
}
this(var1, var2, var3);
}
...
}
复制代码
Kotlin 会为您生成 toString()
、hashCode()
与 equals()
方法。当您修改了数据类或更新了属性之后,也能自动为您更新为正确的实现。就像下面这样,hashCode()
与 equals()
总是需要同步。在 Puppy
类中它们如下所示:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public String toString() {
return "Puppy(name=" + this.name + ", breed=" + this.breed + ", cuteness=" + this.cuteness + ")";
}
public int hashCode() {
String var10000 = this.name;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
String var10001 = this.breed;
return (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31 + this.cuteness;
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Puppy) {
Puppy var2 = (Puppy)var1;
if (Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.breed, var2.breed) && this.cuteness == var2.cuteness) {
return true;
}
}
return false;
} else {
return true;
}
}
...
toString
和 hashCode
函数的实现很直接,跟一般您所实现的类似,而 equals 使用了 Intrinsics.areEqual 以实现结构化比较:
public static boolean areEqual(Object first, Object second) {
return first == null ? second == null : first.equals(second);
}
复制代码
通过使用方法调用而不是直接实现,Kotlin 语言的开发者可以获得更多的灵活性。如果有需要,他们可以在未来的语言版本中修改 areEqual 函数的实现。
为了实现解构,数据类生成了一系列只返回一个字段的 componentN()
方法。component 的数量取决于构造函数参数的数量:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public final String component1() {
return this.name;
}
@NotNull
public final String component2() {
return this.breed;
}
public final int component3() {
return this.cuteness;
}
...
您可以通过阅读我们之前的 Kotlin Vocabulary 文章 来了解更多有关解构的内容。
数据类会生成一个用于创建新对象实例的 copy()
方法,它可以保持任意数量的原对象属性值。您可以认为 copy()
是个含有所有数据对象字段作为参数的函数,它同时用原对象的字段值作为方法参数的默认值。知道了这一点,您就可以理解 Kotlin 为什么会创建两个 copy()
函数: copy
与 copy$default
。后者是一个合成方法,用来保证参数没有传值时,可以正确地使用原对象的值:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public final Puppy copy(@NotNull String name, @NotNull String breed, int cuteness) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(breed, "breed");
return new Puppy(name, breed, cuteness);
}
// $FF: synthetic method
public static Puppy copy$default(Puppy var0, String var1, String var2, int var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.name;
}
if ((var4 & 2) != 0) {
var2 = var0.breed;
}
if ((var4 & 4) != 0) {
var3 = var0.cuteness;
}
return var0.copy(var1, var2, var3);
}
...总结
数据类是 Kotlin 中最常用的功能之一,原因也很简单 —— 它减少了您需要编写的模板代码、提供了诸如解构和拷贝对象这样的功能,从而让您可以专注于重要的事: 您的应用。
rxjava 可以很方便的进行线程切换, 那么rxjava是如何进行线程切换的呢?阅读本文可以了解下rxjava 是如何进行线程切换的及线程切换的影响点。
Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> e) throws Exception {
Log.d("WanRxjava ", "subscrib td ==" + Thread.currentThread().getName());
e.onNext("我在发送next");
e.onComplete();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {
Log.d("WanRxjava ", "onSubscribe td ==" + Thread.currentThread().getName());
}
@Override
public void onNext(String value) {
Log.d("WanRxjava ", "onNext td ==" + Thread.currentThread().getName());
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
Log.d("WanRxjava ", "onComplete td ==" + Thread.currentThread().getName());
}
});
如上代码,实现了线程切换和观察者被观察者绑定的逻辑。我们分四部分看上述代码逻辑create、subscribeOn、observeOn、subscribe
create 顾名思议是 创建被观察者,这里有一个参数是 ObservableOnSubscribe,这是个接口类,我们看下create 的源码:
@SchedulerSupport(SchedulerSupport.NONE)
public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
ObjectHelper.requireNonNull(source, "source is null");
return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source));
}
将ObservableOnSubscribe 传入后 又调用了 new ObservableCreate(source)
public final class ObservableCreate<T> extends Observable<T> {
final ObservableOnSubscribe<T> source;
public ObservableCreate(ObservableOnSubscribe<T> source) {
this.source = source;
}
}
ObservableCreate 有一个变量是 source,这里只是将传入的ObservableOnSubscribe 赋值给source,也就是做了一层包装,然后返回。
调用完create后返回了 ObservableCreate(Observable),然后继续调用subscribeOn,传入了一个变量 Schedulers.io()
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> subscribeOn(Scheduler scheduler) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
}
我们看到调用了new ObservableSubscribeOn(this, scheduler) 将自身和 scheduler 传入
public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;
public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {
super(source);
this.scheduler = scheduler;
}
}
ObservableSubscribeOn 将scheduler 和 create 返回的对象又包装了一层 返回ObservableSubscribeOn
有一个参数是 Scheduler
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> observeOn(Scheduler scheduler) {
return observeOn(scheduler, false, bufferSize());
}
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
ObjectHelper.verifyPositive(bufferSize, "bufferSize");
return RxJavaPlugins.onAssembly(new ObservableObserveOn<T>(this, scheduler, delayError, bufferSize));
}
ObservableSubscribeOn(observable)又调用了observeOn,然后调用了new ObservableObserveOn(this, scheduler, delayError, bufferSize)
public final class ObservableObserveOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;
final boolean delayError;
final int bufferSize;
public ObservableObserveOn(ObservableSource<T> source, Scheduler scheduler, boolean delayError, int bufferSize) {
super(source);
this.scheduler = scheduler;
this.delayError = delayError;
this.bufferSize = bufferSize;
}
}
又是一个包装,将ObservableSubscribeOn 和 scheduler 包装成 ObservableObserveOn
上述最后一步即调用ObservableObserveOn.subscribe,传入参数是一个 observer
//ObservableObserveOn.java
@SchedulerSupport(SchedulerSupport.NONE)
@Override
public final void subscribe(Observer<? super T> observer) {
ObjectHelper.requireNonNull(observer, "observer is null");
try {
observer = RxJavaPlugins.onSubscribe(this, observer);
ObjectHelper.requireNonNull(observer, "Plugin returned null Observer");
subscribeActual(observer);
} catch (NullPointerException e) { // NOPMD
throw e;
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
// can't call onError because no way to know if a Disposable has been set or not
// can't call onSubscribe because the call might have set a Subscription already
RxJavaPlugins.onError(e);
NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
npe.initCause(e);
throw npe;
}
}
可以看到调用subscribe 后调用了subscribeActual(observer);将observer 传入
我们看下 subscribeActual(observer)
//ObservableObserveOn.java
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
}
}
上面的if 先不管,主要看下下面的逻辑,调用了 scheduler.createWorker(),这个scheduler 是 observeOn 传入的,然后调用
new ObserveOnObserver(observer, w, delayError, bufferSize);将worker /observer 又做了一次包装。
//ObservableObserveOn 内部类
static final class ObserveOnObserver<T> extends BasicIntQueueDisposable<T>
implements Observer<T>, Runnable {
private static final long serialVersionUID = 6576896619930983584L;
final Observer<? super T> actual;
final Scheduler.Worker worker;
final boolean delayError;
final int bufferSize;
SimpleQueue<T> queue;
Disposable s;
Throwable error;
volatile boolean done;
volatile boolean cancelled;
int sourceMode;
boolean outputFused;
ObserveOnObserver(Observer<? super T> actual, Scheduler.Worker worker, boolean delayError, int bufferSize) {
this.actual = actual;
this.worker = worker;
this.delayError = delayError;
this.bufferSize = bufferSize;
}
@Override
public void onSubscribe(Disposable s) {
if (DisposableHelper.validate(this.s, s)) {
this.s = s;
if (s instanceof QueueDisposable) {
@SuppressWarnings("unchecked")
QueueDisposable<T> qd = (QueueDisposable<T>) s;
int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY);
if (m == QueueDisposable.SYNC) {
sourceMode = m;
queue = qd;
done = true;
actual.onSubscribe(this);
schedule();
return;
}
if (m == QueueDisposable.ASYNC) {
sourceMode = m;
queue = qd;
actual.onSubscribe(this);
return;
}
}
queue = new SpscLinkedArrayQueue<T>(bufferSize);
actual.onSubscribe(this);
}
}
@Override
public void onNext(T t) {
if (done) {
return;
}
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
schedule();
}
@Override
public void onError(Throwable t) {
if (done) {
RxJavaPlugins.onError(t);
return;
}
error = t;
done = true;
schedule();
}
@Override
public void onComplete() {
if (done) {
return;
}
done = true;
schedule();
}
@Override
public void dispose() {
if (!cancelled) {
cancelled = true;
s.dispose();
worker.dispose();
if (getAndIncrement() == 0) {
queue.clear();
}
}
}
@Override
public boolean isDisposed() {
return cancelled;
}
void schedule() {
if (getAndIncrement() == 0) {
worker.schedule(this);
}
}
void drainNormal() {
int missed = 1;
final SimpleQueue<T> q = queue;
final Observer<? super T> a = actual;
for (;;) {
if (checkTerminated(done, q.isEmpty(), a)) {
return;
}
for (;;) {
boolean d = done;
T v;
try {
v = q.poll();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
s.dispose();
q.clear();
a.onError(ex);
return;
}
boolean empty = v == null;
if (checkTerminated(d, empty, a)) {
return;
}
if (empty) {
break;
}
a.onNext(v);
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
void drainFused() {
int missed = 1;
for (;;) {
if (cancelled) {
return;
}
boolean d = done;
Throwable ex = error;
if (!delayError && d && ex != null) {
actual.onError(error);
worker.dispose();
return;
}
actual.onNext(null);
if (d) {
ex = error;
if (ex != null) {
actual.onError(ex);
} else {
actual.onComplete();
}
worker.dispose();
return;
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
@Override
public void run() {
if (outputFused) {
drainFused();
} else {
drainNormal();
}
}
boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) {
if (cancelled) {
queue.clear();
return true;
}
if (d) {
Throwable e = error;
if (delayError) {
if (empty) {
if (e != null) {
a.onError(e);
} else {
a.onComplete();
}
worker.dispose();
return true;
}
} else {
if (e != null) {
queue.clear();
a.onError(e);
worker.dispose();
return true;
} else
if (empty) {
a.onComplete();
worker.dispose();
return true;
}
}
}
return false;
}
@Override
public int requestFusion(int mode) {
if ((mode & ASYNC) != 0) {
outputFused = true;
return ASYNC;
}
return NONE;
}
@Override
public T poll() throws Exception {
return queue.poll();
}
@Override
public void clear() {
queue.clear();
}
@Override
public boolean isEmpty() {
return queue.isEmpty();
}
}
包装完ObserveOnObserver后,调用了source.subscribe 这里的source 即ObservableSubscribeOn.subscribe,进而调用ObservableSubscribeOn.subscribeActual
//ObservableSubscribeOn.java
@Override
public void subscribeActual(final Observer<? super T> s) {
final SubscribeOnObserver<T> parent = new scheduler<T>(s);
s.onSubscribe(parent);
parent.setDisposable(scheduler.scheduleDirect(new Runnable() {
@Override
public void run() {
source.subscribe(parent);
}
}));
}
static final class SubscribeOnObserver<T> extends AtomicReference<Disposable> implements Observer<T>, Disposable {
private static final long serialVersionUID = 8094547886072529208L;
final Observer<? super T> actual;
final AtomicReference<Disposable> s;
SubscribeOnObserver(Observer<? super T> actual) {
this.actual = actual;
this.s = new AtomicReference<Disposable>();
}
@Override
public void onSubscribe(Disposable s) {
DisposableHelper.setOnce(this.s, s);
}
@Override
public void onNext(T t) {
actual.onNext(t);
}
@Override
public void onError(Throwable t) {
actual.onError(t);
}
@Override
public void onComplete() {
actual.onComplete();
}
@Override
public void dispose() {
DisposableHelper.dispose(s);
DisposableHelper.dispose(this);
}
@Override
public boolean isDisposed() {
return DisposableHelper.isDisposed(get());
}
void setDisposable(Disposable d) {
DisposableHelper.setOnce(this, d);
}
}
ObservableSubscribeOn.subscribeActual
我们看到 run 的方法体即source.subscribe(parent);这里的source 即 ObservableCreate(ObservableOnSubscribe),传入了observer,然后调用 observer的OnNext 和 OnComplete 方法。
Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Android 2.3 及更高版本的默认设置),将单张照片加载到内存大约需要 48MB 内存(4048 * 3036 * 4 字节),如此庞大的内存需求可能会立即耗尽应用的所有可用内存
本篇文章就来讲下 Bitmap 一些比较有用的知识点,希望对你有所帮助 😇😇
全文可以概括为以下几个问题:
在开始讲关于 Bitmap 的知识点前,需要先阐述一些基础概念作为预备知识
我们知道,在不同手机屏幕上 1dp 所对应的 px 值可能是会有很大差异的。例如,在小屏幕手机上 1dp 可能对应 1px,在大屏幕手机上对应的可能是 3px,这也是我们的应用实现屏幕适配的原理基础之一
想要知道在特定一台手机上 1dp 对应多少 px,或者是想要知道屏幕宽高大小,这些信息都可以通过 DisplayMetrics 来获取
val displayMetrics = applicationContext.resources.displayMetrics
打印出本文所使用的模拟器的 DisplayMetrics 信息:
DisplayMetrics{density=3.0, width=1080, height=1920, scaledDensity=3.0, xdpi=480.0, ydpi=480.0}
从中就可以提取出几点信息:
dpi 是一个很重要的值,指的是在系统软件上指定的单位尺寸的像素数量,往往是写在系统出厂配置文件的一个固定值。Android 系统定义的屏幕像素密度基准值是 160dpi,该基准值下 1dp 就等于 1px,依此类推 320dpi 下 1dp 就等于 2px
dpi 决定了应用在显示 drawable 时是选择哪一个文件夹内的切图。每个 drawable 文件夹都对应不同的 dpi 大小,Android 系统会自动根据当前手机的实际 dpi 大小从合适的 drawable 文件夹内选取图片,不同的后缀名对应的 dpi 大小就如以下表格所示。如果 drawable 文件夹名不带后缀,那么该文件夹就对应 160dpi
对于本文所使用的模拟器来说,应用在选择图片时就会优先从 drawable-xxhdpi
文件夹拿,如果该文件夹内没找到图片,就会依照 xxxhdpi -> xhdpi -> hdpi -> mdpi -> ldpi
的顺序进行查找,优先使用高密度版本的图片资源
先将一张大小为 1920 x 1080 px 的图片保存到 drawable-xxhdpi
文件夹内,然后将其显示在一个宽高均为 180dp 的 ImageView 上,该 Bitmap 所占用的内存就通过 bitmap.byteCount
来获取
val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("imageView width: " + imageView.width)
log("imageView height: " + imageView.height)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: imageView width: 540
BitmapMainActivity: imageView height: 540
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 8294400
inDensity
代表的是系统最终选择的 drawable 文件夹类型,等于 480 说明取的是 drawable-xxhdpi
文件夹下的图片inTargetDensity
代表的是当前设备的 dpi从最终结果可以很容易地就逆推出 Bitmap 所占内存大小的计算公式:bitmapWidth * bitmapHeight * 单位像素点所占用的字节数,即 1920 * 1080 * 4 = 8294400
此外,在 Android 2.3 版本之前,Bitmap 像素存储需要的内存是在 native 上分配的,并且生命周期不太可控,可能需要用户自己回收。2.3 - 7.1 之间,Bitmap 的像素存储在 Dalvik 的 Java 堆上,当然,4.4 之前的甚至能在匿名共享内存上分配(Fresco采用),而 8.0 之后的像素内存又重新回到 native 上去分配,不需要用户主动回收,8.0 之后图像资源的管理更加优秀,极大降低了 OOM
上面之所以很容易就逆推出了 Bitmap 所占内存大小的计算公式,是因为所有条件都被我故意设定为最优情况了,才使得计算过程这么简单。而实际上 Bitmap 所占内存大小和其所在的 drawable 文件夹是有很大关系的,虽然计算公式没变
现在的大部分应用为了达到最优的显示效果,会为应用准备多套切图放在不同的 drawable 文件夹下,而BitmapFactory.decodeResource
方法在解码 Bitmap 的时候,就会自动根据当前设备的 dpi 和 drawable 文件夹类型来判断是否需要对图片进行缩放显示
将图片从 drawable-xxhdpi
迁移到 drawable-xhdpi
文件夹,然后再打印日志信息
BitmapMainActivity: imageView width: 540
BitmapMainActivity: imageView height: 540
BitmapMainActivity: bitmap width: 2880
BitmapMainActivity: bitmap height: 1620
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 320
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 18662400
可以看到,Bitmap 的宽高都发生了变化,inDensity
等于 320 也说明了选取的是drawable-xhdpi
文件夹内的图片,Bitmap 所占内存居然增加了一倍多
模拟器的 dpi 是 480,拿到了 dpi 为 320 的drawable-xhdpi
文件夹下的图片,在系统的理解中该文件夹存放的都是小图标,是为小屏幕手机准备的,现在要在大屏幕手机上展示的话就需要对其进行放大,放大的比例就是 480 / 320 = 1.5 倍,因此 Bitmap 的宽就会变为 1920 * 1.5 = 2880 px,高就会变为 1080 * 1.5 = 1620 px,最终占用的内存空间大小就是 2880 * 1620 * 4 = 18662400
所以说,对于同一台手机,Bitmap 在不同 drawable 文件夹下对其最终占用的内存大小是有很大关系的,虽然计算公式没变,但是由于系统会进行自动缩放,Bitmap 的宽高都变为了原先的 1.5 倍,导致最终 Bitmap 的内存大小就变为了 8294400 * 1.5 * 1.5 = 18662400
同理,对于同个 drawable 文件夹下的同一张图片,在不同的手机屏幕上也可能会占用不同的内存空间,因为不同的手机的 dpi 大小可能是不一样的,BitmapFactory 进行缩放的比例也就不一样
在上一个例子里,Bitmap 的宽高是 2880 * 1620 px,ImageView 的宽高是 540 * 540 px,该 Bitmap 肯定是会显示不全的,读者可以试着自己改变 ImageView 的宽高大小来验证是否会对 Bitmap 的大小产生影响
这里就不贴代码了,直接来说结论,答案是没有关系。原因也很简单,毕竟上述例子是先将 Bitmap 加载到内存中后再设置给 ImageView 的,ImageView 自然不会影响到 Bitmap 的加载过程,该 Bitmap 的大小只受其所在的 drawable 文件夹类型以及手机的 dpi 大小这两个因素的影响。但这个结论是需要考虑测试方式的,如果你是使用 Glide 来加载图片,Glide 内部实现了按需加载的机制,避免由于 Bitmap 过大而 ImageView 显示不全导致内存浪费的情况,这种情况下 ImageView 的宽高就会影响到 Bitmap 的内存大小了
BitmapFactory 提供了很多个方法用于加载 Bitmap 对象:decodeFile、decodeResourceStream、decodeResource、decodeByteArray、decodeStream
等多个,但只有 decodeResourceStream
和 decodeResource
这两个方法才会根据 dpi 进行自动缩放
decodeResource
方法也会调用到decodeResourceStream
方法,decodeResourceStream
方法如果判断到inDensity
和 inTargetDensity
两个属性外部没有主动赋值的话,就会根据实际情况进行赋值。如果是从磁盘或者 assert 目录加载图片的话是不会进行自动缩放的,毕竟这些来源也不具备 dpi 信息,Bitmap 的分辨率也只能保持其原有大小
@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//如果 density 没有赋值的话(等于0),那么就使用基准值 160 dpi
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
//在这里进行赋值,density 就等于 drawable 对应的 dpi
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//如果没有主动设置 inTargetDensity 的话,inTargetDensity 就等于设备的 dpi
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
Bitmap.Config 定义了四种常见的编码格式,分别是:
根据 Bitmap 所占内存大小的计算公式:bitmapWidth * bitmapHeight * 单位像素点所占用的字节数,想要尽量减少 Bitmap 占用的内存大小的话就要从降低图片分辨率和降低单位像素需要的字节数这两方面来考虑了
在一开始的情况下加载到的 Bitmap 的宽高是 1920 * 1080,占用的内存空间是 1920 * 1080 * 4 = 8294400,约 7.9 MB,这是优化前的状态
val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 8294400
由于 ImageView 的宽高只有 540 * 540 px,此时 Bitmap 也只能在 ImageView 上显示为一个像素缩略图,如果进行原图加载的话其实会造成很大的内存浪费,此时我们就可以通过 inSampleSize 属性来压缩图片尺寸
例如,将 inSampleSize 设置为 2 后,Bitmap 的宽高就都会缩减为原先的一半,占用的内存空间就变成了原先的四分之一, 960 * 540 * 4 = 2073600,约 1.9 MB
val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 960
BitmapMainActivity: bitmap height: 540
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 2073600
可以看到,inSampleSize 属性应该设置多少是需要根据 Bitmap 的实际宽高和 ImageView 的实际宽高这两个条件来一起决定的。我们在正式加载 Bitmap 前要先获取到 Bitmap 的实际宽高大小,这可以通过 inJustDecodeBounds 属性来实现。设置 inJustDecodeBounds 为 true 后 decodeResource
方法只会去读取 Bitmap 的宽高属性而不会去进行实际加载,这个操作是比较轻量级的。然后通过每次循环对半折减,计算出 inSampleSize 需要设置为多少才能尽量接近到 ImageView 的实际宽高,之后将 inJustDecodeBounds 设置为 false 去实际加载 Bitmap
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
val inSampleSize = calculateInSampleSize(options, imageView.width, imageView.height)
options.inSampleSize = inSampleSize
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
需要注意的是,inSampleSize 使用的最终值将是向下舍入为最接近的 2 的幂,BitmapFactory 内部会自动会该值进行校验修正
如果我们不主动设置 inTargetDensity 的话,decodeResource
方法会自动根据当前设备的 dpi 来对 Bitmap 进行缩放处理,我们可以通过主动设置 inTargetDensity 来控制缩放比例,从而控制 Bitmap 的最终宽高。最终宽高的生成规则: 180 / 480 * 1920 = 720,180 / 480 * 1080 = 405,占用的内存空间是 720 * 405 * 4 = 1166400,约 1.1 MB
val options = BitmapFactory.Options()
options.inTargetDensity = 180
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 720
BitmapMainActivity: bitmap height: 405
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 1166400
BitmapFactory 默认使用的编码图片格式是 ARGB_8888,每个像素点占用四个字节,我们可以按需改变要采用的图片格式。例如,如果要加载的 Bitmap 不包含透明通道的,我们可以使用 RGB_565,该格式每个像素点占用两个字节,占用的内存空间是 1920 * 1080 * 2 = 4147200,约 3.9 MB
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.RGB_565
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: RGB_565
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 4147200
object Singleton
复制代码
class Singleton private constructor() {
companion object {
private var instance: Singleton? = null
get() {
if (field == null) field = Singleton()
return field
}
@Synchronized
fun instance(): Singleton {
return instance!!
}
}
}
复制代码
class KtSingleton3 private constructor() {
companion object {
val instance by lazy { KtSingleton3() }
}
}
复制代码
Lazy
是接受一个 lambda 并返回一个 Lazy
实例的函数,返回的实例可以作为实现延迟属性的委托。第一次调用 get()
会执行已传递给 lazy()
的 lambda 表达式并记录结果,后续调用 get()
只是返回记录的结果。Lazy
默认的线程模式就是 LazyThreadSafetyMode.SYNCHRONIZED
内部默认双重校验锁
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
复制代码
public interface Lazy<out T> {
//当前实例化对象,一旦实例化后,该对象不会再改变
public val value: T
//返回true表示,已经延迟实例化过了,false 表示,没有被实例化,
//一旦方法返回true,该方法会一直返回true,且不会再继续实例化
public fun isInitialized(): Boolean
}
复制代码
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
//判断是否已经初始化过,如果初始化过直接返回,不在调用高级函数内部逻辑
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
}
else {
//调用高级函数获取其返回值
val typedValue = initializer!!()
//将返回值赋值给_value,用于下次判断时,直接返回高级函数的返回值
_value = typedValue
initializer = null
typedValue
}
}
}
//省略部分代码
}
复制代码
class Singleton private constructor() {
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder = Singleton()
}
}
复制代码
enum class Singleton {
INSTANCE;
}
接上一章。。。。。。。
#import
/**
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了
*/
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (NSNotificationCrash)
+ (void)xz_enableNotificationProtector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+NSNotificationCrash.h"
#import "NSObject+XZSwizzle.h"
#import
static const char *isNSNotification = "isNSNotification";
@implementation NSObject (NSNotificationCrash)
+ (void)xz_enableNotificationProtector {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *objc = [[NSObject alloc] init];
[objc xz_instanceSwizzleMethod:@selector(addObserver:selector:name:object:) replaceMethod:@selector(xz_addObserver:selector:name:object:)];
// 在ARC环境下不能显示的@selector dealloc。
[objc xz_instanceSwizzleMethod:NSSelectorFromString(@"dealloc") replaceMethod:NSSelectorFromString(@"xz_dealloc")];
});
}
- (void)xz_addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {
// 添加标志位,在delloc中只有isNSNotification是YES,才会移除通知
[observer setIsNSNotification:YES];
[self xz_addObserver:observer selector:aSelector name:aName object:anObject];
}
- (void)setIsNSNotification:(BOOL)yesOrNo {
objc_setAssociatedObject(self, isNSNotification, @(yesOrNo), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isNSNotification {
NSNumber *number = objc_getAssociatedObject(self, isNSNotification);;
return [number boolValue];
}
/**
如果一个对象从来没有添加过通知,那就不要remove操作
*/
- (void)xz_dealloc
{
if ([self isNSNotification]) {
NSLog(@"CrashProtector: %@ is dealloc,but NSNotificationCenter Also exsit",self);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
[self xz_dealloc];
}
@end
具体方式:
1、定义一个抽象类,抽象类中弱引用target。
将对象以树形结构组织起来,以达成“部分-整体”的层次机构,使得客户端对单个对象和组合对象的使用具有一致性。
是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
部分、整体场景,如树形菜单,文件、文件夹的管理。
树枝和叶子实现统一接口,树枝内部组合该接口。
例:文件与文件夹的关系
先进行普通的实现方式
//文件类
public class File {
public String name;
public File(String name) {
this.name = name;
}
/**
* 操作方法
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//文件夹类
public class Folder{
public String name;
private List<File> mFileList;
public Folder(String name) {
mFileList = new ArrayList<>();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}
public void add(File file) {
mFileList.add(file);
}
public void remove(File file) {
mFileList.remove(file);
}
public File getChild(int pos) {
return mFileList.get(pos);
}
}
//运行
File file = new File("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
文件和文件夹作为两个类来进行操作,将文件类进行添加文件,但是呢?如果文件夹下添加文件夹该咋办呢?就需要再创建一个list来存放文件夹,这样大家都是节点,为啥搞得这么复杂呢?既然存在上下级节点的问题,咱们就抽象为一个抽象类,用抽象类作为节点,子类就是文件夹和文件。
//将文件与文件夹统一看作是一类节点,做一个抽象类来定义这种节点,然后以其实现类来区分文件与目录,在实现类中分别定义各自的具体实现内容,把组合方法写到这个节点类中
public abstract class File {
public String name;
public File(String name) {
this.name = name;
}
/**
* 操作方法
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void watch();
/**
* 组合方法
* @param file
*/
public void add(File file){
throw new UnsupportedOperationException();
}
public void remove(File file){
throw new UnsupportedOperationException();
}
public File getChild(int pos){
throw new UnsupportedOperationException();
}
}
public class Folder extends File{
private List<File> mFileList;
public Folder(String name) {
super(name);
mFileList = new ArrayList<>();
}
@Override
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}
@Override
public void add(File file) {
mFileList.add(file);
}
@Override
public void remove(File file) {
mFileList.remove(file);
}
@Override
public File getChild(int pos) {
return mFileList.get(pos);
}
}
public class TestFile extends File {
public TestFile(String name) {
super(name);
}
@Override
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//运行
TestFile file = new TestFile("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
folder.getChild(0).watch();
这种组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多及目录呈现等树形结构数据的操作。
安全组合模式(简化)如下code
//将文件与文件夹统一看作是一类节点,做一个抽象类来定义这种节点,然后以其实现类来区分文件与目录,在实现类中分别定义各自的具体实现内容,把组合方法写到这个节点类中
public abstract class File {
public String name;
public File(String name) {
this.name = name;
}
/**
* 操作方法
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void watch();
}
public class Folder extends File{
private List<File> mFileList;
public Folder(String name) {
super(name);
mFileList = new ArrayList<>();
}
@Override
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}
public void add(File file) {
mFileList.add(file);
}
public void remove(File file) {
mFileList.remove(file);
}
public File getChild(int pos) {
return mFileList.get(pos);
}
}
public class TestFile extends File {
public TestFile(String name) {
super(name);
}
@Override
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//运行
TestFile file = new TestFile("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
folder.getChild(0).watch();
安全组合模式分工就很明确了。它还有一个好处就是当我们add/remove的时候,我们能知道具体的类是什么了,而透明组合模式就得在运行时去判断,比较麻烦。
外观设计模式的主要目的在于让外部减少与子系统内部多个模块的交互,从而让外部能够更简单的使用子系统。他负责把客户端的请求转发给子系统内部的各个模块进行处理。
/**
* 模块A
*/
public class SubSystemA {
public void testFunA(){
System.out.println("testFunA");
}
}
/**
* 模块B
*/
public class SubSystemB {
public void testFunB(){
System.out.println("testFunB");
}
}
/**
* 模块C
*/
public class SubSystemC {
public void testFunC(){
System.out.println("testFunC");
}
}
/**
* Facade
*/
public class Facade {
private SubSystemA subSystemA;
private SubSystemB subSystemB;
private SubSystemC subSystemC;
private Facade(){
subSystemA = new SubSystemA();
subSystemB = new SubSystemB();
subSystemC = new SubSystemC();
}
private static Facade instance;
public static Facade getInstance(){
if(instance==null){
instance = new Facade();
}
return instance;
}
public void tastOperation(){
subSystemA.testFunA();
subSystemB.testFunB();
subSystemC.testFunC();
}
}
//运行
Facade.getInstance().tastOperation();
由于外观类维持了对多个子系统类的引用,外观对象在系统运行时将占用较多的系统资源,因此需要对外观对象的数量进行限制,避免系统资源的浪费。可以结合单例模式对外观类进行改进,将外观类设计为一个单例类。通过对外观模式单例化,可以确保系统中只有唯一一个访问子系统的入口,降低系统资源的消耗。
我在项目中的实践:
在项目中经常会出现,网络请求,缓存本地,本地有缓存用本地缓存,而且网络请求经常会在多个地方调用,如果不采用外观模式设计,则会出现客户端的代码异常复杂,而且不利于维护。于是我就进行了如下改变,建立中间仓库类来进行数据切换,客户端只需要进行对仓库数据进行调用,不用关心仓库里数据怎样生成的。
/**
* 建立仓库接口类
* TestApiDataSource
*/
public interface TestApiDataSource {
/**
* 登陆接口
* @param params
* @return
*/
Observable<GetLoginResponse> getLogin(GetLoginParams params);
}
/**
* 建立本地数据源(主要是为了方便客户端调用)
* TestApiLocalDataSource
*/
public class TestApiLocalDataSource extends BaseLocalDataSource implements TestApiDataSource {
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
Observable<GetLoginResponse> observable = Observable.create(new ObservableOnSubscribe<GetLoginResponse>() {
@Override
public void subscribe(ObservableEmitter<GetLoginResponse> subscriber) throws Exception {
subscriber.onComplete();
}
});
return observable;
}
}
/**
* 建立网络数据源
* TestApiRemoteDataSource
*/
public class TestApiRemoteDataSource extends BaseRemoteDataSource implements TestApiDataSource {
/**
*
* 请求网络
* @param params
* @return
*/
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
return ApiSource.getApiService(AppHuanJingFactory.getAppModel().getApi()).getApi2Service().getLogin(params);
}
}
/**
* 建立单例仓库类
* TestApiRepository
*/
public class TestApiRepository extends BaseRepository<TestApiLocalDataSource,TestApiRemoteDataSource> implements TestApiDataSource {
public static volatile TestApiRepository instance;
public static TestApiRepository getInstance(){
if(instance==null){
synchronized (TestApiRepository.class){
if(instance==null){
instance = new TestApiRepository(new TestApiLocalDataSource(),new TestApiRemoteDataSource());
}
}
}
return instance;
}
protected TestApiRepository(TestApiLocalDataSource localDataSource, TestApiRemoteDataSource remoteDataSource) {
super(localDataSource, remoteDataSource);
}
/**
* 数据源切换
* #getLogin#
* @param params
* @return
*/
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
Observable<GetLoginResponse> observable = Observable.
concat(localDataSource.getLogin(params),
remoteDataSource.getLogin(params).
doOnNext(new Consumer<GetLoginResponse>() {
@Override
public void accept(GetLoginResponse response) throws Exception {
/**
* cache
*/
}
})).compose(RxTransformerHelper.<GetLoginResponse>ioToUI()).firstOrError().toObservable();
return observable;
}
}
//客户端执行,不需要考虑具体实现
TestApiRepository.getInstance().getLogin(new GetLoginParams()).subscribe(new BaseRxNetworkResponseObserver<GetLoginResponse>() {
@Override
public void onResponse(GetLoginResponse getLoginResponse) {
}
@Override
public void onResponseFail(Exception e) {
}
@Override
protected void onBeforeResponseOperation() {
}
@Override
public void onSubscribe(Disposable d) {
add(d);
}
});
动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。
/**
* 装饰类Component,所有类的父类
*/
public interface Component {
void sampleOperation();
}
/**
* 实现抽象部件,具体装饰过程还是交给子类实现
*/
public class Decorator implements Component {
private Component component;
public Decorator(Component component){
this.component = component;
}
@Override
public void sampleOperation() {
component.sampleOperation();
}
}
/**
* 需要装扮的类
*/
public class ConcreteComponent implements Component{
@Override
public void sampleOperation() {
}
}
/**
* 具体实现
*/
public class ConcreateDecoratorA extends Decorator{
public ConcreateDecoratorA(Component component) {
super(component);
}
@Override
public void sampleOperation() {
super.sampleOperation();
addPingShengm();
}
/**
* 新增业务方法
*/
private void addPingShengm() {
System.out.println("添加绘彩1");
}
}
/**
* 具体实现
*/
public class ConcreateDecoratorB extends Decorator{
public ConcreateDecoratorB(Component component) {
super(component);
}
@Override
public void sampleOperation() {
super.sampleOperation();
addPingShengm();
}
/**
* 新增业务方法
*/
private void addPingShengm() {
System.out.println("添加绘彩2");
}
}
举一个实际例子:
工厂需要产生多种水杯,有瓶身绘彩,有不锈钢盖被子,也有不锈钢盖和瓶身绘彩的杯子。(等各种需求)
假如说采用继承子类的方式。如下code:
/**
* 创建水杯的接口包含四个方法,底座,盖子,瓶身,一个实现功能product
*/
public interface IShuiBei {
void dizuo();
void gaizi();
void pingsheng();
void product();
}
/**
* 水晶杯实现类
*/
public class ShuiJInBei implements IShuiBei,Component {
@Override
public void dizuo() {
System.out.println("水晶底座");
}
@Override
public void gaizi() {
System.out.println("水晶盖子");
}
@Override
public void pingsheng() {
System.out.println("水晶瓶身");
}
@Override
public void product() {
dizuo();
gaizi();
pingsheng();
}
}
/**
* 添加绘彩的水晶杯
*/
public class HuiCaiShuiJinBei extends ShuiJInBei{
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
/**
* 不锈钢杯子盖的水晶杯
*/
public class HuiJinGangGaiBei extends ShuiJInBei{
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
}
/**
* 不锈钢杯子盖的水晶杯带彩绘
*/
public class HuiCaiShuiJinGangGaiBei extends ShuiJInBei{
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
//运行
HuiCaiShuiJinBei huiCaiShuiJinBei = new HuiCaiShuiJinBei();
HuiCaiShuiJinGangGaiBei huiCaiShuiJinGangGaiBei = new HuiCaiShuiJinGangGaiBei();
ShuiJInBei shuiJInBei = new ShuiJInBei();
huiCaiShuiJinBei.product();
huiCaiShuiJinGangGaiBei.product();
shuiJInBei.product();
一共创建三个子类,一个父类,当然如果需求更多的话,子类会不断的增加。
装饰类实现如上功能code:
/**
* 实现抽象部件
*/
public class ShuijinbeiDecorator implements IShuiBei{
IShuiBei iShuiBei;
public ShuijinbeiDecorator(IShuiBei iShuiBei){
this.iShuiBei = iShuiBei;
}
@Override
public void dizuo() {
iShuiBei.dizuo();
}
@Override
public void gaizi() {
iShuiBei.gaizi();
}
@Override
public void pingsheng() {
iShuiBei.pingsheng();
}
@Override
public void product() {
dizuo();
gaizi();
pingsheng();
}
}
/**
* 钢盖实现类
*/
public class GangGaiDecorator extends ShuijinbeiDecorator{
public GangGaiDecorator(IShuiBei iShuiBei) {
super(iShuiBei);
}
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
}
/**
* 彩绘实现类
*/
public class CaihuiDecorator extends ShuijinbeiDecorator{
public CaihuiDecorator(IShuiBei iShuiBei) {
super(iShuiBei);
}
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
//运行
IShuiBei iShuiBei = new ShuiJInBei();
iShuiBei.product();
iShuiBei = new CaihuiDecorator(iShuiBei);
iShuiBei.product();
iShuiBei = new GangGaiDecorator(iShuiBei);
iShuiBei.product();
iShuiBei = new ShuiJInBei();
iShuiBei = new GangGaiDecorator(iShuiBei);
iShuiBei.product();
看到如上代码你大概会恍然大悟,装饰模式如果在你的子类特别多,用装饰模式很好,但是比较容易出错哦。
由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另外一方面,使用装饰模式会产生比使用继承关系所产生的更多的对象。而更多的对象会使得查找错误更为困难,特别是这些对象在看上去极为相似的时候。
context类簇
收起阅读 »将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
定义:
是把适配的类的api转化成为目标类的api。
adapter是为了让adaptee与Target发生关系建立的
adapter 实现Target接口,来继承Adaptee,实现需要实现的方法
代码:
//适配接口
public interface Target {
void request();
}
//需要适配的对象
public class Adaptee {
public void SpecialRequest(){
System.out.println("SpecialRequest");
}
}
//适配器
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
specialRequest();
}
}
//运行代码
Target target = new Adapter();
target.request();
定义:与类的适配器模式一样,对象的适配器模式把被适配的类的api转化为目标类的api,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是通过委派关系连接到adaptee类。
//适配接口
public interface Target {
void request();
}
//需要适配的对象
public class Adaptee {
public void SpecialRequest(){
System.out.println("SpecialRequest");
}
}
//适配器
public class Adapter implements Target {
Adaptee adaptee;
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specialRequest();
}
}
//运行代码
Target target = new Adapter(new Adaptee());
target.request();
如上两个对比,就能看出类适配器和对象适配器的区别。
对象适配器:持有一个对象来实现适配器模式
类适配器:通过继承来实现适配器模式。
adaper在Android中的运用
listview
收起阅读 »建造者模式是较为复杂的创建型模式,将组件和组件的组件过程分开,然后一步一步建造一个复杂的对象。所以建造者模式又叫生成器模式。它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解耦,提升代码的可读性以及扩展性。
构造一个对象需要很多参数的时候,并且参数的个数或者类型不固定的时候
例:
//创建复杂对象Product
public class Product {
private String partA;
private String partB;
private String partC;
public String getPartA() {
return partA;
}
public void setPartA(String partA) {
this.partA = partA;
}
public String getPartB() {
return partB;
}
public void setPartB(String partB) {
this.partB = partB;
}
public String getPartC() {
return partC;
}
public void setPartC(String partC) {
this.partC = partC;
}
}
//创建抽象类Builder
public abstract class Builder {
protected Product product = new Product();
public abstract void builderPartA();
public abstract void builderPartB();
public abstract void builderPartC();
public Product getResult() {
return product;
}
}
//创建实现类ConcreateBuilder
public class ConcreateBuilder extends Builder {
@Override
public void builderPartA() {
}
@Override
public void builderPartB() {
}
@Override
public void builderPartC() {
}
}
//创建组装对象Director
public class Director {
private Builder builder;
public Director(Builder builder){
this.builder = builder;
}
public void setBuilder(Builder builder) {
this.builder = builder;
}
public Product constract(){
builder.builderPartA();
builder.builderPartB();
builder.builderPartC();
return builder.getResult();
}
}
//运行
Builder builder = new ConcreateBuilder();
Director director = new Director(builder);
Product product = director.constract();
缺点:
1.AlertDialog
2.Glide/okhttp
收起阅读 »1.unrecognized selector crash (没找到对应的函数)
2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 )
3.NSNotification crash:(当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification)
4.NSTimer类型crash:(需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash)
5.Container类型crash:(数组,字典,常见的越界,插入,nil)
6.野指针类型的crash
7.非主线程刷UI类型:(在非主线程刷UI将会导致app运行crash)……
unrecognized selector类型的crash,通常是因为一个对象调用了一个不属于它方法的方法导致的。而我们可以从方法调用的过程中,寻找到避免程序崩溃的突破口。
方法调用的过程是哪样的呢?
方法调用的过程--调用实例方法
1.在对象的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.对象的<缓存方法列表> 里没找到,就去<类的方法列表>里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
4.如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。
方法调用的过程--调用类方法
1.在类的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.类的<缓存方法列表> 里没找到,就去里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去meta类的父类里执行1、2。
4.如果找到了根meta类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。
从上面的方法调用过程可以看出,在找不到调用的方法程序崩溃之前,我们可以通过重写NSObject方法进行拦截调用,阻止程序的crash。这里面就用到了消息的转发机制:
runtime提供了3种方式去补救:
1:调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2:调用forwardingTargetForSelector让别的对象去执行这个函数
3:调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。
当调用方法的消息转发给该类后,该类也没有这个方法,回调用resolveInstanceMethod:方法,在消息接受类中重写方法,返回YES,表明该消息已经处理,这样就不会崩溃了。
重写的resolveInstanceMethod:方法中一定要有动态添加方法的处理,不然会继续走消息转发的流程,从而造成死循环。
#import
@interface XZUnrecognizedSelectorSolveObject : NSObject
@property (nonatomic, weak) NSObject *objc;
@end
#Import "XZUnrecognizedSelectorSolveObject.h"
#import
@interface XZUnrecognizedSelectorSolveObject ()
@end
@implementation XZUnrecognizedSelectorSolveObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果没有动态添加方法的话,还会调用forwardingTargetForSelector:方法,从而造成死循环
class_addMethod([self class], sel, (IMP)addMethod, "v@:@");
return YES;
}
id addMethod(id self, SEL _cmd) {
NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return 0;
}
@end
实现原理:在分类中自定义一个xz_forwardingTargetForSelector:方法,然后替换掉系统的forwardingTargetForSelector:方法
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (SelectorCrash)
+ (void)xz_enableSelectorProtector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+SelectorCrash.h"
#import
#import "NSObject+XZSwizzle.h"
#Import "XZUnrecognizedSelectorSolveObject.h"
@implementation NSObject (SelectorCrash)
+ (void)xz_enableSelectorProtector {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *object = [[NSObject alloc] init];
[object xz_instanceSwizzleMethod:@selector(forwardingTargetForSelector:) replaceMethod:@selector(xz_forwardingTargetForSelector:)];
});
}
- (id)xz_forwardingTargetForSelector:(SEL)aSelector {
// 判断某个类是否有某个实例方法,有则返回YES,否则返回NO
if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
// 有forwardInvocation实例方法
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
if (imp != impOfNSObject) {
return nil;
}
}
// 新建桩类转发消息
XZUnrecognizedSelectorSolveObject *solveObject = [XZUnrecognizedSelectorSolveObject new];
solveObject.objc = self;
return solveObject;
}
@end
交换方法代码 如下:
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (XZSwizzle)
/**
对类方法进行拦截并替换
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对类方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对实例方法进行拦截并替换
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对实例方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+XZSwizzle.h"
#import
@implementation NSObject (XZSwizzle)
/**
对类方法进行拦截并替换
@param originalSelector 类原有方法
@param replaceSelector 自定义替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];
[self xz_classSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}
/**
对类方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
// Method中包含IMP函数指针,通过替换IMP,使SEL调用不同函数实现
Method originalMethod = class_getClassMethod(kClass, originalSelector);
Method replaceMethod = class_getClassMethod(kClass, replaceSelector);
// 获取MetaClass (交换、添加等类方法需要用metaClass)
Class metaClass = objc_getMetaClass(NSStringFromClass(kClass).UTF8String);
// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(metaClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(metaClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}
/**
对实例方法进行拦截并替换
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];
[self xz_instanceSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}
/**
对实例方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Method originalMethod = class_getInstanceMethod(kClass, originalSelector);
Method replaceMethod = class_getInstanceMethod(kClass, replaceSelector);
// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(kClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}
@end
1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。
2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash
#import
#import "XZKVOProxy.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVOCrash)
@property (nonatomic, strong) XZKVOProxy * _Nullable KVOProxy; // 自定义的kvo关系的代理
@end
NS_ASSUME_NONNULL_END
#import "NSObject+KVOCrash.h"
#import "XZKVOProxy.h"
#import
#pragma mark - NSObject + KVOCrash
static void *NSObjectKVOProxyKey = &NSObjectKVOProxyKey;
@implementation NSObject (KVOCrash)
- (XZKVOProxy *)KVOProxy {
id proxy = objc_getAssociatedObject(self, NSObjectKVOProxyKey);
if (nil == proxy) {
proxy = [XZKVOProxy kvoProxyWithObserver:self];
self.KVOProxy = proxy;
}
return proxy;
}
- (void)setKVOProxy:(XZKVOProxy *)proxy
{
objc_setAssociatedObject(self, NSObjectKVOProxyKey, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
2、在自定义代理类中建立一个map来维护KVO整个关系
#import
typedef void (^XZKVONitificationBlock)(id _Nullable observer, id _Nullable object, NSDictionary * _Nullable change);
/**
KVO配置类
用于存储KVO里面的相关设置参数
*/
@interface XZKVOInfo : NSObject
//- (instancetype _Nullable)initWithObserver:(id _Nonnull)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock _Nonnull )block;
@end
NS_ASSUME_NONNULL_BEGIN
/**
KVO管理类
用于管理object添加和移除的消息,(通过Map进行KVO之间的关系)(字典应该也可以)
*/
@interface XZKVOProxy : NSObject
@property (nullable, nonatomic, weak, readonly) id observer;
+ (instancetype)kvoProxyWithObserver:(nullable id)observer;
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block;
- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath;
- (void)xz_unobserver:(id _Nullable)object;
- (void)xz_unobserverAll;
@end
NS_ASSUME_NONNULL_END
#import "XZKVOProxy.h"
#import
@interface XZKVOInfo ()
{
@public
__weak id _object; // 观察对象
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
XZKVONitificationBlock _block;
}
@end
@implementation XZKVOInfo
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context {
return [self initWithObserver:object keyPath:keyPath options:options block:NULL action:NULL context:context];
}
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context
block:(XZKVONitificationBlock)block {
return [self initWithObserver:object keyPath:keyPath options:options block:block action:NULL context:context];
}
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
block:(_Nullable XZKVONitificationBlock)block
action:(_Nullable SEL)action
context:(void * _Nullable)context {
if (self = [super init]) {
_object = object;
_block = block;
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}
@end
/**
此类用来管理混乱的KVO关系
让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系
好处:
不会crash如果出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以 1.直接阻止这些非正常的操作。
crash 2.被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。
👇:
重复添加观察者不会crash,即不会走@catch
多次添加对同一个属性观察的观察者,系统方法内部会强应用这个观察者,同理即可remove该观察者同样次数。
*/
@interface XZKVOProxy ()
{
pthread_mutex_t _mutex;
NSMapTable *> *_objectInfoMap;///< map来维护KVO整个关系
}
@end
@implementation XZKVOProxy
+ (instancetype)kvoProxyWithObserver:(nullable id)observer {
return [[self alloc] initWithObserver:observer];
}
- (instancetype)initWithObserver:(nullable id)observer {
if (self = [super init]) {
_observer = observer;
_objectInfoMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality capacity:0];
}
return self;
}
/**
加锁、解锁
*/
- (void)lock {
pthread_mutex_lock(&_mutex);
}
- (void)unlock {
pthread_mutex_unlock(&_mutex);
}
/**
添加、删除 观察者
*/
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block {
// 断言
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:options context:context block:block];
if (info) {
// 将info以key-value的形式存储到map中。key是被观察对象;value是观察信息的集合。
// 加锁
[self lock];
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
break;
}
}
if (_isExisting == YES) {
// 解锁
[self unlock];
return;
}
// // check for info existence
// XZKVOInfo *existingInfo = [infos member:info];
// if (nil != existingInfo) {
// // observation info already exists; do not observe it again
//
// // 解锁
// [self unlock];
// return;
// }
// 不存在
if (infos == nil) {
// 创建set,并将set添加进Map里
infos = [NSMutableSet set];
[_objectInfoMap setObject:infos forKey:object];
}
// 将要添加的KVOInfo添加进set里面
[infos addObject:info];
// 解锁
[self unlock];
// 将 kvoProxy 作为观察者;添加观察者
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:info->_context];
}
}
- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath {
// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:0 context:nil];
// 加锁
[self lock];
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}
if (_isExisting == YES) {
// 存在
[infos removeObject:info];
// remove no longer used infos
if (0 == infos.count) {
[_objectInfoMap removeObjectForKey:object];
}
// 解锁
[self unlock];
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
} else {
// 解锁
[self unlock];
}
// XZKVOInfo *registeredInfo = [infos member:info];
//
// if (nil != registeredInfo) {
// [infos removeObject:registeredInfo];
//
// // remove no longer used infos
// if (0 == infos.count) {
// [_objectInfoMap removeObjectForKey:object];
// }
//
// // 解锁
// [self unlock];
//
//
// // 移除观察者
// [object removeObserver:self forKeyPath:registeredInfo->_keyPath context:registeredInfo->_context];
// } else {
// // 解锁
// [self unlock];
// }
}
- (void)xz_unobserver:(id _Nullable)object {
// 加锁
[self lock];
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
[_objectInfoMap removeObjectForKey:object];
// 解锁
[self unlock];
// 批量移除观察者
for (XZKVOInfo *info in infos) {
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}
- (void)xz_unobserverAll {
if (_objectInfoMap) {
// 加锁
[self lock];
// copy一份map,防止删除数据异常冲突
NSMapTable *objectInfoMaps = [_objectInfoMap copy];
[_objectInfoMap removeAllObjects];
// 解锁
[self unlock];
// 移除全部观察者
for (id object in objectInfoMaps) {
NSSet *infos = [objectInfoMaps objectForKey:object];
if (!infos || infos.count == 0) {
continue;
}
for (XZKVOInfo *info in infos) {
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
NSLog(@"%@",keyPath);
NSLog(@"%@",object);
NSLog(@"%@",change);
NSLog(@"%@",context);
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
XZKVOInfo *info;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}
if (_isExisting == YES && info) {
XZKVOProxy *proxy = info->_object;
id observer = proxy.observer;
XZKVONitificationBlock block = info->_block;
if (block) {
block(observer, object, change);
}
}
}
- (void)dealloc {
// 移除所有观察者
[self xz_unobserverAll];
// 销毁mutex
pthread_mutex_destroy(&_mutex);
}
@end
性能优化的几个点:
在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。
CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。
GPU(Graphics Processing Unit,图形处理器)
纹理的渲染、
所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。
在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。
尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer; 不要频繁地调用 UIView
的相关属性 如:frame、bounds、transform 等;尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改; Autolayout
会比直接设置 frame 消耗更多的 CPU 资源;图片的 size 和 UIImageView 的 size 保持一致; 控制线程的最大并发数量; 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
- 尽量避免短时间内大量图片显示;
- GPU 能处理的最大纹理尺寸是 4096 * 4096,超过这个尺寸就会占用 CPU 资源,所以纹理不能超过这个尺寸;
- 尽量减少透视图的数量和层次;
- 减少透明的视图(alpha < 1),不透明的就设置 opaque 为 YES;
- 尽量避免离屏渲染;
在 OpenGL 中,GPU 有两种渲染方式:
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;
离屏渲染消耗性能的原因:
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。
光栅化, layer.shouldRasterize = YES
遮罩, layer.mask
圆角,同时设置 layer.masksToBounds = YES
,layer.cornerRadius > 0
- 可以用 CoreGraphics 绘制裁剪圆角
阴影
- 如果设置了
layer.shadowPath
不会产生离屏渲染
1.CPU 处理;
2.网络请求;
3.定位;
4.图像渲染;
1.尽可能降低 CPU、GPU 功耗;
2.少用定时器;
3.优化 I/O 操作;
尽量不要频繁写入小数据,最好一次性批量写入;
读写大量重要数据时,可以用 dispatch_io,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;
数据量大时,用数据库管理数据;
4.网络优化;
减少、压缩网络数据(JSON 比 XML 文件性能更高); 若多次网络请求结果相同,尽量使用缓存; 使用断点续传,否则网络不稳定时可能多次传输相同的内容; 网络不可用时,不进行网络请求; 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间; 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
5.定位优化
如果只是需要快速确定用户位置,用 CLLocationManager
的requestLocation
方法定位,定位完成后,定位硬件会自动断电;若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务; 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest
;需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically
为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;
App 的启动分为两种:冷启动(Cold Launch) 和热启动(Warm Launch)。
前者表示从零开始启动 App,后者表示 App 已经存在内存中,在后台依然活着,再次点击图标启动 App。
App 启动的优化主要是针对冷启动的优化,通过添加环境变量可以打印出 App 的启动时间分析:Edit Scheme -> Run -> Arguments -> Environment Variables 添加 DYLD_PRINT_STATISTICS
设置为 1。
这里打印的是在执行 main
函数之前的耗时信息,若想打印更详细的信息则添加环境变量为:DYLD_PRINT_STATISTICS_DETAILS
设置为 1。
冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。
第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。
启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。
map_images
进行可执行文件的内容解析和处理,再 load_images
中调用 call_load_methods
调用所有 Class 和 Category 的 load
方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor))
修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions:
函数。
针对不同的阶段,有不同的优化思路:
dyld
1.减少动态库、合并动态库,定期清理不必要的动态库;
2.减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;
3.减少 C++ 虚函数数量;
4.Swift 开发尽量使用 struct;
虚函数和 Java 中的抽象函数有点类似,但区别是,基类定义的虚函数,子类可以实现也可以不实现,而抽象函数子类一定要实现。
Runtime
用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))、C++ 静态构造器、以及 Objective-C 中的 load 方法;
main
将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中;
安装包(ipa)主要由可执行文件和资源文件组成,若不管理妥善则会造成安装包体积越来越大,所以针对资源优化我们可以将资源采取无损压缩,去除没用的资源。
对于可执行文件的瘦身,我们可以:
1.从编译器层面优化
1.Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES
2.去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加 -fno-exceptions;
3.利用 AppCode,检测未使用代码检测:菜单栏 -> Code -> Inspect Code;
4.编写 LLVM 插件检测重复代码、未调用代码;
5.通过生成 LinkMap 文件检测;
Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES
运行程序可看到:
打开可看见各种信息:
我们可根据这个信息针对某个类进行优化。
摘自链接:https://www.jianshu.com/p/fe566ec32d28
个人感觉,retrofit中的动态代理比较典型,我就拿出来解读一下:
先来阅读一下retrofit 的源码,看retrofit怎么来实现动态代理
ApiService apiService = retrofit.create(ApiService.class);
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
retrofit这段代码主要作用是将类里的注解等参数解析,并包装成网络请求真正的数据,来进行请求数据。
咱模仿retrofit写一套动态代理:
定义注解:
@LeftFace
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LeftFace {
String value() default "左面脸";
}
@UpFace
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UpFace {
String value() default "上面脸";
}
创建接口
public interface IFaceListener {
@LeftFace
String getFace(String name);
@UpFace
String getFacePoint(String name);
}
创建动态代理
public class FaceCreate {
public <T> T create(final Class<T> face){
return (T) Proxy.newProxyInstance(face.getClassLoader(), new Class<?>[]{face}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String result = null;
if(method.isAnnotationPresent(LeftFace.class)){
LeftFace leftFace = method.getAnnotation(LeftFace.class);
result = leftFace.value();
}
if(method.isAnnotationPresent(UpFace.class)){
UpFace upFace = method.getAnnotation(UpFace.class);
result = upFace.value();
}
result = HString.concatObject(null,args)+result;
return result;
}
});
}
}
如此我们就模仿的建造了动态代理,动态代理在开发中相对与静态代理,灵活性更强。
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
Object proxy:我们的真实对象
Method method:对象的方法
Object[] args:对象的参数
Proxy.newProxyInstance(face.getClassLoader(), new Class<?>[]{face}, new InvocationHandler() {
ClassLoader loader:定义了由哪个ClassLoader对象来对生成的代理对象进行加载
Class<?>[] interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
InvocationHandler :InvocationHandler对象
是一种对象创建模式,可以确保项目中一个类只产生一个实例。
对于频繁使用的对象可以减少创建对象所花费的时间,这对于重量级对象来说,简直是福音。由于new的减少,对系统内存使用频率也会降低,减少GC的压力,并缩短GC停顿时间,这也会减少Android项目的UI卡顿。
1、饿汉模式
public class TestSingleton {
private static final TestSingleton testSingleton = new TestSingleton();
private TestSingleton(){
}
public static TestSingleton getInstance(){
return testSingleton;
}
}
细节我就不多写了,大家都应该知道,构造函数为private,用getInstance来获取实例
2.、懒汉模式
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
return testSingleton;
}
}
比饿汉式的优点在于用时再加载,比较重量级的单例,就不适用与饿汉了。
3、线程安全的懒汉模式
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
testSingleton = new TestSingleton();
}
}
return testSingleton;
}
}
可以看到的是比上面的单例多了一个对象锁,着可以保证在创建对象的时候,只有一个线程能够创建对象。
4、线程安全的懒汉模式-DCL双重检查锁机制
public class TestSingleton {
private static volatile TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
}
}
return testSingleton;
}
}
双重检查,同步块加锁机制,保证你的单例能够在加锁后的代码里判断空,还有增加了一个volatile 关键字,保证你的线程在执行指令时候按顺序执行。这也是市面上见的最多的单例。
敲黑板!!知识点:原子操作、指令重排。
什么是原子操作?
简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
m = 6; // 这是个原子操作
假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。
而,声明并赋值就不是一个原子操作:
int n = 6; // 这不是一个原子操作
对于这个语句,至少有两个操作:
这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。
什么是指令重排?
简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
int a ; // 语句1
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4
正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。
但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。
也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
主要在于testSingleton = new TestSingleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 testSingleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
--------------------------------------------一部分的文章可能讲到如上就嘎然而止了----------------------------------------
推荐后两种
public class TestSingleton {
private TestSingleton(){
}
public static TestSingleton getInstance(){
return TestSingletonInner.testSingleton;
}
private static class TestSingletonInner{
static final TestSingleton testSingleton = new TestSingleton();
}
}
static 保证数据独一份
final 初始化完成后不能被修改,线程安全。
敲黑板!!知识点:java在加载类的时候不会将其内部的静态内部类加载,只有在使用该内部类方法时才被调用。这明显是最好的单例,并不需要什么锁一类的机制。
利用了类中静态变量的唯一性
优点:
public enum TestSingleton {
INSTANCE;
public void toSave(){
}
}
使用TestSingleton.INSTANCE.toSave();
创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。如果你要自己添加一些线程安全的方法,记得控制线程安全哦。
优点:写法简单/线程安全
1、application
本身就是单例,生命周期为整个程序的生命周期,可以通过这个特性,能够用来存储一些数据
2、单例模式引起的内存泄漏
在使用Context注意用application中的context
收起阅读 »OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)
NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];
NSLog(@"instance----%p %p",objc1,objc2);
instance实例对象存储的信息:
1.isa指针
Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);
打印结果
2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个
类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)
元类对象
1.元类对象的获取
Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);
打印指针地址
NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0
获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个
Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];
元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)
从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用
/// Person继承自NSObject
@interface Person : NSObject
-(void)perMethod;
+(void)perEat;
@end
@implementation Person
-(void)perMethod{
}
+(void)perEat{
}
@end
/// student继承自Person
@interface Student : Person
-(void)StudentMethod;
+(void)StudentEat;
@end
@implementation Student
-(void)StudentMethod{
}
+(void)StudentEat{
}
Student *student = [[Student alloc]init];
[student StudentMethod]
当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用
[Student perEat];
总的来说,isa,superclass的的关系可以用一副经典的图来表示
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TCPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
- (void)setName:(NSString *)name{
_name = name;
}
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}
/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];
self.person2.name = @"ttttttttt";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
#import "NSKVONotifying_TCPerson.h"
@implementation NSKVONotifying_TCPerson
//NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
- (void)setName:(NSString *)name{
_NSSetIntVaueAndNotify();
}
//改变过程
void _NSSetIntVaueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
//通知观察者
- (void)didChangeValueForKey:(NSString *key){
[observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context];
}
@end