
南尘,GitHub 和各大 Blog 论坛常客,出身 Android,但不仅仅是 Android。写点技术,也吐点情感。做不完的开源,写不完的矫情,你就听听我吹逼,不会错~
面试 11:Java 玩转归并排序 前面讲了冒泡、选择、插入三种简单排序,时间复杂度都是 O(n²),今天,我们终于迎来了更高级的排序:归并排序。 虽然在这之前还有希尔排序和堆排序,但由于时间关系,我们这里就直接跳过,确实感兴趣的请直接 Google。 归并排序 我们总是可以将一个数组一分为二,然后二分为四,直到每一组只有两个元素,这可以理解为个递归的过程,然后将两个元素进行排序,之后再将两个元素为一组进行排序。直到所有的元素都排序完成。同样我们来看下边这个动图。 图片来源于网络 归并排序算法是采用分治法的一个非常典型的应用,且各层分治递归可以同时进行。 归并算法的思想 归并算法其实可以分为递归法和迭代法(自底向上归并),两种实现对于最小集合的归并操作思想是一样的。区别在于如何划分数组,我们先介绍下算法最基本的操作: 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列; 设定两个指针,最初位置分别为两个已经排序序列的起始位置; 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; 重复步骤 3 直到某一指针到达序列尾; 将另一序列剩下的所有元素直接复制到合并序列尾。 我们来看看 Java 递归代码是怎么实现的: public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void mergeSort(int[] arr) { if (arr == null) return; mergeSort(arr, 0, arr.length - 1); } private static void mergeSort(int[] arr, int start, int end) { if (start >= end) return; // 找出中间索引 int mid = start + (end - start >> 1); // 对左边数组进行递归 mergeSort(arr, start, mid); // 对右边数组进行递归 mergeSort(arr, mid + 1, end); // 合并 merge(arr, start, mid, end); } private static void merge(int[] arr, int start, int mid, int end) { // 先建立一个临时数组,用于存放排序后的数据 int[] tmpArr = new int[arr.length]; int start1 = start, end1 = mid, start2 = mid + 1, end2 = end; // 创建一个下标 int pos = start1; // 缓存左边数组的第一个元素的索引 int tmp = start1; while (start1 <= end1 && start2 <= end2) { // 从两个数组中取出最小的放入临时数组 if (arr[start1] <= arr[start2]) tmpArr[pos++] = arr[start1++]; else tmpArr[pos++] = arr[start2++]; } // 剩余部分依次放入临时数组,实际上下面两个 while 只会执行其中一个 while (start1 <= end1) { tmpArr[pos++] = arr[start1++]; } while (start2 <= end2) { tmpArr[pos++] = arr[start2++]; } // 将临时数组中的内容拷贝回原来的数组中 while (tmp <= end) { arr[tmp] = tmpArr[tmp++]; } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; mergeSort(arr); printArr(arr); } } 归并排序算法总的时间复杂度是 O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。 而由于在归并排序过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时压入栈的数据占用的空间:n + logn,所以空间复杂度为 O(n)。 总结 归并排序虽然比较稳定,在时间上也是非常有效的,但是这种算法很消耗空间,一般来说只有在外部排序才会采用这个方法,但在内部排序不会用这种方法,而是用快速排序。明天,我们将带来排序算法中的王牌:快速排序。
面试 10:Java 玩转选择排序和插入排序 昨天给大家讲解了 Java 玩转冒泡排序,大家一定觉得并没有什么难度吧,不知道大佬们玩转了吗?不知道大家有没有多加思考,实际上在我们最后的一种思路上,还可以再继续改进。 我们先看看昨天最终版本的代码。 public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void bubbleSort(int[] arr) { if (arr == null) return; // 定义一个标记 isSort,当其值为 true 的时候代表已经有序。 boolean isSort; for (int i = 0; i < arr.length - 1; i++) { isSort = true; for (int j = 1; j < arr.length - i; j++) { if (arr[j - 1] > arr[j]) { swap(arr, j - 1, j); isSort = false; } } if (isSort) break; } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; bubbleSort(arr); printArr(arr); } } 我们用一个 boolean 变量 isSort 来判断是否已经排序完成,当一整趟遍历都没有发生数据交换的时候,说明已经排序完成,直接 break 退出循环即可。 我们试想一下这样的场景:假设有 100 个数字的数组,仅仅前 10 个无序,后面 90 个均有序并且都大于前面 10 个数字。 我们采用上面的终极算法可以明显看到,第一趟排序后,最后发生交换的位置必定大于 10,且这个位置之后的数据必定已经有序了,但我们还是会去做徒劳的 90 次遍历,而且我们还要遍历 10 次! 显然我们可以找到这样的思路,在第一次排序后,就记住最后发生交换的位置,第二次只要从数组头部遍历到这个位置就 OK 了。 我们不妨直接看看代码实现: public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void bubbleSort(int[] arr) { if (arr == null) return; int flag = arr.length; int k; for (int i = 0; i < arr.length - 1; i++) { k = flag; flag = 0; for (int j = 1; j < k; j++) { if (arr[j - 1] > arr[j]) { swap(arr, j - 1, j); flag = j; } } if (flag == 0) break; } } public static void main(String[] args) { int[] arr = {6, 4, 1, 2, 3, 5, 7, 8, 9}; bubbleSort(arr); printArr(arr); } } 其实算法也就那么一回事儿,用心去理解它的原理,理解后,无论是用哪种语言实现起来都是非常简单的。那我们今天就来看看另外两种排序,选择排序和插入排序。 选择排序 选择排序(Selection sort)是一种简单直观的排序算法。选择排序之所以叫选择排序就是在一次遍历过程中找到最小元素的角标位置,然后把它放到数组的首端。我们排序过程都是在寻找剩余数组中的最小元素,所以就叫做选择排序。 它的思想如下: 从待排序序列中,找到关键字最小的元素;起始假定第一个元素为最小 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换; 从余下的 N - 1 个元素中,找出关键字最小的元素,重复1,2步,直到排序结束。 图片来源于网络 选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n 个元素的表进行排序总共进行至多 n - 1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。 我们来看看用 Java 是怎么实现的。 public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void selectSort(int[] arr) { if (arr == null) return; int i, j, min, len = arr.length; for (i = 0; i < len - 1; i++) { min = i; // 未排序的序列中最小元素的下标 for (j = i + 1; j < len; j++) { //在未排序元素中继续寻找最小元素,并保存其下标 if (arr[min] > arr[j]) { min = j; } } if (min != i) swap(arr, min, i); } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; selectSort(arr); printArr(arr); } } 上述 java 代码可以看出我们除了交换元素并未开辟额外的空间,所以额外的空间复杂度为 O(1)。 对于时间复杂度而言,选择排序序冒泡排序一样都需要遍历 n(n-1)/2 次,但是相对于冒泡排序来说每次遍历只需要交换一次元素,这对于计算机执行来说有一定的优化。但是选择排序也是名副其实的慢性子,即使是有序数组,也需要进行 n(n-1)/2 次比较,所以其时间复杂度为 O(n²)。 即便无论如何也要进行 n(n-1)/2 次比较,选择排序仍是不稳定的排序算法,我们举一个例子如:序列 5 8 5 2 9, 我们知道第一趟选择第 1 个元素 5 会与 2 进行交换,那么原序列中两个 5 的相对先后顺序也就被破坏了。 选择排序总结: 选择排序的算法时间平均复杂度为O(n²)。 选择排序空间复杂度为 O(1)。 选择排序为不稳定排序。 插入排序 对于插入排序,大部分资料都是使用扑克牌整理作为例子来引入的,我们打牌都是一张一张摸牌的,每摸到一张牌就会跟手里所有的牌比较来选择合适的位置插入这张牌,这也就是直接插入排序的中心思想,我们先来看下动图: 图片来源于网络 相信大家看完动图以后大概知道了插入排序的实现思路了。那么我们就来说下插入排序的思想。 插入排序的思想 从第一个元素开始,该元素可以认为已经被排序 取出下一个元素,在已经排序的元素序列中从后向前扫描 如果该元素(已排序)大于新元素,将该元素移到下一位置 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置 将新元素插入到该位置后 重复步骤 2~5 理解上述思想其实并不难,我们来看看用 Java 怎么实现: public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void insertionSort(int[] arr) { if (arr == null) return; int j; int temp; for (int i = 1; i < arr.length; i++) { // 设置哨兵,拿出待插入的值 temp = arr[i]; j = i; // 然后寻找正确插入的位置 while (j > 0 && arr[j - 1] > temp) { arr[j] = arr[j - 1]; j--; } arr[j] = temp; } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; insertionSort(arr); printArr(arr); } } 插入排序的时间复杂度和空间复杂度分析 对于插入的时间复杂度和空间复杂度,通过代码就可以看出跟选择和冒泡来说没什么区别同属于 O(n²) 级别的时间复杂度算法 ,只是遍历方式有原来的 n n-1 n-2 ... 1,变成了 1 2 3 ... n 了。最终得到时间复杂度都是 n(n-1)/2。 对于稳定性来说,插入排序和冒泡一样,并不会改变原有的元素之间的顺序,如果遇见一个与插入元素相等的,那么把待插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序仍是排好序后的顺序,所以插入排序是稳定的。 对于插入排序这里说一个非常重要的一点就是:由于这个算法可以提前终止内层比较( arr[j-1] > arr[j])所以这个排序算法很有用!因此对于一些 NlogN 级别的算法,后边的归并和快速都属于这个级别的,算法来说对于 n 小于一定级别的时候(Array.sort 中使用的是47)都可以用插入算法来优化,另外对于近乎有序的数组来说这个提前终止的方式就显得更加又有优势了。 插入排序总结: 插入排序的算法时间平均复杂度为O(n²)。 插入排序空间复杂度为 O(1)。 插入排序为稳定排序。 插入排序对于近乎有序的数组来说效率更高,插入排序可用来优化高级排序算法 到现在,我们的三种简单排序就告一段落了,下面我们将直接进入 归并排序 和 快速排序 的讲解。这两个算法也是面试上的常客了,所以你准备好了么? 文章参考来源:https://juejin.im/post/5a96d6b15188255efc5f8bbd
昨天在最后给大家留了拓展题,不知道大家有没有思考完成,其实南尘说有巨坑是吓大家的啦,实际上也没什么。我们来继续看看昨天这个拓展题。 面试题:给定单链表的头结点,删除单链表的倒数第 k 个结点。 前面的文章见链接:面试 7:面试常见的链表算法捷径(一) 这个题和前面的文章中增加了一个操作,除了找出来这个结点,我们还要删除它。删除一个结点,想必大家必定也知道:要想操作(添加、删除)单链表的某个结点,那我们还得知道这个节点的前一个节点。所以我们要删除倒数第 k 个结点,就必须要找到倒数第 k+1 个结点。然后把倒数第 k+1 个元素的 next 变量 p.next 指向 p.next.next。 我们找到倒数第 k 个结点的时候,先让 fast 走了 k-1 步,然后再让 slow 变量和 fast 同步走,它们之间就会一直保持 k-1 的距离,所以当 fast 到链表尾结点的时候,slow 刚刚指向的是倒数第 k 个结点。 本题由于我们要知道倒数第 k+1 个结点,所以得让 fast 先走 k 步,待 fast 指向链表尾结点的时候,slow 正好指向倒数第 k+1 个结点。 我们简单思考一下临界值: 当 k = 1 的时候,删除的值是尾结点。我们让 fast 先走 1 步,当 fast.next 为尾结点的时候,倒数第 k+1 个结点正好是我们的倒数第二个结点。我们删除 slow.next,并让slow.next 指向 slow.next.next = null,满足条件。 当 k > len 的时候,我们要找的倒数第 k 个元素不存在,直接出错; 当 1 < k < len 的时候,k 最大为 len-1 的时候,fast 移动 len-1 步,直接到达尾结点,此时,snow 指向头结点。删除倒数第 k 个元素,即删除正数第 2 个结点即可; 当 k = len 的时候比较特殊,当 fast 移动 len 步的时候,已经指向了 fast.next = null,此时我们其实要删除的是头结点,直接返回 head.next 即可。 所以我们自然能得到这样的代码。 public class Test07 { public static class LinkNode { int data; LinkNode next; public LinkNode(int data) { this.data = data; } } private static LinkNode delTheSpecifiedReverse(LinkNode head, int k) { LinkNode slow = head; LinkNode fast = head; if (fast == null) { throw new RuntimeException("your linkNode is null"); } // 先让 fast 先走 k 步 for (int i = 0; i < k; i++) { if (fast == null) { // 说明输入的 k 已经超过了链表长度,直接报错 throw new RuntimeException("the value k is too large."); } fast = fast.next; } // fast == null ,说明已经到了尾结点后面的空区域,说明要删除的就是头结点。 if (fast == null) { return head.next; } while (fast.next != null) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return head; } public static void main(String[] args) { LinkNode head = new LinkNode(1); head.next = new LinkNode(2); head.next.next = new LinkNode(3); head.next.next.next = new LinkNode(4); head.next.next.next.next = new LinkNode(5); LinkNode node = delTheSpecifiedReverse(head, 3); while (node != null) { System.out.print(node.data + "->"); node = node.next; } } } 好了,我们解决了昨天文章中留下的拓展题,今天我们来看看我们链表都还有些怎样的考法。 面试题:定义一个单链表,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。为了方便,我们链表的 data 采用整型。 这是一道反转链表的经典题,我们来屡一下思路:一个结点包含下一结点的引用,反转的意思就是要把原来指向下一结点的引用指向上一个结点。我们可以分为下面的步骤: 找到当前要反转的结点的下一个结点,并用变量保存,因为下一次要反转的是它,如果我们不保存的话一定会因为前面已经反转,导致无法通过遍历得到这个结点; 然后让当前结点的 next 引用指向上一个结点,上一个结点初始 null 因为头结点的反转后变成尾结点; 当前要反转的结点变成下一个要比较元素的上一个结点,用变量保存; 当前要比较的结点赋值为之前保存的未反转前的下一个结点; 当前反转的结点为 null 的时候,保存的上一个结点即反转后的链表头结点。 用代码实现就是: public class Test08 { private static class LinkNode { int data; LinkNode next; LinkNode(int data) { this.data = data; } } private static LinkNode reverseLink(LinkNode head) { // 上一个结点 LinkNode nodePre = null; LinkNode next = null; LinkNode node = head; while (node != null) { // 先用 next 保存下一个要反转的结点,不然会导致链表断裂。 next = node.next; // 再把现在结点的 next 引用指向上一个结点 node.next = nodePre; // 把当前结点赋值给 nodePre 变量,以便于下一次赋值 nodePre = node; // 向后遍历 node = next; } return nodePre; } public static void main(String[] args) { LinkNode head = new LinkNode(1); head.next = new LinkNode(2); head.next.next = new LinkNode(3); head.next.next.next = new LinkNode(4); head.next.next.next.next = new LinkNode(5); LinkNode node = reverseLink(head); while (node != null) { System.out.print(node.data + "->"); node = node.next; } } } 链表可以考的可真多,相信不是小伙伴都和我一样,云里雾里了,那我们今天就讲到这里,后面还要继续考算法,你,打起精神,别睡着了。
面试 7:面试常见的链表类算法捷径 链表是我们数据结构面试中比较容易出错的问题,所以很多面试官总喜欢在这上面下功夫,为了避免出错,我们最好先进行全面的分析。在实际软件开发周期中,设计的时间通常不会比编码的时间短,在面试的时候我们不要着急于写代码,而是一开始仔细分析和设计,这将给面试官留下一个很好的印象。 与其很快写出一段千疮百孔的代码,不容仔细分析后再写出健壮性无敌的程序。 面试题:输入一个单链表的头结点,返回它的中间元素。为了方便,元素值用整型表示。 当应聘者看到这道题的时候,内心一阵狂喜,怎么给自己遇到了这么简单的题。拿起笔就开始写,先遍历整个链表,拿到链表的长度 len,再次遍历链表,位于 len/2 的元素就是链表的中间元素。 所以这个题最重要的点就是拿到链表的长度 len。而拿到这个 len 也比较简单,只需要遍历前设定一个 count 值,遍历的时候 count++ ,第一次遍历结束,就拿到单链表的长度 len 了。 于是我们很快写出了这样的代码: public class Test15 { public static class LinkNode { int data; LinkNode next; public LinkNode(int data) { this.data = data; } } private static int getTheMid(LinkNode head) { int count = 0; LinkNode node = head; while (head != null) { head = head.next; count++; } for (int i = 0; i < count / 2; i++) { node = node.next; } return node.data; } public static void main(String[] args) { LinkNode head = new LinkNode(1); head.next = new LinkNode(2); head.next.next = new LinkNode(3); head.next.next.next = new LinkNode(4); head.next.next.next.next = new LinkNode(5); System.out.println(getTheMid(head)); } } 面试官看到这个代码的时候,他告诉我们上面代码循环了两次,但是他期待的只有一次。 于是我们绞尽脑汁,突然想到了网上介绍过的一个概念:快慢指针法。 假设我们设置两个变量 slow、fast 起始都指向单链表的头结点当中,然后依次向后面移动,fast 的移动速度是 slow 的 2 倍。这样当 fast 指向末尾节点的时候,slow 就正好在正中间了。 想清楚这个思路后,我们很快就能写出如下代码: public class Test15 { public static class LinkNode { int data; LinkNode next; public LinkNode(int data) { this.data = data; } } private static int getTheMid(LinkNode head) { LinkNode slow = head; LinkNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } return slow.data; } public static void main(String[] args) { LinkNode head = new LinkNode(1); head.next = new LinkNode(2); head.next.next = new LinkNode(3); head.next.next.next = new LinkNode(4); head.next.next.next.next = new LinkNode(5); System.out.println(getTheMid(head)); } } 快慢指针法举一反三 快慢指针法 确实在链表类面试题中特别好用,我们不妨在这里举一反三,对原题稍微修改一下,其实也可以实现。 面试题:给定一个单链表的头结点,判断这个链表是否是循环链表。 和前面的问题一样,我们只需要定义两个变量 slow,fast,同时从链表的头结点出发,fast 每次走链表,而 slow 每次只走一步。如果走得快的指针追上了走得慢的指针,那么链表就是环形(循环)链表。如果走得快的指针走到了链表的末尾(fast.next 指向 null)都没有追上走得慢的指针,那么链表就不是环形链表。 有了这样的思路,实现代码那还不是分分钟的事儿。 public class Test15 { public static class LinkNode { int data; LinkNode next; public LinkNode(int data) { this.data = data; } } private static boolean isRingLink(LinkNode head) { LinkNode slow = head; LinkNode fast = head; while (slow != null && fast != null && fast.next != null) { if (slow == fast || fast.next = slow) { return true; } fast = fast.next.next; slow = slow.next; } return false; } public static void main(String[] args) { LinkNode head = new LinkNode(1); head.next = new LinkNode(2); head.next.next = new LinkNode(3); head.next.next.next = new LinkNode(4); head.next.next.next.next = new LinkNode(5); System.out.println(isRingLink(head)); head.next.next.next.next.next = head; System.out.println(isRingLink(head)); } } 确实有意思,快慢指针法 再一次利用它的优势巧妙解决了我们的问题。 快慢指针法的延展 我们上面讲解的「快慢指针法」均是一个变量走 1 步,一个变量走 n 步。我们其实还可以拓展它。这个「快慢」并不是说一定要同时遍历。 比如《剑指Offer》中的第 15 道面试题,就运用到了「快慢指针法」的延展。 面试题:输入一个单链表的头结点,输出该链表中倒数第 k 个节点的值。 初一看这个似乎并不像我们前面学习到的「快慢指针法」的考察。所以大多数人就迷糊了,进入到常规化思考。依然还是设置一个整型变量 count,然后每次循环的时候 count++,拿到链表的长度 n。那么倒数第 k 个节点也就是顺数第 n-k+1 个结点。所以我们只需要在拿到长度 n 后再进行一次 n-k+1 次循环就可以拿到这个倒数第 k 个节点的值了。 但面试官显然不会太满意这个臃肿的解法,他依然希望我们一次循环就能搞定这个事。 为了实现只遍历一次链表就能找到倒数第 k 个结点,我们依然可以定义两个遍历 slow 和 fast。我们让 fast 变量先往前遍历 k-1 步,slow 保持不动。从第 k 步开始,slow 变量也跟着 fast 变量从链表的头结点开始遍历。由于两个变量指向的结点距离始终保持在 k-1,那么当 fast 变量到达链表的尾结点的时候,slow 变量指向的结点正好是我们所需要的倒数第 k 个结点。 我们依然可以在心中默认一遍代码: 假设输入的链表是:1->2->3->4->5; 现在我们要求倒数第三个结点的值,即顺数第 3 个结点,它的值为 3; 定义两个变量 slow、fast,它们均指向结点 1; 先让 fast 向前走 k-1 即 2 步,这时候 fast 指向了第 3 个结点,它的值是 3; 现在 fast 和 slow 同步向右移动; fast 再经过了 2 步到达了链表尾结点;fast 正好指向了第 3 个结点,这显然是符合我们的猜想的。 在心中默走了一遍代码后,我们显然很容易写出下面的代码。 public class Test15 { public static class LinkNode { int data; LinkNode next; public LinkNode(int data) { this.data = data; } } private static int getSpecifiedNodeReverse(LinkNode head, int k) { LinkNode slow = head; LinkNode fast = head; if (fast == null) { throw new RuntimeException("your linkNode is null"); } // 先让 fast 先走 k-1 步 for (int i = 0; i < k - 1; i++) { if (fast.next == null) { // 说明输入的 k 已经超过了链表长度,直接报错 throw new RuntimeException("the value k is too large."); } fast = fast.next; } while (fast.next != null) { slow = slow.next; fast = fast.next; } return slow.data; } public static void main(String[] args) { LinkNode head = new LinkNode(1); head.next = new LinkNode(2); head.next.next = new LinkNode(3); head.next.next.next = new LinkNode(4); head.next.next.next.next = new LinkNode(5); System.out.println(getSpecifiedNodeReverse(head, 3)); System.out.println(getSpecifiedNodeReverse(null, 1)); } } 总结 链表类面试题,真是可以玩出五花八门,当我们用一个变量遍历链表不能解决问题的时候,我们可以尝试用两个变量来遍历链表,可以让其中一个变量遍历的速度快一些,比如一次走两步,或者是走若干步。我们在遇到这类面试的时候,千万不要自乱阵脚,学会理性分析问题。 原本是想给我的小伙伴说再见了,但唯恐大家还没学到真本事,所以在这里再留一个拓展题。 面试题:给定一个单链表的头结点,删除倒数第 k 个结点。 哈哈,和上面的题目仅仅只是把获得它的值变成了删除,不少小伙伴肯定都偷着乐了,但南尘还是先提醒大家,不要太得意忘形哟~ 好啦,咱们明天再见啦~
今天给大家带来的是 《剑指 Offer》习题:调整数组顺序使奇数位于偶数前面,纯 Java 实现希望大家多加思考。 面试题:输入一个整型数组,实现一个函数来调整该数组中的数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分,希望时间复杂度尽量小。 看到这道题,想必大多数人都是能一下就想到从头到尾扫描一遍数组,然后遇到奇数就移动到最前面,遇到偶数就移动到最后面的思路,于是便有了下面的代码。 注:《剑指 Offer》上面的 「遇到奇数移动到最前面,遇到偶数也移动到最后面」其实只需要做其中一种即可。 public class Test14 { private static int[] reOrderArray(int[] arr) { for (int i = 0; i < arr.length; i++) { // 遇到奇数就放到最前面 if (Math.abs(arr[i]) % 2 == 1) { int temp = arr[i]; // 先把 i 前面的都向后移动一个位置 for (int j = i; j > 0; j--) { arr[j] = arr[j - 1]; } arr[0] = temp; } } return arr; } public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; arr = reOrderArray(arr); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); int[] arr1 = {2, 4, 6, 8, 1, 3, 5, 7, 9}; arr1 = reOrderArray(arr1); for (int i = 0; i < arr1.length; i++) { System.out.print(arr1[i] + " "); } System.out.println(); int[] arr2 = {2, 4, 6, 8, 10}; arr2 = reOrderArray(arr2); for (int i = 0; i < arr2.length; i++) { System.out.print(arr2[i] + " "); } } } 上面的代码固然能达到功能,但时间复杂度上完全不能恭维。每找到一个奇数,我们总是要去移动不少个位置的数。 等等。 我们上面算法最大的问题在于移动,我们能否不做这个移动呢? 当然是可以的。题目要求所有奇数都应该在偶数前面,所以我们应该只需要维护两个下标值,让一个下标值从前往后遍历,另外一个下标值从后往前遍历,当发现第一个下标值对应到偶数,第二个下标值对应到奇数的时候,我们就直接对调两个值。直到第一个下标到了第二个下标的后面的时候退出循环。 我们有了这样的想法,可以先拿一个例子在心中走一遍,如果没有问题再写代码,这样也可以让面试官知道,我们并不是那种上来就开始写代码不考虑全面的程序员。 假定输入的数组是 {1,2,3,4,5}; 设定 odd = 0,代表第一个下标;even = arr.length = 4; 从前往后移动第一个下标 odd,直到它等于偶数,即当 odd = 1 的时候,我们停止移动; 再从后往前移动下标 even,直到它等于奇数,即当 even = 4 的时候,我们停止移动; 满足 arr[odd] 为偶数,arr[even] 为奇数,我们对调两个值,得到新数组 {1,5,3,4,2}; 继续循环,此时 odd = 3,even = 2,不满足 odd < even 的条件,退出循环,得到的数组符合条件; 心中默走一遍没问题后,开始手写代码: public class Test14 { private static int[] reOrderArray(int[] arr) { int odd = 0, even = arr.length - 1; // 循环结束条件为 odd >= even while (odd < even) { // 第一个下标为偶数的时候停止 while (odd < even && Math.abs(arr[odd]) % 2 != 0) { odd++; } // 第二个下标为奇数的时候停止 while (odd < even && Math.abs(arr[even]) % 2 == 0) { even--; } // 找到后对调两个值 int temp = arr[odd]; arr[odd] = arr[even]; arr[even] = temp; } return arr; } public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; arr = reOrderArray(arr); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); int[] arr1 = {2, 4, 6, 8, 1, 3, 5, 7, 9}; arr1 = reOrderArray(arr1); for (int i = 0; i < arr1.length; i++) { System.out.print(arr1[i] + " "); } System.out.println(); int[] arr2 = {2, 4, 6, 8, 10}; arr2 = reOrderArray(arr2); for (int i = 0; i < arr2.length; i++) { System.out.print(arr2[i] + " "); } System.out.println(); } } 扩展性更好的代码,能秒杀 Offer 如果是面试应届毕业生或者工作时间不长的程序员,面试官可能会满意前面的代码,但如果应聘者申请的是资深 的开发岗位,那面试官可能会接着问几个问题。 面试官:如果把题目改成把数组中的数组按照大小分为两部分,所有的负数都在非负整数的前面,该怎么做? 应聘者:这很简单,可以重新定义一个函数,在新的函数里,只要修改第二个和第三个 while 循环里面的判断条件就好了。 面试官:如果再把题目改改,变成把数组中的数分为两部分,能被 3 整除的数都在不能被 3 整除的数的前面,怎么办? 应聘者:我们还是可以定义一个新的函数,在这个函数中...... 面试官:(打断应聘者的话),难道就没有更好的方法? 这个时候应聘者应该要反应过来,面试官期待我们能提供的不仅仅是解决一个问题的办法,而是解决一系列同类型问题的通用方法。我们在做解法的时候不能只想着解决当前的问题就好。在《大话设计模式》中,讲解了一个非常有意思的事情就是大鸟让小菜做商场促销活动的时候,各种改变需求,把小菜绕的云里雾里。 《大话设计模式》PDF 版本可以在公众号后台回复「大话设计模式」即可获取。 是呀,哪有不变的需求,需求不变,我们哪来那么多活干呀?不过要是,我们事先就做了这样的准备,省下来的时间那不是正好又可以去玩一盘吃鸡洛? 回到面试官新提出的两个问题来,我们其实新的函数都只需要更改第二个和第三个 while 循环里面的判断条件,而其它都是不需要动的。 public class Test14 { interface ICheck { boolean function(int n); } public static class OrderEven implements ICheck { @Override public boolean function(int n) { return n % 2 == 0; } } private static int[] reOrderArray(int[] arr, ICheck iCheck) { int odd = 0, even = arr.length - 1; // 循环结束条件为 odd >= even while (odd < even) { // 第一个下标为偶数的时候停止 while (odd < even && !iCheck.function(arr[odd])) { odd++; } // 第二个下标为奇数的时候停止 while (odd < even && iCheck.function(arr[even])) { even--; } // 找到后对调两个值 int temp = arr[odd]; arr[odd] = arr[even]; arr[even] = temp; } return arr; } public static void main(String[] args) { OrderEven even = new OrderEven(); int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; arr = reOrderArray(arr,even); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); int[] arr1 = {2, 4, 6, 8, 1, 3, 5, 7, 9}; arr1 = reOrderArray(arr1,even); for (int i = 0; i < arr1.length; i++) { System.out.print(arr1[i] + " "); } System.out.println(); int[] arr2 = {2, 4, 6, 8, 10}; arr2 = reOrderArray(arr2,even); for (int i = 0; i < arr2.length; i++) { System.out.print(arr2[i] + " "); } System.out.println(); } } 写这玩意儿的时候,我内心是拒绝的,由于 Java 没有 Python 一样方便的函数指针,我想了想只想到了用接口方式来处理。要是有其他实现方式的希望大家能在评论区留言~ 好了,今天的面试讲解,就先到这儿吧。
我们在处理一道编程面试题的时候,通常除了注意代码规范以外,千万要记得自己心中模拟一个单元测试。主要通过三方面来处理。 功能性测试 边界值测试 负面性测试 不管如何,一定要保证自己代码考虑的全面,而不要简单地猜想用户的输入一定是正确的,只是去实现功能。通常你编写一个能接受住考验的代码,会让面试官对你刮目相看,你可以不厉害,但已经充分说明了你的靠谱。 今天我们的面试题目是: 面试题:尝试实现 Java 的 Math.pow(double base,int exponent) 函数算法,计算 base 的 exponent 次方,不得使用库函数,同时不需要考虑大数问题。 面试题来源于《剑指 Offer》第 11 题,数字的整数次方。 不要介意 Java 真正的方法是 Math.pow(double var1,double var2)。 由于不需要考虑大数问题,不少小伙伴心中暗自窃喜,这题目也太简单了,给我撞上了,运气真好,于是直接写出下面的代码: public class Test11 { private static double power(double base, int exponent) { double result = 1.0; for (int i = 0; i < exponent; i++) { result *= base; } return result; } public static void main(String[] args) { System.out.println(power(2, 2)); System.out.println(power(2, 4)); System.out.println(power(3, 1)); System.out.println(power(3, 0)); } } 写的快自然是好事,如果正确的话会被面试官认为是思维敏捷。但如果考虑不周的话,恐怕就极容易被面试官认为是不靠谱的人了。在技术能力和靠谱度之间,大多数面试官更青睐于靠谱度。 我们上面确实做到了功能测试,但面试官可能会直接提示我们,假设我们的 exponent 输入一个负值,能得到正确值么? 跟着自己的代码走一遍,终于意识到了这个问题,当 exponent 为负数的时候,循环根本就进不去,无论输入的负数是什么,都会返回 1.0,这显然是不正确的算法。 我们在数学中学过,给一个数值上负数次方,相当于给这个数值上整数次方再求倒数。 意识到这点,我们修正一下代码。 public class Test11 { private static double power(double base, int exponent) { // 因为除了 0 以外,任何数值的 0 次方都为 1,所以我们默认为 1.0; // 0 的 0 次方,在数学书是没有意义的,为了贴切,我们也默认为 1.0 double result = 1.0; // 处理负数次方情况 boolean isNegetive = false; if (exponent < 0) { isNegetive = true; exponent = -exponent; } for (int i = 0; i < exponent; i++) { result *= base; } if (isNegetive) return 1 / result; return result; } public static void main(String[] args) { System.out.println(power(2, 2)); System.out.println(power(2, 4)); System.out.println(power(3, 1)); System.out.println(power(3, -1)); } } 我们在代码中增加了一个判断是否为负数的 isNegetive 变量,当为负数的时候,我们就置为 true,并计算它的绝对值次幂,最后返回结果的时候返回它的倒数。 面试官看到这样的代码,可能就有点按捺不住内心的怒火了,不过由于你此前一直面试回答的较好,也打算再给你点机会,面试官提示你,当 base 传入 0,exponent 传入负数,会怎样? 瞬间发现了自己的问题,这不是犯了数学最常见的问题,给 0 求倒数么? 虽然 Java 的 Math.pow() 方法也存在这个问题,但我们这里忽略不计。 于是马上更新代码。 public class Test11 { private static double power(double base, int exponent) { // 因为除了 0 以外,任何数值的 0 次方都为 1,所以我们默认为 1.0; // 0 的 0 次方,在数学书是没有意义的,为了贴切,我们也默认为 1.0 double result = 1.0; // 处理底数为 0 的情况,底数为 0 其他任意次方结果都应该是 0 if (base == 0) return 0.0; // 处理负数次方情况 boolean isNegetive = false; if (exponent < 0) { isNegetive = true; exponent = -exponent; } for (int i = 0; i < exponent; i++) { result *= base; } if (isNegetive) return 1 / result; return result; } public static void main(String[] args) { System.out.println(power(2, 2)); System.out.println(power(2, 4)); System.out.println(power(3, 1)); System.out.println(power(0, -1)); } } 有了上一次的经验,这次并不敢直接上交代码了,而是认真检查边界值和各种情况。检查 1 遍,2 遍,均没有发现问题,提交代码。 计算机表示小数均有误差,这个在 Python 中尤其严重,但经数次测试,《剑指 Offer》中讲的双精度误差问题似乎在 Java 的 == 运算符中并不存在。如有问题,欢迎指正。 上面的代码基本还算整,健壮性也还不错,但面试官可能还想问问有没有更加优秀的算法。 仔细查看,确实似乎是有办法优化的,比如我们要求 power(2,16) 的值,我们只需要先求出 2 的 8 次方,再平方就可以了;以此类推,我们计算 2 的 8 次方的时候,可以先计算 2 的 4 次方,然后再做平方运算.....妙哉妙哉! 需要注意的是,如果我们的幂数为奇数的话,我们需要在最后再乘一次我们的底数。 我们尝试修改代码如下: public class Test11 { private static double power(double base, int exponent) { // 因为除了 0 以外,任何数值的 0 次方都为 1,所以我们默认为 1.0; // 0 的 0 次方,在数学书是没有意义的,为了贴切,我们也默认为 1.0 double result = 1.0; // 处理底数为 0 的情况,底数为 0 其他任意次方结果都应该是 0 if (base == 0) return 0.0; // 处理负数次方情况 boolean isNegetive = false; if (exponent < 0) { isNegetive = true; exponent = -exponent; } result = getTheResult(base, exponent); if (isNegetive) return 1 / result; return result; } private static double getTheResult(double base, int exponent) { // 如果指数为0,返回1 if (exponent == 0) { return 1; } // 指数为1,返回底数 if (exponent == 1) { return base; } // 递归求一半的值 double result = getTheResult(base, exponent >> 1); // 求最终值,如果是奇数,还要乘一次底数 result *= result; if ((exponent & 0x1) == 1) { result *= base; } return result; } public static void main(String[] args) { System.out.println(power(2, 2)); System.out.println(power(2, 4)); System.out.println(power(3, -1)); System.out.println(power(0.1, 2)); } } 完美解决。 在提交代码的时候,还可以主动提示面试官,我们在上面用右移运算符代替了除以 2,用位与运算符代替了求余运算符 % 来判断是一个奇数还是一个偶数。让他知道我们对编程的细节真的很重视,这大概也就是细节决定成败吧。一两个细节的打动说不定就让面试官下定决心给我们发放 Offer 了。 位运算的效率比乘除法及求余运算的效率要高的多。 因为移位指令占 2 个机器周期,而乘除法指令占 4 个机器周期。从硬件上看,移位对硬件更容易实现,所以我们更优先用移位。 好了,今天我们的面试精讲就到这里,我们明天再见!
在搞「模拟面试」的日子,我发现大家普遍有个问题就是,感觉自己的能力总是到了瓶颈期,写了好几年代码,感觉只是会的框架比以前多了而已。去大公司面试,屡战屡败,问失败原因,大多数人的答案都是,在三面数据结构与算法的时候,直接就挂了。 而不少人表示,我数据结构与算法潜心修炼,把书都啃烂了,倒背如流,但每次一面试,咋就是不会呢? 归根结底,还是思维训练的问题,很多人知其然而不知其所以然,所以,南尘就尽量地贴近大家的常态化思维去帮助大家训练算法吧。 昨天已经给大家预告了,不知道小伙伴们下来有没有去自己尝试处理。但不管怎样,要想训练好算法,但听别人讲不去思考,是肯定没用的。好了废话不多说,进入正题! 来到今天的面试题 面试题:一直青蛙一次可以跳上 1 级台阶,也可以跳上 2 级,求该青蛙跳上 n 级的台阶总共有多少种跳法。 题目来源于《剑指 Offer》 一看这道题,好像没啥思路,感觉和我们的数据结构和常用的算法好像一点都不沾边。 但这看起来就像一道数学题,而且似乎就是高考数学的倒数第一题,所以我们就用数学来做吧。 数学中有个方法叫「数学归纳法」,我们这里就可以巧妙用到。 当 n = 1 时,青蛙有 1 种跳法; 当 n = 2 时,青蛙可以选择一次跳 1 级,跳两次;也可以选择一次跳 2 级;青蛙有 2 种跳法; 当 n = 3 时,青蛙可以选择 1-1-1,1-2,2-1,青蛙有 3 种跳法; 当 n = 4 时,青蛙可以选择 1-1-1-1,1-1-2,1-2-1,2-1-1,2-2,青蛙有 5 种跳法; 似乎能得到 f(3) = f(2) + f(1),f(4) = f(3) + f(2),这是 f(n) = f(n-1) + f(n-2) 的节奏?我们得用 n = 5 验证一下。 当 n = 5 时,青蛙可以选择 1-1-1-1-1,1-1-1-2,1-1-2-1,1-2-1-1,2-1-1-1,1-2-2,2-1-2,2-2-1,青蛙有 8 种跳法,f(5) = f(4) + f(3) 成立。 这是最笨的方法了,得出了这确实就是一个典型的斐波那契数列,唯一不一样的地方就是 n =2 的时候并没有 f(2) = f(0) + f(1)。 稍微有点思维能力的可能更简单。 n = 1 ,青蛙有 1 种跳法; n = 2 ,青蛙有 2 种跳法; n = 3,青蛙在第 1 级可以跳 1 种,后面 2 级相当于 f(3-1) = f(2),还有一种就是先跳 2 级,然后后面 1 级有 f(3-2) = f(1) 种跳法,可以得出 f(3) = f(2) + f(1); ... 当取 n 时,青蛙在第一次跳 1 级,后面的相当于有 f(n-1) 种跳法;假设第一次跳 2 级,后面相当于有 f(n-2) 种跳法;故可以得出 f(n) = f(n-1) + f(n-2); 这样思考可能更不容易出错吧,这就是思维的提炼过程,可见我们高考常考的「数学归纳法」是多么地有用。 既然能分析出这是一道典型的斐波那契数列了,我想教科书都教给大家方法了,不过一定要注意 n = 2 的时候,正常的斐波那契数列值应该是 1,而我们是 2。大多数人肯定会写出下面的代码: public class Test09 { private static int fn(int n) { if (n <= 0) return 0; if (n == 1) return 1; if (n == 2) return 2; else return fn(n - 1) + fn(n - 2); } public static void main(String[] args) { System.out.println(fn(1)); System.out.println(fn(2)); System.out.println(fn(3)); System.out.println(fn(4)); } } 我们教科书上反复用这个问题来讲解递归函数,但并不能说明递归的解法是最适合这个题目的。当我们暗自窃喜完成了这道面试题的时候,或许面试官会告诉我们,上面的这种递归解法存在很严重的效率问题,并让我们分析其中的原因。 我们以求 fn(10) 为例,要想求得 fn(10),需要先求得 fn(9) 和 fn(8);同样,要求得 fn(9),需要先求得 fn(8) 和 fn(7)...... 这存在一个很大的问题,我们一定会去重复计算很多值,我们一定得想办法把这个计算好的值存放起来。 避免重复计算 既然我们找到了问题所在,那改进方法自然是信手拈来了。我们目前的算法是「从大到小」计算,而我们只需要反向「从小到大」计算就可以了。我们根据 fn(1) 和 fn(2) 计算出 fn(3),再根据 fn(2) 和 fn(3) 计算出 fn(4)...... 很容易理解,这样的算法思路时间复杂度是 O(n),实现代码如下: public class Test09 { private static long fn(int n) { if (n <= 0) return 0; if (n == 1) return 1; if (n == 2) return 2; long prePre = 1, pre = 2; long result = 0; for (int i = 3; i <= n; i++) { result = prePre + pre; prePre = pre; pre = result; } return result; } public static void main(String[] args) { System.out.println(fn(1)); System.out.println(fn(3)); System.out.println(fn(50)); System.out.println(fn(100)); } } 上面的代码,一定要注意做了一点小修改,我们把返回值悄悄地改成了 long ,因为我们并不能保证客户端是否会输入一个比较大的数字,比如:100,这样,如果返回值为 int,一定会因为超出了最大值而显示错误的,解决方案就是把值换为更大容量的 long。但有时候你会发现,long 的容量也不够,毕竟整型和长整型,它都会有最大显示值,在遇到这样的情况的时候。我们最好和面试官交流一下,是否处理这样的情况。如果一定要处理这样的情况,那么可能你就得用 String 来做显示处理了。 其实在《剑指 Offer》上还有时间复杂度为 O(logn) 的解法,但因为不够实用,我们这里也就不讲解了,主要还是我们解题的算法思路训练。如果真的很感兴趣的话,那就请移步《剑指 Offer》吧。反正你在公众号后台回复「剑指Offer」就可以拿到 PDF 版本的。 总结 今天的面试讲解就到这吧,大家一定要学会自己去独立思考,训练自己的思维。简单回顾一下我们本周所学习的内容,我们下周再见!
在算法面试中,面试官总是喜欢围绕链表、排序、二叉树、二分查找来做文章,而大多数人都可以跟着专业的书籍来做到倒背如流。而面试官并不希望招收的是一位记忆功底很好,但不会活学活用的程序员。所以学会数学建模和分析问题,并用合理的算法或数据结构来解决问题相当重要。 面试题:打印出旋转数组的最小数字 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3,4,5,1,2} 为数组 {1,2,3,4,5} 的一个旋转,该数组的最小值为 1。 要想实现这个需求很简单,我们只需要遍历一遍数组,找到最小的值后直接退出循环。代码实现如下: public class Test08 { public static int getTheMin(int nums[]) { if (nums == null || nums.length == 0) { throw new RuntimeException("input error!"); } int result = nums[0]; for (int i = 0; i < nums.length - 1; i++) { if (nums[i + 1] < nums[i]) { result = nums[i + 1]; break; } } return result; } public static void main(String[] args) { // 典型输入,单调升序的数组的一个旋转 int[] array1 = {3, 4, 5, 1, 2}; System.out.println(getTheMin(array1)); // 有重复数字,并且重复的数字刚好的最小的数字 int[] array2 = {3, 4, 5, 1, 1, 2}; System.out.println(getTheMin(array2)); // 有重复数字,但重复的数字不是第一个数字和最后一个数字 int[] array3 = {3, 4, 5, 1, 2, 2}; System.out.println(getTheMin(array3)); // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 int[] array4 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array4)); // 单调升序数组,旋转0个元素,也就是单调升序数组本身 int[] array5 = {1, 2, 3, 4, 5}; System.out.println(getTheMin(array5)); // 数组中只有一个数字 int[] array6 = {2}; System.out.println(getTheMin(array6)); // 数组中数字都相同 int[] array7 = {1, 1, 1, 1, 1, 1, 1}; System.out.println(getTheMin(array7)); } } 打印结果没什么毛病。不过这样的方法显然不是最优的,我们看看有没有办法找出更加优质的方法处理。 有序,还要查找? 找到这两个关键字,我们不免会想到我们的二分查找法,但不少小伙伴肯定会问,我们这个数组旋转后已经不是一个真正的有序数组了,不过倒像是两个递增的数组组合而成的,我们可以这样思考。 我们可以设定两个下标 low 和 high,并设定 mid = (low + high)/2,我们自然就可以找到数组中间的元素 array[mid],如果中间的元素位于前面的递增数组,那么它应该大于或者等于 low 下标对应的元素,此时数组中最小的元素应该位于该元素的后面,我们可以把 low 下标指向该中间元素,这样可以缩小查找的范围。 同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于 high 下标对应的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们就可以把 high 下标更新到中位数的下标,这样也可以缩小查找的范围,移动之后的 high 下标对应的元素仍然在后面的递增子数组中。 不管是更新 low 还是 high,我们的查找范围都会缩小为原来的一半,接下来我们再用更新的下标去重复新一轮的查找。直到最后两个下标相邻,也就是我们的循环结束条件。 说了一堆,似乎已经绕的云里雾里了,我们不妨就拿题干中的这个输入来模拟验证一下我们的算法。 input:{3,4,5,1,2} 此时 low = 0,high = 4,mid = 2,对应的值分别是:num[low] = 3,num[high] = 2,num[mid] = 5 由于 num[mid] > num[low],所以 num[mid] 应该是在左边的递增子数组中。 更新 low = mid = 2,num[low] = 5,mid = (low+high)/2 = 3,num[mid] = 1; high - low ≠ 1 ,继续更新 由于 num[mid] < num[high],所以断定 num[mid] = 1 位于右边的自增子数组中; 更新 high = mid = 3,由于 high - mid = 1,所以结束循环,得到最小值 num[high] = 1; 我们再来看看 Java 中如何用代码实现这个思路: public class Test08 { public static int getTheMin(int nums[]) { if (nums == null || nums.length == 0) { throw new RuntimeException("input error!"); } // 如果只有一个元素,直接返回 if (nums.length == 1) return nums[0]; int result = nums[0]; int low = 0, high = nums.length - 1; int mid; // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组 while (nums[low] >= nums[high]) { // 确保循环结束条件 if (high - low == 1) { return nums[high]; } // 取中间位置 mid = (low + high) / 2; // 代表中间元素在左边递增子数组 if (nums[mid] >= nums[low]) { low = mid; } else { high = mid; } } return result; } public static void main(String[] args) { // 典型输入,单调升序的数组的一个旋转 int[] array1 = {3, 4, 5, 1, 2}; System.out.println(getTheMin(array1)); // 有重复数字,并且重复的数字刚好的最小的数字 int[] array2 = {3, 4, 5, 1, 1, 2}; System.out.println(getTheMin(array2)); // 有重复数字,但重复的数字不是第一个数字和最后一个数字 int[] array3 = {3, 4, 5, 1, 2, 2}; System.out.println(getTheMin(array3)); // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 int[] array4 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array4)); // 单调升序数组,旋转0个元素,也就是单调升序数组本身 int[] array5 = {1, 2, 3, 4, 5}; System.out.println(getTheMin(array5)); // 数组中只有一个数字 int[] array6 = {2}; System.out.println(getTheMin(array6)); // 数组中数字都相同 int[] array7 = {1, 1, 1, 1, 1, 1, 1}; System.out.println(getTheMin(array7)); // 特殊的不知道如何移动 int[] array8 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array8)); } } 前面我们提到在旋转数组中,由于是把递增排序数组的前面的若干个数字搬到数组后面,因为第一个数字总是大于或者等于最后一个数字,而还有一种特殊情况是移动了 0 个元素,即数组本身,也是它自己的旋转数组。这种情况本身数组就是有序的了,所以我们只需要返回第一个元素就好了,这也是为什么我先给 result 赋值为 nums[0] 的原因。 上述代码就完美了吗?我们通过测试用例并没有达到我们的要求,我们具体看看 array8 这个输入。先模拟计算机运行分析一下: low = 0, high = 4, mid = 2, nums[low] = 1, nums[high] = 1,nums[mid] = 1; 由于 nums[mid] >= nums[low],故认定 nums[mid] = 1 在左边递增子数组中; 所以更新 high = mid = 2,mid = (low+high)/2 = 1; nums[low] = 1,nums[mid] = 1,nums[high] = 1; high - low ≠ 1,继续循环; 由于 nums[mid] >= nums[low],故认定 nums[mid] = 1 在左边递增子数组中; 所以更新 high = mid = 1,由于 high - low = 1,故退出循环,得到 result = 1; 但我们一眼了然,明显我们的最小值不是 1 ,而是 0 ,所以当 array[low]、array[mid]、array[high] 相等的时候,我们的程序并不知道应该如何移动,按照目前的移动方式就默认 array[mid] 在左边递增子数组了,这显然是不负责任的做法。 我们修正一下代码: public class Test08 { public static int getTheMin(int nums[]) { if (nums == null || nums.length == 0) { throw new RuntimeException("input error!"); } // 如果只有一个元素,直接返回 if (nums.length == 1) return nums[0]; int result = nums[0]; int low = 0, high = nums.length - 1; int mid = low; // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组 while (nums[low] >= nums[high]) { // 确保循环结束条件 if (high - low == 1) { return nums[high]; } // 取中间位置 mid = (low + high) / 2; // 三值相等的特殊情况,则需要从头到尾查找最小的值 if (nums[mid] == nums[low] && nums[mid] == nums[high]) { return midInorder(nums, low, high); } // 代表中间元素在左边递增子数组 if (nums[mid] >= nums[low]) { low = mid; } else { high = mid; } } return result; } /** * 查找数组中的最小值 * * @param nums 数组 * @param start 数组开始位置 * @param end 数组结束位置 * @return 找到的最小的数字 */ public static int midInorder(int[] nums, int start, int end) { int result = nums[start]; for (int i = start + 1; i <= end; i++) { if (result > nums[i]) result = nums[i]; } return result; } public static void main(String[] args) { // 典型输入,单调升序的数组的一个旋转 int[] array1 = {3, 4, 5, 1, 2}; System.out.println(getTheMin(array1)); // 有重复数字,并且重复的数字刚好的最小的数字 int[] array2 = {3, 4, 5, 1, 1, 2}; System.out.println(getTheMin(array2)); // 有重复数字,但重复的数字不是第一个数字和最后一个数字 int[] array3 = {3, 4, 5, 1, 2, 2}; System.out.println(getTheMin(array3)); // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 int[] array4 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array4)); // 单调升序数组,旋转0个元素,也就是单调升序数组本身 int[] array5 = {1, 2, 3, 4, 5}; System.out.println(getTheMin(array5)); // 数组中只有一个数字 int[] array6 = {2}; System.out.println(getTheMin(array6)); // 数组中数字都相同 int[] array7 = {1, 1, 1, 1, 1, 1, 1}; System.out.println(getTheMin(array7)); // 特殊的不知道如何移动 int[] array8 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array8)); } } 我们再用完善的测试用例放进去,测试通过。 总结 本题其实考察的点挺多的,实际上就是考察对二分查找的灵活运用,不少小伙伴死记硬背二分查找必须遵从有序,而没有学会这个二分查找的思想,这样会导致只能想到循环查找最小值了。 不少小伙伴在面试中表态,Android 原生态基本都封装了常用算法,对面试这些无作用的算法表示抗议,其实这是相当愚蠢的。我们不求死记硬背算法的实现,但求学习到其中巧妙的思想。只有不断地提升自己的思维能力,才能助自己收获更好的职业发展。 这也大概是大家一直到处叫大佬,埋怨自己工资总是跟不上别人的一方面原因吧。
面试:用 Java 逆序打印链表 昨天的 Java 实现单例模式 中,我们的双重检验锁机制因为指令重排序问题而引入了 volatile 关键字,不少朋友问我,到底为啥要加 volatile 这个关键字呀,而它,到底又有什么神奇的作用呢? 对 volatile 这个关键字,在昨天的讲解中我们简单说了一下:被 volatile 修饰的共享变量,都会具有下面两个属性: 保证不同线程对该变量操作的内存可见性。 禁止指令重排序。 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。 可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到。 对于重排序,不熟悉的建议直接 Google 一下,这里也就不多提了。只需要记住,在多线程中操作一个共享变量的时候,一定要记住加上 volatile 修饰即可。 由于时间关系,我们还是得先进入今天的正题,对于 volatile 关键字,在要求并发编程能力的面试中还是很容易考察到的,后面我也会简单给大家讲解。 输入一个单链表的头结点,从尾到头打印出每个结点的值。 这是《剑指 Offer》上的第五道面试题,链表是经常在面试中考察的一种数据结构,所以推荐大家一定要掌握。对于链表不熟悉的小伙伴可一定要去《大话数据结构》好好补课哟~ 《剑指 Offer》 PDF 版本在公众号后台回复「剑指Offer」即可获取。 《大话数据结构》PDF 版本在公众号后台回复「大话数据结构」即可获取。 我们的链表有很多,单链表,双向链表,环链表等。这里是最普通的单链表模式,我们一般会在数据存储区域存放数据,然后有一个指针指向下一个结点。虽然 Java 中没有指针这个概念,但 Java 的引用恰如其分的填补了这个问题。 看到这道题,我们往往会很快反应到每个结点都有 next 属性,所以要从头到尾输出很简单。于是我们自然而然就会想到先用一个 while 循环取出所有的结点存放到数组中,然后再通过逆序遍历这个数组,即可实现逆序打印单链表的结点值。 我们假定结点的数据为 int 型的。实现代码如下: public class Test05 { public static class Node { int data; Node next; } public static void printLinkReverse(Node head) { ArrayList<Node> nodes = new ArrayList<>(); while (head != null) { nodes.add(head); head = head.next; } for (int i = nodes.size() - 1; i >= 0; i--) { System.out.print(nodes.get(i).data + " "); } } public static void main(String[] args) { Node head = new Node(); head.data = 1; head.next = new Node(); head.next.data = 2; head.next.next = new Node(); head.next.next.data = 3; head.next.next.next = new Node(); head.next.next.next.data = 4; head.next.next.next.next = new Node(); head.next.next.next.next.data = 5; printLinkReverse(head); } } 这样的方式确实能实现逆序打印链表的数据,但明显用了整整两次循环,时间复杂度为 O(n)。等等!逆序输出?似乎有这样一个数据结构可以完美解决这个问题,这个数据结构就是栈。 栈是一种「后进先出」的数据结构,用栈的原理更好能达到我们的要求,于是实现代码如下: public class Test05 { public static class Node { int data; Node next; } public static void printLinkReverse(Node head) { Stack<Node> stack = new Stack<>(); while (head != null) { stack.push(head); head = head.next; } while (!stack.isEmpty()) { System.out.print(stack.pop().data + " "); } } public static void main(String[] args) { Node head = new Node(); head.data = 1; head.next = new Node(); head.next.data = 2; head.next.next = new Node(); head.next.next.data = 3; head.next.next.next = new Node(); head.next.next.next.data = 4; head.next.next.next.next = new Node(); head.next.next.next.next.data = 5; printLinkReverse(head); } } 既然可以用栈来实现,我们也极容易想到递归也能解决这个问题,因为递归本质上也就是一个栈结构。要实现逆序输出链表,我们每访问一个结点的时候,我们先递归输出它后面的结点,再输出该结点本身,这样链表的输出结果自然也是反过来了。 代码如下: public class Test05 { public static class Node { int data; Node next; } public static void printLinkReverse(Node head) { if (head != null) { printLinkReverse(head.next); System.out.print(head.data+" "); } } public static void main(String[] args) { Node head = new Node(); head.data = 1; head.next = new Node(); head.next.data = 2; head.next.next = new Node(); head.next.next.data = 3; head.next.next.next = new Node(); head.next.next.next.data = 4; head.next.next.next.next = new Node(); head.next.next.next.next.data = 5; printLinkReverse(head); } } 虽然递归代码看起来确实很整洁,但有个问题:当链表非常长的时候,一定会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。所以显示用栈基于循环实现的代码,健壮性还是要好一些的。 好了,今天的面试讲解就到这,我们明天再见!
在算法面试中,面试官总是喜欢围绕链表、排序、二叉树、二分查找来做文章,而大多数人都可以跟着专业的书籍来做到倒背如流。而面试官并不希望招收的是一位记忆功底很好,但不会活学活用的程序员。所以学会数学建模和分析问题,并用合理的算法或数据结构来解决问题相当重要。 面试题:打印出旋转数组的最小数字 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3,4,5,1,2} 为数组 {1,2,3,4,5} 的一个旋转,该数组的最小值为 1。 要想实现这个需求很简单,我们只需要遍历一遍数组,找到最小的值后直接退出循环。代码实现如下: public class Test08 { public static int getTheMin(int nums[]) { if (nums == null || nums.length == 0) { throw new RuntimeException("input error!"); } int result = nums[0]; for (int i = 0; i < nums.length - 1; i++) { if (nums[i + 1] < nums[i]) { result = nums[i + 1]; break; } } return result; } public static void main(String[] args) { // 典型输入,单调升序的数组的一个旋转 int[] array1 = {3, 4, 5, 1, 2}; System.out.println(getTheMin(array1)); // 有重复数字,并且重复的数字刚好的最小的数字 int[] array2 = {3, 4, 5, 1, 1, 2}; System.out.println(getTheMin(array2)); // 有重复数字,但重复的数字不是第一个数字和最后一个数字 int[] array3 = {3, 4, 5, 1, 2, 2}; System.out.println(getTheMin(array3)); // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 int[] array4 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array4)); // 单调升序数组,旋转0个元素,也就是单调升序数组本身 int[] array5 = {1, 2, 3, 4, 5}; System.out.println(getTheMin(array5)); // 数组中只有一个数字 int[] array6 = {2}; System.out.println(getTheMin(array6)); // 数组中数字都相同 int[] array7 = {1, 1, 1, 1, 1, 1, 1}; System.out.println(getTheMin(array7)); } } 打印结果没什么毛病。不过这样的方法显然不是最优的,我们看看有没有办法找出更加优质的方法处理。 有序,还要查找? 找到这两个关键字,我们不免会想到我们的二分查找法,但不少小伙伴肯定会问,我们这个数组旋转后已经不是一个真正的有序数组了,不过倒像是两个递增的数组组合而成的,我们可以这样思考。 我们可以设定两个下标 low 和 high,并设定 mid = (low + high)/2,我们自然就可以找到数组中间的元素 array[mid],如果中间的元素位于前面的递增数组,那么它应该大于或者等于 low 下标对应的元素,此时数组中最小的元素应该位于该元素的后面,我们可以把 low 下标指向该中间元素,这样可以缩小查找的范围。 同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于 high 下标对应的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们就可以把 high 下标更新到中位数的下标,这样也可以缩小查找的范围,移动之后的 high 下标对应的元素仍然在后面的递增子数组中。 不管是更新 low 还是 high,我们的查找范围都会缩小为原来的一半,接下来我们再用更新的下标去重复新一轮的查找。直到最后两个下标相邻,也就是我们的循环结束条件。 说了一堆,似乎已经绕的云里雾里了,我们不妨就拿题干中的这个输入来模拟验证一下我们的算法。 input:{3,4,5,1,2} 此时 low = 0,high = 4,mid = 2,对应的值分别是:num[low] = 3,num[high] = 2,num[mid] = 5 由于 num[mid] > num[low],所以 num[mid] 应该是在左边的递增子数组中。 更新 low = mid = 2,num[low] = 5,mid = (low+high)/2 = 3,num[mid] = 1; high - low ≠ 1 ,继续更新 由于 num[mid] < num[high],所以断定 num[mid] = 1 位于右边的自增子数组中; 更新 high = mid = 3,由于 high - mid = 1,所以结束循环,得到最小值 num[high] = 1; 我们再来看看 Java 中如何用代码实现这个思路: public class Test08 { public static int getTheMin(int nums[]) { if (nums == null || nums.length == 0) { throw new RuntimeException("input error!"); } // 如果只有一个元素,直接返回 if (nums.length == 1) return nums[0]; int result = nums[0]; int low = 0, high = nums.length - 1; int mid; // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组 while (nums[low] >= nums[high]) { // 确保循环结束条件 if (high - low == 1) { return nums[high]; } // 取中间位置 mid = (low + high) / 2; // 代表中间元素在左边递增子数组 if (nums[mid] >= nums[low]) { low = mid; } else { high = mid; } } return result; } public static void main(String[] args) { // 典型输入,单调升序的数组的一个旋转 int[] array1 = {3, 4, 5, 1, 2}; System.out.println(getTheMin(array1)); // 有重复数字,并且重复的数字刚好的最小的数字 int[] array2 = {3, 4, 5, 1, 1, 2}; System.out.println(getTheMin(array2)); // 有重复数字,但重复的数字不是第一个数字和最后一个数字 int[] array3 = {3, 4, 5, 1, 2, 2}; System.out.println(getTheMin(array3)); // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 int[] array4 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array4)); // 单调升序数组,旋转0个元素,也就是单调升序数组本身 int[] array5 = {1, 2, 3, 4, 5}; System.out.println(getTheMin(array5)); // 数组中只有一个数字 int[] array6 = {2}; System.out.println(getTheMin(array6)); // 数组中数字都相同 int[] array7 = {1, 1, 1, 1, 1, 1, 1}; System.out.println(getTheMin(array7)); // 特殊的不知道如何移动 int[] array8 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array8)); } } 前面我们提到在旋转数组中,由于是把递增排序数组的前面的若干个数字搬到数组后面,因为第一个数字总是大于或者等于最后一个数字,而还有一种特殊情况是移动了 0 个元素,即数组本身,也是它自己的旋转数组。这种情况本身数组就是有序的了,所以我们只需要返回第一个元素就好了,这也是为什么我先给 result 赋值为 nums[0] 的原因。 上述代码就完美了吗?我们通过测试用例并没有达到我们的要求,我们具体看看 array8 这个输入。先模拟计算机运行分析一下: low = 0, high = 4, mid = 2, nums[low] = 1, nums[high] = 1,nums[mid] = 1; 由于 nums[mid] >= nums[low],故认定 nums[mid] = 1 在左边递增子数组中; 所以更新 high = mid = 2,mid = (low+high)/2 = 1; nums[low] = 1,nums[mid] = 1,nums[high] = 1; high - low ≠ 1,继续循环; 由于 nums[mid] >= nums[low],故认定 nums[mid] = 1 在左边递增子数组中; 所以更新 high = mid = 1,由于 high - low = 1,故退出循环,得到 result = 1; 但我们一眼了然,明显我们的最小值不是 1 ,而是 0 ,所以当 array[low]、array[mid]、array[high] 相等的时候,我们的程序并不知道应该如何移动,按照目前的移动方式就默认 array[mid] 在左边递增子数组了,这显然是不负责任的做法。 我们修正一下代码: public class Test08 { public static int getTheMin(int nums[]) { if (nums == null || nums.length == 0) { throw new RuntimeException("input error!"); } // 如果只有一个元素,直接返回 if (nums.length == 1) return nums[0]; int result = nums[0]; int low = 0, high = nums.length - 1; int mid = low; // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组 while (nums[low] >= nums[high]) { // 确保循环结束条件 if (high - low == 1) { return nums[high]; } // 取中间位置 mid = (low + high) / 2; // 三值相等的特殊情况,则需要从头到尾查找最小的值 if (nums[mid] == nums[low] && nums[mid] == nums[high]) { return midInorder(nums, low, high); } // 代表中间元素在左边递增子数组 if (nums[mid] >= nums[low]) { low = mid; } else { high = mid; } } return result; } /** * 查找数组中的最小值 * * @param nums 数组 * @param start 数组开始位置 * @param end 数组结束位置 * @return 找到的最小的数字 */ public static int midInorder(int[] nums, int start, int end) { int result = nums[start]; for (int i = start + 1; i <= end; i++) { if (result > nums[i]) result = nums[i]; } return result; } public static void main(String[] args) { // 典型输入,单调升序的数组的一个旋转 int[] array1 = {3, 4, 5, 1, 2}; System.out.println(getTheMin(array1)); // 有重复数字,并且重复的数字刚好的最小的数字 int[] array2 = {3, 4, 5, 1, 1, 2}; System.out.println(getTheMin(array2)); // 有重复数字,但重复的数字不是第一个数字和最后一个数字 int[] array3 = {3, 4, 5, 1, 2, 2}; System.out.println(getTheMin(array3)); // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 int[] array4 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array4)); // 单调升序数组,旋转0个元素,也就是单调升序数组本身 int[] array5 = {1, 2, 3, 4, 5}; System.out.println(getTheMin(array5)); // 数组中只有一个数字 int[] array6 = {2}; System.out.println(getTheMin(array6)); // 数组中数字都相同 int[] array7 = {1, 1, 1, 1, 1, 1, 1}; System.out.println(getTheMin(array7)); // 特殊的不知道如何移动 int[] array8 = {1, 0, 1, 1, 1}; System.out.println(getTheMin(array8)); } } 我们再用完善的测试用例放进去,测试通过。 总结 本题其实考察的点挺多的,实际上就是考察对二分查找的灵活运用,不少小伙伴死记硬背二分查找必须遵从有序,而没有学会这个二分查找的思想,这样会导致只能想到循环查找最小值了。 不少小伙伴在面试中表态,Android 原生态基本都封装了常用算法,对面试这些无作用的算法表示抗议,其实这是相当愚蠢的。我们不求死记硬背算法的实现,但求学习到其中巧妙的思想。只有不断地提升自己的思维能力,才能助自己收获更好的职业发展。 这也大概是大家一直到处叫大佬,埋怨自己工资总是跟不上别人的一方面原因吧。
面试系列更新后,终于迎来了我们的第一期,我们也将贴近《剑指 Offer》的题目给大家带来 Java 的讲解,个人还是非常推荐《剑指 Offer》作为面试必刷的书籍的,这不,再一次把这本书分享给大家,PDF 版本在公众号后台回复「剑指Offer」即可获取。 我们在面试中总会遇到不少设计模式的问题,而设计模式中的 Singleton 模式又是我们最容易出现的考题,大多数人可能在此前已经有充分的了解,但不少人仅仅是停留在比较浅显的层次,今天我们就结合《剑指 Offer》给大家带来更加深入的讲解。 题目:请用 Java 手写一个单例模式代码,希望尽可能考虑地全面。 不论是 Java 还是 Android 中单例模式肯定是我们经常用到的,所以这道题可能大多数人会第一时间想到饿汉式代码。 public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() { } public static Singleton getInstance() { return INSTANCE; } } 上面是典型的饿汉式写法,因为单例的实例被声明成 static 和 final 变量了,所以在第一次加载类到内存中时就会初始化,所以也不会存在多线程问题,但它的缺点非常显而易见,也经常为人诟病。这明显不是一种懒加载模式(lazy initialization),就因为它是 static 和 final 的,所以类会在加载后就被初始化,导致我们代码的健壮性很差,假如后面更改需求,希望在 getInstance() 之前调用某个方法给它设置参数,这个就明显不符合使用场景了,面试官极有可能在看到这个代码后觉得你就是一个只知道完成功能没有大局观的人。 当然还会有不少人直接采用我们的懒汉式代码,这样就解决了延展性和懒加载了。 public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 上述代码可能是大多数面试者的解法,包括教科书上也是这么教我们的,但这段代码却存在了一个致命的问题,那就是当多个线程并行调用 getInstance() 的时候,就会创建多个实例,这显然违背了面试官的意思。正好面试官加了一句希望尽可能考虑地全面,所以这样的代码肯定不能虏获面试官的芳心。 既然要线程安全,那我直接加锁呗。于是并有了下面的代码。他们也是懒汉式的,只不过线程安全了。 public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 这样的解法实现了线程安全,但它并不是那么高效,因为在任何时候只能有一个线程去调用 getInstance() 方法,但实际上加锁操作也是耗时的,我们应该尽量地避免使用它。所以自然就引出了双重检验锁。 public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } 这段代码看起来很完美,很可惜,它是有问题。主要在于 instance = new Singleton() 这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。 给 instance 分配内存 调用 Singleton 的构造函数来初始化成员变量 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了) 但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。 我们只需要将 instance 变量声明成 volatile 就可以了。 public class Singleton { private volatile static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } 有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。 但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。 那么,有没有一种既有懒加载,又保证了线程安全,还简单的方法呢? 当然有,静态内部类,就是一种我们想要的方法。我们完全可以把 Singleton 实例放在一个静态内部类中,这样就避免了静态实例在 Singleton 类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。 public class Singleton { private static class Holder { private static Singleton INSTANCE = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return Holder.INSTANCE; } } 这是我比较推荐的解法,这种写法用 JVM 本身的机制保证了线程安全的问题,同时读取实例的时候也不会进行同步,没什么性能缺陷,还不依赖 JDK 版本。 虽说如此,但看《Effective Java》中第三点来说,还是有必要提醒一下:享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制来调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。 《Effective Java 中文版》PDF 在公众号后台回复「Effective Java」即可获取。 我们其实还有更简单的枚举单例。 用过枚举写单例的人都说:用枚举写单例真是太简单了。下面的这段代码就是声明枚举单例的通常做法。 public enum EasySingleton{ INSTANCE; } 这是从 Java 1.5 发行版本后就可以实用的单例方法,我们可以通过 EasySingleton.INSTANCE 来访问实例,这比调用 getInstance() 方法简单多了。创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。 总结 一个总结肯定是必不可少的,上面也只是列举了我们常见的单例实现方式。当然也不完全,比如我们还可以用 static 代码块的方式实现懒汉式代码,但这里就不一一例举了。 就我个人而言,我还是比较推荐用静态内部类的方式使用单例模式,如果涉及到反序列化创建对象的话,不妨也试试枚举呗~ 文章参考链接:http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
近年来关于区块链、比特币、ICO 类似的概念非常的普遍,是眼下的超级大热门,新闻媒体大量报道,宣称它将创造未来。十传百,百传千,难免也就会有许多人把它们全部混淆在一起,去年年底比特币的暴涨和今年年初比特币的暴跌,让不少人非常地感兴趣,而又持观望态度。甚至也有人认为比特币等价于金融风险,就是传销组织搞出来的花样。 今天主要想给大家科普一下关于区块链的一些基础知识,以免混淆。我想说的是:区块链 ≠ 比特币 ≠ ICO ≠ 金融风险。 什么是区块链 区块链,英文名为 blockchain 或 block chain,实质就是一个 去中心化的分布式数据库,它是用 分布式数据库 识别、传播和记载信息的智能化对等网络,也可称为价值互联网。 首先,区块链的主要作用是储存信息。任何需要保存的信息,都可以写入区块链,也可以从里面读取,所以它是数据库。 其次,任何人都可以架设服务器,加入区块链网络,成为一个节点。区块链的世界里面,没有中心节点,每个节点都是平等的,都保存着整个数据库。你可以向任何一个节点,写入/读取数据,因为所有节点最后都会同步,保证区块链一致。 去中心化? 前面说到一个非常关键的词儿「去中心化」,到底什么是「去中心化」呢? 大家知道我们国家的货币系统,其实就是非常典型的「中央化系统」,我们有非常垄断的国家银行,下面还有一些城市银行,我们把钱存在银行,本质上就是因为我们信任国家,相信国家的能力。倘若不相信,我们根本不可能凭借一个数字,就大方地告诉别人,我有几百万乃至几千万。 我们进行一笔交易,实际上就是在银行的数据库进行了一定的数据更改,并定期让各大银行进行数据同步,这样的过程一定是繁琐的。 而区块链在这方面就有一个非常革命化的特点了。 区块链是完全没有管理员的,它是去中心化的,测试无中心的! 虽然它也是数据库的一员,但传统的数据库都是有管理员的。它的这个特点,让它变得无比安全,因为不可能存在任何对数据进行强行垄断的可能。假设黑客想要处理这个数据的话,必须要黑掉全网 50% 以上的网络,就目前的技术来看,基本是不可能实现的。这也让区块链完好无损运行了 10 年之久,还是那么地安全。 如何保证数据的可信度 前面说到,区块链没有管理员,人人都可以写数据,那怎么保证数据的可信度呢?这就是区块链最神奇的地方。 我们必须得先了解一下区块链的最小单元:区块(block)。 简而言之,每一个区块就是一个数据。而一个区块由区块头和区块体两部分共同组成。 区块头(Head) 区块头主要包含了区块的头信息,包含上一个区块的哈希值(preHash),本区块的哈希值(Hash),以及时间戳(timestamp)等。 区块体(Body) 区块体主要就用于存放区块的详细数据了,它可以存放交易记录信息或者更多的其它相关。 Hash 说到 Hash,我想很多程序员大概不用问都明白,Hash 值就是一个计算机对不定长内容计算出来的等价特征值。我们平时提到很多的 MD5 算法就是典型的 Hash 算法,在 Java 上的 Hash 值判断两个变量是否相等也是运用的非常多。 由此可见,Hash 值是非常难重合的。 既然如此,我们的字符串 Hash 值是近乎不可能重复的,那么我们的区块中的内容发生变化的话,那区块头里面的 Hash 值也一定会改变。 区块和 Hash 是一一对应的,区块链采用了一个典型的 SHA256 算法。计算公式为: Hash = SHA256(区块头) 前面说过,区块头包含很多内容,其中有当前区块体的哈希,还有上一个区块的哈希。这意味着,如果当前区块体的内容变了,或者上一个区块的哈希变了,一定会引起当前区块的哈希改变。 这一点对区块链有重大意义。 如果有人修改了一个区块,该区块的哈希就变了。为了让后面的区块还能连到它(因为下一个区块包含上一个区块的哈希),该人必须依次修改后面所有的区块,否则被改掉的区块就脱离区块链了。由于后面要提到的原因,哈希的计算很耗时,短时间内修改多个区块几乎不可能发生,除非有人掌握了全网51%以上的计算能力。 正是通过这种联动机制,区块链保证了自身的可靠性,数据一旦写入,就无法被篡改。这就像历史一样,发生了就是发生了,从此再无法改变。 这一点和我们所学习的链表非常相似,所以它就有了「区块链」这个名字。 挖矿 「挖矿」是我们听的比较多的另一个名词儿。所谓「挖矿」,其实就是计算最新数据的 Hash 值,生成新区块的一个过程。 前面说到了一个点,区块链的每个节点都是一个数据库,为了保证各个节点之间的数据同步,那么新区快的添加速度,当然应该得到控制。 所以,区块链的发明者中本聪(这是假名,至今仍然没人知道他的真实身份,但确实这人太神了。)故意加深了添加新区块的难度。 他的设计是,平均每 10 分枝,全网只能生成一个新区块,一小时也就最多能生成 6 个。 这种产出速度不是通过命令达成的,而是故意设置了海量的计算。也就是说,只有通过极其大量的计算,才能得到当前区块的有效哈希,从而把新区块添加到区块链。由于计算量太大,所以快不起来。 这个过程就叫做挖矿,因为计算有效哈希的难度,好比在全世界的沙子里面,找到一粒符合条件的沙子。计算哈希的机器就叫做矿机,操作矿机的人就叫做矿工。 也许有人在这里会疑问,计算机最厉害的地方不就是把非常难的计算变的简单吗?为啥在这里会觉得很难呢? 实际上这是一个非常困难的事情,完全就是碰运气的事情。计算 Hash 值当然是非常简单的,但区块链所要求的 Hash 值是做了非常苛刻的要求的,并不是所有的 Hash 值都能被它接受。 试想,当你算出了一个符合条件的 Hash 值,你算下一个 Hash 值进行到快要完成的时候,别人计算出了一个新的符合条件的 Hash 值接链,这时候不管你做了多少工作,你的进度如何,那你都只有重头开始了。 比特币 比特币(bitcoin)相比区块链,可能有更多的人知道吧,毫不夸张的说,不少人就是因为比特币才知道了区块链这玩意儿的。 去年年底比特币的疯涨,和今年年初比特币的暴跌,让很多家新闻媒体,对比特币进行了疯狂的报道。但很可惜的是,新闻媒体往往都只关注它的火爆表现,忽视了很多非常基础的问题。 我今年春节的时候回家,身边就有不少人问了我比特币相关的问题,而且非常想入手,考虑倒卖的方式来进行盈利。 可除了知道比特币这个概念,和知道它那时候值 10 多万的事情,其余的一无所知。 比特币相当于区块链 1.0 的产物,它始于 2008 年的第一条区块链分布式账本。这仅仅是中本聪提出的一个革命性的构想:「创造一种不受政府或其他任何人控制的货币。」 这个想法实在是太疯狂了,仅仅是一串数字,不需要任何资金支持,也不需要任何人来负责任,就想当做钱来做交易,怎么可能会有人接受? 但现实就是那么奇葩,自一位极客使用 2000 个比特币购买了一个披萨开始,这个狂想就在慢慢变成现实。 货币这个东西,其实就是被大家认可的问题。就像几千年前,我们可以用贝壳,可以用石头,再后来用金银做货币一样。只要大家都认可这个东西的价值,那它就会成为有价值的东西。 我们传统的货币都来源于国家发行,所有的存储也是由国家的银行统一管理,这是典型的中心化系统。 而比特币则是部署在一个全世界众多对等节点组成的去中心化网络之上。没一个节点都有资格对这种数字货币进行记录和发行。 比特币的底层数据存储,实际上就是区块链技术的典型应用。在每个区块体中,我们可以存储所有的交易信息在里面。 交易信息的每一行,都会包含时间戳,交易明细,和数字签名。 可能在这里有不少人会问:「怎么保证交易的可信度呢?」 实际上这就是上面提到的「数字签名」的伟大用处。 数字签名简单而言就是一条交易的标识,但它的来头不简单,是由非对称加密算法处理生成的。 可能又有很多人会问,非对称加密是什么玩意儿。 非对称加密原理很简单,和我们目前主流的处理也很相似,简单而言就是加密和解密需要不一样的钥匙。你首先会把自己的公钥上传到网络上,这个公钥是公开的,任何人都可以获取。别人可以用你的公钥加密信息,然后发送给你,而你必须采用你的私钥才能解开这条信息,并且会采用验证方式来保证中途没有被篡改。即使你这条数据被别人截获了,别人没有你的私钥也是完全不可能打开的。 非对称算法保证了信息的真实性,但仅此还不能保证一次完整的交易。 交易的数据必须写入到数据库,才算成立,对方才能真正地收到钱。 一笔交易一旦写入了区块链,就无法反悔了。这里需要建立一个观念:比特币不存放在钱包或其他别的地方,而是只存在于区块链上面。区块链记载了你参与的每一笔交易,你得到过多少比特币,你又支付了多少比特币,因此可以算出来你拥有多少资产。 交易的确认离不开矿工。为什么有人愿意做矿工呢? 比特币协议规定,挖到新区块的矿工将获得奖励,一开始(2008 年)是 50 个比特币,然后每 4 年减半,目前( 2018 年)是 12.5 个比特币。这也是比特币的供给增加机制,流通中新增的比特币都是这样诞生的。 你可能看出来了,每 4 年奖励减半,由于比特币可以分割到小数点后八位,那么到了 2140 年,矿工将得不到任何奖励,比特币的数量也将停止增加。这时,矿工的收益就完全依靠交易手续费了。 所谓交易手续费,就是矿工可以从每笔交易抽成,具体的金额由支付方自愿决定。你完全可以一毛不拔,一分钱也不给矿工,但是那样的话,你的交易就会没人处理,迟迟无法写入区块链,得到确认。矿工们总是优先处理手续费最高的交易。 目前由于交易数量猛增,手续费已经水涨船高,一个区块2000多笔交易的手续费总额可以达到3~10个比特币。如果你的手续费给低了,很可能过了一个星期,交易还没确认。 一个区块的奖励金12.5个比特币,再加上手续费,收益是相当可观的。按照目前的价格,可以达到100万~200万人民币。想想看,运气好的话,几分钟就能挖到一个区块,拿到这样一大笔钱,怪不得人们对挖矿趋之若鹜。 以太坊 以太坊(Etherenum)是区块链 2.0 的产物,它是一个开源的有智能合约功能的公共区块链平台。通过其专用加密货币以太币(Ether)提供去中心化的虚拟机(称为“以太虚拟机”Ethereum Virtual Machine)来处理点对点合约。 许多人相信,它是目前最有前景的去中心化只能合约协议之一。 以太坊的概念是一个叫 Vitalik Buterin 的程序员提出,它最开始是一个众筹活动,截至目前,它的专用货币以太币已经成为市值仅次于比特币的加密货币。 这一块由于时间关系,在这里就不多做赘述。如果有时间的小伙伴们也可以上 Google 进行搜索学习。 ICO ICO(是 Initial Coin Offering 缩写),首次币发行,源自股票市场的首次公开发行(IPO)概念,是区块链项目首次发行代币,募集比特币、解决以太坊等通用数字货币的行为。 它是一种区块链行业术语,是一种为加密数字货币/区块链项目筹措资金的常用方式,早期参与者可以从中获得初始产生的加密数字货币作为回报。由于代币具有市场价值,可以兑换成法币,从而支持项目的开发成本。ICO 所发行的代币,可以基于不同的区块链。 一些总结 看到这里,不用我重复了吧。 区块链 ≠ 比特币 ≠ ICO ≠ 金融风险,它们有联系,但绝不对等,倘若做得好,它可以服务群众,但做的不好,它也可能成为罪犯的帮凶。 我们有理由相信,现在就是区块链 3.0 时代,我们的区块链有能力变革传统的生产关系,打造一个可信价值的全新网络。
上期文章链接:Fiddler 抓包浅析(一) 上期文章中我们简单介绍了 Fiddler 的几大面板以及一些大体的介绍,那么本期,我们将为大家带来一些更加详细的解读。 Statistic 我们可以通过 Statistic 查看到该条请求的基本性能数据,如上图,发送了 523 bytes,收到了 1456 bytes,DNS 消耗时间、TCP/IP 连接时间都为 0 ms。 Inspectors Inspectors 上一篇讲过啦,不过这里还是再简单提一下。 Inspectors 分为上下两部分,上半部分为请求部分,下半部分为响应部分。在其中的 WebForms 版块,可以看到请求参数。其中的 QueryString 代表 GET 请求的参数,Body 代表 POST 请求的参数。下半部分的响应,因为该接口返回的是 Json 格式,所以可以直接在 JSON 版块直接展示。 AutoResponder AutoResponder 版块是 Fiddler 比较重要且比较强大的功能之一。可用于拦截某一请求,并重定向到本地的资源,或者使用 Fiddler 的内置响应。可用于调试服务器端代码而无需修改服务器端的代码和配置,实际上访问的是本地的文件或者得到的是 Fiddler 的内置响应。当设置了相应的规则后,会将该请求进行拦截处理。 Composer Composer 在上一章节也提过了,顾名思义就是构建请求。 Parsed 这个应该很简单吧?该方式是把一个请求分割为 3 部分,请求方式 + Host + Header,如果不是 GET 请求的话,还会包括请求体。 这跟一个常用的网络测试工具很像,所以就不多做介绍了。 Raw 直接使用 HTTP header 头部构建 http 请求。 这个请求方式需要自己写请求所需要的原始数据。 Scratchpad 跟 Raw 一样,只是 Raw 是一条,这个是可以多条来进行访问。具体自己测试,可以直接拖拽前边的请求回话。但是要注意一点:执行的时候必须要选中你所要执行的所有请求,否则会提示 in the box below , please highlight the complete HTTP request to be sent before pressing Execute Options Options 里面里面有四个选项,我们来简单介绍一下它们分别有什么作用。 Inspect Session 勾选,点击 execute 后,直接跳转到 inspector 页面。不勾选,点击 execute 后,仍保留在当前页面 Fix Content-Length header adjusts the value of the Content-Length request header (if present) to match the size of the request body. Follow Redirects causes a HTTP/3xx redirect to trigger a new request, if possible. The Composer will follow up to fiddler.composer.followredirects.max default redirections. Automatically Authenticate causes Fiddler to automatically respond to HTTP/401 and HTTP/407 challenges that use NTLM or Negotiate protocols using the current user’s Windows credentials. Tear off 点击后,composer 面板单独窗口显示 Filters Filters 过滤器,作为 Fiddler 的另一个强大的功能,提供了多维度的过滤规则,足以满足日常开发调试的需求。 可以通过各种方式对请求进行过滤,基本平时我们日常用到 Host 过滤和进程过滤即可,更多的操作,可以移步 Fiddler 官网。
Fiddler 工具浅析 Fiddler 是位于客户端和服务器端的 HTTP 代理,也是目前最常用的 HTTP 抓包工具之一。(Mac OS 建议采用 Charles) 它可以记录客户端和服务器之间的所有 HTTP 请求,并可以针对特定的 HTTP 请求,分析请求数据、设置断点、调试 web 应用、修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 web 调试和网络请求分析的利器。 Fiddler 作为一个代理,自然客户端的所有请求都会先经过它,再转发给相应的服务器。反之,服务器端的所有响应,也都会先经过 Fiddler 然后发送给客户端。所以,Fiddler 支持所有可以设置 HTTP 代理为 127.0.0.1:8888 的浏览器和应用程序。 Fiddler 界面解析 我们不妨直接看到 Fiddler 的界面,可以看到很多不错的东西。 这里是大概的主界面,由于内容涵盖还是比较多,所以我们分块解析。 Web Session 面板区域 web session 面板区域主要包含了每条 HTTP 请求(每一条称为 session),主要包含了请求的 url,状态码,body 等信息。 # HTTP Request 的顺序,从 1 开始,按照页面加载请求的顺序递增。其中序列号列中一般还有图标,代表不同的响应类型。具体类型包括: 图片来源于网络 Result HTTP 响应的状态。状态码解析请参考专业的 HTTP 书籍。 Protocol 请求使用的协议,如 HTTP/HTTPS/FTP 等。 Host 请求地址域名。 URL 请求的服务器路径和文件名,也包括 GET 参数。 Body 请求的大小,单位为 byte。 Caching 请求的缓存过期时间或缓存控制 header 等值。 Content-Type 请求响应的类型(Content-Type) Process 发出此请求的进程及进程 ID。 Comments 用户通过脚本或者右键菜单给此 session 增加的备注。 Custom 用户可以通过脚本设置的自定义值。 详情和数据面板区域 该区域基本是我们必须关注的点,我们可以在这个区域看到每条 HTTP 请求的具体数据统计和数据包分析。其中的 inspector 面板下的数据往往最受我们的重视。 inspector 这里可以看到我们请求的诸多数据,由于时间关系,我们也就仅拿出其中的 Header 部分做讲解,其他部分同理。 上面的 Header 中包含了我们常见的请求头,比如客户端的设备相关信息、Cookies、连接状态。 下面是我们的响应头 Header,同时也包含了响应时间、过期时间、响应格式、数据包大小、连接状态等。 Composer 我们经常在做一些接口测试的需要做一些骚操作,比如上传文件等,这个完全不像 GET 请求那般可以直接带上我们的参数在浏览器访问。这时候我们又不想去写一个脚本。好伙计,Composer 正好满足了我们的需求。 正如截图中样式,我们可以设置请求方式、请求地址,参数、甚至是上传文件等,这一些请求我们都可以模拟。 不得不感叹,确实黑科技。 一些其他菜单说明 Filters 强大的过滤机制 Fiddler 支持域名过滤,只需要在 Filters 里面设置即可。 过滤条件涵盖非常广泛,比如过滤特定 HTTP 状态码的请求、特定请求类型的 HTTP 请求等,更多的过滤方式还需要大家自己去挖掘。 选择抓取区域 可以选择是否支持 HTTPS,选择是否抓取所有进程的请求,还是仅仅浏览器的请求,抑或是仅仅远程设备的请求,功能都是非常强大的。 一些必备的处理 对于 Web 抓包 要想抓包到相应的数据,必须设置浏览器代理为 127.0.0.1:8888,否则无法捕获到 HTTP 请求。 对于 HTTPS 的支持 默认情况下,Fiddler 是不支持直接查看 HTTPS 请求的,我们必须在设备上安装它的证书。 这里姑且直接以抓取 Android 手机的 HTTPS 请求做示例。 首先设置 Fiddler。打开工具栏 => Tools => Fiddler Options => HTTPS 导出证书到手机。 直接在手机端安装。 在手机上安装证书这个操作,太多机型了,都不太一样,姑且大家自行百度。
什么是 adb 命令? adb 工具即 Android Debug Bridge(安卓调试桥) tools。它就是一个命令行窗口,用于通过电脑端与模拟器或者真实设备交互。在某些特殊的情况下进入不了系统,adb 就派上用场啦! 源码传送地址 分类命令 ADB Debugging adb devices 主要是用于打印当前连接的所有模拟器或者设备。 adb forward 端口映射,将 PC 端的某端口数据重定向到手机端的一个端口。 adb forward <local> <remote> adb kill-server 终止 adb 进程。 adb kill-server Wireless adb connect 无限调试必备命令,需要保证设备和 PC 在同一局域网内,所以可通过远程桌面达到远程调试的结果。 adb connect <host>[:<port>] 需要保证设备的 /system/build.prop 文件中有命令 service.adb.tcp.port=5555,否则会遭到拒绝。 此处安利一下无限调试设置方法: 打开设备的调试模式,允许 USB 以 MTP 模式传输,确保设备和 PC 能正常连接,可以通过 adb shell 或者 adb devices 等进行验证。 确保已连接后,依次执行以下命令:adb rootadb remountadb pull /system/build.prop ./ 在 adb 命令执行的文件夹下的 build.prop 中加入命令 service.adb.tcp.port=5555 执行 adb push ./build.prop /system/ 后重启设备 结束后断开 USB 连接线,输入 adb connect 设备IP:5555 确认可以正常连接。 adb usb 设置设备以 USB 形式连接 PC。 Package Manager adb install 主要用于往 Android 设备 push 应用。 adb install [option] <path> 其中的 option 也是比较有讲究的,下面就只说最常用的。 adb install test.apk 直接安装应用 adb install -r test.apk 替代存在的应用,不会删除应用数据,用于更新应用特别方便。 其余的不是很常用的就不多提了,感兴趣的可以自行了解。 adb uninstall 从设备或者模拟器卸载应用。 adb uninstall [options] <package> 两种情况,假设我们的应用 APP 包名是 com.example.application adb uninstall com.example.application 直接删除应用和所有数据adb uninstall -k com.example.application 删除应用,但会保留应用数据和缓存数据。 adb shell pm list packages 打印设备下面的所有应用包名。 adb shell pm list packages [options] <FiLTER> 其他的过滤方式和限定条件这里也不举例了。 adb shell pm path 打印 apk 的路径。 adb shell pm path <package> adb shell pm clear 清除应用缓存。 adb shell pm clear <package> File Manager adb pull 从 Android 设备下载文件到 PC。 adb pull <remote> [local] 其中 <remote> 代表文件在设备中的地址,[local] 代表存放目录。 adb push 把 PC 的文件存放到 Android 设备。 adb push <local> <remote> adb shell ls 列出目录内容。 adb shell ls [options] <directory> adb shell cd 和一般的 PC 的 cd 差不多,主要用于切换目录。 adb shell cd <directory> adb shell rm 删除文件或者目录 adb shell rm [options] <file or directory> adb shell mkdir 创建一个文件夹 adb shell mkdir [options] <directory name> adb shell touch 创建一个新文件或者改变文件修改时间 adb shell touch [options] <file> adb shell pwd 定位当前的操作位置 adb shell pwd adb shell cp 字面意思,很好理解,复制。 adb shell cp [options] <source> <dest> adb shell mv 移动或者更名文件 adb shell mv [options] <source> <dest> Network adb shell netstat 主要用于网络统计。 adb shell ping 没啥好说的,和 PC 的 ping 命令一样的。 adb shell netcfg 通过配置文件配置和管理网络连接。 adb shell netcfg [<interface> {dhcp|up|down}] adb shell ip 主要用于显示一些数据 adb shell ip [OPTIONS] OBJECT Logcat adb logcat 打印日志文件。 adb logcat [options] [filter-specs] 当然可以像 Android Studio 一样只打印固定的日志 adb logcat *:V lowest priority, filter to only show Verbose leveladb logcat *:D filter to only show Debug leveladb logcat *:I filter to only show Info leveladb logcat *:W filter to only show Warning leveladb logcat *:E filter to only show Error leveladb logcat *:F filter to only show Fatal leveladb logcat *:S Silent, highest priority, on which nothing is ever printed adb logcat -b <Buffer> adb logcat -b radio View the buffer that contains radio/telephony related messages.adb logcat -b event View the buffer containing events-related messages.adb logcat -b main defaultadb logcat -c Clears the entire log and exits.adb logcat -d Dumps the log to the screen and exits.adb logcat -f test.logs Writes log message output to test.logs .adb logcat -g Prints the size of the specified log buffer and exits.adb logcat -n <count> *Sets the maximum number of rotated logs to <count>. * adb shell dumpsys 获取系统数据。 adb shell dumpsys [options] 其中有个非常好用的是,当你在新接触一个项目的时候,不熟悉项目流程,此时正好可以用这个直接定位到你的 Activity 位置。 adb shell dumpsys activity activities 如图,直接在打印出来内容的后半段找到了当前 Activity 的定位,是 NewLoginActivity。 adb shell dumpstate 和命令直译差不多,dumps state。 Screenshot adb shell screencap 一般的手机都有截图功能(一般下拉菜单中有),但不代表所有 Android 设备都在可视化中开启了这个功能,所以这时候这个 adb 命令就显得特别重要。 adb shell screencap <filename> 结合前面的 pull 命令,就可以让我们轻松拿到屏幕截图。 adb shell screencap /sdcard/test.png 截图存放adb pull /sdcard/test.png 取到 PC 当前文件夹 adb shell screencord 有了屏幕截图,自然也得有屏幕录制,可惜这个必须在 Android 4.4 (API level 19) 以上才可使用。 adb shell screencord /sdcard/test.mp4 这个还可以对大小 size 和 时间做限制,感兴趣的可以自行了解。 System adb root 获取 root 权限。 adb sideload adb shell ps 打印进程状态。 adb shell top 展现上层 CPU 进程信息。 adb shell getprop 获取 Android 系统服务属性 adb shell setprop 设置服务属性。
面试场景 平时开发用到其他线程吗?都是如何处理的? 基本都用 RxJava 的线程调度切换,嗯对,就是那个 observeOn 和 subscribeOn 可以直接处理,比如网络操作,RxJava 提供了一个叫 io 线程的处理。 在 RxJava 的广泛使用之前,有使用过其他操作方式吗?比如 Handler 什么的? 当然用过呀。 那你讲讲 Handler 的工作原理吧。 Handler 工作流程基本包括 Handler、Looper、Message、MessageQueue 四个部分。但我们在日常开发中,经常都只会用到 Handler 和 Message 两个类。Message 负责消息的搭载,里面有个 target 用于标记消息,obj 用于存放内容,Handler 负责消息的分发和处理。 一般在开发中是怎么使用 Handler 的? 官方不允许在子线程中更新 UI,所以我们经常会把需要更新 UI 的消息直接发给处理器 Handler,通过重写 Handler 的 handleMessage() 方法进行 UI 的相关操作。 那使用中就没什么需要注意的吗? 有,Handler 如果设置为私有变量的话,Android Studio 会报警告,提示可能会造成内存泄漏,这种情况可以通过设置为静态内部类 + 弱引用,或者在 onDestroy() 方法中调用 Handler.removeCallbacksAndMessages(null) 即可避免; 正文 总的来说这位面试的童鞋答的其实还是没那么差,不过细节程度还不够,所以南尘就来带大家一起走进 Handler。 Handler 工作流程浅析 异步通信准备 => 消息入队 => 消息循环 => 消息处理 异步通信准备 假定是在主线程创建 Handler,则会直接在主线程中创建处理器对象 Looper、消息队列对象 MessageQueue 和 Handler 对象。需要注意的是,Looper 和 MessageQueue 均是属于其 创建线程 的。Looper 对象的创建一般通过 Looper.prepareMainLooper() 和 Looper.prepare() 两个方法,而创建 Looper 对象的同时,将会自动创建 MessageQueue,创建好 MessageQueue 后,Looper 将自动进入消息循环。此时,Handler 自动绑定了主线程的 Looper 和 MessageQueue。 消息入队 工作线程通过 Handler 发送消息 Message 到消息队列 MessageQueue 中,消息内容一般是 UI 操作。发送消息一般都是通过 Handler.sendMessage(Message msg) 和 Handler.post(Runnabe r) 两个方法来进行的。而入队一般是通过 MessageQueue.enqueueeMessage(Message msg,long when) 来处理。 消息循环 主要分为「消息出队」和「消息分发」两个步骤,Looper 会通过循环 取出 消息队列 MessageQueue 里面的消息 Message,并 分发 到创建该消息的处理者 Handler。如果消息循环过程中,消息队列 MessageQueue 为空队列的话,则线程阻塞。 消息处理Handler 接收到 Looper 发来的消息,开始进行处理。 对于 Handler ,一些需要注意的地方 1 个线程 Thread 只能绑定 1个循环器 Looper,但可以有多个处理者 Handler 1 个循环器 Looper 可绑定多个处理者 Handler 1 个处理者 Handler 只能绑定 1 个 1 个循环器 Looper 常规情况下,这些相关对象是怎么创建的? 前面我们说到 Looper 是通过 Looper.prepare() 和 Looper.prepareMainLooer() 创建的,我们不妨看看源码里面到底做了什么。 我们不得不看看 Looper 的构造方法都做了什么。 显而易见,确实在创建了 Looper 对象的时候,自动创建了消息队列对象 MessageQueue。 而 Looper.prepareMainLooper() 从名称也很容易看出来,是直接在主线程内创建对象了。而在我们日常开发中,经常都是在主线程使用 Handler,所以导致了很少用到 Looper.prepare() 方法。 而生成 Looper 和 MessageQueue 对象后,则自动进入消息循环:Looper.loop(),我们不妨再看看里面到底做了什么? 截图中的代码比较简单,大家应该不难看懂,我们再看看如何通过 MessageQueue.next() 来取消息设置阻塞状态的。 我们取消息采用了一个无限 for 循环,当没有消息的时候,则把标记位 nextPollTimeOutMillis 设置为 -1,在进行下一次循环的时候,通过 nativePollOnce() 直接让其处于线程阻塞状态。 再看看我们的消息分发是怎么处理的,主要看上面的 msg.target.dispatchMessage(msg) 方法。 原来 msg.target 返回的是一个 Handler 对象,我们直接看看 Handler.dipatchMessage(Message msg) 做了什么。 总结: 在主线程中 Looper 对象自动生成,无需手动生成。而在子线程中,一定要调用 Looper.prepare() 创建 Looper 对象。如果在子线程不手动创建,则无法生成 Handler 对象。 分发消息给 Handler 的过程为:根据出队消息的归属者,通过 dispatchMessage(msg) 进行分发,最终回调复写的 handleMessage(Message msg)。 在消息分发 dispatchMessage(msg) 方法中,会进行 1 次发送方式判断: 1. 若 msg.callback 属性为空,则代表使用了 post(Runnable r) 发送消息,则直接回调 Runnable 对象里面复写的 run()。 2. 若 msg.callback 属性不为空,则代表使用了 sendMessage(Message msg) 发送消息,直接回调复写的 handleMessage(msg)。 常规的消息 Message 是如何创建的? 我们经常会在 Handler 的使用中创建消息对象 Message,创建方式也有两个 new Message() 或者 Message.obtain()。我们通常都更青睐于 Message.obtain() 这种方式,因为这样的方式,可以有效避免重复创建 Message 对象。实际上在代码中也是显而易见的。 Handler 的另外一种使用方式 前面主要讲解了 Handler.sendMessage(Message msg) 这种常规使用方式,实际上,我们有时候也会用 Handler.post(Runnable r) 进行处理,我们当然应该看看里面是怎么处理的。 从官方注释可以看到,这会直接将 Runnable 对象加到消息队列中,我们来看看 `getPostMessage(r) 到底做了什么。 我们上面的分析是对的。在 getPostMessage(Runnable r) 方法中,我们除了通过 Message.obtain() 方法来创建消息对象外,专门把 Runnable 对象赋值给了 callback,这样才用了上面做消息分发的时候,通过这个标记来判断是用的 post() 还是 sendMessage() 方式。 到底是如何发消息的? 一直在说通过 sendMessage() 方式来发消息,到底这个消息是怎么发送的呢? 直接看 sendMessageAtTime()。 enqueueMessage() 里面做了什么? 至此,你大概明白了两种方式的区别了。 写在最后 本次内容可能讲的比较多和乱,还望大家跟着到源码中一步一步分析,最困难的时候,就是提升最大的时候!
写在前面 转眼间 面试系列 已经到了第九期了,由于文章将会持续更新,导致标题难看性,所以以后的标题将更正为本文类似的格式。 好了,话不多说,还是直入主题吧。 面试场景 讲讲 Android 的事件分发机制? 基本会遵从 Activity => ViewGroup => View 的顺序进行事件分发,然后通过调用 onTouchEvent() 方法进行事件的处理。我们在项目中一般会对 MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_CANCEL 分情况进行操作。 有去查看源码中的事件拦截方法吗?或者说在进行事件分发的时候如何让正常的分发方式进行拦截? 我知道有个拦截事件的方法叫...叫,onInterceptEvent()?应该是,不过由于平时项目较多,确实没时间去关注太多源码。 厄,那你觉得在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样? 肯定是优先响应子 View 的,至于为什么这样,平时知道这个结论,所以没去太深入研究,但我相信我简单看一下源码是肯定知道的。 先发表点扯淡 我们可能经常会遇到上面的这种情况,面试官希望了解我们知识的深入情况,或者说是平时学习欲望到底怎样。可很不幸的是,我搞 模拟面试 以来,80% 的小伙伴都属于开发能力不错,可对类似事件分发这样的基础问题一概不知。究其原因,除去忙以外,大多数小伙伴还是觉得平时开发也用不上什么,即使用到了,直接 Google 一下便能得到正确答案。 这大概就是很多人不会自定义 View 的原因吧,大多数效果在 GitHub 上都是现成的了,即使不太一样,也可以简单改改完事。 可很遗憾的是,我模拟面试那额外的 20% 的人,总拿到了令大多数人羡慕嫉妒恨的 offer,这不是没有原因的。可能别人就平时的开发中保持了更多的一点求知欲,就学到了很多至关重要的细节知识。 正文 还是不能偏题,其实这样的一个面试问题,确实是一个较为普遍的问题,我相信同类型的文章,网上一搜也是比比皆是,而且简单看一下关注度就能知道有多少人倒在了这种源码类型的面试上。 一般情况下,事件列都是从用户按下(ACTION_DOWN)的那一刻产生的,不得不提到,三个非常重要的与事件相关的方法。 dispatchTouchEvent() onTouchEvent() onInterceptTouchEvent() Activity 的事件分发机制 从英文单词中已经很明显的知道,dispatchTouchEvent() 是负责事件分发的。当点击事件产生后,事件首先会传递给当前的 Activity,这会调用 Activity 的 dispatchTouchEvent() 方法,我们来看看源码中是怎么处理的。 注意截图中,我增加了一些注释,便于我们更加方便的理解,由于我们一般产生点击事件都是 MotionEvent.ACTION_DOWN,所以一般都会调用到 onUserInteraction() 这个方法。我们不妨来看看都做了什么。 很遗憾,这个方法实现是空的,不过我们可以从注释和其他途径可以了解到,该方法主要的作用是实现屏保功能,并且当此 Activity 在栈顶的时候,触屏点击 Home、Back、Recent 键等都会触发这个方法。 再来看看第二个 if 语句,getWindow().superDispatchTouchEvent(),getWindow() 明显是获取 Window,由于 Window 是一个抽象类,所以我们能拿到其子类 PhoneWindow,我们直接看看 PhoneWindows.superDispatchTouchEvent() 到底做了什么操作。 直接调用了 DecorView 的 superDispatchTrackballEvent() 方法。DecorView 继承于 FrameLayout,作为顶层 View,是所有界面的父类。而 FrameLayout 作为 ViewGroup 的子类,所以直接调用了 ViewGroup 的 dispatchTouchEvent()。 ViewGroup 的事件分发机制 我们通过查看 ViewGroup 的 dispatchTouchEvent() 可以发现。 注意其中红框里面的代码,看注释也能知道,定义了一个 boolean 值变量 intercept 来表示是否要拦截事件。 其中采用到了 onInterceptTouchEvent(ev) 对 intercept 进行赋值。大多数情况下,onInterceptTouchEvent() 返回值为 false,但我们完全可以通过重写 onInterceptTouchEvent(ev) 来改变它的返回值,不妨继续往下看,我们后面对这个 intercept 做了什么处理。 暂时忽略 判断的 canceled,该值同样大多数时候都返回 false,所以当我们没有重写 onInterceptTouchEvent() 并使它的返回值为 true 时,一般情况下都是可以进入到该方法的。 继续阅读源码可以发现,里面做了一个 For 循环,通过倒序遍历 ViewGroup 下面的所有子 View,然后一个一个判断点击位置是否是该子 View 的布局区域,当然还有一些其他的,由于篇幅原因,这里就不细讲了。 View 的事件分发机制 ViewGroup 说到底还是一个 View,所以我们不得不继续看看 View 的 dispatchTouchEvent()。 截图中的代码是有删减的,我们重点看看没有删减的代码。 红框中的三个条件,第一个我就不用说了。 (mViewFlags & ENABLED_MASK) == ENABLED 该条件是判断当前点击的控件是否为 enable,但由于基本 View 都是 enable 的,所以这个条件基本都返回 true。 mOnTouchListener.onTouch(this, event) 即我们调用 setOnTouchListener() 时必须覆盖的方法 onTouch() 的返回值。 从上述的分析,终于知道「onTouch() 方法优先级高于 onTouchEvent(event) 方法」是怎么来的了吧。 再来看看 onTouchEvent() 从上面的代码可以明显地看到,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,那么 onTouchEvent() 就会返回 true 消耗这个事件。CLICKABLE 和 LONG_CLICKABLE 代表 View 可以被点击和长按点击,我们通常都会采用 setOnClickListener() 和 setOnLongClickListener() 做设置。接着在 ACTION_UP 事件中会调用 performClick() 方法,我们看看都做了什么。 从截图中可以看到,如果 mOnClickListener 不为空,那么它的 onClick() 方法就会调用。 总结 本来写到这就结束了,但回顾一遍还是打算给大家稍微总结一下。 需要总结的小点: 1、Android 事件分发总是遵循 Activity => ViewGroup => View 的传递顺序; 2、onTouch() 执行总优先于 onClick() 原本想用文字总结的,结果发现简书上还有这样一篇神文:Android事件分发机制详解:史上最全面、最易懂,所以直接引用一下其中的图片。 Activity 的事件分发示意图 ViewGroup 事件分发示意图 View 的事件分发示意图 事件分发工作流程总结
连载内容镇楼:Android 面试(一):说说 Android 的四种启动模式Android 面试(二):如何理解 Activity 的生命周期Android 面试(三):用广播 BroadcastReceiver 更新 UI 界面真的好吗?Android 面试(四):Android Service 你真的能应答自如了吗?Android 面试(五):探索 Android 的 HandlerAndroid 面试(六):你已经用 SharedPrefrence 的 apply() 替换 commit() 了吗?Android 面试(七):Serializable 这么牛逼,Parcelable 要你何用? 写在前面 面试系列已经相隔很久没更新了,主要是因为南尘近期搞的「模拟面试」活动花费了太多时间,所以对我的广大读者朋友们深表歉意,不过现在开始依然是一有时间就会更新的。毕竟金三银四,想必想换东家的小伙伴也是比比皆是。 面试场景 什么是 Activity、View、Window? Activity 是四大组件之一,也是我们的界面载体,可以展示页面;而 View 实际上就是一个一个的视图,这些视图可以搭载在一个 Layout 文件上,通过 Activity 的 setContentView() 方法传递给 Activity;Window 是一个窗体,每个 Activity 对应一个 Window,通常我们在代码中用 getWindow() 来获取它。 你是怎样理解它们三者之间的关系的? Activity 像一个工匠 ( 控制单元 ),Window 像窗户 ( 承载模型 ),View 像窗花( 显示视图 ) LayoutInflater 像剪刀,Xml 配置像窗花图纸。 比喻挺生动,请问可以通俗一点么? Activity 下装了一个 Window,Window 下装了 View,呃... 正文 这是我在「模拟面试」活动中一个真实的场景,应试者的答案并不能忽悠到我,因为这个答案网上早就传遍了,但一旦稍微变动一下,应试者的表现就差强人意,很明显,这位小伙伴没对源码进行更加深刻的理解,而只是简单地背下了答案。 我们来直接看看实战中的代码,相信大家都知道 Activity 通过 setContentView() 方法来加载布局,我们来看看 setContentView() 方法到底是怎样做的。 实际上是 getWindow().setContentView() 做的处理,那这个 getWindow()? 你想的没错,这个 mWindow 实际上就是 PhoneWindow。Window 是一个抽象类,而 PhoneWindow 实际上就是 Window 的实现继承类。我们直接看看 PhoneWindow 的 setContentView() 方法,看看会有什么新发现? 先判断了 mContentParent 是否为空,这个 mContentParent 是什么玩意儿? 这个 mContentParent 是一个 ViewGroup 对象,而从注释中可以明显地看到 Window 中的内容就放置在这里。如果为空,则直接执行 installDecor(),这里想都不用想都知道是在实例这个 mContentParent,我们可以直接进入源码来验证我们的猜想。 这里代码挺多,我就不截完了,但逻辑不难,我们先判断 mDecor 是否为 null,如果是,则直接初始化它。然后判断 mContentParent 是否为 null,如果是,则直接通过 mDecor 去初始化 mContentParent。 这块其实讲到这里大家就差不多了解了,这个问题也就不那么难答。 每个 Activity 包含了一个 Window 对象,这个对象是由 PhoneWindow 做的实现。而 PhoneWindow 将 DecorView 作为了一个应用窗口的根 View,这个 DecorView 又把屏幕划分为了两个区域:一个是 TitleView,一个是 ContentView,而我们平时在 Xml 文件中写的布局正好是展示在 ContentView 中的。 用个图展示一下。
前途比现金重要,公司带给你的成长,才是你最宝贵的财富。 —— 题记 写在前面 大家周末愉快,完全没想到上一篇白话文 我为什么想离职?看起来还挺受欢迎,虽然写的不好,真的不好,哈哈,不过我会好好努力提升自己的文笔的,争取给大家带来赏心悦目的文字分享。 怎么最近都没写纯技术文章了? 这个问题,很多人问我,统一再次回复: 个人慢慢觉得技术在 Blog 网站学习可能更佳有效,因为在公众号学习这些东西的话,实在是太零散了。公众号是更加推崇分享的地方,所以我愿意把我平时的思考和想法首先给大家分享。那么,说好的 面试系列 呢?依然进行!!!技术文章,也照发,谁让你们的小伙伴 nanchen 就是一个纯粹的技术控呢?哈哈,我以后会把 面试系列 跳转链接直接放在一篇总结推文中,供大家系统学习。 本篇推文,且听 nanchen 再来吹逼吧,准备好了吗?我要开始啦。 正文 又是一年年终时,不知道各位的年终奖是否已经到位,也不知道各位的年会都抽到了什么大奖。不要问 nanchen 我,贫穷已经限制了我的想象,什么都没有,昨天举行了我们 「致学教育」 的年会,作为操纵抽奖的我,也是为了我的人生安全,愣是不敢让自己中奖。 其实年终奖什么的,都是一个企业的文化,虽然我们互联网公司可能大多数都有这项福利,不过没有的小伙伴们也不要灰心。公司要做,肯定是为了挣钱,绝对不是为了省成本。当公司挣钱了,大家的福利当然是少不了,而且只要你为公司做了足够的贡献,要是公司没看到,那是公司的问题,你该走就走,完全不需要磨磨唧唧。 干了五年,薪资却与一个刚进来的毕业生持平。 这是今天在我的 QQ 群看到的一段这样的聊天。 image 看到这样的聊天,不得不说,其实心里酸酸的,这位北京的兄弟,干了整整 5 年 Android 开发,而且涉及了 Android 底层源码应用,却只拿到与普通应届生持平甚至更少的薪资,这是非常令人遗憾的,也难怪来自腾讯的 XX 码农一副为他打抱不平的样子。 猜猜出现这样情况的可能原因 其实出现这样的情况,意料之外,也是情理之中。抛开这位北京兄弟说的真实性,在北京作为一个 Android 开发拿这样的薪资都是非常意外的,但这也跟他在这家企业整整待了 4 年有一定的关系。首先就不吐槽他可能存在的个人能力上局限和公司可能存在的各种各样的原因了。以前看了不少关于薪资的文章,特别想与大家分享一下,大概是这样的一个意思,一贯的白话文风格,专业人士轻喷。 你的薪资水平出现这样的情况,最主要的原因是,现在应届生的起薪一直在增加,可能五年前应届生 3k 就可以招到,而现在却要 5k 了。而一直在一家公司干,仅仅靠内部调薪,一次也不可能调太多,要调得多,那肯定要大家都调的多,这样做,你公司的预算能支撑吗?不看应届生,公司在招其他人的时候,假设给的薪资和内部薪资一样,极有可能简历都没人投,或者,是一群不靠谱的人投递,这样的人,换做你是公司领导,你敢要吗?所以,只有简单粗暴,给出的价格高于市场平均才行。毕竟,抛开企业文化来讲,可能也只有薪资待遇最吸引人了。所以,才出现了你的薪资被市场越甩越远。 如何涨薪? 说了这么多,那如何涨薪呀?下面是一些人的答案。 讨好直属上级 这是很多人的想法,在中国这样的一个人情味十足的社会,很多人会认为这是一个行之有效的方法。只要把直属上级讨好了,让上级在领导面前给你美言几句,分分钟获得加薪。 个人不敢苟同。且不说你表现不够强行给你加分是给他拖后腿,而且大多数直属上级都没有决定你薪资的权利。很简单的问题,上面也提到了,给你加了,别的人不加吗?不给别人加,别人会跳槽吗?全部加薪的话,那这个资金的坑谁来填补?不差钱?土豪公司,带带我。 提离职 这也是相当一部分人的想法,想着。反正在公司薪资福利也不行,直接暗示甚至明示领导,想离职,看领导挽留与否。一来可以看看领导是否重视自己,二来看看能不能达到自己的目(加)的(薪)。 这种法子可用么?在下就不在这评论了。如果你能遇到一个给你加了薪,而且胸怀宽广后面不给你穿小鞋,也不会寻求可以替代你的人的领导,那我真的服气,你去试试,我什么都不想说了。 正确的加薪姿势 有不少企业会墨守成规,比如我女朋友的公司(今年还是对我女朋友破例了),固定一年两调,一次封顶 600。这么做的最大可能结局就是:最优秀、最有能力的人因为外部更好的机会和更丰厚的薪资福利跑路了,只留下一批中庸甚至能力不足的员工,这些人在市场上没有更好的机会,所以只好留下来,或者是生于安乐,已经习惯了现有的舒适环境。 根据二八法则,一个企业的利润 80% 可能只由那么几十个甚至是几个核心员工拿下,所以假设你觉得你目前薪资确实不及你的能力,不妨直接去找领导交谈吧。记住,千万不要把「离职」什么的挂在嘴上,在领导面前,谈「离职」这样的敏感话题,真的是找死,不管你领导是个多么多么慈爱的人。一个公司,肯定是想方设法的赚钱,而不是在你一个人身上怎么省成本。所以如果你能带给公司足够的利益,而公司没给到你足够的奖励和重视的话,我认为去找领导谈一谈绝对是一个行之有效的方法。能不能谈下来,那是另外一回事。 都去提离职了,公司却加薪了,怎么办? 屏幕前的小伙伴,有遇到这样的情况的人吗?我猜有,嗯,一定有。 不少人遇到了这样的问题,非常的犹豫不决,公司加薪的薪资待遇自己能接受,但自己已经用「离职」表示了自己对公司的不忠心。假设走吧,这么久的公司,还是多舍不得。不走吧,领导会不会怀恨在心,以后借机给自己穿小鞋,甚至有了备胎马上换掉自己。 这个问题,至少我没遇到,反正我提想离职的时候,不是这样的结局,但个人还是觉得应该看老板,老板小肚鸡肠,缺乏安全感,还记仇?那你得好好想一想,你是瞎眼了么?跟了这样一个领导,还跟了那么久? 如果你的老板心胸宽广,完全认可你的表现和能力以及你对公司的价值,那么好好思考吧,我们真的应该心怀感恩。 留你除了你的价值,还有公司的成本 我司年前疯狂招人,实知招人不易,招到一个靠谱的人更加不易。其实公司招一个新人是很需要成本的。 首先,现在市场价可能比你的薪资高,招进来可能需要花更高的薪资; 招聘需要成本,需要安排专人筛选简历,需要安排专人面试,需要安排专人培训,需要新人用时间来适应,还有可能招到不靠谱的人,那就更加糟糕了。 而你还对公司有足够的价值,那公司领导加薪留你,人之常情,无可厚非呀。 说点题外的 如题,说点题外的,上篇文章,看到有小伙伴给我反馈,本来不想离职,但看了文章,反而想离职了。这就尴尬了,伤害你,本不是我的本意~ 结合昨天年会好几位领导给我说的话,奉劝大家,跳槽,不要仅仅为了钱。我在上篇文章写到,我想离职,不是完全因为钱,而是因为觉得做了太多杂事,技术提升太慢。 哈哈,好像说到底还是为了钱,我提升技术,很大程度也是想创造自己的价值,挣更多的钱。 奉劝大家,前途比现金重要,公司带给你的成长,才是你最宝贵的财富。你的辞呈可能是你正确的加薪姿势,但前提是,你的实力足够强,公司认可你的价值,而不是一个谁都能替代的角色!!! 我是南尘,只做比心,欢迎关注我。
写在前面 本周六晚上面试了一位来自杭州的 17 年应届毕业生,来自计算机科学学院软件工程专业,也是一位非常爱学上进的小伙伴,在微信上约了我无数次「模拟面试」。因为非常着急着找工作,而前面三家面试也是均以失败告终,所以一阵纠结下来,还是选择了先帮帮他。 首先从交流态度上,这是一位态度非常好的小伙伴,可在做事风格上一直觉得有待商榷。比如,简历直接给我一个 word 文档,结果还因为格式有问题,我这边打不开。而后直接给我发了俩简历手机截图,在一番交流后,终于明白了把 PDF 格式转给我。其实我很能理解可能一些应届生或者是工作几年的同学都会这样,但我真的再重申一遍,你不能保证你的 word 格式等出现一些问题,请尽量使用 PDF 格式简历吧。 面试过程 其实和他的面试过程没啥重要的东西,因为问到一半,就已经没啥好问的了,作为一个应届生,确实在项目经验上可能这是一个概况,我们从来不应该像要求高级 Android 开发工程师那样去要求别人。不过也不应该有这样一个误区,不要觉得应届生,项目经验少就是正常的。至少我前面接到我「模拟面试者」有些才大二,就已经有非常成熟的几个项目经验了,关键,还做的挺不错。 那作为应届生,实在没项目经验怎么办? 写点你丰富的在校实践经验呀,哪怕把你的小事做的重要性讲一讲也好。大多数公司,其实最看重的并不会是你目前的项目能力,而是你的学习能力和你的处事态度,俗称靠不靠谱。 只做了很少的项目自然也没事,写出你在该项目中做出的突出贡献,写出你的技术亮点,而不要像记流水账一样泛泛而论。 面试反馈 说实话,该小伙伴,在找工作方面存在很多问题,虽然我其实挺喜欢他。不过,写在这,权当忠言逆耳利于行吧。当然,这肯定不是只写给他一个人的,年后是很好找机会的时间段,也希望对大家有所帮助。 请做一下自我介绍 至少在我接触过的面试中,这是面试的第一个环节,「请做一下自我介绍」。 自我介绍是你和面试官的第一次面对面交流接触,一个好的自我介绍能让你获得相当不错的印象分。这个自我介绍最好控制在两分钟左右,不要太长,也不要太短。 那么这是一个问题,很多人根本不知道怎么做自我介绍。比如昨天的面试者,自我介绍不到 30 秒,没了。就介绍了自己的名字,然后说自己之前任职于一家互联网公司,然后没了。对,没了。正当我想听到都在之前公司做了些什么工作的时候,告诉我,没了。 后面经过交流得知,这位上进的小伙伴因为平时很少和别人交流,也没刻意准备过自我介绍,面试经验较少,所以自我介绍不知道说什么。 我这里给一个大概的套路,先介绍自己的基本信息没问题吧,再介绍自己之前的工作经历,例举一到两个自己在工作中的亮点,这是极好的,一来说了自己的闪光点,二来也引领了面试官可能会问的话题。当然,这仅仅是个人观点,还请大家见谅。 忘了自己的自我介绍 另外一点,就是真正到了面试时候,忘了自己之前准备的要介绍什么。个人比较推荐的一种方式是,你去一家公司面试之前,先想好自己的自我介绍,最好是直接写在纸上,自己多加斟酌,然后再刻意背一下。其实这时候根本不需要背了,我说的对不对,你后面验证一下便知。 可怕的简历格式 每次都要强调简历格式,我真的不想把我这个「模拟面试」改成一个「简历修改」活动,劳烦大家多看看前面的反馈,先把简历做好再来。我从来都不是一个会写好简历的人,甚至也是被面试官吐槽过简历的人,所以我也不能说能给到什么足够好的简历,但下面这些基本要素,大家还是应该遵守的。 第一页放重点 想必现在大家都应该明白了,简历的第一页要放更吸引人的地方,这也是别人给到面试邀请最重要的东西。主次分明,把最重要最闪光的放在最前面。不知道的话,建议以这样一个排版:个人信息 => 技术亮点 => 项目经历 or 工作经历 => 其他。没有绝对的模板,适合你的,才是最好的。 不相关的技术不要杂糅在一起 这是该小伙伴简历中比较明显的一个问题,两个完全不相关的技术,给写到一点里,实际上这是非常不好的一种体现。虽然看似没什么,但可能有些有心的面试官,起码可以从这里猜测,你的代码或许也会这样,乱分包乱放,当然,或许你其实是一个分包命名非常规范的好同志。 项目经验不要全是标签 这同样是一个非常普遍的问题,在我面试的很多人中,确实就存在这样的情况,全是运用了 XXX 框架,会用 XXX 设计模式,会用 XXX 技术。亲爹耶,虽然这好像其实也没什么,但在这里,非常建议大家能写点实质的东西好伐。这对你引领面试官问你想要的问题非常有帮助的。 别告诉面试官你忘了 这同样是一个面试中好像很常见的问题,其实作为一名开发者,是很能理解大家的。忘记自己的代码,是非常正常的,包括简历上所写的。我们在开发中会写几十万甚至几百万行代码,谁能把自己的代码都记住,谁能保证自己学习过的东西几年不用还能完全会用,不能。生活中,这样的人才太少了。 但自己简历上写了,恰好面试官又问到了,怎么办? 千万千万不要直接用一个「忘了」应付这个问题,你至少也得说点思路吧,当时的开发场景肯定还是能记住一些的。即使你思路也忘了,假设现在让你实现,我不相信你一点想法都没有。 夸张简历是正常的,但明说就不好了。 这位小伙伴,也是很奇葩,当我问到简历上的「事件分发机制」的时候,他竟然告诉我,简历是夸张的。醉了醉了,不带这么直男的好伐,这就尴尬了。非常佩服该小伙伴的勇气和坦白精神,但这是面试呀,学会委婉的表明你的意思,这是一项很受用的本领。 写在最后 姑且就写这么多,本次面试在一定意义上不是很成功,受网络和环境的印象,后面都是用电话交流的。但因为存在太多的问题,后面可以重面,希望该小伙伴,能把简历弄好,在下周的面试中百战不殆。
写在前面 想和这位面试者一样拥有这样的一份面试反馈报告吗?那就赶紧参与报名吧~,报名原文在这里:给 Android 开发者的福利:免费模拟面试。 模拟面试活动反馈 没错,原谅我每篇文章可能都会加上这个前缀,这是我公众号最新推出的福利,旨在帮助到更多的人。 虽然这偏重于给活动参与者的面试报告,但纯反馈建议,也许一样适合你! 为什么发起这个「模拟面试」? 大方向肯定是互利双向的,我了解了目前大方向的情况,你得到了可能你在大多数公司都得不到的面试反馈,而且不用占用你的工作时间,因为我们全部采用晚上 9 ~ 10 点呀。所以还在犹豫什么?赶紧加入我们吧?只需要在后台回复「模拟面试」即可哦~ 面试准备 上周五也是进行了本次活动的第一场「模拟面试」,面试者是从 13 年毕业的一个本科生,目前也是在职于上海一家上市公司。工作经验挺丰富的,也是准备近期跳槽到阿里,而且目前已经得到了面试邀请。非常棒的一位 Android 开发工程师,对技术有着十足的热情,哈哈,在某些技术方面我自愧不如。 从简历上可以看出,该面试者拥有非常全面的 Android 应用开发技术,对市场部比较出名的三方库 RxJava、Retrofit、Dagger2 等都有着非常长远的了解,并且熟悉知名库的源码设计思想;精通 Android Framework 层,对 Android 的四大组件、IPC 通信、多线程编程都得心应手;同事熟悉 Android 的性能优化、内存优化,以及插件化和热修复;业余时间也是一名写作爱好者,有自己的博客论坛。当然,除 Android 外,也有了解微信小程序和一些 JS 前端相关知识。 面试反馈 针对面试前后面试者的表现,我基本面试结束马上反馈了一些问题,所以现在也分享给大家,鉴于南尘的水平,肯定无法保证观点都正确,所以还恳请专业的同学们在评论区给与指正和建议。 多关注一下算法吧 面试者本次在算法面试中,发挥不是足够的理想,所以希望先对算法方面做一定的面试储备。相对其他小公司,阿里在内的大公司可能并不那么在乎你目前的应用开发能力,而在乎你的代码是否赏心悦目,你的算法和数据结构基础是否足够给力。 如果没有到白问无一答误的境界,尽量不要写「精通」吧。 在该面试者的简历中,可以看到,前两个有明显的「精通」,精通 Android Framework,精通 RxJava 等开源库。所以我在面试的时候着重询问了这两方面的知识,还问了源码设计等。得到的答案大部分还是相当满意,但深度不是非常足够,虽然该面试者真的在这两方面很厉害了,但可能距离「精通」所要求的深度还是有一定的距离的。 后面详细的问了该面试者这样做的原因,原来是和当年的我一样,觉得写个「精通」更容易得到面试机会,而且自己觉得这两方面确实自己很强。我们投简历前不要被招聘要求上的各种「精通要求」吓到,大多数情况下招聘要求都是大概意思,不是代表我们必须要达到。 项目经验希望写的更加有特色 该面试者的简历中,写了四个项目,看起来除了目前在做的项目做了网络框架的大换血以外,其他的项目貌似都是写用了什么框架和技术,达到了什么效果。这样看起来杂然无味,基本就是流水账的记录了工作内容,没有看到工作的两点和进步。 所以可以的话,把自己的项目缩减到三个为宜,因为有广为流传的「黄金 3 项目」,所以希望大家除非项目都特别出色的话,建议就挑选 3 个项目为宜。3 个项目的介绍如果可以,尽量不要写的格式一样吧,尽量地写的更有你的特色并重点介绍你的工作成长。比如「X 个月完成了 XXX 模块的重构,以及重构的原因」等。 注意扬长避短 该面试者原本毕业于生物专业,毕业后做了半年的教师,后面由于女朋友的关系(因为女朋友是 Java 开发),所以转行为开发。没有任何基础,怎么转行呢?除了女友帮忙外,还加入了培训机构学习了半年,之后顺利加入一家运动类型公司做运动圈开发,这整个过程在面试者自我介绍的时候一五一十地说了出来,而且在简历上还标注了自己的这部分经历。 个人觉得首先在简历上没必要添加这项东西,只需要写上自己的毕业时间和学校即可,无需让别人知道你是什么专业的( 计算机相关专业写上也没问题 ),这当然没有任何歧视其他专业或者通过培训机构出来的人,其实好多从培训机构出来的大牛,我这里也不一一例举了。三百六十行,行行出状元,这里没有对培训机构的歧视,这不是不光彩的事情,但我认为它不重要,花更多的时间对自己的项目做更加详细的介绍,才是真正必要的。 尽量保持你的专业性 该面试者的简历中,经常出现 “android”、”androidstudio” 这样的字段,希望大家不要犯这样的低级错误,用更加专业的词语:”Android”、”Android Studio”,合适的细节助你更加自信,即使可能面试官并不一定注意这些东西。 写在最后 杂一看写了挺多,别说些有用没用的,面试本身是一个随机含运气成分的东西,多夯实你的技术功底,才是重中之重。 祝我们一起为该面试者祈祷,阿里面试成功突围!
写在前面 大家好,我是「南尘」,一个爱分享爱学习的 Android 技术控。目前在 GitHub 上有着差不多 6k 的个人项目 Star 数,之前也为其他开源库贡献过大量的源码。在 掘金 和 简书 上也有着一定量的读者,是个不折不扣的 Android 技术控,目前在运营公众号「nanchen」。 做这个决定,确实纠结了挺久,在 17 年 8 月做 Android 巴士成都站讲师的时候认识了「兰柳学 Even」,当时非常受他的「一块编程和模拟面试」启发,后面一直保持联系,并且想把这项工作继续下去。所以,我来了。 到底是什么福利? 回到主题,本次福利主要是「模拟面试」。我作为一个还是面试了 200+ 员工的面试官,见过工作经验非常丰富的网易大咖,也领过刚刚毕业的应届生入门。虽不能面面俱到,但也希望能在有效的时间内给出我能给出的力所能及的帮助。 什么是模拟面试? 「模拟面试」的意思就是你作为一个面试者,我作为一个面试官进行面试,由于各种原因和地域的影响,「模拟面试」只能通过「微信语音」或者「微信视频」的方式进行,成都本地的可以直接面对面进行。 微信添加方式可以在公众号后台回复「面试」获取,面试前请准备好你的简历,如果有心仪的公司名称最好一并带上。 虽然原则上只能是「Android」,但也欢迎其他岗位的人加入我们的「模拟面试」,碰巧我也在面试其他岗位的人员,只是质量肯定没有「Android」那么好啦。 怎么参加? 只需要在公众号后台回复「模拟面试」,添加我的微信,备注「面试」,即可添加我的微信好友,我会和你联系。 这种方式收费吗? 当然不,收费还能叫福利吗?该项流程是全程免费的。当然,你实在觉得对你帮助很大,也可以选择在该文中进行打赏。 面试时长怎么处理? 时间可以微信约,微信面试最好是每天晚上的 9 ~ 10 点,大概的面试时间是 1 个小时,其中 30 ~ 40 分钟做「模拟面试」,后面的时间主要做面试的总结和交流建议。 你能收获什么? 技能评估:基本了解面试者的角色定位,技能权重,优缺点和技能亮点。 简历指导:结合面试者的技能和特点,对简历进行增删改。 模拟场景:模拟面试,让面试者尽可能收获真实的体验。 面试报告:主要包括情况分析和面试打分等。 我能收获什么? 实际上这是一个双赢的过程,虽然准备面试确实需要花一定的时间,但对我来说面试更多的人能让我对当前大家的知识储备有个相应的了解。而对你来说,可能随着我经验的增加,也能带给你更多不一样的思考。 这在一定意义上也将成为我的 面试系列 的灵感来源。 写在最后 永远不要想一个简单的「模拟面试」能让你轻松找到工作,也不是一定要到要换工作了才想起来「模拟面试」和「撰写简历」,作为一个 Android 开发者,需要时时刻刻跟随技术的浪潮,引领时代。 希望能帮助到更多的人,让我看看有多少人点赞分享。 最后,祝大家面试全过,Offer 任选。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
时光荏苒,恍惚间 2018 开始已经 3 天了,我知道这两天一定又是开始了一堆年终总结,当然我也不是跟风,只是去年立下的 Flag,从 2017 年开始,必须要做个年终总结。 关于工作 在「致学教育」已经两年了,公司从无到有,目前刚刚完成 B 轮融资,也是非常开心。今年初对我们的 APP 进行了完整的重构,APP 也是在风格上更加贴近 Design 设计。除了 APP,公司也是承接了相当大量的开发板上开发 Android APP 的业务。 2017,前半年我主要负责的是 APP 的迭代开发和重构,后半年主要负责了 APP 的功能迭代和开发板 APP 的任务,当然,还多了一份职责「打杂」。当然这个「打杂」我划了引号,主要是一些部门日常的管理和任务分发。 总体而言:2017,有成长,有压力,有认可,有涨薪,还结识了更多的朋友。 关于开源 大家都知道我喜欢在开源圈子里混,2017 也是我 GitHub 关注度飞涨的一年。这一年我开源了图片压缩库 「CompressHelper」,也开源了 Retrofit & RxJava & MVP 的架构 APP「AiYaGirl」,当然还有自动获取银行名称和卡片类型的「BankCardUtils」以及 RxJava2 的一整套 Blog 教程 和库「RxJava2Example」。 目前我的 GitHub 社区 Star 总数马上 6k,也是对我本年开源欢迎度的一种肯定,虽然其实并不能说明什么,但还是挺开心的。 对了,上半年维护的图片选择库 「ImagePicker」我已经没有维护了,而且我和网络库「OkGo」的作者「廖子尧」也只是朋友,发现总有人把我俩认混淆,一个是「若尘」,一个是「南尘」,有那么容易混淆吗?哈哈。 总之,GitHub 2017 年情况概览为: nanchen GitHub 2017 数据:个人仓库项目总 Star 5.8k,Java 语言中国区排名 49。 nanchen 关于写作 2017 年在写作方面也是辗转反侧,年初在 博客园 加入了访问统计功能,目前收集到总访问量是 不到 20w,当然,博客园我只更新到了 5 月份,此后就在 简书 重新写作了,目前也是 2k 粉丝不到。作为一个 Android 开发工程师,自然也是在年中去 掘金 注册了账号,并开始了同步更新。 总有人问我,为什么自始至终没有选择「CSDN」,毕竟「CSDN」可是巨佬很多呀,这是因为个人觉得 「CSDN」 广告偏多了,所以选择了放弃。 5 月份的时候,还受邀加入了「Android 巴士」的写作阵营,并作为 Android 巴士线下交流会成都站 的讲师,与成都的小伙伴们分享了自己的成长经历和心得。 简书 7 个月成果:粉丝:1.7k,文章 37 篇,字数 74686。 nanchen 掘金 4 个月成果:粉丝:1.3k,阅读 35k,喜欢 3.1k。 nanchen 博客园 5 个月成果:粉丝 278,阅读 200k。 nanchen 关于公众号 在 2017 年我的生日当天,我开启了我的公众号之旅,目前也运营了半年,虽然粉丝和阅读量都不多,但我从未想过放弃。或许我没有太大的流量,但我始终不渝地在坚持。 原本「公众号」是应该放在「关于写作」专栏的,但我却选择了单独提出来,这是因为我比较看重我的公众号这个栏目。关注我的人都知道,目前虽然我公众号支持投稿,但目前也是基本是纯原创。不是说从来没人投稿,而是大多数人投稿的内容,我都觉得太次了,而选择自己都觉得不够档次的内容推送给大家,显然是非常不负责任的。 近期公众号更新频率非常低,主要也是因为家庭的事情一直处理不好,前段时间出的车祸,到现在还完全谈不到一起,真的非常难受。等我稍微轻松一点,我一定会继续保持平稳的更新频率的。另外,也像大佬们学习一下,给大家尽量的发点福利。 写在最后 最后,想感谢又陪伴我一年的所有粉丝、朋友和亲人,感谢你们的不离不弃和包容,让我在一次又一次的命途多舛,还在不断前行。 期待 2018,我们都能拥有更加明显的进步!
连载内容镇楼:Android 面试(一 ):说说 Android 的四种启动模式Android 面试(二): 如何理解 Activity 的生命周期Android 面试(三): 用广播 BroadcastReceiver 更新 UI 界面真的好吗?Android 面试(四):Android Service 你真的能应答自如了吗?Android 面试(五):探索 Android 的 HandlerAndroid 面试(六):你已经用 SharedPrefrence 的 apply() 替换 commit() 了吗? 一些闲聊 距离上一篇文章似乎又是很久了,看起来也没有很多反馈,催更就更不用说了。哈哈,放弃了。 nanchen 话说最近公司在招聘一批至少 5 年开发经验的 Android 开发工程师,我也是忙开了花,激动得不行呀。虽说我面试过的技术开发至少 50 人以上,但这还是第一次开始面试 Android,此时犹如大姑娘上轿,还真是头一回呀! 所以非常非常非常用心地准备了良久,然后满怀激动地开始了我的 Android 面试官角色。 无奈,面试后的感觉,均是开发效率听起来很牛逼,第三方 API 用起来非常顺手,但问到基础,就拿我面试系列的题去问,没一个答得上的,甚至是循循善诱,都没法好好回答。 nanchen 面试场景 Android 开发中对两个 Activity 之前传递数据,应该很熟悉吧? 嗯,当然没问题。一般采用 Intent.putXXX() 就可以实现各种轻量级数据的传递。 那对于自定义的 Object 呢? 直接使用 Bundle 的 putSerializable() 即可。需要把对象实现 Serializable 接口,最后使用 Intent.putExtras(Bundle) 把数据放进 Intent 即可。 除了这种方式,还有其它方式吗?和这种方式有什么区别呢? 我知道还有 Bundle.putParcelable() ,不过我们平时基本都只用 Serializable 方式。 为什么不用 Parcelable 方式呢?它们有什么不同呢? 因为简单呀,Serializable 方式只需要实现接口一句代码就好了,Parcelable 我记得有很多代码。对于它们的区别嘛,em......额......嗯....... 正文 上面的场景,实际上就是在我近期发生的。作为一个简历上 09 年入行的大龄 Android 程序员,我非常肯定他的开发能力和解决问题的能力,在这方面肯定甩我很多条街,不过至少在我问的问题上让我有点大跌眼镜,问到自定义 View 的绘制顺序,直接回答不知道。问到 LaunchMode,支支吾吾,不清楚。实际上不由得让我们思考,到底是怎么了,难道现在对于这么多的程序猿,写出符合需求的代码就变得这么重要了么?还好,当下还有很多坚持在一线,努力把基础带给大家的大神,比如,扔物线朱凯,还有非常非常多的伙伴们。 大多数人可能都知道,Serializable 和 Parcelable 方式最大的区别是效率上的差异,而且对于小数据,其实差异并不是很大,这些差别其实用户层面是并不容易发现的。但这并不代表着,我们的开发就可以忽视这几十毫秒甚至是几毫秒的差距。 Serializable 和 Parcelable 的区别 可以肯定的是,两者都是支持序列化和反序列化的操作。 两者最大的区别在于 存储媒介的不同,Serializable 使用 I/O 读写存储在硬盘上,而 Parcelable 是直接 在内存中读写。很明显,内存的读写速度通常大于 IO 读写,所以在 Android 中传递数据优先选择 Parcelable。 Serializable 会使用反射,序列化和反序列化过程需要大量 I/O 操作, Parcelable 自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在 Native 内存中,效率要快很多。 有人直接比较过两个的效率差别 nanchen 我们可以来看看分别怎么写? Serializable 「简单易用」一直都是它的代名词 public class TestSerializable implements Serializable { String msg; List<ItemBean> datas; public static class ItemBean implements Serializable{ String name; } } Parcelable 速度至上 public class TestParcelable implements Parcelable { String msg; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.msg); } TestParcelable(String msg) { this.msg = msg; } private TestParcelable(Parcel in) { this.msg = in.readString(); } public static final Creator<TestParcelable> CREATOR = new Creator<TestParcelable>() { @Override public TestParcelable createFromParcel(Parcel source) { return new TestParcelable(source); } @Override public TestParcelable[] newArray(int size) { return new TestParcelable[size]; } }; } 很明显,Parcelable 实现起来并不容易,它有成吨的模板代码,这使得对象变得难以阅读和维护。但如果你真的想成为一个优秀的 Android 开发工程师,你可能就得多在 Parcelable 上花点时间了。实在想偷懒也没事,因为有人在 GitHub 上已经上传了 Android Studio 的插件,帮助你自动生成这一堆模板。 地址:https://github.com/mcharmas/android-parcelable-intellij-plugin 在两个 Activity 之间传递对象还需要注意什么呢? 对象的大小,对象的大小,对象的大小!!! 重要的事情说三遍,一定要注意对象的大小。Intent 中的 Bundle 是使用 Binder 机制进行数据传送的。能使用的 Binder 的缓冲区是有大小限制的(有些手机是 2 M),而一个进程默认有 16 个 Binder 线程,所以一个线程能占用的缓冲区就更小了( 有人以前做过测试,大约一个线程可以占用 128 KB)。所以当你看到 The Binder transaction failed because it was too large 这类 TransactionTooLargeException 异常时,你应该知道怎么解决了。
这是 面试系列 的第六期。本期我们将来探讨一个有趣的东西 —— SharePrefrence 的两种提交方式 apply() 和 commit()。 往期内容传递:Android 面试(一):说说 Android 的四种启动模式Android 面试(二):如何理解 Activity 的生命周期Android 面试(三):用广播 BroadcastReceiver 更新 UI 界面真的好吗?Android 面试(四):Android Service 你真的能应答自如了吗?Android 面试(五):探索 Android 的 Handler 开始 其实非常有趣,我们经常在开发中使用 SharePrefrence 保存一些轻量级数据,比如判断是否是首次启动,首次启动进入引导页,否则直接到主页面,或者是其它的一些应用场景。 而我们也耳熟能详这样的写法。 根据 Context 获取 SharedPreferences 对象 利用 edit() 方法获取 Editor 对象。 通过 Editor 对象存储 key-value 键值对数据。 通过 commit() 方法提交数据。 public class SplashActivity extends AppCompatActivity { public static final String SP_KEY = "com.zxedu.myapplication.SplashActivity_SP_KEY"; public static final String IS_FIRST_IN = "com.zxedu.myapplication.SplashActivity_IS_FIRST_IN"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 一些业务代码 ...... SharedPreferences preferences = getSharedPreferences("name",MODE_PRIVATE); if (preferences.getBoolean(IS_FIRST_IN,true)){ // 跳转引导页面 startActivity(new Intent(this,GuideActivity.class)); finish(); }else{ // 跳转主页面 startActivity(new Intent(this,MainActivity.class)); } } } public class GuideActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); getSharedPreferences(SplashActivity.SP_KEY,MODE_PRIVATE).edit().putBoolean(SplashActivity.IS_FIRST_IN,false).apply(); } } 从代码中可以看到,一阵混乱操作,没啥特别的地方,但早期开发的人员应该知道,之前我们都是比较青睐 commit() 进行提交的。而现在 Android Studio 在我们使用 commit() 直接提交的时候会直接报黄色警告。 nanchen commit() 和 apply() 到底有什么异同? 先说说相同点: 二者都是提交 Prefrence 修改数据; 二者都是原子过程。 不同点直接上源码吧,先看看 commit() 方法的定义: /** * Commit your preferences changes back from this Editor to the * {@link SharedPreferences} object it is editing. This atomically * performs the requested modifications, replacing whatever is currently * in the SharedPreferences. * * <p>Note that when two editors are modifying preferences at the same * time, the last one to call commit wins. * * <p>If you don't care about the return value and you're * using this from your application's main thread, consider * using {@link #apply} instead. * * @return Returns true if the new values were successfully written * to persistent storage. */ boolean commit(); 综合一下 commit() 方法的注释也就是两点: 会返回执行结果。 如果不考虑结果并且是在主线程执行可以考虑 apply()。 再看看 apply() 方法的定义: /** * Commit your preferences changes back from this Editor to the * {@link SharedPreferences} object it is editing. This atomically * performs the requested modifications, replacing whatever is currently * in the SharedPreferences. * * <p>Note that when two editors are modifying preferences at the same * time, the last one to call apply wins. * * <p>Unlike {@link #commit}, which writes its preferences out * to persistent storage synchronously, {@link #apply} * commits its changes to the in-memory * {@link SharedPreferences} immediately but starts an * asynchronous commit to disk and you won't be notified of * any failures. If another editor on this * {@link SharedPreferences} does a regular {@link #commit} * while a {@link #apply} is still outstanding, the * {@link #commit} will block until all async commits are * completed as well as the commit itself. * * <p>As {@link SharedPreferences} instances are singletons within * a process, it's safe to replace any instance of {@link #commit} with * {@link #apply} if you were already ignoring the return value. * * <p>You don't need to worry about Android component * lifecycles and their interaction with <code>apply()</code> * writing to disk. The framework makes sure in-flight disk * writes from <code>apply()</code> complete before switching * states. * * <p class='note'>The SharedPreferences.Editor interface * isn't expected to be implemented directly. However, if you * previously did implement it and are now getting errors * about missing <code>apply()</code>, you can simply call * {@link #commit} from <code>apply()</code>. */ void apply(); 略微有点长,大概意思就是 apply() 跟 commit() 不一样的地方是,它使用的是异步而不是同步,它会立即将更改提交到内存,然后异步提交到硬盘,并且如果失败将没有任何提示。 总结一下不同点: commit() 是直接同步地提交到硬件磁盘,因此,多个并发的采用 commit() 做提交的时候,它们会等待正在处理的 commit() 保存到磁盘后再进行操作,从而降低了效率。而 apply() 只是原子的提交到内容,后面再调用 apply() 的函数进行异步操作。 翻源码可以发现 apply() 返回值为 void,而 commit() 返回一个 boolean 值代表是否提交成功。 apply() 方法不会有任何失败的提示。 那到底使用 commit() 还是 apply()? 大多数情况下,我们都是在同一个进程中,这时候的 SharedPrefrence 都是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,我们非常建议用 apply() ,当然需要确保操作成功且有后续操作的话,还是需要用 commit() 的。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
这是 面试系列 的第五期。本期我们将来探讨一下 Android 异步消息处理线程 —— Handler。 往期内容传递:Android 面试(一):说说 Android 的四种启动模式Android 面试(二):如何理解 Activity 的生命周期Android 面试(三):用广播 BroadcastReceiver 更新 UI 界面真的好吗?Android 面试(四):Android Service 你真的能应答自如了吗? 开始 Android 的消息机制,也就是 Handler 机制,相信各位都已经是烂熟于心了吧。即创建一个 Message 对象,然后借助 Handler 发送出去,之后在 Handler 的 handleMessage() 方法中获取刚才发送的 Message 对象,然后在这里进行 UI 操作就不会出现崩溃了。 既然 Handler 操作都烂熟于心,还讲这个干什么? 嗯,对,在 Android 开发中,我们确实经常用到它,对于基本代码流程自然也是倒背如流,但了解它的原理的人却不是很多,所以面试官通常会考验你对 Handler 源码机制的理解,毕竟只有知己知彼,才能百战不殆嘛。 我们都知道 UI 操作只能在主线程进行,通常是怎么在子线程更新 UI 的? Handler Activity.runOnUiThread() View.post(Runnable r) 讲讲 Handler 机制吧 Handler 主要由以下部分组成。 HandlerHandler 是一个消息辅助类,主要负责向消息池发送各种消息事件Handler.sendMessage() 和处理相应的消息事件Handler.handleMessage()。 MessageMessage 即消息,它能容纳任意数据,相当于一个信息载体。 MessageQueueMessageQueue 如其名,消息队列。它按时序将消息插入队列,最小的时间戳将被优先处理。 LooperLooper 负责从消息队列读取消息,然后分发给对应的 Handler 进行处理。它是一个死循环,不断地调用 MessageQueue.next() 去读取消息,在没有消息分发的时候会变成阻塞状态,在有消息可用时继续轮询。 在 Android 开发中使用 Handler 有什么需要注意的 首先自然是在工作线程中创建自己的消息队列必须要调用 Looper.prepare(),并且在一个线程中只能调用一次。当然,仅仅创建了 Looper 还不行,还必须使用 Looper.loop() 开启消息循环,要不然要 Looper 也没用。 我们平时在开发中不用调用是因为默认会调用主线程的 Looper。 此外,一个线程中只能有一个 Looper 对象和一个 MessageQueue 对象。 大概的标准写法是这样。 Looper.prepare(); Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { Log.i(TAG, "在子线程中定义Handler,并接收到消息。。。"); } }; Looper.loop(); 另外一个比较常考察的就是 Handler 可能引起的内存泄漏了。 Handler 可能引起的内存泄漏 我们经常会写这样的代码。 private final Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }; 当你这样写的时候,你一定会收到编译器的黄色警告。 In Android, Handler classes should be static or leaks might occur, Messages enqueued on the application thread’s MessageQueue also retain their target Handler. If the Handler is an inner class, its outer class will be retained as well. To avoid leaking the outer class, declare the Handler as a static nested class with a WeakReference to its outer class 在 Java 中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,而静态的内部类不会持有外部类的引用。 要解决这样的问题,我们在继承 Handler 的时候,要么是放在单独的类文件中,要么直接使用静态内部类。当需要在静态内部类中调用外部的 Activity 的时候,我们可以直接采用弱引用进行处理,所以我们大概修改后的代码如下。 private static final class MyHandler extends Handler{ private final WeakReference<MainActivity> mWeakReference; public MyHandler(MainActivity activity){ mWeakReference = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity activity = mWeakReference.get(); if (activity != null){ // 开始写业务代码 } } } private MyHandler mMyHandler = new MyHandler(this); 其实在我们实际开发中,不止一个地方可能会用到内部类,我们都需要在这样的情况下尽量使用静态内部类加弱引用的方式解决我们可能出现的内存泄漏问题。 用过 HandlerThread 吗?它是干嘛的? HandlerThread 是 Android API 提供的一个便捷的类,使用它可以让我们快速地创建一个带有 Looper 的线程,有了 Looper 这个线程,我们又可以生成 Handler,本质上也就是一个建立了内部 Looper 的普通 Thread。 我们在上面提到的子线程建立 Handler 大概代码是这样。 new Thread(new Runnable() { @Override public void run() { // 准备一个 Looper,Looper 创建时对应的 MessageQueue 也会被创建 Looper.prepare(); // 创建 Handler 并 post 一个 Message 到 MessageQueue new Handler().post(new Runnable() { @Override public void run() { MLog.i("Handler in " + Thread.currentThread().getName()); } }); // Looper 开始不断的从 MessageQueue 取出消息并再次交给 Handler 执行 // 此时 Lopper 进入到一个无限循环中,后面的代码都不会被执行 Looper.loop(); } }).start(); 而采用 HandlerThread 可以直接把步骤简化为这样: // 1. 创建 HandlerThread 并准备 Looper handlerThread = new HandlerThread("myHandlerThread"); handlerThread.start(); // 2. 创建 Handler 并绑定 handlerThread 的 Looper new Handler(handlerThread.getLooper()).post(new Runnable() { @Override public void run() { // 注意:Handler 绑定了子线程的 Looper,这个方法也会运行在子线程,不可以更新 UI MLog.i("Handler in " + Thread.currentThread().getName()); } }); // 3. 退出 @Override public void onDestroy() { super.onDestroy(); handlerThread.quit(); } 其中必须注意的是,workerThread.start() 是必须要执行的。 至于如何使用 HandlerThread 来执行任务,主要是调用 Handler 的 API。 使用 post 方法提交任务,postAtFrontOfQueue() 将任务加入到队列前端, postAtTime() 指定时间提交任务, postDelayed() 延后提交任务。 使用 sendMessage() 方法可以发送消息,sendMessageAtFrontOfQueue() 将该消息放入消息队列前端,sendMessageAtTime() 指定时间发送消息, sendMessageDelayed() 延后提交消息。 HandlerThread 的 quit() 和 quitSafety() 有啥区别? 两个方法作用都是结束 Looper 的运行。它们的区别是,quit() 方法会直接移除 MessageQueue 中的所有消息,然后终止 MesseageQueue,而 quitSafety() 会将 MessageQueue 中已有的消息处理完成后(不再接收新消息)再终止 MessageQueue。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
面试系列 不继续了吗? 知道我的人都知道,之前我写了这个 面试系列宣言,如今好像一直都没有连载,而是隔三差五地来一篇,其实也是因为笔者也能力有限,构思一篇文章需要足够的时间去印证其准确性,而之前的部分就因为印证不够造成了勘误。 值得注意的是,本系列不会停止的。面试的很多知识点在于平时的积累,但自定义 View 这个东西,就得牢牢掌握了。自定义 View 将分为几期,本期我们只讲绘制。 为什么我们要学自定义 View? 大多数时候,我们都可以采用官方自带或者 GitHub 上的三方开源库实现各种各样炫酷的效果。但,需求却是五花八门的,你永远无法改变设计师们的想象力和创造力。而我们要做的,就是把他们的想象力和创造力变成现实。 图片来自扔物线 这期怎么变成第二好了? 对,我没有写错,本期自定义 View 教程再也不是最好的了,因为这期基本是 HenCoder 的浓缩总结版。 HenCoder,给高级 Android 工程师的进阶手册 ,笔者也是一直在像追剧一样的追。好像这里确实有了给我凯哥打广告的嫌疑,但把好东西,分享给大家,才是最最重要的。 笔者也是七进七出自定义 View,确实是看了不少教程和书籍,都没有一个很好的自定义 View 能力。而作为 Android 开发中必不可少的能(装)力(逼)手段,也是一个很好的可以让我们在面试以及开发中脱颖而出。 废话不能太多,我要开始啦! 自定义 View 可以简单的分为三步,绘制、布局、触摸反馈。本期,我们首先讲绘制。 自定义 View 绘制的重中之重 自定义的绘制就是重写绘制方法,其中最常用的就是 onDraw()。(当然有其它的,后面会提及,这里先卖个关子。)而绘制的关键就是 Canvas 的使用: Canvas 的绘制类方法:drawXXX() (关键参数:Paint) Canvas 的辅助类方法:范围裁切和几何变换。 一切的开始:onDraw() 自定义绘制的上手非常容易:提前创建好 Paint 对象,重写 onDraw(),把绘制代码写在 onDraw() 里面,就是自定义绘制最基本的实现。大概就像这样: Paint paint = new Paint(); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制一个圆 canvas.drawCircle(300, 300, 200, paint); } 就这么简单。所以关于 onDraw() 其实没什么好说的,一个很普通的方法重写,唯一需要注意的是别漏写了 super.onDraw()。你可能会点击进去查看到 super.onDraw() 其实是一个空实现,那可能只是因为你继承的是 View 吧,你继承 View 的其它子类试试? Canvas.drawXXX() 系列方法的使用 Canvas 下面的 drawXXX() 系列的方法真没啥好讲的,你想画什么图形直接画就好了。而参数其实也给的非常的明了。你一定要全部了解学习的话,直接可以去看官方文档或者凯哥的 自定义View 1-1 填充颜色:Canvas.drawColor(@ColorInt int color) 画圆:drawCircle(float centerX, float centerY, float radius, Paint paint) 画矩形:drawRect(float left, float top, float right, float bottom, Paint paint) 画点:drawPoint(float x, float y, Paint paint) 批量画点:drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 画椭圆:drawOval(float left, float top, float right, float bottom, Paint paint) 画线:drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画弧线或者扇形:drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 画自定义图形:drawPath(Path path, Paint paint) 画 Bitmap:drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画文字:drawText(String text, float x, float y, Paint paint) 其中可以看到有不少的坐标值参数,你只需要明白的一点是,在 Android 的绘制中,坐标系是这样的。 图片来自扔物线 值得注意的是: 在画弧线或者扇形中的角度 angle,x 轴正方向为 0°,顺时针方向为正角度,逆时针为负角度。 画弧线或者扇形中的 sweepAngle 参数,代表的是绘制的角度,不要被其它方法误导成了以为是绘制结束时候的角度,官方为何在这里做了个变换,其实我也不知道。 drawPath() 方法可能相对其它较难,但却是自定义 View 实际应用中最多的。非常需要了解其三类方法。这里直接摘抄凯哥的 自定义 View 1-1。 drawBitmap() 方法中有个参数是 Bitmap,友情提示:Bitmap 可以通过 BitmapFactory.decodeXXX() 获得。 Path 可以描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形。把这些图形结合起来,就可以描述出很多复杂的图形。Path 可以归结为两类方法: 直接描述路径,也可以分为两组: 添加子图形:addXXX(), 此类方法在特定情况下几个 Canvas.drawPath() 等同于 Canvas.drawXXX()。 画直线或曲线:xxxTo(): 这一组和第一组 addXxx() 方法的区别在于,第一组是添加的完整封闭图形(除了 addPath() ),而这一组添加的只是一条线。 辅助设置或计算,因为应用场景很少,凯哥也只讲了其中一个方法: Path.setFillType(Path.FillType ft) 设置填充方式 上面有比较多的提到 Paint 这个参数,实际上它是真的很好用,直接在下面讲解。 Paint 的使用 Paint 真的很重要,在自定义绘制中充当关键角色:画笔,所以我们自然可以为「画笔」做很多操作,比如设置颜色、绘制模式、粗细等。 Paint.setStyle(Style style) 设置绘制模式 Paint.setColor(int color) 设置颜色 Paint.setStrokeWidth(float width) 设置线条宽度 Paint.setTextSize(float textSize) 设置文字大小 Paint.setAntiAlias(boolean aa) 设置抗锯齿开关 嗯,对,抗锯齿开关还可以直接在 Paint 初始化的时候直接作为构造参数:Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG) Paint 的 API 大致可以分为 4 类: 颜色 效果 drawText() 相关 初始化 凯哥专门拿了一期对 Paint 做了重点讲解,依然在实际场景应该用处不大,所以需要的直接点击 这里 跳转。 如果你想先知道凯哥都讲了什么,我这里也单独给你总结一下: 首先是给 Paint 设置着色器。 Paint.setShader(Shader shader):设置着色器,实际上我们一般传递的参数不会直接传递 Shader,而会选择直接传递它的子类,具体效果下面给出。 线性渐变:LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,TileMode tile) 图片来自扔物线 辐射渐变:RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode) 图片来自扔物线 扫描渐变:SweepGradient(float cx, float cy, int color0, int color1) 图片来自扔物线 还有很多,就不一一给图了。 BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY) 混合着色:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode) 其中需要注意的是: Paint.setShader() 优先级高于 Paint.setColor() 系列方法。 最后一个 tile 参数,代表的是断点范围之外的着色规则。它是一个枚举类型,有三种参数。 CLAMP : 直译是「夹子模式」,会在端点之外延续端点处的颜色。 MIRROR : 镜像模式。 REPEAT : 重复模式。 其次是设置颜色过滤 设置颜色过滤可以采用 Paint.setColorFilter(ColorFilter colorFilter) 方法。它的名字已经足够解释它的作用:为绘制设置颜色过滤。颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX() 方法会对每个像素都进行过滤后再绘制出来。 这个其实貌似在拍照或者照片整理类应用上用的比较多,其它方面貌似我还很少遇到过,GitHub 上的库 StyleImageView 诠释的很棒。 再其它也就没啥好说的,感兴趣直接去看 HenCoder 吧 这里可以重点说一下:Paint.setStrokeCap(Paint.Cap cap),设置线头的形状。线头形状有三种:BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT。 图片来自扔物线 虚线是额外加的,虚线左边是线的实际长度,虚线右边是线头。有了虚线作为辅助,可以清楚地看出 BUTT 和 SQUARE 的区别。 Canvas 的文字绘制 Canvas 的文字绘制方法有三个: drawText() drawTextRun() drawTextOnPath() 我们大多数情况用不了那么多,所以同样这里不做详解,对于始终想追根到底的同学,同样给你提供了 凯哥的链接。 下面只对部分需要注意的重点总结一下。 drawText() drawText(@NonNull String text, float x, float y, @NonNull Paint paint) 其中的参数很简单:text 是文字内容,x 和 y 是文字的坐标。但需要注意:这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。大概在这里: 图片来自扔物线 而如果你像绘制其他内容一样,在绘制文字的时候把坐标填成 (0, 0),文字并不会显示在 View 的左上角,而是会几乎完全显示在 View 的上方,到了 View 外部看不到的位置:canvas.drawText(text, 0, 0, paint); 大概是这样: 图片来自扔物线 另外,Canvas.drawText() 只能绘制单行的文字,而不能换行。就算显示不完,也会直接绘制到屏幕外面去。 那如果要换行,得 drawText() 很多次吗?并没有,还有一个 StaticLayout 可以完美达到我们的效果。对于详细使用,这里也不多提了。 对 drawTextRun() 和 drawTextOnPath(),运用的可能并不多,这里就不说了。 简单提一下设置效果辅助类吧,这个可能直接就有用。 Paint 对文字绘制的辅助 设置文字大小:Paint.setTextSize(float textSize) 设置字体:Paint.setTypeface(Typeface typeface),其中的 Typeface 里面涵盖了相关字体。另外,还可以通过 Typeface.createFromAsset(AssetManager mgr, String path) 来设置自定义字体,其中 mgr 可以给 getResources().getAssets(),path 给文件名字,需要把字体文件 .ttf 放在工程的 res/assets 下,「assets」是新建的专用目录。 设置文字是否加粗:Paint.setFakeBoldText(boolean fakeBoldText) 设置文字是否加删除线:Paint.setStrikeThruText(boolean strikeThruText) 设置文字是否加下划线:Paint.setUnderlineText(boolean underlineText) 设置字体倾斜度:Paint.setTextSkewX(float skewX) 「skewX」 向左倾斜为正。 设置文字横向放缩:Paint.setTextScaleX(float scaleX) 设置字体间距,默认值为 0:Paint.setLetterSpacing(float letterSpacing) 这个不是行间距哦。 设置文字对齐方式:Paint.setTextAlign(Paint.Align align),其中「align」有三个值:LEFT、CENTER 和 RIGHT,默认值是 LEFT。 设置绘制所使用的 Locale:Paint.setTextLocale(Locale locale) / Paint.setTextLocales(LocaleList locales) 实际上,这些方法基本都在我们 TextView 里面的。 自定义 View 之范围裁切 范围裁切主要采用两个方法: clipRect() clipPath() clipRect() 很简单,只需要传递和 RectF 一样的参数即可。你可以除了裁剪矩形,还想做其它样式的裁剪,可惜这里只有通过 path 的方法了(我也很奇怪为啥没有看到其它方法),再一次印证了 path 的重要性有木有。 值得注意的是:我们通常会在范围裁切前后加上 Canvas.save() 和 Canvas.restore() 来及时恢复绘制范围。大概代码是这样。 canvas.save(); canvas.clipRect(left, top, right, bottom); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore(); 另一个值得注意的点是:一定是先做范围裁切操作,再做 Canvas.drawXXX() 操作,顺序放反的话你会发现毛效果都没有。除了裁切,几何变换也是如此。 几何变换 几何变换的使用大概分为三类: 使用 Canvas 来做常见的二维变换; 使用 Matrix 来做常见和不常见的二维变换; 使用 Camera 来做三维变换 直接采用 Canvas 自带方法进行二维变换 Canvas.translate(float dx, float dy) 平移,其中,dx 和 dy 分别表示横向和纵向的位移。 Canvas.rotate(float degrees, float px, float py) 旋转,其中 degrees 是旋转角度,顺时针为正向,px 和 py 代表轴心坐标。 Canvas.scale(float sx, float sy, float px, float py) 放缩,其中 sx,sy 分别是横向和纵向的放缩倍数,px 、py 为放缩的轴心,这里千万不要受到重载方法 Canvas.scale(float sx,float sy) 的影响。 skew(float sx, float sy) 错切。这里的 sx 和 sy 分别是 x 方向和 y 方向的错切系数。值得注意的是,这里 sx 和 sy 值为 0 的时候代表自己的方向不错切。 再次重申,需要先做了二维变换,再执行 「drawXXX」操作,重要的事情一定会说三遍。 二维变换的另一种方式 —— Matrix 用 Matrix 做常见变换的基本套路 创建 Matrix 对象; 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换; 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas。 Matrix matrix = new Matrix(); ... matrix.reset(); matrix.postTranslate(); matrix.postRotate(); canvas.save(); canvas.concat(matrix); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore(); 把 Matrix 应用到 Canvas 有两个方法: Canvas.setMatrix(matrix) 和 Canvas.concat(matrix)。 Canvas.setMatrix(matrix):用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换(注:根据凯哥收到的反馈,不同的系统中 setMatrix(matrix) 的行为可能不一致,所以还是尽量用 concat(matrix) 吧); Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。 其中需要注意的是:当多个 Matrix 需要用到的时候,你并不需要初始化多个 Matrix,而可以直接通过调用 Matrix.reset() 对 Matrix 进行重置。 对于采用 Matrix 来实现不规则变换以及采用 Camera 实现三维变换这里也就不多说了,实际遇到的时候,你也可以 点击这里 复习一下呀。 精彩的绘制顺序 前面讲了一大堆绘制方法,以及范围裁切和变换,我们这里再说说绘制顺序。 Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。比如你在重叠的位置先画圆再画方,和先画方再画圆所呈现出来的结果肯定是不同的: 图片来自扔物线 到底放在 super.onDraw() 上面还是下面? 通常如果我们继承的是 View 的话,super.onDraw() 只是一个空实现,所以它的位置放在哪儿都没事,甚至直接不要也没事,但反正加上也没啥影响,尽量还是加上吧。 由于 Android 的绘制顺序性,当你继承自已经有绘制的其他 View(比如 TextView)的时候,放在 super.onDraw() 上面就意味着绘制代码会被控件的原内容盖住。 dispatchDraw():绘制子 View 的方法 还记得我上面卖的关子吗?自定义绘制其实不止 onDraw() 一个方法。onDraw() 只是负责自身主体内容绘制的。而有的时候,你想要的遮盖关系无法通过 onDraw() 来实现,而是需要通过别的绘制方法。 凯哥这块真的写的是太有意思了,所以我也是直接 copy 了过来。 例如,你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀: public class SpottedLinearLayout extends LinearLayout { ... protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... // 绘制斑点 } } 图片来自扔物线 看起来确实没有问题,但是你会发现,当你添加了子 View 之后,你的斑点不见了: 图片来自扔物线 造成这种情况的原因是 Android 的绘制顺序:在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。对于上面这个例子来说,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成之后,先前绘制的斑点就被子 View 盖住了。 具体来讲,这里说的「绘制子 View」是通过另一个绘制方法的调用来发生的,这个绘制方法叫做:dispatchDraw()。也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方法来绘制子 View。 注:虽然 View 和 ViewGroup 都有 dispatchDraw() 方法,不过由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。 图片来自扔物线 回到刚才的问题:怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让它的绘制代码在子 View 的绘制之后再执行就好了。所以直接执行在 super.dispatchDraw() 的下面即可。 简单总结一下绘制顺序 凯哥确实强势,在文章的最后,直接贴图,不能再清晰了,所以我也是直接跳过了其中 N 个环节,直接上图。 图片来自扔物线 图片来自扔物线 注意: 在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false); 在重写的方法有多个选择时,优先选择 onDraw()。 写在最后 本期的自定义 View 之绘制就到这里结束了,强烈推荐 点击链接 跟着凯哥操,不得挨飞刀。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
今天我们不继续说面试,讲点其他的,有一些废话,关注标题的请直接拉到下面。 近期呀,笔者除了整理 面试系列,其实还在做一件事,就是在费心费力地准备把 AiYaGirl 进行重构。 AiYaGirl 是一款涵盖 Retrofit & MVP & RxJava 开发体系的 App ,主要采用代码家的 干货集中营 做数据来源,知识点不多,你选择了我,就是我的幸运。 直接看看之前的效果图吧。 nanchen.gif 由于有 MVP 的地方,最好还是加上 Dagger2,所以本次重构主要是加入 Dagger2 、RxJava2 的升级,以及各种性能上的优化。 但并不是所有人都喜欢 Dagger2,所以我将保留之前的 1.x 版本,本次重构不会覆盖之前的页面,而是在 GitHub 仓库中新建了一个 mvp-dagger 分支。 期待以后的路上有你的陪伴和交流,因为我也曾遇到各种棘手的问题,到处询问而得不到答案。那个时候的我,也如现在的你,而我,还在这条路上默默前行。 说说标题上的事儿 重构的第一件事,就是对库进行了升级。可当我开开心心地把库一个一个更新到最新版,并 build 的时候,发现直接炸了。 大家更新库,得先看官方用法是否更新,更新需谨慎。 nanchen nanchen support 库出现了 26.1.0 和 26.0.2 两个版本,而我的 App 下是自己只依赖了 26.0.2 的。 很明显的库兼容性错误,但完全找不着头脑,不知道是哪儿的锅。是呀,这么多库,我哪儿知道是哪个库的毛病。 nanchen 一番倒腾后,采用命令行 gradlew -q app:dependencies 找出了问题。 nanchen 咦?这个库? 到这个库的官方地址一看,确实如此。 nanchen 。 于是,直接修正了版本,编译成功,完美。 应一些读者的要求,目前个人比较常用的 Gradle 命令。 这肯定是不完整的,因为这只是我平时用的比较多的。其实也还好,命令行习惯了是真的不想手动操作了。 注意:我是 windows 所以我下面全是针对 windows 操作系统的。如果是 Linux / Mac 请直接用 ./ 前缀。 gradlew build 检查依赖并编译打包,即使你采用了多渠道打包,依然可以,可以生成所有的 apk。(涵盖 release 和 dubug) gradlew clean 这没啥好说的,就跟 Android Studio 下面的 clean 差不多。 gradlew installDebug 编译并打包 debug 包。 gradlew -v 查看构建的版本。 gradlew build --info 编译并打印日志。 gradlew dependencies --info 查看详细的依赖信息 gradlew assembleRelease 命令行打包不签名的release包生活中总是会遇到各种各样奇怪的问题,但路,我们还得继续走下去。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
这是 面试系列 的第三期。本期我们将来探讨一下 Android 四大组件的重要组成部分:广播 BroadcastReceiver。 往期内容传递:Android 面试:说说 Android 的四种启动模式Android 面试:如何理解 Activity 的生命周期 前言 BroadcastReceiver 作为 Android 四大组件之一,应用场景可谓非常之多。所以我相信任何一个有一定 Android 开发经验的工程师都不会在这个题上栽跟斗。但,某些细节,或许我们可以注意一下。 实际上我在面试过程中也遇到了这样的题。下面请允许我用「柳学兄」的思路带大家进入面试营。 BroadcastReceiver 内部基本原理是什么? Android 的广播 BroadcastReceiver 是一个全局的监听器,主要用于监听 / 接收应用发出的广播消息,并作出响应。其采用了设计模式中的 观察者模式 ,可将广播基于 消息订阅者 、消息发布者、消息中心(AMS:即 Activity Manager Service)解耦,通过 Binder 机制形成订阅关系。 图片来源于网络 说说 BroadcastReceiver 的两种注册方式 Android 广播的两种注册方式肯定难不倒任何人,实际上我估计也只有对少量的 Android 开发面试者才会遇到这样的题,这里不会有什么特别的,熟悉的可以直接跳过。 静态注册 静态注册广播的方式只需要在 AndroidManifest.xml 里通过 <receiver> 标签声明。下面附上一些属性说明。 <receiver android:enabled=["true" | "false"] //此 broadcastReceiver 能否接收其他 App 发出的广播 //默认值是由 receiver 中有无 intent-filter 决定的:如果有 intent-filter,默认值为 true,否则为 false android:exported=["true" | "false"] android:icon="drawable resource" android:label="string resource" //继承 BroadcastReceiver 子类的类名 android:name=".mBroadcastReceiver" //具有相应权限的广播发送者发送的广播才能被此 BroadcastReceiver 所接收; android:permission="string" // BroadcastReceiver 运行所处的进程 // 默认为 App 的进程,可以指定独立的进程 //注:Android 四大基本组件都可以通过此属性指定自己的独立进程 android:process="string" > //用于指定此广播接收器将接收的广播类型 //本示例中给出的是用于接收网络状态改变时发出的广播 <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> </intent-filter> </receiver> 动态注册 动态注册方式是通过调用 Context 下面的 registerReceiver() 进行注册,可以调用 unregisterReceiver() 进行注销。需要注意的是:动态广播最好在 Activity 的 onResume() 注册,并在 onPause() 进行注销。 为什么建议动态广播尽量在 onPause() 进行注销? 我们可以先看看 Activity 的生命周期。 图片来源于网络 首先有注册就得有注销,否则一定会造成内存泄漏。注意上面途中红框圈住的部分。,阅读官方源码发现,当系统因为内存不足需要回收 Activity 占用的资源时,Activity 在执行完 onPause() 方法后就可能面临着被销毁的危险,有些生命周期方法,如:onStop()、onDestroy() 根本就不会执行,而 onPause() 由于一定会调用的特殊性,自然是避免内存泄漏的好方法。 两种注册方式的区别也是可以用图一目了然。 图片来源于网络 说说 Android 的常用广播类型吧 基本在 Android 领域常用的方式就是直接调用 Context 提供的方法 sendBroadcast() 和 sendOrderBroadcase() 发送无序广播和有序广播。 无序广播 无序广播是完全异步的,通过 Context.sendBroadcast() 方法来发送,从效率上来看,还算是比较高的。正如它的名称一样,无序广播对所有的广播接收者而言,是无序的。也就是说,所有接收者无法确定接收时序的顺序,这样也导致了,无序广播无法被停止。当它被发送出去之后,它将通知所有这条广播的接收者,直到没有与之匹配的广播接收者为止。 有序广播 有序广播通过 Context.sendOrderedBroadcast() 方法来发送。有序广播和无序广播最大的不同,就是它可以允许接收者设定优先级,它会按照接收者设定的优先级依次传播。而高优先级的接收者,可以对广播的数据进行处理或者停止掉此条广播的继续传播。广播会先发送给优先级高 (android:priority) 的 Receiver,而且这个 Receiver 有权决定是继续发送到下一个 Receiver 或者是直接终止广播。 除了无序广播和有序广播,还有其他的类型吗? 可能还是有不少的朋友知道 Sticky 广播方式。 粘性广播 Sticky Sticky 广播和它的名字很像,它是一个具有粘性的广播。它被发出去之后,会一直滞留在系统中,直到有与之匹配的接收者,才会将其发出去。它采用 Context.sendStickyBroadcast() 方法进行发送广播。 从官方文档上可以看到,如果想要发送一个 Sticky 广播,需要具有 BROADCAST_STICKY 权限,这个可以在 AndroidManifest.xml 中进行注册,而如果没有此权限,则会抛出 SecurityException 异常。 对于系统而言,只会保留最后一条 Sticky 广播,并且会一直保留下去,也就是说,如果我们发送的 Sticky 广播不被取消,当有一个接收者的时候就会收到它,再来一个还是能收到。所有我们需要在合适的实际,调用 removeStickyBoradcast() 方法,将其取消掉。 从官方文档中也可以看到 StickyBroadcast 已经被标记为 @Deprecated ,出于一些安全的考虑,已经将其标记为废弃,不再推荐使用。我们作为开发者,对于一些被标记为 @Depracated 的方法,使用起来还是需要谨慎的。 有时候基于数据安全考虑,我们想发送广播只有自己(本进程)能接收到,怎么处理? 首先,Android 中的广播可以跨进程通信,因为 exported 对于有 Intent-filter 的情况下默认为 true。所以我们难以有这样的需求: 对于某些敏感性的广播,我们不希望暴露给外部。 其他 App 可能会发出和当前 App intent-filter 相匹配的广播,导致 App 不断进行广播接收和处理。 这真是一个坏消息,我们必须让我们的应用变得有效率并足够的安全。 一般我们能自然地想到在注册广播的时候把 exported 值设为 false 并给 App 的广播增加上权限,可问题是权限不够是一个字符串,面对当前如此强大的反编译技术,这终究是不安全的。 为了解决这样的问题,我们不难想到可以通过往主线程的消息池(Message Queue)里发送消息,让其做到只有主线程的 Handler 可以分发处理它。或者在发送广播的时候直接通过 Intent.setPackage(packageName) 指定广播接收器的包名。 要不是我们项目中有个 BroadcastUtil 工具类,我还之前真不知道 Support V4 包下还有这么一个 LocalBroadcastManager 本地广播类。 本地广播 在 Android Support v4 : 21 版本后加入了我们的大家庭。它使用 LocalBroadcastManager (以下简称 LBM)类来管理。 LocalBroadcast 的使用非常的简单,只需要将 Broadcast 的对应 API,替换为 LBM 为我们提供的 API 即可。 LBM 是一个单例对象,可以使用 LocalBroadcastManager.getInstance(Context context) 方法获取到。在 Context 中定义的和 Broadcast 相关的方法,在 LBM 中都有对应的 API 。非常有意思的是,LBM 为了区分异步和同步,使用了 sendBroadcast() 和 sendBroadcastSync() 方法来做为区分。 在 Android 中用广播来更新 UI 界面好吗? 废话扯了这么多,终于说到标题上的问题了。 直接回答:可以,为什么不可以呢?在实际开发中我们不是经常这么用么? 很好,可以肯定你是一个真实的 Android 开发者了,不过在认证你的「合格」之前,想问问 BroadcastReceiver 的生命周期。 什么?BroadcastReceiver 的生命周期?糟糕,面试前只复习了 Activity 和 Fragment 的生命周期,杂还有人问 BroadcastReceiver 的生命周期。 所以,你支支吾吾了。 其实还是有比较多的人了解 BroadcastReceiver 的生命周期的。BroadcastReceiver 有生命周期,但比较短,而且很短。当它的 onReceive() 方法执行完成后,它的生命周期也就随之结束了。这时候由于 BroadcastReceiver 已经不处于 active 状态,所以极有可能被系统干掉。也就是说如果你在 onReceive() 去开线程进行异步操作或者打开 Dialog 都有可能在没达到你要的结果时进程就被系统杀掉了。 所以,正确答案是? 更新 UI 界面这个定义太广泛了。实际开发中其实大多数情况都是可以采用 BroadcastReceiver 来更新 UI,所以也造成了很多人回答就想上面很肯定和自信的回答可以。 实际上我们知道 Receiver 也是运行在主线程的,不能做耗时操作。虽然超时时间相对于 Activity 的 5 秒更高,有足足的 10 秒。但不意味着我们实际开发中所有的更新 UI 界面操作时间都在安全范围之内。 此外,对于频繁更新 UI,也不推荐这种方式。Android 广播的发送和接收都包含了一定的代价,它的传输都是通过 Binder 进程间通信机制来实现的,那么系统肯定会为了广播能顺利传递而做一些进程间通信的准备。而且可能会由于其它因素导致广播发送和到达不准时(或者说接收会延迟)。 这种情况可能吗? 很可能,而且很容易发生。我们要先了解 Android 的 ActivityManagerService 有一个专门的消息队列来接收发送出来的广播,sendBroadcast() 执行完后就立即返回,但这时发送来的广播只是被放入到队列,并不一定马上被处理。当处理到当前广播时,又会把这个广播分发给注册的广播接收分发器ReceiverDispatcher,ReceiverDispatcher 最后又把广播交给接 Receiver 所在的线程的消息队列去处理(就是你熟悉的 UI 线程的 Message Queue)。 整个过程从发送 ActivityManagerService 到 ReceiverDispatcher 进行了两次 Binder 进程间通信,最后还要交到 UI 的消息队列,如果基中有一个消息的处理阻塞了 UI,当然也会延迟你的 onReceive() 的执行。 BroadcastReceiver 和 EventBus 有啥不同? EventBus 作为 GitHub 上一个颇受欢迎的库,目前也是有着 16.3 k 的星星,足以见其强大。 所以在不少面试中当然会遇到这样的提问。这不,笔者在咕咚面试的时候就被面试官问到了这个题,又一个打脸,当时我像被电了一番,答的并不怎么样。 众所周知,广播是 Android 的四大组件之一。系统系统级的事件都是通过广播来通知的,比如说网络的变化、电量的变化、短信接收和发送状态等。所以,如果是和 Android 系统相关的通知,我们还得选择本地广播。 但是!!!广播相对于其他实现方式,是很重量级的,它消耗的资源较多。它的优势体现在和 SDK 的紧密联系,onReceive() 方法自带了 Context 和 Intent 参数,所以在一定意义上实现了便捷性,但如果对 Context 和 Intent 应用很少或者说只做很少的交互的话,使用广播真的就是一种浪费!!! 那 EventBus 呢? 先说说其优点: 调度灵活 要说到优点,这一定是我最先想到的。因为它真的是太灵活了,在实际开发中感觉它就是一个机灵鬼,想去哪就去哪,根本就不需要像广播一样关注 Context 的注入与传递。父类对于通知的监听和处理还可以直接继承给子类,可以设置优先级让 Subscriber 关注到优先级更高的通知,其粘滞事件(sticky events)能够保证通知不会因 Subscriber 的不在场而忽略。可继承、优先级、粘滞,是 EventBus 比之于广播、观察者等方式最大的优点,它们使得创建结构良好组织紧密的通知系统成为可能。 使用简单 进入到 EventBus 的官网,看一眼 README.md,简直不能再简单,简简单单三个步骤,再在 build.gradle 中添加一个依赖,轻轻松松搞定有木有?如果不想创建 EventBus 的实例,还可以直接调用静态方法 EventBus.getDefault() 获取。 快速且轻量 作为一个 GitHub 的明星项目,性能方面是可以放心的。 EventBus 这么棒,那我们有组建通信就用 EventBus 吧。 还真是人无完人,物无完物。EventBus 也有着它的致命弱点。EventBus 最大的缺点在于其逻辑性,直接看其代码,一不小心根本看不通有没有?另外一个问题是,当程序较大后,观察者独有的接口膨胀缺点也会伴随着你的项目,你能想象很多 Event 后缀类的感觉吗? 综上,EventBus 由于其针对统一进程,所以在某些复杂的情况下单纯依靠接口回调不好处理组件通信的时候,直接去尝试 EventBus 吧。 说了这么多,在广播和 EventBus 这个十字路口犹豫不决的时候,还会纠结选择吗? 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
我说好的 面试系列 总算开始了。我姑且是想到哪儿写到哪儿,其中肯定是和我被面试的实际情况息息相关。 说说 Android 的四种启动模式 这基本是一道必考题,和「 Activity 的生命周期 」一样,基本为必考题。 其实很多人可能存在一个误区,觉得知道这个启动模式「launchMode」没什么意义,但我在毫无准备的前提下,被问到这个问题的时候,我被问的瑟瑟发抖。 这些都是基本 先普及下可能大多数人都知道的基本见解。 standard 这是 Activity 的默认启动模式,每次激活 Activity 的时候都会创建一个新的 Activity 实例,并放入任务栈中。使用场景:基本绝大多数地方都可以用。 图片来源于网络 singleTop 这可能也是非常常用的 launchMode 了。如果在任务的栈顶正好存有该 Activity 的实例,则会通过调用 onNewIntent() 方法进行重用,否则就会同 standard 模式一样,创建新的实例并放入栈顶。即便栈中已经存在了该 Activity 的实例,也会创建新的实例,即:A -> B ->A,此时栈内为 A -> B -> A,但 A -> B ->B ,此时栈内为 A -> B。一句话概述就是:当且仅当启动的 Activity 和上一个 Activity 一致的时候才会通过调用 onNewIntent() 方法重用 Activity 。使用场景:资讯阅读类 APP 的内容界面。 图片来源于网络 singleTask 这个 launchMode专门用于解决上面 singleTop 的另外一种情况,只要栈中已经存在了该 Activity 的实例,就会直接调用 onNewIntent() 方法来实现重用实例。重用时,直接让该 Activity 的实例回到栈顶,并且移除之前它上面的所有 Activity 实例。如果栈中不存在这样的实例,则和 standard 模式相同。即: A ->B -> C -> D -> B,此时栈内变成了 A -> B。而 A -> B -> C,栈内还是 A -> B -> C。使用场景:浏览器的主页面,或者大部分 APP 的主页面。 图片来源于网络 singleInstance 在一个新栈中创建该 Activity 的实例,并让多个应用共享该栈中的该 Activity 实例。一旦该模式的 Activity 实例已经存在于某个栈中,任何应用再激活该 Activity 时都会重用该栈中的实例,是的,依然是调用 onNewIntent() 方法。其效果相当于多个应用共享一个应用,不管是谁激活,该 Activity 都会进入同一个应用中。但值得引起注意的是:singleInstance 不要用于中间页面,如果用户中间页面,跳转会出现很难受的问题。 这个在实际开发中我暂未遇到过,不过 Android 系统的来电页面,多次来电均是使用的同一个 Activity 。 图片来源于网络 四种模式的背书式理解记忆讲完了,你认为这样就结束了吗? 对,我也一度是这样认为的。 再说说我们的 Intent 标签 我们除了需要知道在 AndroidManifest.xml 里面设置 android:launchMode 属性,我们还需要了解下面这几个 Intent 标签的用法。 我们当然可以选择看看官方文档 Tasks and Back Stack (你可能需要梯子)。 在 Android 中,我们除了在清单文件 AndroidManifest.xml 中配置 launchMode,当然可以用 Intent 标签说事儿。启动 Activity ,我们需要传递一个 Intent,完全可以通过设置 Intent.setFlags(int flags) 来设置启动的 Activity 的启动模式。 需要注意的是:通过代码来设置 Activity 的启动模式的方式,优先级比清单文件设置更高。 FLAG_ACTIVITY_NEW_TASK 这个标识会使新启动的 Activity 独立创建一个 Task。 FLAG_ACTIVITY_CLEAR_TOP 这个标识会使新启动的 Activity 检查是否存在于 Task 中,如果存在则清除其之上的 Activity,使它获得焦点,并不重新实例化一个 Activity,一般结合 FLAG_ACTIVITY_NEW_TASK 一起使用。 FLAG_ACTIVITY_SINGLE_TOP 等同于在 launcherMode 属性设置为 singleTop。 nanchen 前面讲了这么多,似乎相当全面了,但你以为这样就结束了?No,面试官一般情况下已经不会这么问你了,这样问你完全可以背出来。 面试官怎么问 Activity 的启动模式(launchMode)? 怎么问? 1、设置为 singleTask 的启动模式,当 Activity 的实例已经存在时,再启动它,它的哪个回调函数会被执行?我们可以在哪个回调中处理新的 Intent 协带的参数?(通过 startActivity(Intent) 方式启动) 2、设置为 singleTop 的启动模式,当 Activity 的实例已经存在于 Task 的栈顶,我们可以在哪个回调中处理新的 Intent 协带的参数?(在当前 Activity 中从通知栏点击再跳转到此 Activity 就是这种在栈顶的情况) 这两个问题如果你看了上面的,一定对你来说实在太简单了。面试官只是想直接考察你是否真正做过这样的设置,或者是否知道 onNewIntent() 这个方法的存在。 有没有其他想说的? 有。 之前在「柳学兄」的文章中看到过这样一种情况,发现之前同事写的代码导致了这样一个问题。 startActivityForResult 启动一个 Activity,还没有开始界面跳转,直接就执行了 onActivityResult()。 不知道有没有人也遇到过这样的问题,但我想遇到这个问题的时候,你一样会因此而抓耳挠腮。(可能是因为我们一般在排查问题的时候,很少去关注配置清单文件 AndroidManifests.xml。) 我们在 Activity.java 的 startActivityForResult() 方法中可以看到这样一串说明。 nanchen 很多人出现这个问题,确实是因为 startActivityForResult() 启动的 Activity 设置了 singleTask 的启动模式。 好在 Android 5.0 以后,修正了这个问题。不过当你在 Intent 中设置 FLAG_ACTIVITY_NEW_TASK 后还是会出现这样的问题。 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 注意:MainActivity 的 onResume() 也会被触发。因为 onActivityResult() 被执行时,它会重新获得焦点。很多人也会遇到 onResume() 被无故调用,也许就是这种情况。 所以,最终我们发现只要是不和原来的 Activity 在同一个 Task 就会产生这种立即执行 onActivityResult() 的情况,从原代码也可以得到验证,详情查看 ActivityStackSupervisor.java。 小结 关于 Activity 的启动模式相关的问题,其实还会有很多种考察你掌握情况的问法,建议采用 STAR 法则 进行面试回答。其实只要你掌握了实质,后面的运用只看你个人的运用能力和创新了。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
连载内容镇楼:Android 面试(一 ):说说 Android 的四种启动模式Android 面试(二): 如何理解 Activity 的生命周期Android 面试(三): 用广播 BroadcastReceiver 更新 UI 界面真的好吗?Android 面试(四):Android Service 你真的能应答自如了吗?Android 面试(五):探索 Android 的 HandlerAndroid 面试(六):你已经用 SharedPrefrence 的 apply() 替换 commit() 了吗? 这个系列不是最好的了? 对,再也不敢说「 这可能是最好的 XXX」了。虽然我的 RxJava 2.0 系列 获得了较好的反馈,甚至还有人说「能望见传世神文 《给 Android 开发者的 RxJava 详解》 的脚步」,那都不重要了。 而且知道我的人也知晓,我不是扎根互联网的老程序员,对面试也是知之甚少,所以我不敢说这是「最好」的。 虽然这个系列已经不是「最好」的了,但我依然会花很多时间去完善他,感谢好友「兰柳学」的强势赞助。 为什么要写这个系列? nanchen 装逼从来都不是必要的,必要的是我们始终如一的想着装逼。 咳咳,讲点现实的,是因为今天面试被虐了。 一直以来我以为自己还算是个不错的 Android 开发工程师,GitHub 好像有较为欢迎的假象。还有一大批诸如 RxJava、Dagger2、Retrofit 在内的框架文章可以装装逼,但实际上大多数企业,尤其是互联网公司,根本就不会看重你娴熟运用框架的能力。 面试最后收到一句话:「 我们非常肯定你的开源分享能力和学习能力,我们也相信你能做好开发,但你的基础确实是太 low 了!」 其实从我的回答来说,我自己也觉得非常 low,用「舍本逐末」四个字来形容我再合适不过。 划重点!!! 为了防止一些类似我这样的 Android 开发工程师「严重偏科」,我决定出这么一个系列,我不知道这个系列多久可以出完,也许明天,也许三年,也许会因为工作的繁忙而拖更。 但有一件不变的事是,我会一如既往地坚持分享下去。 你可以从这里学习到什么? 这重要吗?这不重要。 nanchen 说点 tips 吧。( 不是说我要讲这个 ) Android 开发要求 3 - 5 年,我才两年可以投简历吗? 作为 Android 开发工程师,到底要先注重基础还是框架利用? 讲讲 Android 的四种启动模式? 怎么看待 Activity 的生命周期? Handler、AsyncTask 有啥区别和注意事项? 说说你的优势,或者你和别人有什么不同? Service 有几种启动模式,应用场景,有些什么需要注意的? nanchen 干嘛揭穿我,说你都会。 nanchen 这个系列,我是认真的。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
前言 严格地说,这不算一篇 Blog,这里只会不定期更新一些小东西,是的,关于 GitHub 命令行操作,我给它取名 —— GitHub 命令行大全。 为什么要用命令行? 众所周知,GitHub 早已有各种各样的 GUI 版本,比如我们 Windows 系统就有 GitHub for Windows,「哎哟,我的亲娘,我等穷鬼,自然用不起 Mac。」功能可谓十分齐全。 那为啥还要用命令行操作呢? nanchen 装逼从来都是次要的,作为一个程序员,你能说你一辈子都只用 Windows 吗? 也许你现在买不起 Mac,但你得有梦想呀!反正我从一开始就用的命令行,SVN 也是如此。 nanchen 以下是命令整理 一般的命令 git init => 初始化一个本地 git 仓库 git config --global user.name "Your Name" => 设置自己的 Name git config --global user.email you@example.com => 设置自己的邮箱 git add -A => 将本次增加的文件全部加入到缓冲区中 git commit -m "message" => 向本地做一次提交 git status => 查看文件状态 git remote add origin https://github.com/nanchen2251/AiYaGirl.git => 把本地仓库和远程仓库做连接。「其中的 https://github.com/nanchen2251/AiYaGirl.git 应该替换为你的地址」 git push -u origin master => 提交文件到你的远程仓库 多分支管理 git branch branch1 => 在本地新建一个分支,其中「branch1」为分支名 git checkout branch1 => 切换到你的新分支,其中「branch1」为分支名 git push origin branch1 => 把新分支发布到远程的仓库 git branch -d branch1 => 在本地删除一个分支 git push origin :branch1 => 在远成仓库删除一个分支,分支前的「:」代表删除 git merge branch1 => 分支合并 git push -u origin master => 推送主分支 git branch => 查看分支,并查看当前隶属哪个分支
干嘛要搭建个人博客? 首先,不管你是不是搞开发的,自己搭建一个专属自己的小窝,这是不是本身就很酷? nanchen 而如果作为一名程序员,肯定有很多人像我一样酷酷地喜欢写 Blog,除了装逼,还可以记录自己的成长,分享帮助到更多人。兄弟,没自己的个人博客,你好意思说你是软件开发工程师么? nanchen 现在我们就来开始装逼之路,利用 Hexo + Next + GitHub 来搭建我们的专属小窝。 没图我都不好瞎逼逼。 nanchen nanchen 什么是 Hexo? Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown (或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。 Hexo 的安装前提 安装 Hexo 相当简单。然而在安装前,您必须检查电脑中是否已安装下列应用程序: Node.js Git nanchen Hexo 的安装 如果您的电脑中已经安装上述必备程序,那么恭喜您!接下来只需要使用 npm 即可完成 Hexo 的安装。 安装 Hexo $ npm install -g hexo-cli 新建一个文件夹,然后定位到该文件夹下依次执行 $ hexo init $ hexo g $ hexo s 这时候你已经可以通过在浏览器输入 localhost:4000 看到你的本地初始 Blog 了。 nanchen Mac 用户 您在编译时可能会遇到问题,请先到 App Store 安装 Xcode,Xcode 完成后,启动并进入 Preferences -> Download -> Command Line Tools -> Install 安装命令行工具。 GitHub 如果没有 GitHub 账号的我也没话说,自己乖乖去 GitHub 官网 注册一个吧。如果你恰巧运气不好遇上了 SSH key 设置问题,我建议你直接百度吧。 nanchen 当然你得在 GitHub 上 new 一个 repository,并且仓库命名为:你的GitHub名字.github.io。如:我的 GitHub 名字是 「nanchen2251」,所以 我的仓库 名字就是:nanchen2251.github.io 名字不可以乱取,乱取不行别怪我 Next Next 只是 Hexo 下的一种主题配置文件,由于个人比较偏爱 Next,所以暂且用 Next 作为主讲,如果你喜欢其他的可以自行下载使用。 Next 的安装下载 如果你熟悉 Git, 建议你使用 克隆最新版本 的方式,之后的更新可以通过 git pull 来快速更新, 而不用再次下载压缩包替换。 在终端窗口下,定位到 Hexo 站点目录下。使用 Git checkout 代码: $ cd your-hexo-site $ git clone https://github.com/iissnan/hexo-theme-next themes/next nanchen 如果你对 Git 感兴趣,可以通过《Pro Git》这本书来学习。作为一个乐于分享的人,怎么会忍心让你去看书,你可以通过点击 这里 进到在线版本。 启动你的 Next 主题 修改 Hexo 目录下的 config.yml 配置文件中的theme属性,将其设置为 next。 修改后别忘了执行下方命令查看效果 $ hexo g -- 生成 $ hexo s -- 启动本地预览 nanchen 我反手就是一个 文档。 几个必要的常用命令 这里有必要提下Hexo常用的几个命令: hexo generate (hexo g) 生成静态文件,会在当前目录下生成一个新的叫做 public 的文件夹 hexo server (hexo s) 启动本地 web 服务,用于博客的预览 hexo deploy (hexo d) 部署博客到远端(比如 GitHub) $ hexo new "postName" #新建文章 $ hexo new page "pageName" #新建页面 postName 是你自己起的文章名字 , 内容就在这里面写 生成的文章会存在于 Hexo\source_posts 目录下 文章默认是一个 .md 的格式 , 意思它用的是 Markdown 语法 , 不知道的百度 , 关于语法的问题不知道的完了学 , 先下载一个 Markdown 编辑器 Markdown 编辑器我喜欢用 Typora , 你可以点击链接直接进入下载。 划重点 首先使用 Git 命令 clone 你前面新建的仓库。 注意下方的 「nanchen2251」需要替换为你的 GitHub 名字。 $ git clone https://github.com/nanchen2251/nanchen2251.github.io .deploy/nanchen2251.github.io 强烈建议在 Hexo 根目录下创建一个 .txt 文件,然后把下面的命令复制进去。 hexo generate cp -R public/* .deploy/nanchen2251.github.io cd .deploy/nanchen2251.github.io git add . git commit -m "update" git push origin master 将这个 .txt 文件的后缀改成 .sh , 它就变成了脚本文件 , 我们就将文件改成 deploy.sh 吧!意思就是部署。 nanchen 从此以后需要部署本地博客到 GitHub , 直接可以双击这个文件就可以了。 nanchen 最后,你只需要在浏览器输入:https://nanchen2251.github.io/ 就可以访问了。(注意其中的 「nanchen2251」 是我的 GitHub 用户名,实际上你得替换为你的 GitHub 名字)。 棒呆,听说点赞的人长得都帅!!! 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
写在前面 昨天也是为大家分享了 7.0 相机适配,今天就来为大家讲讲 Android 之相机适配。 提起 Android 调用系统相机拍照上传图片或者是显示图片,想必任何一位开发 Android 的朋友都不会陌生,基本这个功能已经涵盖各个应用了,今天,我就来给大家聊聊网上并不多见却有经常听到大家吐槽的问题。 根据相机适配对图片的操作,所以有了这款图片压缩库:[https://github.com/nanchen2251/CompressHelper 拍照功能实现 对于拍照功能的实现方式我这里就不多谈了,无非两种,一种是利用相机的 API 来自定义相机,另一种是利用 Intent 调用系统指定的相机拍照。而这两种方式的实现网上搜索一大把,我就不在这里啰嗦了。 有没有相机可用? 前面讲到我们是调用系统指定的相机 APP 来拍照,那么系统是否存在可以被我们调用的 APP 呢?这个我们不敢确定,毕竟 Android 奇葩问题多,还真有遇到过这种极端的情况导致闪退的。虽然很极端,但作为客户端人员还是要进行处理,方式有二: 调用相机时,简单粗暴的 try-catch 调用相机前,检测系统有没有相机 APP 可用 try-catch 这种粗暴的方式大家肯定很熟悉了,那么要如何检测系统有没有相机 APP 可用呢?系统在 PackageManager 里为我们提供这样一个 API: 通过这样一个 API ,可以知道系统是否存在 Action 为 MediaStore.ACTION_IMAGE_CAPTURE 的 Intent 可以唤起的拍照界面,具体实现代码如下: /** * 判断系统中是否存在可以启动的相机应用 * * @return 存在返回true,不存在返回false */ public boolean hasCamera() { PackageManager packageManager = mActivity.getPackageManager(); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return list.size() > 0; } 拍出来的照片“歪了”!!! 经常会遇到一种情况,拍照时看到照片是正的,但是当我们的 APP 获取到这张照片时,却发现旋转了 90 度(也有可能是 180、270,不过 90 度比较多见,貌似都是由于手机传感器导致的)。很多童鞋对此感到很困扰,因为不是所有手机都会出现这种情况,就算会是出现这种情况的手机上,也并非每次必现。要怎么解决这个问题呢?从解决的思路上看,只要获取到照片旋转的角度,利用 Matrix 来进行角度纠正即可。那么问题来了,要怎么知道照片旋转的角度呢?细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据。 没错,这里面就有一个旋转角度,倘若拍照后保存的成像照片文件发生了角度旋转,这个图片的属性参数就能告诉我们到底旋转了多少度。只要获取到这个角度值,我们就能进行纠正的工作了。 Android 系统提供了 ExifInterface 类来满足获取图片各个属性的操作。 通过 ExifInterface 类拿到 TAG_ORIENTATION 属性对应的值,即为我们想要得到旋转角度。再根据利用 Matrix 进行旋转纠正即可。实现代码大致如下: /** * 获取图片的旋转角度 * * @param path 图片绝对路径 * @return 图片的旋转角度 */ public static int getBitmapDegree(String path) { int degree = 0; try { // 从指定路径下读取图片,并获取其EXIF信息 ExifInterface exifInterface = new ExifInterface(path); // 获取图片的旋转信息 int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: degree = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: degree = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: degree = 270; break; } } catch (IOException e) { e.printStackTrace(); } return degree; } /** * 将图片按照指定的角度进行旋转 * * @param bitmap 需要旋转的图片 * @param degree 指定的旋转角度 * @return 旋转后的图片 */ public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) { // 根据旋转角度,生成旋转矩阵 Matrix matrix = new Matrix(); matrix.postRotate(degree); // 将原始图片按照旋转矩阵进行旋转,并得到新的图片 Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } return newBitmap; } ExifInterface 能拿到的信息远远不止旋转角度,其他的参数感兴趣的童鞋可以看看 API 文档。 拍完照怎么闪退了? 曾在小米和魅族的某些机型上遇到过这样的问题,调用系统相机拍照,拍完点击确定回到自己的 APP 里面却莫名奇妙的闪退了。这种闪退有两个特点: 没有什么错误日志(有些机子啥日志都没有,有些机子会出来个空异常错误日志); 同个机子上非必现(有时候怎么拍都不闪退,有时候一拍就闪退); 对待非必现问题往往比较头疼,当初遇到这样的问题也是非常不解。上网搜罗了一圈也没方案,后来留意到一个比较有意思信息:有些系统厂商的 ROM 会给自带相机应用做优化,当某个 APP 通过 Intent 进入相机拍照界面时,系统会把这个 APP 当前最上层的 Activity 销毁回收。(注意:我遇到的情况是有时候很快就回收掉,有时候怎么等也不回收,没有什么必现规律)为了验证一下,便在启动相机的 Activity 中对 onDestory() 方法进行加 Log 。果不其然,终于发现进入拍照界面的时候 onDestory() 方法被执行了。所以,前面提到的闪退基本可以推测是 Activity 被回收导致某些非UI控件的成员变量为空导致的。(有些机子会报出空异常错误日志,但是有些机子闪退了什么都不报,是不是觉得很奇葩!) 既然涉及到 Activity 被回收的问题,自然要想起 onSaveInstanceState() 和 onRestoreInstanceState() 这对方法。去到 onSaveInstanceState() 把数据保存,并在 onRestoreInstanceState() 方法中进行恢复即可。大体代码思路如下: @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mRestorePhotoFile = mCapturePhotoHelper.getPhoto(); if (mRestorePhotoFile != null) { outState.putSerializable(EXTRA_RESTORE_PHOTO, mRestorePhotoFile); } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mRestorePhotoFile = (File) savedInstanceState.getSerializable(EXTRA_RESTORE_PHOTO); mCapturePhotoHelper.setPhoto(mRestorePhotoFile); } 对于 onSaveInstanceState() 和 onRestoreInstanceState() 方法的作用还不熟悉的童鞋,网上资料很多,可以自行搜索。 到这里,可能有童鞋要问,这种闪退并不能保证复现,我要怎么知道问题所在和是否修复了呢?我们可以去到开发者选项里开启不保留活动这一项进行调试验证。 它的作用是保留当前和用户接触的 Activity ,并将目前无法和用户交互 Activity 进行销毁回收。打开这个调试选项就可以满足验证的需求,当你的 app 的某个 Activity 跳转到拍照的 Activity 后,这个 Activity 立马就会被系统销毁回收,这样就可以很好的完全复现闪退的场景,帮助开发者确认问题有没有修复了。 涉及到 Activity 被销毁,还想提一下代码实现上的问题。假设当前有两个 Activity ,MainActivity 中有个 Button ,点击可以调用系统相机拍照并显示到 PreviewActivity 进行预览。有下面两种实现方案: MainActivity 中点击 Button 后,启动系统相机拍照,并在 MainActivity 的 onActivityResult() 方法中获取拍下来的照片,并启动跳转到 PreviewActivity 界面进行效果预览; MainActivity 中点击 Button 后,启动 PreviewActivity 界面,在 PreviewActivity 的 onCreate()(或者 onStart()、onResume())方法中启动系统相机拍照,然后在 PreviewActivity 的 onActivityResult() 方法中获取拍下来的照片进行预览; 上面两种方案得到的实现效果是一模一样的,但是第二种方案却存在很大的问题。因为启动相机的代码放在 onCreate()(或者 onStart() 、onResume())中,当进入拍照界面后,PreviewActivity 随即被销毁,拍完照确认后回到 PreviewActivity 时,被销毁的 PreviewActivity 需要重建,又要走一遍 onCreate() 、onStart() 、onResume(),又调用了启动相机拍照的代码,周而复始的进入了死循环状态。为了避免让你的用户抓狂,果断明智的选择方案一。 以上这种情况提到调用系统拍照时,Activity 就回收的情况,在小米 4S 和小米 4 LTE 机子上(MIUI 的版本是 7.3,Android 系统版本是 6.0)出现的概率很高。 所以,建议看到此文的童鞋也可以去验证适配一下。 图片无法显示 图片无法显示这个问题也是略坑,如何坑法?往下看,同样是在小米 4S 和小米 4 LTE 机子上(MIUI 的版本是 7.3,Android 系统版本是 6.0)出现概率很高的场景(当然,不保证其他机子没出现过)。按照我们前面提到的业务场景,调用相机拍照完成后,我们的 APP 会有一个预览图片的界面。但是在用了小米的机子进行拍照后,自己 APP 的预览界面却怎么也无法显示出照片来,同样是相当郁闷,郁闷完后还是要一步一步去排查解决问题的!为此,需要一步一步猜测验证问题所在。 猜测一:没有拿到照片路径,所以无法显示? 直接断点打 log 跟踪,猜测一很快被推翻,路径是有的。 猜测二:Bitmap太大了,无法显示? 直接在 Android Studio 的 log 控制台仔细的观察了一下系统 log ,发现了一些蛛丝马迹OpenGLRenderer: Bitmap too large to be uploaded into a texture 每次拍完照片,都会出现上面这样的 log ,果然,因为图片太大而导致在 ImageView 上无法显示。到这里有童鞋要吐槽了,没对图片的采样率 inSampleSize 做处理?天地良心啊,绝对做处理了,直接看代码: /** * 压缩Bitmap的大小 * * @param imagePath 图片文件路径 * @param requestWidth 压缩到想要的宽度 * @param requestHeight 压缩到想要的高度 * @return */ public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { if (!TextUtils.isEmpty(imagePath)) { if (requestWidth <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 BitmapFactory.decodeFile(imagePath, options); options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imagePath, options); } else { return null; } } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; Log.i(TAG, "height: " + height); Log.i(TAG, "width: " + width); if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } long totalPixels = width * height / inSampleSize; final long totalReqPixelsCap = reqWidth * reqHeight * 2; while (totalPixels > totalReqPixelsCap) { inSampleSize *= 2; totalPixels /= 2; } } return inSampleSize; } 瞄了代码后,是不是觉得没有问题了?没错,inSampleSize 确确实实经过处理,那为什么图片还是太大而显示不出来呢? requestWidth、requestHeight 设置得太大导致 inSampleSize 太小了?不可能啊,我都试着把长宽都设置成 100 了还是没法显示!干脆,直接打印 inSampleSize 值,一打印,inSampleSize 值居然为 1 。 我去,彻底打脸了,明明说好的处理过了,居然还是 1 !!!!为了一探究竟,干脆加 log 。 public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { if (!TextUtils.isEmpty(imagePath)) { Log.i(TAG, "requestWidth: " + requestWidth); Log.i(TAG, "requestHeight: " + requestHeight); if (requestWidth <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 BitmapFactory.decodeFile(imagePath, options); Log.i(TAG, "original height: " + options.outHeight); Log.i(TAG, "original width: " + options.outWidth); options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 Log.i(TAG, "inSampleSize: " + options.inSampleSize); options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imagePath, options); } else { return null; } } 运行打印出来的日志如下: 图片原来的宽高居然都是 -1 ,真是奇葩了!难怪,inSampleSize 经过处理之后结果还是 1 。狠狠的吐槽了之后,总是要回来解决问题的。那么,图片的宽高信息都丢失了,我去哪里找啊? 像下面这样? public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { ... BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options); bitmap.getWidth(); bitmap.getHeight(); ... } else { return null; } } no,此方案行不通,inJustDecodeBounds = true 时,BitmapFactory 获得 Bitmap 对象是 null; 那要怎样才能获图片的宽高呢?前面提到的 ExifInterface 再次帮了我们大忙,通过它的下面两个属性即可拿到图片真正的宽高。 顺手吐槽一下,为什么高不是 TAG_IMAGE_HEIGHT 而是 TAG_IMAGE_LENGTH。改良过后的代码实现如下: public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { if (!TextUtils.isEmpty(imagePath)) { Log.i(TAG, "requestWidth: " + requestWidth); Log.i(TAG, "requestHeight: " + requestHeight); if (requestWidth <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 BitmapFactory.decodeFile(imagePath, options); Log.i(TAG, "original height: " + options.outHeight); Log.i(TAG, "original width: " + options.outWidth); if (options.outHeight == -1 || options.outWidth == -1) { try { ExifInterface exifInterface = new ExifInterface(imagePath); int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 Log.i(TAG, "exif height: " + height); Log.i(TAG, "exif width: " + width); options.outWidth = width; options.outHeight = height; } catch (IOException e) { e.printStackTrace(); } } options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 Log.i(TAG, "inSampleSize: " + options.inSampleSize); options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imagePath, options); } else { return null; } } 再看一下,打印出来的 log 以上总结了这么些身边童鞋经常问起,但网上又不多见的适配问题,希望可以帮到一些开发童鞋少走弯路。文中多次提到小米的机子,并不代表只有MIUI上有这样的问题存在,仅仅只是因为我身边带的几部机子大都是小米的。对待适配问题,在搜索引擎都无法提供多少有效的信息时,我们只能靠断点、打 log、观察控制台的日志、以及 API 文档来寻找一些蛛丝马迹作为突破口,相信办法总比困难多。 以上内容采自:There 至于为什么基本全文 copy,是因为我觉得作者已经讲的特别清楚了,我没必要做二次重复,也只是给大家分享一下。 那下面就让我来补充一下不一样的开发情景。 如果同事写好了压缩九宫格显示图片呢? 这时候你们大框架已经搞定了,只需要你传回一个文件的path,你可能会这样写:(下面所有代码都在 onActivityResult()` 方法) if (null != imageGridAdapter.uri) { final String url = PhotoUtil.getImageUrlFromActivityResult(this, imageGridAdapter.uri);//这个方法可以拿到图片的path Log.e(TAG, "onActivityResult: url:" + url); if (!TextUtils.isEmpty(url)) { file = new File(url); size = file.length(); Log.e(TAG, "onActivityResult: size:" + size); if (size > 0 ){ ImageItem imageItem = new ImageItem(); imageItem.ImageId = ImageItem.NEW_ID; imageItem.PhotoPath = url; imageGridAdapter.getmDataList().add(imageItem); imageGridAdapter.notifyDataSetChanged(); imageGridAdapter.uri = null; } } } 好像没啥问题呀,拿到图片的 path,new 一个文件,如果文件 size = 0,则不显示,大于 0 说明图片存在,则显示图片,提示 UI 刷新。 嗯,的确,绝大部分手机都测试通过了,然而在坑爹的部分MIUI系统上出现了,返回 size 为 0,进不到判断循环,自然不会显示那个图片。 这时候肯定要到 google 上去搜上一圈,一圈下来收获不少,却没找到真正解决的办法。 这里有位朋友就说啦,这个 size 为0,但是间隔一定的时间就可以 new 出 size 不为 0 的 file,而这个时间是不固定的。 可见遇上这个坑的小伙伴也是尽心尽力,想方设法。 所以我也尝试了这个方法,考虑到不能休眠主线程,就采用新开一个线程来延时处理,采用加载框,一旦 size 不为 0 了,才进行显示处理。 可能图片出问题,所以我们设置一个最大休眠时间。代码为:(其中 flag 和 time 为全局变量) flag = true; time = 0; final String url = PhotoUtil.getImageUrlFromActivityResult(this, imageGridAdapter.uri); if (size <= 0) { // 如果size小于0出现了,则使用加载框 new Thread(new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { showLoading(SendQuestionActivity.this);// 该方法为显示加载框 } }); while (flag) { // 设置延迟步长是0.5s try { Thread.sleep(500); time += 0.5; file = null; file = new File(url); size = file.length(); Log.e(TAG, "onActivityResult: size:" + size); if (size > 0 || time >= 10) { flag = false; } } catch (InterruptedException e) { e.printStackTrace(); } } runOnUiThread(new Runnable() { @Override public void run() { stopLoading();//该方法为取消加载框 } }); } }).start(); } ImageItem imageItem = new ImageItem(); imageItem.ImageId = ImageItem.NEW_ID; imageItem.PhotoPath = url; imageGridAdapter.getmDataList().add(imageItem); imageGridAdapter.notifyDataSetChanged(); imageGridAdapter.uri = null; 这样问题是得到解决啦,但是用户取消的时候 size 也为 0 呀,这样无疑是画蛇添足,用户取消拍照都要显示加载框 10s,想想都可怕!!! 又听说在 new File 的前面加上下面的这句话可以解决,倒腾一番,然并卵。 // 小米4 LTE MIUI 7.0 版本下,file的size始终为0;通过提前获取ExifInterface信息,保证文件确实写入到外存 try { ExifInterface exifInterface = new ExifInterface(url); } catch (Exception e) { e.printStackTrace(); } 额,等等。上面说了,ExifInterface 可以拿到图片的宽高等参数,那我是不是可以直接通过判断图片宽高是否为 0 来判断用户是否拍照呢?如果 width 和 height 为 0,说明用户取消了拍照,不显示。否则显示图片,心动不如行动,直接上代码。 flag = true; Log.e(TAG, "onActivityResult: uri:" + imageGridAdapter.uri); if (null != imageGridAdapter.uri) { final String url = PhotoUtil.getImageUrlFromActivityResult(this, imageGridAdapter.uri); Log.e(TAG, "onActivityResult: url:" + url); boolean flag = true; // 小米4 LTE MIUI 7.0 版本下,file的size始终为0;通过提前获取ExifInterface信息,保证文件确实写入到外存 try { ExifInterface exifInterface = new ExifInterface(url); int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 Log.e(TAG, "onActivityResult: height:" + height); Log.e(TAG, "onActivityResult: width:" + width); if (height == 0 && width == 0) { flag = false; } } catch (Exception e) { e.printStackTrace(); } if (!TextUtils.isEmpty(url)) { file = new File(url); size = file.length(); Log.e(TAG, "onActivityResult: size:" + size); /** * MIUI8.0上面方案无法解决,经测试发现在一定时间后能保证size不为0 * 奇怪的发现当size为0的时候依然可以拿到图片,多款手机测试通过 */ if (flag) { ImageItem imageItem = new ImageItem(); imageItem.ImageId = ImageItem.NEW_ID; imageItem.PhotoPath = url; imageGridAdapter.getmDataList().add(imageItem); imageGridAdapter.notifyDataSetChanged(); imageGridAdapter.uri = null; } } } OK,终于解决了。希望能帮到大家! 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
写在前面 这是最近一些朋友问我的问题,我把它整理成了一个库,供大家享用,GitHub 地址:https://github.com/nanchen2251/AppManager 从四个应用场景说起 退出应用 相信各位朋友或多或少都会有遇到过需要在某个特定的地方退出应用的需求,这个场景一定非常普遍。 崩溃后重启 程序总是无法做到尽善尽美,有时候你也不知道因为什么原因导致了 APP 的崩溃,这无疑是非常糟糕的用户体验。这时候我们可以采用重启机制来增强用户舒适体验感。 莫名其妙重启 然而心细的小伙伴肯定会发现,在部分手机上会出现莫名其妙的崩溃后重启(后面会讲原因),而且最要命的是,假设你有三个 Activity,他们分别是 Act1, Act2, Act3,它们的启动顺序是 Act1 -> Act2 -> Act3,而如果在 Act3 发生了崩溃,这时候极有可能应用重启后进入的是 Act2,而 Act2 中需要某个来源于 Act1 (或者在 Act1 中通过接口获取) 的参数,当没有这个参数的时候会引发崩溃(或者数据不全)。这时候你可能最直观的想法就是禁止应用重启,但或许这并不是最佳的方式。 崩溃时弹出一个对话框 在部分手机上,当崩溃的时候,会弹出一个提示对话框。在这种情况下,用户只有点击 “强行关闭” 来结束程序。当该对话框出现,对用户来说是相当不友好的。或许我们可以通过某种方式拦截掉系统的处理,让应用出错时不再显示它。 退出应用的几种方式 Andorid 退出应用的方式很多,常见的也就下面四种。 System.exit(0) 使用系统的方法,强制退出System.exit(0) 表示的是终止程序,终止当前正在运行的 Java 虚拟机,在 Java 中我们也使用这种方式来关闭整个应用,在前期很多开发人员都是使用这种方式,我自己在开发项目过程中也用过这种方式来退出,但是有时候会在部分机型中,当退出应用后弹出应用程序崩溃的对话框,有时退出后还会再次启动,少部分的用户体验不太好。但现在也依旧还会有少部分的开发人员会使用这种方式,因为使用方式很简单,只需要在需要退出的地方加上这句代码就行。 抛出异常,强制退出 这种方式现在基本上已经看不到了,用户体验比第一种方式更差,就是让抛出异常、是系统崩溃、从而达到退出应用的效果 使用 Application 退出 目前比较常用方法之一,我们都知道 Application 是 Android 的系统组件,当应用程序启动时,会自动帮我们创建一个 Application,而且一个应用程序只能存在一个 Application ,它的生命周期也是最长的,如果需要使用自己创建的 Application 时,这个时候我们只需要在 Androidmanifest.xml 中的 <Application> 标签中添加 name 属性:把创建的 Application 完整的包名 + 类名放进了就行了。 使用广播退出 使用广播来实现退出应用程序,其实实现的思路相对于第三种更简单,我们编写一个 BaseActivity,让其他的 Activity 都继承于它,当我需要退出时,我们就销毁 BaseActivity,那么其他继承与它的 Activity 都会销毁。 四种方式的代码也就不多提,需要的自己去 Android:销毁所有的 Activity 退出应用程序几种方式 莫名其妙重启? 上面的场景中说到了,再部分手机上会出现崩溃后自动重启的情况,这让我们很不好控制。经本人测试,在 Android 的 API 21 ( Android 5.0 ) 以下,Crash 会直接退出应用,但是在 API 21 ( Android 5.0 ) 以上,系统会遵循以下原则进行重启: 包含 Service,如果应用 Crash 的时候,运行着 Service,那么系统会重新启动 Service。 不包含 Service,只有一个 Activity,那么系统不会重新启动该 Activity。 不包含 Service,但当前堆栈中存在两个 Activity:Act1 -> Act2,如果 Act2 发生了 Crash ,那么系统会重启 Act1。 不包含 Service,但是当前堆栈中存在三个 Activity:Act1 -> Act2 -> Act3,如果 Act3 崩溃,那么系统会重启 Act2,并且 Act1 依然存在,即可以从重启的 Act2 回到 Act1。 在这样的情况下,我们或许会有两种需求: 崩溃后不允许重启 崩溃后需要重启 怎么办 翻看 API 我们发现,Java 中存在一个 UncaughtExceotionHandler 的接口,而在 Android 中我们沿用了它,我们可以采用这个接口实现我们想要的功能。 (为了方便,我把它做成了库,传送门:https://github.com/nanchen2251/AppManager) 讲一些核心 CrashApplication 首先是我们的 CrashApplication 类,因为我们崩溃的时候需要结束程序后再重启,所以我们需要退出应用,这里我们采用上面的第三种方式。 public class CrashApplication extends Application { private List<Activity> mActivityList; @Override public void onCreate() { super.onCreate(); mActivityList = new ArrayList<>(); } /** * 添加单个Activity */ public void addActivity(Activity activity) { // 为了避免重复添加,需要判断当前集合是否满足不存在该Activity if (!mActivityList.contains(activity)) { mActivityList.add(activity); // 把当前Activity添加到集合中 } } /** * 销毁单个Activity */ public void removeActivity(Activity activity) { // 判断当前集合是否存在该Activity if (mActivityList.contains(activity)) { mActivityList.remove(activity); // 从集合中移除 if (activity != null){ activity.finish(); // 销毁当前Activity } } } /** * 销毁所有的Activity */ public void removeAllActivity() { // 通过循环,把集合中的所有Activity销毁 for (Activity activity : mActivityList) { if (activity != null){ activity.finish(); } } //杀死该应用进程 android.os.Process.killProcess(android.os.Process.myPid()); } } UncaughtExceptionHandlerImpl 我们当然少不了新建一个 UncaughtExceptionHandlerImpl 类去实现我们的 UncaughtExceptionHandler 接口,它必须实现我们的 uncaughtException(thread, throwable) 方法,我们接下来可以在这中间作文章。需要特别注意的是:重启必须清除堆栈内的 Activity。 /** * 当 UncaughtException 发生时会转入该函数来处理 */ @SuppressWarnings("WrongConstant") @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) { // 如果用户没有处理则让系统默认的异常处理器来处理 mDefaultHandler.uncaughtException(thread, ex); } else { try { Thread.sleep(2000); } catch (InterruptedException e) { Log.e(TAG, "error : ", e); } if (mIsRestartApp) { // 如果需要重启 Intent intent = new Intent(mContext.getApplicationContext(), mRestartActivity); AlarmManager mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); //重启应用,得使用PendingIntent PendingIntent restartIntent = PendingIntent.getActivity( mContext.getApplicationContext(), 0, intent, Intent.FLAG_ACTIVITY_NEW_TASK); mAlarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + mRestartTime, restartIntent); // 重启应用 } // 结束应用 ((CrashApplication) mContext.getApplicationContext()).removeAllActivity(); } } 我们的 handleException(throwable) 方法用于弹出 Toast 和收集 Crash 信息。 /** * 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成 * * @param ex * @return true:如果处理了该异常信息;否则返回 false */ private boolean handleException(final Throwable ex) { if (ex == null) { return false; } // 使用 Toast 来显示异常信息 new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(mContext, getTips(ex), Toast.LENGTH_LONG).show(); Looper.loop(); } }.start(); // 如果用户不赋予外部存储卡的写权限导致的崩溃,会造成循环崩溃 // if (mIsDebug) { // // 收集设备参数信息 // collectDeviceInfo(mContext); // // 保存日志文件 // saveCrashInfo2File(ex); // } return true; } 封装好的使用 1、添加依赖 Step 1. Add it in your root build.gradle at the end of repositories: allprojects { repositories { ... maven { url 'https://jitpack.io' } } } Step 2. Add the dependency dependencies { compile 'com.github.nanchen2251:AppManager:1.0.1' } 2、在需要使用的地方使用 // 设置崩溃后自动重启 APP UncaughtExceptionHandlerImpl.getInstance().init(this, BuildConfig.DEBUG, true, 0, MainActivity.class); 3、你也可以禁止重启 // 禁止重启 UncaughtExceptionHandlerImpl.getInstance().init(this,BuildConfig.DEBUG); 效果图 [图片上传失败...(image-ad15ca-1509694874814)] 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
一、写在前面 其实博主在之前已经对 Design 包的各个控件都做了博文说明,无奈个人觉得理解不够深入,所以有了这篇更加深入的介绍,希望各位看官拍砖~ 二、从是什么开始 1、首先我们得知道 CoordinatorLayout 是什么玩意儿,到底有什么用,我们不妨看看官方文档的描述: CoordinatorLayout 是一个 “加强版” FrameLayout, 它主要有两个用途: 用作应用的顶层布局管理器,也就是作为用户界面中所有 UI 控件的容器; 用作相互之间具有特定交互行为的 UI 控件的容器,通过为 CoordinatorLayout 的子 View 指定 Behavior, 就可以实现它们之间的交互行为。 Behavior 可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的 UI 元素,以及跟随着其他 UI 控件移动的按钮等。 其实总结出来就是 CoordinatorLayout 是一个布局管理器,相当于一个增强版的 FrameLayout,但是它神奇在于可以实现它的子 View 之间的交互行为。 2、交互行为? 先看个简单的效果图 可能大家看到这,就自然能想到观察者模式,或者我前面写的Rx模式:这可能是最好的RxJava 2.x 教程(完结版) 我们的 Button 就是一个被观察者,TextView 作为一个观察者,当 Button 移动的时候通知 TextView, TextView 就跟着移动。看看其布局: <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_coordinator" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.nanchen.coordinatorlayoutdemo.CoordinatorActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="观察者" app:layout_behavior=".FollowBehavior"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="被观察者" android:layout_gravity="center" android:id="@+id/btn"/> </android.support.design.widget.CoordinatorLayout> 很简单,一个 TextView, 一个 Button, 外层用 CoordinatorLayout, 然后给我们的 TextView 加一个自定义的 Behavior 文件,内容如下: package com.nanchen.coordinatorlayoutdemo; import android.content.Context; import android.support.design.widget.CoordinatorLayout; import android.util.AttributeSet; import android.view.View; import android.widget.Button; import android.widget.TextView; /** * * 自定义 CoordinatorLayout 的 Behavior, 泛型为观察者 View ( 要跟着别人动的那个 ) * * 必须重写两个方法,layoutDependOn和onDependentViewChanged * * @author nanchen * @fileName CoordinatorLayoutDemo * @packageName com.nanchen.coordinatorlayoutdemo * @date 2016/12/13 10:13 */ public class FollowBehavior extends CoordinatorLayout.Behavior<TextView>{ /** * 构造方法 */ public FollowBehavior(Context context, AttributeSet attrs) { super(context, attrs); } /** * 判断child的布局是否依赖 dependency * * 根据逻辑来判断返回值,返回 false 表示不依赖,返回 true 表示依赖 * * 在一个交互行为中,Dependent View 的变化决定了另一个相关 View 的行为。 * 在这个例子中, Button 就是 Dependent View,因为 TextView 跟随着它。 * 实际上 Dependent View 就相当于我们前面介绍的被观察者 * */ @Override public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) { return dependency instanceof Button; } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) { child.setX(dependency.getX()); child.setY(dependency.getY() + 100); return true; } } 重点看看其中重写的两个方法 layoutDependsOn() 和 onDependentViewChanged() 。在介绍这两个方法的作用前,我们先来介绍一下 Dependent View。在一个交互行为中,Dependent View 的变化决定了另一个相关 View 的行为。在这个例子中, Button 就是 Dependent View, 因为 TextView 跟随着它。实际上 Dependent View 就相当于我们前面介绍的被观察者。 知道了这个概念,让我们看看重写的两个方法的作用: layoutDependsOn():这个方法在对界面进行布局时至少会调用一次,用来确定本次交互行为中的 Dependent View,在上面的代码中,当 Dependency 是Button 类的实例时返回 true,就可以让系统知道布局文件中的 Button 就是本次交互行为中的 Dependent View。 onDependentViewChanged():当 Dependent View 发生变化时,这个方法会被调用,参数中的child相当于本次交互行为中的观察者,观察者可以在这个方法中对被观察者的变化做出响应,从而完成一次交互行为。 所以我们现在可以开始写Activity中的代码: findViewById(R.id.btn).setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_MOVE){ view.setX(motionEvent.getRawX()-view.getWidth()/2); view.setY(motionEvent.getRawY()-view.getHeight()/2); } return true; } }); 这样一来,我们就完成了为 TextView 和Button 设置跟随移动这个交互行为。很简单有木有,其实为 CoordinatorLayout 的子 View 设置交互行为只需三步: 自定义一个继承自 Behavior 类的交互行为类; 把观察者的 layout_behavior 属性设置为自定义行为类的类名; 重写 Behavior 类的相关方法来实现我们想要的交互行为。 值得注意的是,有些时候,并不需要我们自己来定义一个 Behavior 类,因为系统为我们预定义了不少 Behavior 类。在接下来的篇章中,我们会做出进一步的介绍。 3、更进一步 现在我们已经知道了怎么通过给 CoordinatorLayout 的子 View 设置 Behavior 来实现交互行为。现在,让我们更进一步地挖掘下 CoordinatorLayout, 深入了解一下隐藏在表象背后的神秘细节。 实际上, CoordinatorLayout 本身并没有做过多工作,实现交互行为的主要幕后推手是 CoordinatorLayout 的内部类—— Behavior。通过为 CoordinatorLayout 的**直接子 View **绑定一个 Behavior ,这个 Behavior 就会拦截发生在这个 View 上的 Touch 事件、嵌套滚动等。不仅如此,Behavior 还能拦截对与它绑定的 View 的测量及布局。关于嵌套滚动,我们会在后续文章中进行详细介绍。下面我们来深入了解一下 Behavior 是如何做到这一切的。 4、深入理解 Behavior 拦截 Touch 事件 当我们为一个 CoordinatorLayout 的直接子 View 设置了 Behavior 时,这个 Behavior 就能拦截发生在这个 View 上的 Touch 事件,那么它是如何做到的呢?实际上, CoordinatorLayout 重写了 onInterceptTouchEvent() 方法,并在其中给 Behavior 开了个后门,让它能够先于 View 本身处理 Touch 事件。具体来说, CoordinatorLayout 的 onInterceptTouchEvent() 方法中会遍历所有直接子 View ,对于绑定了 Behavior 的直接子 View 调用 Behavior 的 onInterceptTouchEvent() 方法,若这个方法返回 true, 那么后续本该由相应子 View 处理的 Touch 事件都会交由 Behavior 处理,而 View 本身表示懵逼,完全不知道发生了什么。 拦截测量及布局 了解了 Behavior 是怎养拦截 Touch 事件的,想必大家已经猜出来了它拦截测量及布局事件的方式 —— CoordinatorLayout 重写了测量及布局相关的方法并为 Behavior 开了个后门。没错,真相就是如此。 CoordinatorLayout 在 onMeasure() 方法中,会遍历所有直接子 View ,若该子 View 绑定了一个 Behavior ,就会调用相应 Behavior 的 onMeasureChild() 方法,若此方法返回 true,那么 CoordinatorLayout 对该子 View 的测量就不会进行。这样一来, Behavior 就成功接管了对 View 的测量。 同样,CoordinatorLayout 在 onLayout() 方法中也做了与 onMeasure() 方法中相似的事,让 Behavior 能够接管对相关子 View 的布局。 View 的依赖关系的确定 现在,我们在探究一下交互行为中的两个 View 之间的依赖关系是怎么确定的。我们称 child 为交互行为中根据另一个 View 的变化做出响应的那个个体,而 Dependent View 为child所依赖的 View。实际上,确立 child 和 Dependent View 的依赖关系有两种方式: 显式依赖:为 child 绑定一个 Behavior,并在 Behavior 类的 layoutDependsOn() 方法中做手脚。即当传入的 dependency 为 Dependent View 时返回 true,这样就建立了 child 和 Dependent View 之间的依赖关系。 隐式依赖:通过我们最开始提到的锚(anchor)来确立。具体做法可以这样:在 XML 布局文件中,把 child 的 layout_anchor 属性设为 Dependent View 的id,然后 child 的 layout_anchorGravity 属性用来描述为它想对 Dependent View 的变化做出什么样的响应。关于这个我们会在后续篇章给出具体示例。 无论是隐式依赖还是显式依赖,在 Dependent View 发生变化时,相应 Behavior 类的 onDependentViewChanged() 方法都会被调用,在这个方法中,我们可以让 child 做出改变以响应 Dependent View 的变化。 三、玩转AppBarLayout 实际上我们在应用中有 CoordinatorLayout 的地方通常都会有 AppBarLayout 的联用,作为同样的出自 Design 包的库,我们看看官方文档怎么说: AppBarLayout 是一个垂直的 LinearLayout,实现了 Material Design 中 App bar 的 Scrolling Gestures 特性。AppBarLayout 的子 View 应该声明想要具有的“滚动行为”,这可以通过 layout_scrollFlags 属性或是 setScrollFlags() 方法来指定。 AppBarLayout 只有作为 CoordinatorLayout 的直接子 View 时才能正常工作,为了让 AppBarLayout 能够知道何时滚动其子 View,我们还应该在 CoordinatorLayout 布局中提供一个可滚动 View,我们称之为 Scrolling View。 Scrolling View 和 AppBarLayout 之间的关联,通过将 Scrolling View 的 Behavior 设为 AppBarLayout.ScrollingViewBehavior 来建立。 1、一般怎么用?AppBar 是 Design 的一个概念,其实我们也可以把它看做一种 5.0 出的 ToolBar,先感受一下 AppBarLayout + CoordinatorLayout 的魅力。 实际效果就是这样,当向上滑动 View 的时候,ToolBar 会小时,向下滑动的时候,ToolBar 又会出现,但别忘了,这是 AppBarLayout 的功能,ToolBar 可办不到。由于要滑动,那么我们的 AppBarLayout 一定是和可以滑动的 View 一起使用的,比如 RecyclerView,ScollView 等。 我们看看上面的到底怎么实现的: <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_coor_app_bar" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.nanchen.coordinatorlayoutdemo.CoorAppBarActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_scrollFlags="scroll|enterAlways"> </android.support.v7.widget.Toolbar> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/recycler" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> </android.support.design.widget.CoordinatorLayout> 我们可以看到,上面出现了一个 app:layouy_scrollFrags 的自定义属性设置,这个属性可以定义我们不同的滚动行为。 **2、layout_scrollFlags ** 根据官方文档,layout_scrollFlags 的取值可以为以下几种。 scroll 设成这个值的效果就好比本 View 和 Scrolling view 是“一体”的。具体示例我们在上面已经给出。有一点特别需要我们的注意,为了其他的滚动行为生效,必须同时指定 Scroll 和相应的标记,比如我们想要 exitUntilCollapsed 所表现的滚动行为,必须将 layout_scrollFlags 指定为 scroll|exitUntilCollapsed 。 exitUntilCollapsed 当本 View 离开屏幕时,会被“折叠”直到达到其最小高度。我们可以这样理解这个效果:当我们开始向上滚动 Scrolling view 时,本 View 会先接管滚动事件,这样本 View 会先进行滚动,直到滚动到了最小高度(折叠了),Scrolling view 才开始实际滚动。而当本 View 已完全折叠后,再向下滚动 Scrolling view,直到 Scrolling view 顶部的内容完全显示后,本 View 才会开始向下滚动以显现出来。 enterAlways 当 Scrolling view 向下滚动时,本 View 会一起跟着向下滚动。实际上就好比我们同时对 Scrolling view 和本 View 进行向下滚动。 enterAlwaysCollapsed 从名字上就可以看出,这是在 enterAlways 的基础上,加上了“折叠”的效果。当我们开始向下滚动 Scrolling View 时,本 View 会一起跟着滚动直到达到其“折叠高度”(即最小高度)。然后当 Scrolling View 滚动至顶部内容完全显示后,再向下滚动 Scrolling View,本 View 会继续滚动到完全显示出来。 snap 在一次滚动结束时,本 View 很可能只处于“部分显示”的状态,加上这个标记能够达到“要么完全隐藏,要么完全显示”的效果。 四、CollapsingToolBarLayout 这个东西,我相信很多博客和技术文章都会把 CollapsingToolBarLayout 和 CoordinatorLayout 放一起讲,这个东西的确很牛。我们同样先看看官方文档介绍: CollapsingToolbarLayout 通常用来在布局中包裹一个 Toolbar,以实现具有“折叠效果“”的顶部栏。它需要是 AppBarLayout 的直接子 View,这样才能发挥出效果。 CollapsingToolbarLayout包含以下特性: Collasping title(可折叠标题):当布局完全可见时,这个标题比较大;当折叠起来时,标题也会变小。标题的外观可以通过 expandedTextAppearance 和 collapsedTextAppearance 属性来调整。 Content scrim(内容纱布):根据 CollapsingToolbarLayout 是否滚动到一个临界点,内容纱布会显示或隐藏。可以通过 setContentScrim(Drawable) 来设置内容纱布。 Status bar scrim(状态栏纱布):也是根据是否滚动到临界点,来决定是否显示。可以通过 setStatusBarScrim(Drawable) 方法来设置。这个特性只有在 Android 5.0 及其以上版本,我们设置 fitSystemWindows 为 ture 时才能生效。 Parallax scrolling children(视差滚动子 View):子 View 可以选择以“视差”的方式来进行滚动。(视觉效果上就是子 View 滚动的比其他 View 稍微慢些) Pinned position children:子 View 可以选择固定在某一位置上。 上面的描述有些抽象,实际上对于 Content scrim 、Status bar scrim 我们可以暂时予以忽略,只要留个大概印象待以后需要时再查阅相关资料即可。下面我们通过一个常见的例子介绍下 CollapsingToolbarLayout 的基本使用姿势。 我们来看看一个常用的效果: 看看布局: <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_coor_tool_bar" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context="com.nanchen.coordinatorlayoutdemo.CoorToolBarActivity"> <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:fitsSystemWindows="true" android:layout_width="match_parent" android:layout_height="wrap_content" app:theme="@style/AppTheme.AppBarOverlay"> <android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="200dp" app:contentScrim="@color/colorPrimary" app:expandedTitleMarginStart="100dp" app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:statusBarScrim="@android:color/transparent" app:titleEnabled="false"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:scaleType="centerCrop" android:src="@mipmap/logo" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.6"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/AppTheme.PopupOverlay" app:title=""/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/recycler" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <TextView android:id="@+id/toolbar_title" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:layout_marginLeft="16dp" android:layout_marginTop="-100dp" android:alpha="0" android:elevation="10dp" android:gravity="center_vertical" android:text="爱吖校推-你关注的,我们才推" android:textColor="@android:color/white" android:textSize="20sp" android:textStyle="bold" app:layout_behavior=".SimpleViewBehavior" app:svb_dependOn="@id/appbar" app:svb_dependType="y" app:svb_targetAlpha="1" app:svb_targetY="0dp"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@mipmap/ic_start" app:layout_anchor="@id/appbar" app:layout_anchorGravity="bottom|right"/> </android.support.design.widget.CoordinatorLayout> 我们在 XML 文件中为 CollapsingToolBarLayout 的 layout_scrollFlags 指定为 scroll|exitUntilCollapsed|snap,这样便实现了向上滚动的折叠效果。 CollapsingToolbarLayout 本质上同样是一个 FrameLayout,我们在布局文件中指定了一个 ImageView 和一个 Toolbar。ImageView 的layout_collapseMode 属性设为了 parallax,也就是我们前面介绍的视差滚动;而 Toolbar 的 layout_collaspeMode 设为了 pin ,也就是 Toolbar 会始终固定在顶部。 五、写在最后 本次的 Design 包下的 CoordinatorLayout 和 AppBarLayout 就讲述到这里,后续还将持续更新,欢迎拍砖~ 查看源码请移步 Github:https://github.com/nanchen2251/CoordinatorAppBarDemo 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
分享即是一件能够让自己成长又是能帮助他人的事情。 让我们一起分享,共同成长,分享使我们并不孤独。 本公众号欢迎大家投稿,如果你希望你的文章可以被更多人看到,直接将文章链接发我邮箱即可(nanchen2251@163.com),标题需要注明(投稿),谢谢。 文章要求: 百分百原创 你觉得通过文章你能学到一些有价值的东西。 带来的福利: 您的文章还可以发到CSDN,简书等其他平台,但不能投稿至其他微信公众号了; 更多人能够发现你的文章,间接提升自己的知名度 你的署名 你的原文链接即可,不需要单独发链接啦~关于打赏(所有打赏金额全部归你所有) 如果你希望你的文章能够被更多的人看到,能够帮助到更多的人,赶快来投稿吧~~ 一般我会在24小时内回复您,如果比较忙,会在周末统一阅读和回复。 欢迎投稿我的唯一公众号,公众号搜索 nanchen 或者扫描下方二维码:
这可能是最好的 RxJava 2.x 入门教程系列专栏 文章链接:这可能是最好的RxJava 2.x 入门教程(一)这可能是最好的RxJava 2.x 入门教程(二)这可能是最好的RxJava 2.x 入门教程(三)这可能是最好的RxJava 2.x 入门教程(四)这可能是最好的RxJava 2.x 入门教程(五) GitHub 代码同步更新:https://github.com/nanchen2251/RxJava2Examples 为了满足大家的饥渴难耐,GitHub将同步更新代码,主要包含基本的代码封装,RxJava 2.x所有操作符应用场景介绍和实际应用场景,后期除了RxJava可能还会增添其他东西,总之,GitHub上的Demo专为大家倾心打造。传送门:https://github.com/nanchen2251/RxJava2Examples 为什么要学 RxJava? 提升开发效率,降低维护成本一直是开发团队永恒不变的宗旨。近两年来国内的技术圈子中越来越多的开始提及 RxJava ,越来越多的应用和面试中都会有 RxJava ,而就目前的情况,Android 的网络库基本被 Retrofit + OkHttp 一统天下了,而配合上响应式编程 RxJava 可谓如鱼得水。想必大家肯定被近期的 Kotlin 炸开了锅,笔者也在闲暇之时去了解了一番(作为一个与时俱进的有理想的青年怎么可能不与时俱进?),发现其中有个非常好的优点就是简洁,支持函数式编程。是的, RxJava 最大的优点也是简洁,但它不止是简洁,而且是** 随着程序逻辑变得越来越复杂,它依然能够保持简洁 **(这货洁身自好呀有木有)。 咳咳,要例子,猛戳这里:给 Android 开发者的 RxJava 详解 什么是响应式编程 上面我们提及了响应式编程,不少新司机对它可谓一脸懵逼,那什么是响应式编程呢?响应式编程是一种基于异步数据流概念的编程模式。数据流就像一条河:它可以被观测,被过滤,被操作,或者为新的消费者与另外一条流合并为一条新的流。 响应式编程的一个关键概念是事件。事件可以被等待,可以触发过程,也可以触发其它事件。事件是唯一的以合适的方式将我们的现实世界映射到我们的软件中:如果屋里太热了我们就打开一扇窗户。同样的,当我们的天气app从服务端获取到新的天气数据后,我们需要更新app上展示天气信息的UI;汽车上的车道偏移系统探测到车辆偏移了正常路线就会提醒驾驶者纠正,就是是响应事件。 今天,响应式编程最通用的一个场景是UI:我们的移动App必须做出对网络调用、用户触摸输入和系统弹框的响应。在这个世界上,软件之所以是事件驱动并响应的是因为现实生活也是如此。 为什么出了一个系列后还有完结版? RxJava 这些年可谓越来越流行,而在去年的晚些时候发布了2.0正式版。大半年已过,虽然网上已经出现了大部分的 RxJava 教程(其实细心的你还是会发现 1.x 的超级多),前些日子,笔者花了大约两周的闲暇之时写了 RxJava 2.x 系列教程,也得到了不少反馈,其中就有不少读者觉得每一篇的教程太短,抑或是希望更多的侧重适用场景的介绍,在这样的大前提下,这篇完结版教程就此诞生,仅供各位新司机采纳。 开始 RxJava 2.x 已经按照 Reactive-Streams specification 规范完全的重写了,maven也被放在了io.reactivex.rxjava2:rxjava:2.x.y 下,所以 RxJava 2.x 独立于 RxJava 1.x 而存在,而随后官方宣布的将在一段时间后终止对 RxJava 1.x 的维护,所以对于熟悉 RxJava 1.x 的老司机自然可以直接看一下 2.x 的文档和异同就能轻松上手了,而对于不熟悉的年轻司机,不要慌,本酱带你装逼带你飞,马上就发车,坐稳了:https://github.com/nanchen2251/RxJava2Examples 你只需要在 build.gradle 中加上:compile 'io.reactivex.rxjava2:rxjava:2.1.1'(2.1.1为写此文章时的最新版本) 接口变化 RxJava 2.x 拥有了新的特性,其依赖于4个基础接口,它们分别是 Publisher Subscriber Subscription Processor 其中最核心的莫过于 Publisher 和 Subscriber。Publisher 可以发出一系列的事件,而 Subscriber 负责和处理这些事件。 其中用的比较多的自然是 Publisher 的 Flowable,它支持背压。关于背压给个简洁的定义就是: 背压是指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略。 简而言之,背压是流速控制的一种策略。有兴趣的可以看一下官方对于背压的讲解。 可以明显地发现,RxJava 2.x 最大的改动就是对于 backpressure 的处理,为此将原来的 Observable 拆分成了新的 Observable 和 Flowable,同时其他相关部分也同时进行了拆分,但令人庆幸的是,是它,是它,还是它,还是我们最熟悉和最喜欢的 RxJava。 观察者模式 大家可能都知道, RxJava 以观察者模式为骨架,在 2.0 中依旧如此。 不过此次更新中,出现了两种观察者模式: Observable ( 被观察者 ) / Observer ( 观察者 ) Flowable (被观察者)/ Subscriber (观察者) 在 RxJava 2.x 中,Observable 用于订阅 Observer,不再支持背压(1.x 中可以使用背压策略),而 Flowable 用于订阅 Subscriber , 是支持背压(Backpressure)的。 Observable 在 RxJava 1.x 中,我们最熟悉的莫过于 Observable 这个类了,笔者在刚刚使用 RxJava 2.x 的时候,创建了 一个 Observable,瞬间一脸懵逼有木有,居然连我们最最熟悉的 Subscriber 都没了,取而代之的是 ObservableEmmiter,俗称发射器。此外,由于没有了Subscriber的踪影,我们创建观察者时需使用 Observer。而 Observer 也不是我们熟悉的那个 Observer,又出现了一个 Disposable 参数带你装逼带你飞。 废话不多说,从会用开始,还记得 RxJava 的三部曲吗? ** 第一步:初始化 Observable ** ** 第二步:初始化 Observer ** ** 第三步:建立订阅关系 ** Observable.create(new ObservableOnSubscribe<Integer>() { // 第一步:初始化Observable @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { Log.e(TAG, "Observable emit 1" + "\n"); e.onNext(1); Log.e(TAG, "Observable emit 2" + "\n"); e.onNext(2); Log.e(TAG, "Observable emit 3" + "\n"); e.onNext(3); e.onComplete(); Log.e(TAG, "Observable emit 4" + "\n" ); e.onNext(4); } }).subscribe(new Observer<Integer>() { // 第三步:订阅 // 第二步:初始化Observer private int i; private Disposable mDisposable; @Override public void onSubscribe(@NonNull Disposable d) { mDisposable = d; } @Override public void onNext(@NonNull Integer integer) { i++; if (i == 2) { // 在RxJava 2.x 中,新增的Disposable可以做到切断的操作,让Observer观察者不再接收上游事件 mDisposable.dispose(); } } @Override public void onError(@NonNull Throwable e) { Log.e(TAG, "onError : value : " + e.getMessage() + "\n" ); } @Override public void onComplete() { Log.e(TAG, "onComplete" + "\n" ); } }); 不难看出,RxJava 2.x 与 1.x 还是存在着一些区别的。首先,创建 Observable 时,回调的是 ObservableEmitter ,字面意思即发射器,并且直接 throws Exception。其次,在创建的 Observer 中,也多了一个回调方法:onSubscribe,传递参数为Disposable,Disposable 相当于 RxJava 1.x 中的 Subscription, 用于解除订阅。可以看到示例代码中,在 i 自增到 2 的时候,订阅关系被切断。 07-03 14:24:11.663 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: onSubscribe : false 07-03 14:24:11.664 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: Observable emit 1 07-03 14:24:11.665 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: onNext : value : 1 07-03 14:24:11.666 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: Observable emit 2 07-03 14:24:11.667 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: onNext : value : 2 07-03 14:24:11.668 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: onNext : isDisposable : true 07-03 14:24:11.669 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: Observable emit 3 07-03 14:24:11.670 18467-18467/com.nanchen.rxjava2examples E/RxCreateActivity: Observable emit 4 当然,我们的 RxJava 2.x 也为我们保留了简化订阅方法,我们可以根据需求,进行相应的简化订阅,只不过传入对象改为了 Consumer。 Consumer 即消费者,用于接收单个值,BiConsumer 则是接收两个值,Function 用于变换对象,Predicate 用于判断。这些接口命名大多参照了 Java 8 ,熟悉 Java 8 新特性的应该都知道意思,这里也不再赘述。 线程调度 关于线程切换这点,RxJava 1.x 和 RxJava 2.x 的实现思路是一样的。这里简单的说一下,以便于我们的新司机入手。 subScribeOn 同 RxJava 1.x 一样,subscribeOn 用于指定 subscribe() 时所发生的线程,从源码角度可以看出,内部线程调度是通过 ObservableSubscribeOn来实现的。 @SchedulerSupport(SchedulerSupport.CUSTOM) public final Observable<T> subscribeOn(Scheduler scheduler) { ObjectHelper.requireNonNull(scheduler, "scheduler is null"); return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler)); } ObservableSubscribeOn 的核心源码在 subscribeActual 方法中,通过代理的方式使用 SubscribeOnObserver 包装 Observer 后,设置 Disposable 来将 subscribe 切换到 Scheduler 线程中。 observeOn observeOn 方法用于指定下游 Observer 回调发生的线程。 @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)); } 线程切换需要注意的 RxJava 内置的线程调度器的确可以让我们的线程切换得心应手,但其中也有些需要注意的地方。 简单地说,subscribeOn() 指定的就是发射事件的线程,observerOn 指定的就是订阅者接收事件的线程。 多次指定发射事件的线程只有第一次指定的有效,也就是说多次调用 subscribeOn() 只有第一次的有效,其余的会被忽略。 但多次指定订阅者接收线程是可以的,也就是说每调用一次 observerOn(),下游的线程就会切换一次。 Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { Log.e(TAG, "Observable thread is : " + Thread.currentThread().getName()); e.onNext(1); e.onComplete(); } }).subscribeOn(Schedulers.newThread()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnNext(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { Log.e(TAG, "After observeOn(mainThread),Current thread is " + Thread.currentThread().getName()); } }) .observeOn(Schedulers.io()) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { Log.e(TAG, "After observeOn(io),Current thread is " + Thread.currentThread().getName()); } }); 输出: 07-03 14:54:01.177 15121-15438/com.nanchen.rxjava2examples E/RxThreadActivity: Observable thread is : RxNewThreadScheduler-1 07-03 14:54:01.178 15121-15121/com.nanchen.rxjava2examples E/RxThreadActivity: After observeOn(mainThread),Current thread is main 07-03 14:54:01.179 15121-15439/com.nanchen.rxjava2examples E/RxThreadActivity: After observeOn(io),Current thread is RxCachedThreadScheduler-2 实例代码中,分别用 Schedulers.newThread() 和 Schedulers.io() 对发射线程进行切换,并采用 observeOn(AndroidSchedulers.mainThread() 和 Schedulers.io() 进行了接收线程的切换。可以看到输出中发射线程仅仅响应了第一个 newThread,但每调用一次 observeOn() ,线程便会切换一次,因此如果我们有类似的需求时,便知道如何处理了。 RxJava 中,已经内置了很多线程选项供我们选择,例如有: Schedulers.io() 代表io操作的线程, 通常用于网络,读写文件等io密集型的操作; Schedulers.computation() 代表CPU计算密集型的操作, 例如需要大量计算的操作; Schedulers.newThread() 代表一个常规的新线程; AndroidSchedulers.mainThread() 代表Android的主线程 这些内置的 Scheduler 已经足够满足我们开发的需求,因此我们应该使用内置的这些选项,而 RxJava 内部使用的是线程池来维护这些线程,所以效率也比较高。 操作符 关于操作符,在官方文档中已经做了非常完善的讲解,并且笔者前面的系列教程中也着重讲解了绝大多数的操作符作用,这里受于篇幅限制,就不多做赘述,只挑选几个进行实际情景的讲解。 map map 操作符可以将一个 Observable 对象通过某种关系转换为另一个Observable 对象。在 2.x 中和 1.x 中作用几乎一致,不同点在于:2.x 将 1.x 中的 Func1 和 Func2 改为了 Function 和 BiFunction。 采用 map 操作符进行网络数据解析 想必大家都知道,很多时候我们在使用 RxJava 的时候总是和 Retrofit 进行结合使用,而为了方便演示,这里我们就暂且采用 OkHttp3 进行演示,配合 map,doOnNext ,线程切换进行简单的网络请求: 1)通过 Observable.create() 方法,调用 OkHttp 网络请求; 2)通过 map 操作符集合 gson,将 Response 转换为 bean 类; 3)通过 doOnNext() 方法,解析 bean 中的数据,并进行数据库存储等操作; 4)调度线程,在子线程中进行耗时操作任务,在主线程中更新 UI ; 5)通过 subscribe(),根据请求成功或者失败来更新 UI 。 Observable.create(new ObservableOnSubscribe<Response>() { @Override public void subscribe(@NonNull ObservableEmitter<Response> e) throws Exception { Builder builder = new Builder() .url("http://api.avatardata.cn/MobilePlace/LookUp?key=ec47b85086be4dc8b5d941f5abd37a4e&mobileNumber=13021671512") .get(); Request request = builder.build(); Call call = new OkHttpClient().newCall(request); Response response = call.execute(); e.onNext(response); } }).map(new Function<Response, MobileAddress>() { @Override public MobileAddress apply(@NonNull Response response) throws Exception { if (response.isSuccessful()) { ResponseBody body = response.body(); if (body != null) { Log.e(TAG, "map:转换前:" + response.body()); return new Gson().fromJson(body.string(), MobileAddress.class); } } return null; } }).observeOn(AndroidSchedulers.mainThread()) .doOnNext(new Consumer<MobileAddress>() { @Override public void accept(@NonNull MobileAddress s) throws Exception { Log.e(TAG, "doOnNext: 保存成功:" + s.toString() + "\n"); } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<MobileAddress>() { @Override public void accept(@NonNull MobileAddress data) throws Exception { Log.e(TAG, "成功:" + data.toString() + "\n"); }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "失败:" + throwable.getMessage() + "\n"); } }); concat concat 可以做到不交错的发射两个甚至多个 Observable 的发射事件,并且只有前一个 Observable 终止(onComplete) 后才会订阅下一个 Observable。 采用 concat 操作符先读取缓存再通过网络请求获取数据 想必在实际应用中,很多时候(对数据操作不敏感时)都需要我们先读取缓存的数据,如果缓存没有数据,再通过网络请求获取,随后在主线程更新我们的UI。 concat 操作符简直就是为我们这种需求量身定做。 利用 concat 的必须调用 onComplete 后才能订阅下一个 Observable 的特性,我们就可以先读取缓存数据,倘若获取到的缓存数据不是我们想要的,再调用 onComplete() 以执行获取网络数据的 Observable,如果缓存数据能应我们所需,则直接调用 onNext(),防止过度的网络请求,浪费用户的流量。 Observable<FoodList> cache = Observable.create(new ObservableOnSubscribe<FoodList>() { @Override public void subscribe(@NonNull ObservableEmitter<FoodList> e) throws Exception { Log.e(TAG, "create当前线程:"+Thread.currentThread().getName() ); FoodList data = CacheManager.getInstance().getFoodListData(); // 在操作符 concat 中,只有调用 onComplete 之后才会执行下一个 Observable if (data != null){ // 如果缓存数据不为空,则直接读取缓存数据,而不读取网络数据 isFromNet = false; Log.e(TAG, "\nsubscribe: 读取缓存数据:" ); runOnUiThread(new Runnable() { @Override public void run() { mRxOperatorsText.append("\nsubscribe: 读取缓存数据:\n"); } }); e.onNext(data); }else { isFromNet = true; runOnUiThread(new Runnable() { @Override public void run() { mRxOperatorsText.append("\nsubscribe: 读取网络数据:\n"); } }); Log.e(TAG, "\nsubscribe: 读取网络数据:" ); e.onComplete(); } } }); Observable<FoodList> network = Rx2AndroidNetworking.get("http://www.tngou.net/api/food/list") .addQueryParameter("rows",10+"") .build() .getObjectObservable(FoodList.class); // 两个 Observable 的泛型应当保持一致 Observable.concat(cache,network) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<FoodList>() { @Override public void accept(@NonNull FoodList tngouBeen) throws Exception { Log.e(TAG, "subscribe 成功:"+Thread.currentThread().getName() ); if (isFromNet){ mRxOperatorsText.append("accept : 网络获取数据设置缓存: \n"); Log.e(TAG, "accept : 网络获取数据设置缓存: \n"+tngouBeen.toString() ); CacheManager.getInstance().setFoodListData(tngouBeen); } mRxOperatorsText.append("accept: 读取数据成功:" + tngouBeen.toString()+"\n"); Log.e(TAG, "accept: 读取数据成功:" + tngouBeen.toString()); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "subscribe 失败:"+Thread.currentThread().getName() ); Log.e(TAG, "accept: 读取数据失败:"+throwable.getMessage() ); mRxOperatorsText.append("accept: 读取数据失败:"+throwable.getMessage()+"\n"); } }); 有时候我们的缓存可能还会分为 memory 和 disk ,实际上都差不多,无非是多写点 Observable ,然后通过 concat 合并即可。 flatMap 实现多个网络请求依次依赖 想必这种情况也在实际情况中比比皆是,例如用户注册成功后需要自动登录,我们只需要先通过注册接口注册用户信息,注册成功后马上调用登录接口进行自动登录即可。 我们的 flatMap 恰好解决了这种应用场景,flatMap 操作符可以将一个发射数据的 Observable 变换为多个 Observables ,然后将它们发射的数据合并后放到一个单独的 Observable,利用这个特性,我们很轻松地达到了我们的需求。 Rx2AndroidNetworking.get("http://www.tngou.net/api/food/list") .addQueryParameter("rows", 1 + "") .build() .getObjectObservable(FoodList.class) // 发起获取食品列表的请求,并解析到FootList .subscribeOn(Schedulers.io()) // 在io线程进行网络请求 .observeOn(AndroidSchedulers.mainThread()) // 在主线程处理获取食品列表的请求结果 .doOnNext(new Consumer<FoodList>() { @Override public void accept(@NonNull FoodList foodList) throws Exception { // 先根据获取食品列表的响应结果做一些操作 Log.e(TAG, "accept: doOnNext :" + foodList.toString()); mRxOperatorsText.append("accept: doOnNext :" + foodList.toString()+"\n"); } }) .observeOn(Schedulers.io()) // 回到 io 线程去处理获取食品详情的请求 .flatMap(new Function<FoodList, ObservableSource<FoodDetail>>() { @Override public ObservableSource<FoodDetail> apply(@NonNull FoodList foodList) throws Exception { if (foodList != null && foodList.getTngou() != null && foodList.getTngou().size() > 0) { return Rx2AndroidNetworking.post("http://www.tngou.net/api/food/show") .addBodyParameter("id", foodList.getTngou().get(0).getId() + "") .build() .getObjectObservable(FoodDetail.class); } return null; } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<FoodDetail>() { @Override public void accept(@NonNull FoodDetail foodDetail) throws Exception { Log.e(TAG, "accept: success :" + foodDetail.toString()); mRxOperatorsText.append("accept: success :" + foodDetail.toString()+"\n"); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "accept: error :" + throwable.getMessage()); mRxOperatorsText.append("accept: error :" + throwable.getMessage()+"\n"); } }); 善用 zip 操作符,实现多个接口数据共同更新 UI 在实际应用中,我们极有可能会在一个页面显示的数据来源于多个接口,这时候我们的 zip 操作符为我们排忧解难。 zip 操作符可以将多个 Observable 的数据结合为一个数据源再发射出去。 Observable<MobileAddress> observable1 = Rx2AndroidNetworking.get("http://api.avatardata.cn/MobilePlace/LookUp?key=ec47b85086be4dc8b5d941f5abd37a4e&mobileNumber=13021671512") .build() .getObjectObservable(MobileAddress.class); Observable<CategoryResult> observable2 = Network.getGankApi() .getCategoryData("Android",1,1); Observable.zip(observable1, observable2, new BiFunction<MobileAddress, CategoryResult, String>() { @Override public String apply(@NonNull MobileAddress mobileAddress, @NonNull CategoryResult categoryResult) throws Exception { return "合并后的数据为:手机归属地:"+mobileAddress.getResult().getMobilearea()+"人名:"+categoryResult.results.get(0).who; } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { Log.e(TAG, "accept: 成功:" + s+"\n"); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "accept: 失败:" + throwable+"\n"); } }); 采用 interval 操作符实现心跳间隔任务 想必即时通讯等需要轮训的任务在如今的 APP 中已是很常见,而 RxJava 2.x 的 interval 操作符可谓完美地解决了我们的疑惑。 这里就简单的意思一下轮训。 private Disposable mDisposable; @Override protected void doSomething() { mDisposable = Flowable.interval(1, TimeUnit.SECONDS) .doOnNext(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { Log.e(TAG, "accept: doOnNext : "+aLong ); } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { Log.e(TAG, "accept: 设置文本 :"+aLong ); mRxOperatorsText.append("accept: 设置文本 :"+aLong +"\n"); } }); } /** * 销毁时停止心跳 */ @Override protected void onDestroy() { super.onDestroy(); if (mDisposable != null){ mDisposable.dispose(); } } RxJava 1.x 如何平滑升级到 RxJava 2.x? 由于 RxJava 2.x 变化较大无法直接升级,幸运的是,官方为我们提供了 RxJava2Interrop 这个库,可以方便地把 RxJava 1.x 升级到 RxJava 2.x,或者将 RxJava 2.x 转回到 RxJava 1.x。 写在最后 本酱看你都看到这儿了,实为未来的栋梁之才,所以且送你一本经书:https://github.com/nanchen2251/RxJava2Examples GIF.gif 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。现在还有纯面试系列哟~涵盖 Java、数据结构与算法、计算机网络、Android 基础。如果你喜欢,为我点赞分享吧~ nanchen
这可能是最好的 RxJava 2.x入门教程系列专栏 文章链接:这可能是最好的 RxJava 2.x 入门教程(完结版)【重磅推出】这可能是最好的 RxJava 2.x 入门教程(一)这可能是最好的 RxJava 2.x 入门教程(二)这可能是最好的 RxJava 2.x 入门教程(三)这可能是最好的 RxJava 2.x 入门教程(四)这可能是最好的 RxJava 2.x 入门教程(五) GitHub 代码同步更新:https://github.com/nanchen2251/RxJava2Examples 为了满足大家的饥渴难耐,GitHub 将同步更新代码,主要包含基本的代码封装,RxJava 2.x 所有操作符应用场景介绍和实际应用场景,后期除了 RxJava 可能还会增添其他东西,总之,GitHub 上的 Demo 专为大家倾心打造。传送门:https://github.com/nanchen2251/RxJava2Examples 前言 终于如愿来到让我小伙伴们亢奋的 RxJava 2 使用场景举例了,前面几章中我们讲解完了 RxJava 1.x 到 RxJava 2.x 的异同以及 RxJava 2.x 的各种操作符使用,如有疑问,欢迎点击上方的链接进入你想要的环节。 正题 简单的网络请求 想必大家都知道,很多时候我们在使用 RxJava 的时候总是和 Retrofit 进行结合使用,而为了方便演示,这里我们就暂且采用 OkHttp3 进行演示,配合 map,doOnNext ,线程切换进行简单的网络请求: 1)通过 Observable.create() 方法,调用 OkHttp 网络请求; 2)通过 map 操作符集合 gson,将 Response 转换为 bean 类; 3)通过 doOnNext() 方法,解析 bean 中的数据,并进行数据库存储等操作; 4)调度线程,在子线程中进行耗时操作任务,在主线程中更新 UI ; 5)通过 subscribe(),根据请求成功或者失败来更新 UI 。 Observable.create(new ObservableOnSubscribe<Response>() { @Override public void subscribe(@NonNull ObservableEmitter<Response> e) throws Exception { Builder builder = new Builder() .url("http://api.avatardata.cn/MobilePlace/LookUp?key=ec47b85086be4dc8b5d941f5abd37a4e&mobileNumber=13021671512") .get(); Request request = builder.build(); Call call = new OkHttpClient().newCall(request); Response response = call.execute(); e.onNext(response); } }).map(new Function<Response, MobileAddress>() { @Override public MobileAddress apply(@NonNull Response response) throws Exception { Log.e(TAG, "map 线程:" + Thread.currentThread().getName() + "\n"); if (response.isSuccessful()) { ResponseBody body = response.body(); if (body != null) { Log.e(TAG, "map:转换前:" + response.body()); return new Gson().fromJson(body.string(), MobileAddress.class); } } return null; } }).observeOn(AndroidSchedulers.mainThread()) .doOnNext(new Consumer<MobileAddress>() { @Override public void accept(@NonNull MobileAddress s) throws Exception { Log.e(TAG, "doOnNext 线程:" + Thread.currentThread().getName() + "\n"); mRxOperatorsText.append("\ndoOnNext 线程:" + Thread.currentThread().getName() + "\n"); Log.e(TAG, "doOnNext: 保存成功:" + s.toString() + "\n"); mRxOperatorsText.append("doOnNext: 保存成功:" + s.toString() + "\n"); } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<MobileAddress>() { @Override public void accept(@NonNull MobileAddress data) throws Exception { Log.e(TAG, "subscribe 线程:" + Thread.currentThread().getName() + "\n"); mRxOperatorsText.append("\nsubscribe 线程:" + Thread.currentThread().getName() + "\n"); Log.e(TAG, "成功:" + data.toString() + "\n"); mRxOperatorsText.append("成功:" + data.toString() + "\n"); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "subscribe 线程:" + Thread.currentThread().getName() + "\n"); mRxOperatorsText.append("\nsubscribe 线程:" + Thread.currentThread().getName() + "\n"); Log.e(TAG, "失败:" + throwable.getMessage() + "\n"); mRxOperatorsText.append("失败:" + throwable.getMessage() + "\n"); } }); 为了方便,我们后面的讲解大部分采用开源的 Rx2AndroidNetworking 来处理,数据来源于天狗网等多个公共API接口。 mRxOperatorsText.append("RxNetworkActivity\n"); Rx2AndroidNetworking.get("http://api.avatardata.cn/MobilePlace/LookUp?key=ec47b85086be4dc8b5d941f5abd37a4e&mobileNumber=13021671512") .build() .getObjectObservable(MobileAddress.class) .observeOn(AndroidSchedulers.mainThread()) // 为doOnNext() 指定在主线程,否则报错 .doOnNext(new Consumer<MobileAddress>() { @Override public void accept(@NonNull MobileAddress data) throws Exception { Log.e(TAG, "doOnNext:"+Thread.currentThread().getName()+"\n" ); mRxOperatorsText.append("\ndoOnNext:"+Thread.currentThread().getName()+"\n" ); Log.e(TAG,"doOnNext:"+data.toString()+"\n"); mRxOperatorsText.append("doOnNext:"+data.toString()+"\n"); } }) .map(new Function<MobileAddress, ResultBean>() { @Override public ResultBean apply(@NonNull MobileAddress mobileAddress) throws Exception { Log.e(TAG, "\n" ); mRxOperatorsText.append("\n"); Log.e(TAG, "map:"+Thread.currentThread().getName()+"\n" ); mRxOperatorsText.append("map:"+Thread.currentThread().getName()+"\n" ); return mobileAddress.getResult(); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<ResultBean>() { @Override public void accept(@NonNull ResultBean data) throws Exception { Log.e(TAG, "subscribe 成功:"+Thread.currentThread().getName()+"\n" ); mRxOperatorsText.append("\nsubscribe 成功:"+Thread.currentThread().getName()+"\n" ); Log.e(TAG, "成功:" + data.toString() + "\n"); mRxOperatorsText.append("成功:" + data.toString() + "\n"); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "subscribe 失败:"+Thread.currentThread().getName()+"\n" ); mRxOperatorsText.append("\nsubscribe 失败:"+Thread.currentThread().getName()+"\n" ); Log.e(TAG, "失败:"+ throwable.getMessage()+"\n" ); mRxOperatorsText.append("失败:"+ throwable.getMessage()+"\n"); } }); 先读取缓存,如果缓存没数据再通过网络请求获取数据后更新UI 想必在实际应用中,很多时候(对数据操作不敏感时)都需要我们先读取缓存的数据,如果缓存没有数据,再通过网络请求获取,随后在主线程更新我们的 UI。 concat 操作符简直就是为我们这种需求量身定做。 concat 可以做到不交错的发射两个甚至多个 Observable 的发射事件,并且只有前一个 Observable 终止( onComplete() ) 后才会定义下一个 Observable。 利用这个特性,我们就可以先读取缓存数据,倘若获取到的缓存数据不是我们想要的,再调用 onComplete() 以执行获取网络数据的 Observable,如果缓存数据能应我们所需,则直接调用 onNext() ,防止过度的网络请求,浪费用户的流量。 Observable<FoodList> cache = Observable.create(new ObservableOnSubscribe<FoodList>() { @Override public void subscribe(@NonNull ObservableEmitter<FoodList> e) throws Exception { Log.e(TAG, "create当前线程:"+Thread.currentThread().getName() ); FoodList data = CacheManager.getInstance().getFoodListData(); // 在操作符 concat 中,只有调用 onComplete 之后才会执行下一个 Observable if (data != null){ // 如果缓存数据不为空,则直接读取缓存数据,而不读取网络数据 isFromNet = false; Log.e(TAG, "\nsubscribe: 读取缓存数据:" ); runOnUiThread(new Runnable() { @Override public void run() { mRxOperatorsText.append("\nsubscribe: 读取缓存数据:\n"); } }); e.onNext(data); }else { isFromNet = true; runOnUiThread(new Runnable() { @Override public void run() { mRxOperatorsText.append("\nsubscribe: 读取网络数据:\n"); } }); Log.e(TAG, "\nsubscribe: 读取网络数据:" ); e.onComplete(); } } }); Observable<FoodList> network = Rx2AndroidNetworking.get("http://www.tngou.net/api/food/list") .addQueryParameter("rows",10+"") .build() .getObjectObservable(FoodList.class); // 两个 Observable 的泛型应当保持一致 Observable.concat(cache,network) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<FoodList>() { @Override public void accept(@NonNull FoodList tngouBeen) throws Exception { Log.e(TAG, "subscribe 成功:"+Thread.currentThread().getName() ); if (isFromNet){ mRxOperatorsText.append("accept : 网络获取数据设置缓存: \n"); Log.e(TAG, "accept : 网络获取数据设置缓存: \n"+tngouBeen.toString() ); CacheManager.getInstance().setFoodListData(tngouBeen); } mRxOperatorsText.append("accept: 读取数据成功:" + tngouBeen.toString()+"\n"); Log.e(TAG, "accept: 读取数据成功:" + tngouBeen.toString()); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "subscribe 失败:"+Thread.currentThread().getName() ); Log.e(TAG, "accept: 读取数据失败:"+throwable.getMessage() ); mRxOperatorsText.append("accept: 读取数据失败:"+throwable.getMessage()+"\n"); } }); 有时候我们的缓存可能还会分为 memory 和 disk ,实际上都差不多,无非是多写点 Observable ,然后通过 concat 合并即可。 多个网络请求依次依赖 想必这种情况也在实际情况中比比皆是,例如用户注册成功后需要自动登录,我们只需要先通过注册接口注册用户信息,注册成功后马上调用登录接口进行自动登录即可。 我们的 flatMap 恰好解决了这种应用场景,flatMap 操作符可以将一个发射数据的 Observable 变换为多个 Observables ,然后将它们发射的数据合并后放到一个单独的 Observable,利用这个特性,我们很轻松地达到了我们的需求。 Rx2AndroidNetworking.get("http://www.tngou.net/api/food/list") .addQueryParameter("rows", 1 + "") .build() .getObjectObservable(FoodList.class) // 发起获取食品列表的请求,并解析到FootList .subscribeOn(Schedulers.io()) // 在io线程进行网络请求 .observeOn(AndroidSchedulers.mainThread()) // 在主线程处理获取食品列表的请求结果 .doOnNext(new Consumer<FoodList>() { @Override public void accept(@NonNull FoodList foodList) throws Exception { // 先根据获取食品列表的响应结果做一些操作 Log.e(TAG, "accept: doOnNext :" + foodList.toString()); mRxOperatorsText.append("accept: doOnNext :" + foodList.toString()+"\n"); } }) .observeOn(Schedulers.io()) // 回到 io 线程去处理获取食品详情的请求 .flatMap(new Function<FoodList, ObservableSource<FoodDetail>>() { @Override public ObservableSource<FoodDetail> apply(@NonNull FoodList foodList) throws Exception { if (foodList != null && foodList.getTngou() != null && foodList.getTngou().size() > 0) { return Rx2AndroidNetworking.post("http://www.tngou.net/api/food/show") .addBodyParameter("id", foodList.getTngou().get(0).getId() + "") .build() .getObjectObservable(FoodDetail.class); } return null; } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<FoodDetail>() { @Override public void accept(@NonNull FoodDetail foodDetail) throws Exception { Log.e(TAG, "accept: success :" + foodDetail.toString()); mRxOperatorsText.append("accept: success :" + foodDetail.toString()+"\n"); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "accept: error :" + throwable.getMessage()); mRxOperatorsText.append("accept: error :" + throwable.getMessage()+"\n"); } }); 结合多个接口的数据更新 UI 在实际应用中,我们极有可能会在一个页面显示的数据来源于多个接口,这时候我们的 zip 操作符为我们排忧解难。 zip 操作符可以将多个 Observable 的数据结合为一个数据源再发射出去。 Observable<MobileAddress> observable1 = Rx2AndroidNetworking.get("http://api.avatardata.cn/MobilePlace/LookUp?key=ec47b85086be4dc8b5d941f5abd37a4e&mobileNumber=13021671512") .build() .getObjectObservable(MobileAddress.class); Observable<CategoryResult> observable2 = Network.getGankApi() .getCategoryData("Android",1,1); Observable.zip(observable1, observable2, new BiFunction<MobileAddress, CategoryResult, String>() { @Override public String apply(@NonNull MobileAddress mobileAddress, @NonNull CategoryResult categoryResult) throws Exception { return "合并后的数据为:手机归属地:"+mobileAddress.getResult().getMobilearea()+"人名:"+categoryResult.results.get(0).who; } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { Log.e(TAG, "accept: 成功:" + s+"\n"); } }, new Consumer<Throwable>() { @Override public void accept(@NonNull Throwable throwable) throws Exception { Log.e(TAG, "accept: 失败:" + throwable+"\n"); } }); 间隔任务实现心跳 想必即时通讯等需要轮训的任务在如今的 APP 中已是很常见,而 RxJava 2.x 的 interval 操作符可谓完美地解决了我们的疑惑。 这里就简单的意思一下轮训。 private Disposable mDisposable; @Override protected void doSomething() { mDisposable = Flowable.interval(1, TimeUnit.SECONDS) .doOnNext(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { Log.e(TAG, "accept: doOnNext : "+aLong ); } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { Log.e(TAG, "accept: 设置文本 :"+aLong ); mRxOperatorsText.append("accept: 设置文本 :"+aLong +"\n"); } }); } /** * 销毁时停止心跳 */ @Override protected void onDestroy() { super.onDestroy(); if (mDisposable != null){ mDisposable.dispose(); } } 后记 姑且先讲到这里吧,喜欢的小伙伴别忘了关注和点赞哦~ https://github.com/nanchen2251/RxJava2Examples 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
这可能是最好的 RxJava 2.x 入门教程系列专栏 文章链接:这可能是最好的 RxJava 2.x 入门教程(完结版)【重磅推出】这可能是最好的 RxJava 2.x 入门教程(一)这可能是最好的 RxJava 2.x 入门教程(二)这可能是最好的 RxJava 2.x 入门教程(三)这可能是最好的 RxJava 2.x 入门教程(四)这可能是最好的 RxJava 2.x 入门教程(五) GitHub 代码同步更新:https://github.com/nanchen2251/RxJava2Examples 为了满足大家的饥渴难耐,GitHub 将同步更新代码,主要包含基本的代码封装,RxJava 2.x 所有操作符应用场景介绍和实际应用场景,后期除了 RxJava 可能还会增添其他东西,总之,GitHub 上的 Demo 专为大家倾心打造。传送门:https://github.com/nanchen2251/RxJava2Examples 前言 最近很多小伙伴私信我,说自己很懊恼,对于 RxJava 2.x 系列一看就能明白,但自己写却又写不出来。如果 LZ 能放上实战情景教程就最好不过了。也是哈,单讲我们的操作符,也让我们的教程不温不火,但 LZ 自己选择的路,那跪着也要走完呀。所以,也就让我可怜的小伙伴们忍忍了,操作符马上就讲完了。 正题 Single 顾名思义,Single 只会接收一个参数,而 SingleObserver 只会调用 onError() 或者 onSuccess()。 Single.just(new Random().nextInt()) .subscribe(new SingleObserver<Integer>() { @Override public void onSubscribe(@NonNull Disposable d) { } @Override public void onSuccess(@NonNull Integer integer) { mRxOperatorsText.append("single : onSuccess : "+integer+"\n"); Log.e(TAG, "single : onSuccess : "+integer+"\n" ); } @Override public void onError(@NonNull Throwable e) { mRxOperatorsText.append("single : onError : "+e.getMessage()+"\n"); Log.e(TAG, "single : onError : "+e.getMessage()+"\n"); } }); 输出: distinct 去重操作符,简单的作用就是去重。 Observable.just(1, 1, 1, 2, 2, 3, 4, 5) .distinct() .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("distinct : " + integer + "\n"); Log.e(TAG, "distinct : " + integer + "\n"); } }); 输出: 很明显,发射器发送的事件,在接收的时候被去重了。 debounce 去除发送频率过快的项,看起来好像没啥用处,但你信我,后面绝对有地方很有用武之地。 Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> emitter) throws Exception { // send events with simulated time wait emitter.onNext(1); // skip Thread.sleep(400); emitter.onNext(2); // deliver Thread.sleep(505); emitter.onNext(3); // skip Thread.sleep(100); emitter.onNext(4); // deliver Thread.sleep(605); emitter.onNext(5); // deliver Thread.sleep(510); emitter.onComplete(); } }).debounce(500, TimeUnit.MILLISECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("debounce :" + integer + "\n"); Log.e(TAG,"debounce :" + integer + "\n"); } }); 输出: 代码很清晰,去除发送间隔时间小于 500 毫秒的发射事件,所以 1 和 3 被去掉了。 defer 简单地时候就是每次订阅都会创建一个新的 Observable,并且如果没有被订阅,就不会产生新的 Observable。 Observable<Integer> observable = Observable.defer(new Callable<ObservableSource<Integer>>() { @Override public ObservableSource<Integer> call() throws Exception { return Observable.just(1, 2, 3); } }); observable.subscribe(new Observer<Integer>() { @Override public void onSubscribe(@NonNull Disposable d) { } @Override public void onNext(@NonNull Integer integer) { mRxOperatorsText.append("defer : " + integer + "\n"); Log.e(TAG, "defer : " + integer + "\n"); } @Override public void onError(@NonNull Throwable e) { mRxOperatorsText.append("defer : onError : " + e.getMessage() + "\n"); Log.e(TAG, "defer : onError : " + e.getMessage() + "\n"); } @Override public void onComplete() { mRxOperatorsText.append("defer : onComplete\n"); Log.e(TAG, "defer : onComplete\n"); } }); 输出: last last 操作符仅取出可观察到的最后一个值,或者是满足某些条件的最后一项。 Observable.just(1, 2, 3) .last(4) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("last : " + integer + "\n"); Log.e(TAG, "last : " + integer + "\n"); } }); 输出: merge merge 顾名思义,熟悉版本控制工具的你一定不会不知道 merge 命令,而在 Rx 操作符中,merge 的作用是把多个 Observable 结合起来,接受可变参数,也支持迭代器集合。注意它和 concat 的区别在于,不用等到 发射器 A 发送完所有的事件再进行发射器 B 的发送。 Observable.merge(Observable.just(1, 2), Observable.just(3, 4, 5)) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("merge :" + integer + "\n"); Log.e(TAG, "accept: merge :" + integer + "\n" ); } }); 输出: reduce reduce 操作符每次用一个方法处理一个值,可以有一个 seed 作为初始值。 Observable.just(1, 2, 3) .reduce(new BiFunction<Integer, Integer, Integer>() { @Override public Integer apply(@NonNull Integer integer, @NonNull Integer integer2) throws Exception { return integer + integer2; } }).subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("reduce : " + integer + "\n"); Log.e(TAG, "accept: reduce : " + integer + "\n"); } }); 输出: 可以看到,代码中,我们中间采用 reduce ,支持一个 function 为两数值相加,所以应该最后的值是:1 + 2 = 3 + 3 = 6 , 而Log 日志完美解决了我们的问题。 scan scan 操作符作用和上面的 reduce 一致,唯一区别是 reduce 是个只追求结果的坏人,而 scan 会始终如一地把每一个步骤都输出。 Observable.just(1, 2, 3) .scan(new BiFunction<Integer, Integer, Integer>() { @Override public Integer apply(@NonNull Integer integer, @NonNull Integer integer2) throws Exception { return integer + integer2; } }).subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("scan " + integer + "\n"); Log.e(TAG, "accept: scan " + integer + "\n"); } }); 输出: 看日志,没毛病。 window 按照实际划分窗口,将数据发送给不同的 Observable mRxOperatorsText.append("window\n"); Log.e(TAG, "window\n"); Observable.interval(1, TimeUnit.SECONDS) // 间隔一秒发一次 .take(15) // 最多接收15个 .window(3, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Observable<Long>>() { @Override public void accept(@NonNull Observable<Long> longObservable) throws Exception { mRxOperatorsText.append("Sub Divide begin...\n"); Log.e(TAG, "Sub Divide begin...\n"); longObservable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { mRxOperatorsText.append("Next:" + aLong + "\n"); Log.e(TAG, "Next:" + aLong + "\n"); } }); } }); 输出: 写在最后 至此,大部分 RxJava 2.x 的操作符就告一段落了,当然还有一些没有提到的操作符,不是说它们不重要,而是 LZ 也要考虑大家的情况,接下来就会根据实际应用场景来对 RxJava 2.x 发起冲锋。如果想看更多的数据,请移步 GitHub:https://github.com/nanchen2251/RxJava2Examples 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
这可能是最好的 RxJava 2.x 入门教程系列专栏 文章链接:这可能是最好的 RxJava 2.x 入门教程(完结版)【重磅推出】这可能是最好的 RxJava 2.x 入门教程(一)这可能是最好的 RxJava 2.x 入门教程(二)这可能是最好的 RxJava 2.x 入门教程(三)这可能是最好的 RxJava 2.x 入门教程(四)这可能是最好的 RxJava 2.x 入门教程(五) GitHub 代码同步更新:https://github.com/nanchen2251/RxJava2Examples 为了满足大家的饥渴难耐,GitHub 将同步更新代码,主要包含基本的代码封装,RxJava 2.x 所有操作符应用场景介绍和实际应用场景,后期除了 RxJava 可能还会增添其他东西,总之,GitHub 上的 Demo 专为大家倾心打造。传送门:https://github.com/nanchen2251/RxJava2Examples 前言 年轻的老司机们,我这么勤的为大家分享,却少有催更的,好吧。其实写这个系列不是为了吸睛,那咱们继续写我们的 RxJava 2.x 的操作符。 正题 distinct 这个操作符非常的简单、通俗、易懂,就是简单的去重嘛,我甚至都不想贴代码,但人嘛,总得持之以恒。 Observable.just(1, 1, 1, 2, 2, 3, 4, 5) .distinct() .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("distinct : " + integer + "\n"); Log.e(TAG, "distinct : " + integer + "\n"); } }); 输出: Log 日志显而易见,我们在经过 dinstinct() 后接收器接收到的事件只有1,2,3,4,5了。 Filter 信我,Filter 你会很常用的,它的作用也很简单,过滤器嘛。可以接受一个参数,让其过滤掉不符合我们条件的值 Observable.just(1, 20, 65, -5, 7, 19) .filter(new Predicate<Integer>() { @Override public boolean test(@NonNull Integer integer) throws Exception { return integer >= 10; } }).subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("filter : " + integer + "\n"); Log.e(TAG, "filter : " + integer + "\n"); } }); 输出: 可以看到,我们过滤器舍去了小于 10 的值,所以最好的输出只有 20, 65, 19。 buffer buffer 操作符接受两个参数,buffer(count,skip),作用是将 Observable 中的数据按 skip (步长) 分成最大不超过 count 的 buffer ,然后生成一个 Observable 。也许你还不太理解,我们可以通过我们的示例图和示例代码来进一步深化它。 Observable.just(1, 2, 3, 4, 5) .buffer(3, 2) .subscribe(new Consumer<List<Integer>>() { @Override public void accept(@NonNull List<Integer> integers) throws Exception { mRxOperatorsText.append("buffer size : " + integers.size() + "\n"); Log.e(TAG, "buffer size : " + integers.size() + "\n"); mRxOperatorsText.append("buffer value : "); Log.e(TAG, "buffer value : " ); for (Integer i : integers) { mRxOperatorsText.append(i + ""); Log.e(TAG, i + ""); } mRxOperatorsText.append("\n"); Log.e(TAG, "\n"); } }); 输出: 如图,我们把 1, 2, 3, 4, 5 依次发射出来,经过 buffer 操作符,其中参数 skip 为 2, count 为 3,而我们的输出 依次是 123,345,5。显而易见,我们 buffer 的第一个参数是 count,代表最大取值,在事件足够的时候,一般都是取 count 个值,然后每次跳过 skip 个事件。其实看 Log 日志,我相信大家都明白了。 timer timer 很有意思,相当于一个定时任务。在 1.x 中它还可以执行间隔逻辑,但在 2.x 中此功能被交给了 interval,下一个会介绍。但需要注意的是,timer 和 interval 均默认在新线程。 mRxOperatorsText.append("timer start : " + TimeUtil.getNowStrTime() + "\n"); Log.e(TAG, "timer start : " + TimeUtil.getNowStrTime() + "\n"); Observable.timer(2, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) // timer 默认在新线程,所以需要切换回主线程 .subscribe(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { mRxOperatorsText.append("timer :" + aLong + " at " + TimeUtil.getNowStrTime() + "\n"); Log.e(TAG, "timer :" + aLong + " at " + TimeUtil.getNowStrTime() + "\n"); } }); 输出: 显而易见,当我们两次点击按钮触发这个事件的时候,接收被延迟了 2 秒。 interval 如同我们上面可说,interval 操作符用于间隔时间执行某个操作,其接受三个参数,分别是第一次发送延迟,间隔时间,时间单位。 mRxOperatorsText.append("interval start : " + TimeUtil.getNowStrTime() + "\n"); Log.e(TAG, "interval start : " + TimeUtil.getNowStrTime() + "\n"); Observable.interval(3,2, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) // 由于interval默认在新线程,所以我们应该切回主线程 .subscribe(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { mRxOperatorsText.append("interval :" + aLong + " at " + TimeUtil.getNowStrTime() + "\n"); Log.e(TAG, "interval :" + aLong + " at " + TimeUtil.getNowStrTime() + "\n"); } }); 输出: 如同 Log 日志一样,第一次延迟了 3 秒后接收到,后面每次间隔了 2 秒。 然而,心细的小伙伴可能会发现,由于我们这个是间隔执行,所以当我们的Activity 都销毁的时候,实际上这个操作还依然在进行,所以,我们得花点小心思让我们在不需要它的时候干掉它。查看源码发现,我们subscribe(Cousumer<? super T> onNext)返回的是Disposable,我们可以在这上面做文章。 @Override protected void doSomething() { mRxOperatorsText.append("interval start : " + TimeUtil.getNowStrTime() + "\n"); Log.e(TAG, "interval start : " + TimeUtil.getNowStrTime() + "\n"); mDisposable = Observable.interval(3, 2, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) // 由于interval默认在新线程,所以我们应该切回主线程 .subscribe(new Consumer<Long>() { @Override public void accept(@NonNull Long aLong) throws Exception { mRxOperatorsText.append("interval :" + aLong + " at " + TimeUtil.getNowStrTime() + "\n"); Log.e(TAG, "interval :" + aLong + " at " + TimeUtil.getNowStrTime() + "\n"); } }); } @Override protected void onDestroy() { super.onDestroy(); if (mDisposable != null && !mDisposable.isDisposed()) { mDisposable.dispose(); } } 哈哈,再次验证,解决了我们的疑惑。 doOnNext 其实觉得 doOnNext 应该不算一个操作符,但考虑到其常用性,我们还是咬咬牙将它放在了这里。它的作用是让订阅者在接收到数据之前干点有意思的事情。假如我们在获取到数据之前想先保存一下它,无疑我们可以这样实现。 Observable.just(1, 2, 3, 4) .doOnNext(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("doOnNext 保存 " + integer + "成功" + "\n"); Log.e(TAG, "doOnNext 保存 " + integer + "成功" + "\n"); } }).subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("doOnNext :" + integer + "\n"); Log.e(TAG, "doOnNext :" + integer + "\n"); } }); 输出: skip skip 很有意思,其实作用就和字面意思一样,接受一个 long 型参数 count ,代表跳过 count 个数目开始接收。 Observable.just(1,2,3,4,5) .skip(2) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("skip : "+integer + "\n"); Log.e(TAG, "skip : "+integer + "\n"); } }); 输出: take take,接受一个 long 型参数 count ,代表至多接收 count 个数据。 Flowable.fromArray(1,2,3,4,5) .take(2) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("take : "+integer + "\n"); Log.e(TAG, "accept: take : "+integer + "\n" ); } }); 输出: just just,没什么好说的,其实在前面各种例子都说明了,就是一个简单的发射器依次调用 onNext() 方法。 Observable.just("1", "2", "3") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { mRxOperatorsText.append("accept : onNext : " + s + "\n"); Log.e(TAG,"accept : onNext : " + s + "\n" ); } }); 输出: 写在最后 好吧,本节先讲到这里,下节我们还是继续讲简单的操作符,虽然我们的教程比较枯燥,现在也不那么受人关注,但后面的系列我相信大家一定会非常喜欢的,我们下期再见! 代码全部同步到GitHub:https://github.com/nanchen2251/RxJava2Examples 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
这可能是最好的 RxJava 2.x 入门教程系列专栏 文章链接:这可能是最好的 RxJava 2.x 入门教程(完结版)[推荐直接看这个]这可能是最好的RxJava 2.x 入门教程(一)这可能是最好的RxJava 2.x 入门教程(二)这可能是最好的RxJava 2.x 入门教程(三)这可能是最好的RxJava 2.x 入门教程(四)这可能是最好的RxJava 2.x 入门教程(五) GitHub 代码同步更新:https://github.com/nanchen2251/RxJava2Examples 为了满足大家的饥渴难耐,GitHub 将同步更新代码,主要包含基本的代码封装,RxJava 2.x 所有操作符应用场景介绍和实际应用场景,后期除了 RxJava 可能还会增添其他东西,总之,GitHub 上的 Demo 专为大家倾心打造。传送门:https://github.com/nanchen2251/RxJava2Examples 前言 很快我们就迎来了第二期,上一期我们主要讲解了 RxJava 1.x 到 2.x 的变化概览,相信各位熟练掌握RxJava 1.x的老司机们随便看一下变化概览就可以上手RxJava 2.x了,但为了满足更广大的年轻一代司机(未来也是老司机),在本节中,我们将学习RxJava 2.x 强大的操作符章节。 【注】以下所有操作符标题都可直接点击进入官方doc查看。 正题 Create create 操作符应该是最常见的操作符了,主要用于产生一个 Obserable 被观察者对象,为了方便大家的认知,以后的教程中统一把被观察者 Observable 称为发射器(上游事件),观察者 Observer 称为接收器(下游事件)。 Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { mRxOperatorsText.append("Observable emit 1" + "\n"); Log.e(TAG, "Observable emit 1" + "\n"); e.onNext(1); mRxOperatorsText.append("Observable emit 2" + "\n"); Log.e(TAG, "Observable emit 2" + "\n"); e.onNext(2); mRxOperatorsText.append("Observable emit 3" + "\n"); Log.e(TAG, "Observable emit 3" + "\n"); e.onNext(3); e.onComplete(); mRxOperatorsText.append("Observable emit 4" + "\n"); Log.e(TAG, "Observable emit 4" + "\n" ); e.onNext(4); } }).subscribe(new Observer<Integer>() { private int i; private Disposable mDisposable; @Override public void onSubscribe(@NonNull Disposable d) { mRxOperatorsText.append("onSubscribe : " + d.isDisposed() + "\n"); Log.e(TAG, "onSubscribe : " + d.isDisposed() + "\n" ); mDisposable = d; } @Override public void onNext(@NonNull Integer integer) { mRxOperatorsText.append("onNext : value : " + integer + "\n"); Log.e(TAG, "onNext : value : " + integer + "\n" ); i++; if (i == 2) { // 在RxJava 2.x 中,新增的Disposable可以做到切断的操作,让Observer观察者不再接收上游事件 mDisposable.dispose(); mRxOperatorsText.append("onNext : isDisposable : " + mDisposable.isDisposed() + "\n"); Log.e(TAG, "onNext : isDisposable : " + mDisposable.isDisposed() + "\n"); } } @Override public void onError(@NonNull Throwable e) { mRxOperatorsText.append("onError : value : " + e.getMessage() + "\n"); Log.e(TAG, "onError : value : " + e.getMessage() + "\n" ); } @Override public void onComplete() { mRxOperatorsText.append("onComplete" + "\n"); Log.e(TAG, "onComplete" + "\n" ); } }); 输出: 需要注意的几点是: 在发射事件中,我们在发射了数值 3 之后,直接调用了 e.onComlete(),虽然无法接收事件,但发送事件还是继续的。 另外一个值得注意的点是,在 RxJava 2.x 中,可以看到发射事件方法相比 1.x 多了一个 throws Excetion,意味着我们做一些特定操作再也不用 try-catch 了。 并且 2.x 中有一个 Disposable 概念,这个东西可以直接调用切断,可以看到,当它的 isDisposed() 返回为 false 的时候,接收器能正常接收事件,但当其为 true 的时候,接收器停止了接收。所以可以通过此参数动态控制接收事件了。 Map Map 基本算是 RxJava 中一个最简单的操作符了,熟悉 RxJava 1.x 的知道,它的作用是对发射时间发送的每一个事件应用一个函数,是的每一个事件都按照指定的函数去变化,而在 2.x 中它的作用几乎一致。 Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { e.onNext(1); e.onNext(2); e.onNext(3); } }).map(new Function<Integer, String>() { @Override public String apply(@NonNull Integer integer) throws Exception { return "This is result " + integer; } }).subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { mRxOperatorsText.append("accept : " + s +"\n"); Log.e(TAG, "accept : " + s +"\n" ); } }); 输出: 是的,map 基本作用就是将一个 Observable 通过某种函数关系,转换为另一种 Observable,上面例子中就是把我们的 Integer 数据变成了 String 类型。从Log日志显而易见。 Zip zip 专用于合并事件,该合并不是连接(连接操作符后面会说),而是两两配对,也就意味着,最终配对出的 Observable 发射事件数目只和少的那个相同。 Observable.zip(getStringObservable(), getIntegerObservable(), new BiFunction<String, Integer, String>() { @Override public String apply(@NonNull String s, @NonNull Integer integer) throws Exception { return s + integer; } }).subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { mRxOperatorsText.append("zip : accept : " + s + "\n"); Log.e(TAG, "zip : accept : " + s + "\n"); } }); private Observable<String> getStringObservable() { return Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(@NonNull ObservableEmitter<String> e) throws Exception { if (!e.isDisposed()) { e.onNext("A"); mRxOperatorsText.append("String emit : A \n"); Log.e(TAG, "String emit : A \n"); e.onNext("B"); mRxOperatorsText.append("String emit : B \n"); Log.e(TAG, "String emit : B \n"); e.onNext("C"); mRxOperatorsText.append("String emit : C \n"); Log.e(TAG, "String emit : C \n"); } } }); } private Observable<Integer> getIntegerObservable() { return Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { if (!e.isDisposed()) { e.onNext(1); mRxOperatorsText.append("Integer emit : 1 \n"); Log.e(TAG, "Integer emit : 1 \n"); e.onNext(2); mRxOperatorsText.append("Integer emit : 2 \n"); Log.e(TAG, "Integer emit : 2 \n"); e.onNext(3); mRxOperatorsText.append("Integer emit : 3 \n"); Log.e(TAG, "Integer emit : 3 \n"); e.onNext(4); mRxOperatorsText.append("Integer emit : 4 \n"); Log.e(TAG, "Integer emit : 4 \n"); e.onNext(5); mRxOperatorsText.append("Integer emit : 5 \n"); Log.e(TAG, "Integer emit : 5 \n"); } } }); } 输出: 需要注意的是: zip 组合事件的过程就是分别从发射器 A 和发射器 B 各取出一个事件来组合,并且一个事件只能被使用一次,组合的顺序是严格按照事件发送的顺序来进行的,所以上面截图中,可以看到,1 永远是和 A 结合的,2 永远是和 B 结合的。 最终接收器收到的事件数量是和发送器发送事件最少的那个发送器的发送事件数目相同,所以如截图中,5 很孤单,没有人愿意和它交往,孤独终老的单身狗。 Concat 对于单一的把两个发射器连接成一个发射器,虽然 zip 不能完成,但我们还是可以自力更生,官方提供的 concat 让我们的问题得到了完美解决。 Observable.concat(Observable.just(1,2,3), Observable.just(4,5,6)) .subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { mRxOperatorsText.append("concat : "+ integer + "\n"); Log.e(TAG, "concat : "+ integer + "\n" ); } }); 输出: 如图,可以看到。发射器 B 把自己的三个孩子送给了发射器 A,让他们组合成了一个新的发射器,非常懂事的孩子,有条不紊的排序接收。 FlatMap FlatMap 是一个很有趣的东西,我坚信你在实际开发中会经常用到。它可以把一个发射器 Observable 通过某种方法转换为多个 Observables,然后再把这些分散的 Observables装进一个单一的发射器 Observable。但有个需要注意的是,flatMap 并不能保证事件的顺序,如果需要保证,需要用到我们下面要讲的 ConcatMap。 Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { e.onNext(1); e.onNext(2); e.onNext(3); } }).flatMap(new Function<Integer, ObservableSource<String>>() { @Override public ObservableSource<String> apply(@NonNull Integer integer) throws Exception { List<String> list = new ArrayList<>(); for (int i = 0; i < 3; i++) { list.add("I am value " + integer); } int delayTime = (int) (1 + Math.random() * 10); return Observable.fromIterable(list).delay(delayTime, TimeUnit.MILLISECONDS); } }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { Log.e(TAG, "flatMap : accept : " + s + "\n"); mRxOperatorsText.append("flatMap : accept : " + s + "\n"); } }); 输出: 一切都如我们预期中的有意思,为了区分 concatMap(下一个会讲),我在代码中特意动了一点小手脚,我采用一个随机数,生成一个时间,然后通过 delay(后面会讲)操作符,做一个小延时操作,而查看 Log 日志也确认验证了我们上面的说法,它是无序的。 concatMap 上面其实就说了,concatMap 与 FlatMap 的唯一区别就是 concatMap 保证了顺序,所以,我们就直接把 flatMap 替换为 concatMap 验证吧。 Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(@NonNull ObservableEmitter<Integer> e) throws Exception { e.onNext(1); e.onNext(2); e.onNext(3); } }).concatMap(new Function<Integer, ObservableSource<String>>() { @Override public ObservableSource<String> apply(@NonNull Integer integer) throws Exception { List<String> list = new ArrayList<>(); for (int i = 0; i < 3; i++) { list.add("I am value " + integer); } int delayTime = (int) (1 + Math.random() * 10); return Observable.fromIterable(list).delay(delayTime, TimeUnit.MILLISECONDS); } }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<String>() { @Override public void accept(@NonNull String s) throws Exception { Log.e(TAG, "flatMap : accept : " + s + "\n"); mRxOperatorsText.append("flatMap : accept : " + s + "\n"); } }); 输出: 结果的确和我们预想的一样。 写在最后 好了,这一节就先介绍到这里,下一节我们将学习其它的一些操作符,在操作符讲完后再带大家进入实际情景,希望持续关注,代码传送门 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
写在前面 这也是久违的一整个月没有写 Blog,也是由于近期给妹纸找工作,各种坑蒙拐骗,然而都没卵用。额,广大朋友们,成都需要软件测试、线上运维、产品助理的伙伴,赶紧私聊我了。这妹纸,学习能力挺好,资质也不错,专业成绩总体排名年级第二,保送研究生(近期已决定放弃),心动不如行动,晚了就没机会了,赶紧私信我吧。 惊现 RecyclerView 内部 bug? 扯淡就不扯淡了,咱们还是说说这个早就可能被写烂吐槽的 RecyclerView 的 bug 吧。 不知道你们遇见没有,在 RecyclerView 被推的如火如荼的时候,你喜欢它,你默默用它,甚至对它的健壮性爱不释手。你觉得,这玩意儿都出来这么久了,一定没问题。额,没毛病。然而,在某一次快速滑动中,Boom,崩溃了!瞬间打脸。 查看 Log 得到下面的玩意儿。 java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 157(offset:157).state:588 at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3300) at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3258) at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1803) at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1302) at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1265) at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1093) at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:956) at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:2715) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:725) at android.view.Choreographer.doCallbacks(Choreographer.java:555) at android.view.Choreographer.doFrame(Choreographer.java:524) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:711) at android.os.Handler.handleCallback(Handler.java:615) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:137) at android.app.ActivityThread.main(ActivityThread.java:4921) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1027) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:794) at dalvik.system.NativeStart.main(Native Method) 杂一看像是数组越界?NO NO NO,这日志看上去根本就跟我们代码无关呀。多番 Google 发现,这貌似是 Google 程序员的锅?内部 bug?这TM官方的问题,关你何事?要不咱们不用 RecyclerView 了吧? 你是一个优秀的程序猿,不应该总是逃避问题,而应该思考如何去解决它。不过这说明了一个问题,人非圣贤孰能无过,连 Google 程序员那么牛逼的存在都会出问题,我们是不是......嘿嘿。 这玩意儿崩溃的原因比较清楚,就是如果绑定的集合List中的数据和 RecycerView 的数据不一致的时候,调用更新方法的时候会复现。 怎么解决? 有人这么说,造成崩溃的原因极有可能是当 clear 了之后,迅速上滑,但由于新数据还没来,导致 RecyclerView 需要更新加载下面的 Item 的时候,找不到数据源,导致了崩溃的发生。 所以,既然如此,一定可以通过让 Clear 的时候,禁止 RecyclerView 的滑动来解决它。代码如下: private boolean mIsRefreshing=false; mRecyclerView.setOnTouchListener( new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (mIsRefreshing) { return true; } else { return false; } } } ); //当刷新时设置 //mIsRefreshing=true; //刷新完毕后还原为false //mIsRefreshing=false; 其它人的意见 人,想法,总是千奇百怪。 造成崩溃的原因其实很明显,如果你更新集合 List 后,调用 RVAdapter 的 notifyXXXX 方法时,adapter 的更新预期接口和实际集合更新结果不同,就会出现这个异常!不信你可以随便模拟这个情况的发生。 所以有人就得到了这样的结论: RVAdapter 的 notifyDataSetChanged 方法执行后,在一定时间内,如果你更新了你的集合(无论是否在主线程更新集合),那么这个更新会实时反应到控件上,也就是说你的控件显示也会更新。 调用诸如 notifyItemRangeInserted 这样的方法之前,考虑清楚你的集合到底更新成什么样了!要注意参考结论1,结论1会影响你的判断。 解决该问题的正确姿势? 显然,上面的方法都不太好用,继续研究发现,直接采用下面的方法可以很好的解决。 经过多番研究发现,直接像下面这样,可以完美解决我们的问题。 1、复写 LinearLayoutManager package com.zxedu.ischool.common; import android.content.Context; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; /** * Author: nanchen * Email: liushilin520@foxmail.com * Date: 2017-05-19 15:56 */ public class WrapContentLinearLayoutManager extends LinearLayoutManager { public WrapContentLinearLayoutManager(Context context) { super(context); } public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } } } 对,没错,直接更换LayoutManaer就OK了 // mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); // 解决RecyclerView可能出现的holder数组越界Bug mRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 写在最后 请别问我为什么这样就能解决?我会大声告诉你,我也不知道! 我能怎么办,我也很无奈~ 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
写在前面 最近由于廖子尧忙于自己公司的事情和 OkGo (一款专注于让网络请求更简单的网络框架) ,故让LZ 接替维护 ImagePicker(一款支持单、多选、旋转和裁剪的图片选择器),也是处理了诸多bug,最近总算趋于稳定了,这里就把 Android N (API 24) 以上的相机适配方案分享给大家。 Android Nougat 也是被更新很久了,作为一名 Andorid 开发者,我们有义务时刻准备自己调整 TargetSdkVersion 为最近的一个,于是从之前的 23 直接提高到了 25 。 和往常一样,每当我们调整 TargetSdkVersion,我们需要检查我们的代码的每一部分工作的非常好。如果你只是简单地更改代码,我可以说,你的应用程序正在崩溃或故障的高风险。在这种情况下,当你改变你的应用程序的 TargetSdkVersion 24,我们需要检查每一个功能完美的作品在 Android 的牛轧糖(24)以上。 拿到 7.0 的小米 5 测试机后,迫不及待对自己维护的 ImagePicker 测试了一个遍,然而的确和大家所提的 issuse 一样,在调用系统相机的时候直接崩溃了。 到底是什么引发了 7.0 相机崩溃 跟进错误日志到源码发现,在我们调用相机获取 Uri 的时候发生了崩溃。 原因很明显,file:// 不被允许作为一个附加的 Uri 的意图,否则会抛出 FileUriExposedException 。 为什么在 Android Nougat 下 file:// 不被允许? 你可能会很好奇为什么 Android 团队决定改变这种行为。 其实背后有一个很好的理由,如果文件路径被发送到目标应用程序(相机应用程序在这种情况下),文件将完全访问通过相机应用程序的过程,而不仅仅只有发起者能收到。 但让我们考虑一下,实际上是由我们的应用程序去启动摄像头拍照,并保存作为我们的应用程序的代表文件。因此,该文件的访问权限应该是我们的应用程序而不是摄像头应用程序本身。这就是为什么现在 file:// 在 TargetSdkVersion 24 中要求每一位开发者都去完成这个任务。 那到底怎么解决? 既然 file:// 不再被允许,那我们应该怎么处理呢?答案是通过 FileProvider 去解决它。 我们应该怎么让 FileProvider 解决好它。 1、首先是在 AndroidManifest.xml 中申明 <provider android:authorities="${applicationId}.provider" android:name=".ImagePickerProvider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> 2、创建一个 provider_paths.xml 文件在 res 文件夹下的 xml 文件夹下。 <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="external_files" path="."/> </paths> 3、在适当的地方去替换它 Uri uri; if (VERSION.SDK_INT <= VERSION_CODES.M){ uri = Uri.fromFile(takeImageFile); }else{ /** * 7.0 调用系统相机拍照不再允许使用Uri方式,应该替换为FileProvider * 并且这样可以解决MIUI系统上拍照返回size为0的情况 */ uri = FileProvider.getUriForFile(activity, ProviderUtil.getFileProviderName(activity), takeImageFile); } Log.e("nanchen",ProviderUtil.getFileProviderName(activity)); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,uri); 几点说明 对于上面的三步操作,做几点说明: 1、在 AndroidManifest.xml 文件中对 Provider 的 name 属性申明为什么是 .ImagePickerProvider (实际上这是一个继承自 FileProvider 但什么也没实现的类) 而不直接把 name 赋为 android.support.v4.content.FileProvider ? 这是因为 ImagePicker 作为一个图片选择框架,而你的 App 中同样可能会有申明,为了避免 Android Studio 在编译的时候 merge 各个 Module 导致冲突,这里保险起见的申明了一个不一样的名字。 2、为什么在 AndroidManifest.xml 文件中申明的 authority 属性为 ${applicationId}.provider , 而不是固定的名字。 这是因为在 Android 中,要求 authority 必须是唯一的,如果你在定义一个 provider 的时候为它指定一个唯一的 authority,这里且拿 ImagePicker 做比方,假如你在一个 App 上使用了 ImagePicker 作为图片选择框架,而你在另外一个应用中再次使用 ImagePicker 的时候,系统会检查当然已安装应用的 authority 是否和你要安装应用的 authority 相同,如果相同则会弹出下面的警告,并安装失败。 所以我们在定义 authorities 的时候采用 ApplicationId + Provider 的形式,在获取 authorities 的时候,我们可以通过包名 + Provider 的方式获取。代码如下: package com.lzy.imagepicker.util; import android.content.Context; /** * 用于解决 Provider 冲突的 util * * Author: nanchen * Email: liushilin520@foxmail.com * Date: 2017-03-22 18:55 */ public class ProviderUtil { public static String getFileProviderName(Context context){ return context.getPackageName()+".provider"; } } 写在最后 以上便解决了 Android N 的相机崩溃问题,如有写的不对的地方,欢迎大家在评论区留言。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
文/南尘 谁的青春不曾迷茫,谁的年少不曾混乱,谁在青春年少时,可以真正明确自己的目标?也许有这种人,但却不是我,我在青春里乱闯,已失去了方向。 闲暇时来到弟弟的教室,看着教室上方挂着的横幅“给自己一个目标,让生命为它燃烧。”我突然迷惘,我猛得茫然,我冥思苦想自己的目标是什么?答案是不明了。也曾作自己的春秋白日大梦:有家财万贯,富可敌国;有美女如云,貌赛西施;有滔滔权势,只手遮天。也许那时候是沉浸在小说主人公生活中的吧,无奈那毕竟是作梦,并且还是白日梦,醒来我只是一个普普通通的学生,还是一个笨学生,又是一个没有目标的学生,在学校里貌似只是混日子的学生。自己清楚的明白“醉卧美人膝,醒掌天下权”的生活,距离自己何止是十万八千里! 每天在压抑中度过,失去了童年儿时的欢快大方,变得郁郁寡欢。我不清楚这是因为学习的压力,还是因为社会现实的逼迫,又或者因为爱情的失落。自己认不清楚自己或许是一个人最大的失败,还有可能是自己很清楚自己,却往往无法战胜自己,无法克服自己的欲望,最后是不敢承认自己的错误,这便许是一个人的悲哀。我大概就属于第二种人吧,明明知道自己哪里不够好,却做不到更好,白白浪费了青春。 曾经自己选择当时自以为对的路时,大声告诉自己:不会后悔,可惜时间告诉我们,方向偏了角度。当事情不像我们想象的那样发展时,我们即便再不愿承认,心里也明白的知道,自己选错了路。也许自以为是地不后悔,但那只是对自己的慰藉,因为知道后悔也没有用,所以觉得自己不曾后悔过。 记得上高中时,黄安辉对我说:“如果你毕业时,可以拍着胸脯说:‘我问心无愧’,那高中就没有白上。”那时,我的目标应该是高考,但之后的成绩告诉我,那或许不是属于我的舞台。惨绝人寰的成绩单,使我再次对目标失去了方向感。学习的困惑,成绩的落寞,我只能尽可能的去学习更多的可以学会的东西,自己告诉自己,不只是只有高考这一条路,但又清楚的很,这确实是一条捷径。那些成绩优秀的好学生,应该早就明确了自己的目标,他们是聪明的,即使自己总觉得他们是在机械性的学习,仿佛傻子一般,但不可否认他们比自己更有前途。现在在学习上不利的我们,当然也不能怨天尤人,还有三年时间值得我们去争取,这是这一年里的方向。但三年以后,方向会指向哪,却是个未知数了,希望真的会不悔。 感情上的事,让我变了许多,我以为我们可以走到最后,但结局总是不尽人意,每次都好像天意弄人。曾经恋人却宛如仇人一般,曾经誓言也化作了轻烟,曾经熟悉如今陌生,或许真的是我太差。我一遍遍不厌其烦地回忆着曾经的每一刻,想着那些幸福的点点滴滴,生活在自己编织的梦里。当抬头望天,却发现,天是空的,梦是假的。在来来回回、聚聚散散中,我迷失了方向,我知道没有时间的磨合,我是不可能再找到方向了。 在下一个路口,等待某一种思念,当心再跳动时,我知道那是我追逐的方向。 我相信一见衷情,也相信日久生情,或许我还不懂爱情,但已经历了思念的痛。青春年华里,我本应以学习为主,但乱闯时,却丢失了方向感。 ——2014-01-27 落笔大川师(现在入驻简书,发送分享)
一、写在前面 最近入驻了简书,准备把我博客园上的部分认为很有意义的文章搬过来分享给大家,后面我会同步更新。希望大家喜欢,原文地址:【真实感悟】8元钱,你也许不如8万元来的实在,但却带走了我的灵魂... 我没有文笔,所以我原本在博客园是只写技术干货的,因为我本身就是一个老实朴素的码农,没有煽情的文笔,唯有烂漫的感情却无处投入。 上周五我对我的开源毕设第二次较大更新做了分享:【开源毕设】一款精美的家校互动APP分享——爱吖校推 [你关注的,我们才推](持续开源更新2),也同步到了github:https://github.com/nanchen2251/AiYaSchoolPush,突然灵机一动,我学习了张哥一如既往的风格,在github后面加了一个支付宝奖赏。 我不过是随手一弄,并没有让大家奖赏的意思,只是觉得如果大家有所支持的话,给个star或者fork奖励就好了,然而我怎么都没有想到,就这么一个小小的举动,让我真的在支付宝得到了8元钱打入。 当时看到后台通知的时候,我心里真的很感动,因为我何德何能,做的并不那么完善(一直在完善)的开源项目,却得到了这位支付宝用户的打赏,我心里是受宠若惊的。 8元钱,你不如8万元来的实在,但是真真切切的带走了我的灵魂~ 二、切身感悟 不由得回想起过去,从小成绩优异,但每每到关键时刻却总是发挥失常,高考后进了一个二流大学,从此人生不再相信这世界原本的美好。 自从遇上了编程语言,我才找到了自己真正的路,我才发现,以往的一切对我都没有那么大的吸引力。真的是兴趣,我不知道在坐的小伙伴们,有没有这样一种感觉,做开发,完全没有感觉在工作,每天都能学到新的东西,也许时有弯路,但总能一步一步前行,所以无论何时,都是有收获并快乐着。笔者还未毕业,也算一名大四的在校大学生,却真真实实的感觉到这个社会的无情与冷漠,却也感觉到这个世界有时候会让你觉得无比的真诚,以至于让自己忘乎所以。但这一次意外的打赏,人生第一次尝试,也得到了第一次最最真实的反馈,这样的真情远比那用一些肮脏的东西换来的金钱来的令人珍惜。 笔者也曾面试阿里京东,各个公司的切入点不一致,但除了面试基础知识和创新意识,都要求有一个真正的情怀。 情怀,这个说不清道不明的玩意儿,有时候,就是那么独特奇怪。 我知道现在我们行业的培训机构比比皆是,大部分还有一定的从众心理,我身边这样的朋友群体也是信手拈来,不过我真心有话告诉你们。 如果,你有一定的编程基础,我真的不推荐参加培训机构,我想培训机构存在有它的意义,不过对于有一定基础的你,我想性价比值得权衡,因为我想培训机构要想一个班一个班的培训,班级水平总有高低,讲师一定会以服从多数的讲解思路做讲解的,所以对于有基础的你就不那么必要了。我知道你或许是因为觉得自控能力太差,需要培训机构这样的高强度学习压抑自己,但你一定会经历这样的心烦,脱节后怎么都跟不上,还好,编程不如数学,不会如同数理化那样层层相关,所以你可以选择性的学习。但其实,不管你在哪里,你都可以学习,因为笔者一开始也是什么都不懂,经常面对成堆的知识,发呆一上午。但万事开头难,当你稍微有点入门以后,你会发现,你每天都是开心的,真的。 对于初学者,包括给我支付宝赚钱那位小伙伴,我真心的想跟你们分享我的心路过程。我来自农村,家里一堆一堆的事儿压得自己喘不过气,有些人,一辈子就只能依靠我自己了。虽有压力,但我从来不缺动力。因为我真的感觉,做起开发来,没有一丝时间是光阴虚度。我大学一二年级的时候,天天打英雄联盟,不去上课,整个学渣形象。辅导员要开除我,班级同学不喜欢我,所以我破罐子破摔。直到遇见java,我真真正正地开始了自己的编程之路,我从一个一个的小demo,慢慢地成长,虽然慢,但总在前行。 三、学习思路 我真心想给这群和我一样一步一个脚印走过来的小伙伴们,带来一些自己的看法。 博客真的是一个很好很好的东西,绝对是你的良药,坚持写逛博客,你可以和大牛们切身交流,你可以总结你的问题和分享你的快乐,你也可以完全地把自己的想法告诉他人。 怎么学习?如果你什么基础都没有,上面说了,培训机构的确是速成的一条道路,但是它并不是一帆风顺的,相对于学校的课程,除了全面我认为没有什么差别,除非,你能进到一个很好的培训机构,或者,你真的一点基础都没有。下面我针对自学,说下我的切身总结。 现在网上有很多自学型的网站,什么扣丁课堂,网易云课堂,mooc大学等,比比皆是,上面知识五花八门,但如果做一个巧妙的总结,还是很全面的。不过用网页学习的唯一缺点就是很耗时,好处呢,当前是更加通俗易懂一些,虽然全面性不如书籍来的踏实,但实时性要比书籍学习快的多。 网上学习的话我推荐的方法是不要一边看视频一边写代码,而采取视频后再自己写代码的方式,前期会比较困难,你可以看几分钟写一点,重在理解,不要去背诵。当你慢慢做的多了以后,你会发现,这个东西真的跟数理化差距不大,就是一层一层的东西,你入门后会发现一切都上手很快的。 总结相当有必要,不管你是写在博客,还是写在印象日记一系列的文本记录工具上,总结都是你最应该做的工作。在这里再次推荐博客,因为你如果能自己把自己所学给别人讲明白了,那我想你是真的会的一大半了,而且你以后也很方便自己查看总结。 如果你用书籍学习,一样推荐是看后再自己去写代码,而不是一步一步的照着代码敲上去,这样你获取前期会存在背代码一样的嫌疑,但你始终是在进步的。 多关注浏览大牛博客,吸取博文中有用的营养,特别是那些活跃的大牛,真的是值的自己去经常关注的,因为他们所讲解的东西,很多时候对你的帮助很大。 这么久以来,个人觉得真的是你想学一门开发语言,你倒不如直接上手做一个小项目出来,这样是学习最快的方式,不过要求有一定的基本功,不过只要熟悉一门语言了,那你会发现,其实你要上手另外一门语言真的是以10天为单位来计量的。 四、企业要求 最后还是说说自己到公司上班后的一些感慨,取名为企业要求,倒不如说一些实时感悟来的痛快。 1、让人心烦的“软素质”** 我常听我上级抱怨现在招人麻烦,招有经验的吧,对创业公司来说,抛开成本大的因素,也存在因为有些人有经验,所以可塑性很低,难以实现创业公司“一人多用,八面玲珑”的需求,或许还会给新生的团队带来一些前公司的不良习惯。所以初始团队在基本工种配置齐全后,更愿意招聘大学生进来,好在学习能力强,可塑性高,说白了,成本也低。所以我很推荐应届生直接进入创业公司,但是创业公司很容易死,你要做好心理准备,也没有BAT公司完善的培养体系。 苦劳 = 功劳? 公司难免会有这样一群人,抱怨自己每天都在加班,(因为在创业公司加班是常有的事),但公司却没有考虑加班薪酬。其实完全没有必要,一个很简单的功能,你花的时间过久,这不是别人的问题,而是你自己的问题,你看着是挺辛苦的,但是别人二十分钟能解决的问题,为什么你用了一个通宵? 一番热血洒沱江 我也是一位新人,虽慢慢的成了我们公司相对较早进入的员工了。不过我很了解这样的心态,基本刚进公司的时候都是满腔热血,想着低工资没事儿,只要自己做出一番成绩,升职加薪,出任CEO,赢取白富美,走向人生巅峰,那都是理所当然的。但过不了一段时间,这个劲儿就过了,就开始想着,我都来了这么久了,为啥还是这么点钱?于是就开始想着各种法子暗示老板,是时候给我涨工资了!又或者,整天都嚷着,哪个哥们,哪个同学,以前不如自己能力,现在却拿着五位数的工资。所以很多公司会问你,你觉得你值多少钱? 到底值多少钱? 这样的问题,我也被问上好几次了。不过我从来没有正面回答过,因为我生怕要的太高,会让别人觉得你自命不凡,又生怕要的太低,别人觉得你不够自信。到底多少钱合适?我心里一直认为公式是这样的:现阶段能力+潜力 = 薪资。潜力的比重一定是大于现阶段能力的,但是这个东西很虚,因为潜力不是自己来评估的,不要自己骗自己了,你的潜力,在你工作中的点点滴滴早已体现出来了,老板可能嘴上不说,但其实心里早已给你开了一份成绩单。所以未来,你的薪资如何,基本和这个成绩单息息相关。 五、靠什么来涨工资 终于还是说到了每个人都很关心的问题,靠什么来涨工资? 前面我已经说到,工资所占比重更倾向于发展潜力。那么到底如何体现?下面是一些个人见解,请轻喷。 1、工作态度 从小就被老师捏着鼻子,灌了十几年的鸡汤:态度决定一切。当年还以为是老师为了让我们好好写作业才编下的鬼话,后来看看的确是这样,态度能决定到方方面面。一件事儿的成败最终决定的不是个人能力的强弱,不是时运机会,不是天时地利人和,而是态度。你个人能力再强,懂的再多,说的再多,对工作没有一个良好的态度,那至少对于一个worker来说,你已经不合格了,你整天得过且过,却还成天抱怨着公司怎么还不给自己涨工资,你到底凭什么? 2、学习能力 互联网行业不像传统行业,它的更新速度不是我们所能比拟的,所以对于任何一家互联网公司的一员,你都得有出众的学习能力,这才能让你更加容易融入进去。年轻是你的优势,但不是资本,不要觉得自己现在年轻,将来就一定可以吹牛逼。我上级是北大博士,资历很老,但从来没见他觉得自己很了不得一样,还是照常天天跟我拉家常,跟我一起探知未知的领域,因为他的主研方向是大数据算法,所以和我所擅长的有所冲突,但一有他懂的,从来都不啬于知识,对我各种细心讲解,如果都不会的,我们会一起去google,去学习,这才是最重要的,这样的上级,和你没有任何鸿沟,我觉得自己是幸运的。 都是年轻人,年轻气盛,我们的优势在于接受新知识的能力和学习能力,但并不是你有能力就行了,如果你不去学,那真是浪费青春,浪费表情。如果你已经找到了工作,你觉得你是万事无忧吗?你满足一个月几千块钱?你满足现在的地位阶层?满足双十一后还在吃土?想让自己升值,最快也是唯一的途径就是投资现在的自己,而最实惠的选择就是学习。 虽然我不是老板,但我想这一类员工绝对让老板足够头疼,就是刚来没多久,还没为企业带来效益,就急着说要涨工资,老板在没看到你的成绩之前,哪有理由给你涨,就算给你涨,又怎么服众?你可以试试期末考试交白卷,看老师会不会给你高分? 3、老板如何看你 一般来讲,除了黑心老板会压榨你以外,大多数老板都会想着给你们创造更好的工作环境,提高福利待遇,毕竟你们是在给他创造财富,但是正因为是老板,才会时刻在心里树立一个标准,什么人应该涨工资,什么人能涨工资。 这里总结几点: 时刻维护公司利益,尝试站在老板的角度思考问题,多为他们排忧解难,而不是制造麻烦; 工作的目的不仅仅是为了获取报酬,而且要提供超出报酬的服务和努力; 重视工作中的每一个环节,从“要我做”完美过渡到“我要做”,主动分担一些“份外”的事; 责任的核心就是责任心,将自己的工作负责到底,做错事千万不要找理由找借口,勇于承担自己的错误,要善于总结,在自己身上发现一个坑就填掉一个坑; 高效工作,量化、细化每天的工作,千万别瞎忙,更不要拖延; 不要说“我觉得……”、“我认为……”,带着方案去提问题,再让老板去评估; 不做团队的短板,挤出时间来给自己充电; 拒绝摆架子,你是谁啊你摆架子;拒绝邀功,屁事儿都没放出来一点,邀什么功;拒绝耍小聪明,老板混了这么多年,你确定你耍得过? 感恩,不管现在如何,公司给了你工作,事业,给了你展示自己的舞台,给了你成长的空间,凡此种种,要用感恩的心去看待。 4、几点建议 做公司的主人 永远不要把自己当成一个员工,否则,你也就只是一个员工了。也许你会认为,这又不是你的公司,拿什么做主人?其实不然,你除了没有控制权和决策权,事实上,你的每一步工作,都关系到公司的发展,哪怕你永远都是一颗小螺丝钉。老板会看到,是谁在为公司努力着。 这是事业,不是工作 如果你是一个每天按时上下班,混混日子,每个月拿几千块就能满足的人,你可以忽略着一些了。别骗自己了,把你的每份工作都当做自己的事业吧,只有你去像做事业一样的努力,你才能得到更多,而且不止是财富。 永远不要停下学习的脚步 前面再三强调的学习,真的很有必要,比如你来的时候会做简单的业务逻辑处理,一年后你还是这点水平,凭什么给你涨工资?因为你帅?因为你在公司待了一年?别做梦了,企业最看重的还是你能给他创造多少效益,你一直这个水平,跟新进来的有啥区别,凭啥给你涨工资?你二十几岁,正是人之清晨,你觉得用来打游戏,抱怨生活,抱怨工作,抱怨薪资,有意思么?不如投资资金,不要让别人给你定的价一成不变,去给老板要求涨工资的时候也只有拿苦劳说事儿。那不值钱,真的,我相信你在电影里面一定有看到有些人待了七八年,还是没涨工资,说到底,都是有原因的。 从“要我做”到“我要做” 别做被人支配的人,脑子和手长在你自己身上,别就那样甘于做一个执行者,别人让你干什么你就干什么,多思考,多主动,要从体力劳动渐渐转向脑力劳动,但别只想不做;你还年轻,你的潜力是无限的。 在月薪三千的时候,做月薪八千的事儿 决定你工资的还有一个核心点:供需关系。仔细想想,你所提供的劳动力是稀缺的么?是别人愿意高价购买的么?是别人无可替代的么?你的价值又是什么?当你找到了这些问题的答案,并且做到了正解,在月薪三千的时候拿出八千的实力,那老板要是还不给你涨工资,就!跳!槽! 低薪并不可怕,可怕的是你适应了低薪 可能大部分大学生出来都无法拿到一个令自己满意的薪资,无法满足自己的需求或曾经美好的幻想,但是,千万不要着急,也不要就这么气馁,都二十来岁风华正茂,抽空看看书,学学习,给自己阶段性地设定一个天花板,然后一层一层地去突破,保持这种态度,不断进步不断突破,在这波大潮中你才能成为适者,适者生存。 六、写在最后 杂一看也写了这么多了,本文并非全是自己观点,有些来自网上语录,不过真的是想帮助到大家,如果你喜欢,不妨推荐一下,让更多的人看到。 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ nanchen
GitHub 还在持续更新:https://github.com/nanchen2251/StudyForAndroid 一、写在前面 最近项目重构,时间贼多,也没什么时间更新博客,个人的开源项目也是多时没有更新了:https://github.com/nanchen2251/AiYaSchoolPush,然而没有更新不代表我不在乎,后面一有空还是会继续提交的。 还是来冒个泡,给大家献上一些福利,这些项目要么是 GitHub 上影响力很大,要么是对你们很有用的项目: 本文github链接:https://github.com/nanchen2251/StudyForAndroid 【本文内容资源全部来自张哥:http://stormzhang.com/】 1、free-programming-books https://github.com/vhf/free-programming-books 这个项目目前 star 数排名 GitHub 第三,总 star 数超过6w,这个项目整理了所有跟编程相关的免费书籍,而且全球多国语言版的都有,,有了这个项目,理论上你可以获取任何编程相关的学习资料,强烈推荐给你们! 2、oh-my-zsh https://github.com/robbyrussell/oh-my-zsh 俗话说,不会用 shell 的程序员不是真正的程序员,所以建议每个程序员都懂点 shell,有用不说,装逼利器啊!而 oh-my-zsh 毫无疑问就是目前最流行,最酷炫的 shell,不多说了,懂得自然懂,不懂的以后你们会懂的! 3、awesome https://github.com/sindresorhus/awesome GitHub 上有各种 awesome 系列,简单来说就是这个系列搜罗整理了 GitHub 上各领域的资源大汇总,比如有 awesome-android, awesome-ios, awesome-java, awesome-python 等等等。 4、github-cheat-sheet https://github.com/tiimgreen/github-cheat-sheet/ GitHub 的使用有各种技巧,只不过基本的就够我们用了,但是如果你对 GitHub 超级感兴趣,想更多的了解 GitHub 的使用技巧,那么这个项目就刚好是你需要的,每个 GitHub 粉都应该知道这个项目。 5、android-open-project https://github.com/Trinea/android-open-project 这个项目是Trinea 整理的一个项目,基本囊括了所有 GitHub 上的 Android 优秀开源项目,但是缺点就是内容太多了不适合快速搜索定位,但是身为 Android 开发无论如何你们应该知道这个项目。 6、awesome-android-ui https://github.com/wasabeef/awesome-android-ui 这个项目跟上面的区别是,这个项目只整理了所有跟 Android UI 相关的优秀开源项目,基本你在实际开发中用到的各种效果上面都几乎能找到类似的,简直是 UI 开发必备。 7、Android_Data https://github.com/Freelander/Android_Data 这个项目是张哥的邪教群的一位管理员整理的,几乎包括了国内各种学习 Android 的资料,简直太全了,他为这个项目也稍微出了点力,强烈推荐你们收藏起来。 8、AndroidInterview-Q-A https://github.com/JackyAndroid/AndroidInterview-Q-A/blob/master/README-CN.md 这个就不多说了,干货无疑,之前给大家推荐过的,国内一线互联网公司内部面试题库。 9、LearningNotes https://github.com/GeniusVJR/LearningNotes 这是一份非常详细的面试资料,涉及 Android、Java、设计模式、算法等等等,你能想到的,你不能想到的基本都包含了,可以说是适应于任何准备面试的 Android 开发者,看完这个之后别说你还不知道怎么面试! 做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~ 