0%

1114、按序打印:类中的3个方法分别运行在3个线程中,如何保证它们按顺序执行?

点击看答案

没找到方便记忆的版本,自己写个。当然,链接中通过 AtomInteger 来实现自旋也是不错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JavaMainClass {

volatile int a = 0;
void test1(){
System.out.println("test1");
a = 1;
}
void test2(){
while (a != 1){
Thread.yield();
}
System.out.println("test2");
a= 2;
}
void test3(){
while (a != 2){
Thread.yield();
}
System.out.println("test3 \n");
}
}

自己写的时候的问题:看懂题目,volatile 关键字使用即可

LeetCode

1195、 交替打印字符串

编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:

  • 如果这个数字可以被 3 整除,输出 “fizz”。
  • 如果这个数字可以被 5 整除,输出 “buzz”。
  • 如果这个数字可以同时被 3 和 5 整除,输出 “fizzbuzz”。

假设有这么一个类:

1
2
3
4
5
6
7
class FizzBuzz {
  public FizzBuzz(int n) { ... }  // constructor
public void fizz(printFizz) { ... } // only output "fizz"
public void buzz(printBuzz) { ... } // only output "buzz"
public void fizzbuzz(printFizzBuzz) { ... } // only output "fizzbuzz"
public void number(printNumber) { ... } // only output the numbers
}
点击看答案

看参考答案吧

自己写的时候的问题:使用Semaphore 关键字会简单很多,使用volatile 关键字稍稍麻烦些。

这个问题也可以关联到后续的 打印零与奇偶数

LeetCode

1115、 交替打印FooBar

我们提供一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FooBar {
public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
  }
}

public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
}
}

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。请设计修改程序,以确保 “foobar” 被输出 n 次。

点击看答案

注意在空闲的时候Thread.yield()

自己写的时候的问题:可以使用 volatile 关键字,不过在 while 自旋过程中,需要 Thread.yield(); 让出cpu,否则容易出现超时。

LeetCode

1226、 哲学家进餐

点击看答案

这个问题还没想通,后续再看

LeetCode

H2O 生成

点击看答案

使用 Semaphore简单粗暴。这个问题还没想通,后续再看

自己写的时候的问题:看别人的再自己写的,h的Semaphore(2),而 o 的 Semaphore(0) ,但是生成h 一个 就release一个 o (虽然o的permit 数量为 0 ,但是在另一个线程release 一个就相当于create 一个,所以并不矛盾,同理,在生成 o 的时候, o.acquire(2))

LeetCode

注: 以下题目都是根据 LeetCode App上的顺序,按照由易到难排列(与其他列表有重复的题目就略过了)

704、二分查找,给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

点击看答案

这题目自己写得还不错

自己写的时候的问题:

参考链接:LeetCode

最小k个数

点击看答案

使用 PriorityQueue 代替自己建堆

自己写的时候的问题:Comparator 单词没写对,还有 Comparator 应该是个泛型,后面要接上

参考链接LeetCode

  1. 二叉树的最大深度

参考别人的思想

https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/

  1. 平衡二叉树

参考别人的思想

https://leetcode-cn.com/problems/balanced-binary-tree/solution/balanced-binary-tree-di-gui-fang-fa-by-jin40789108/

  1. N叉树的最大深度

依葫芦画瓢

https://leetcode-cn.com/problems/maximum-depth-of-n-ary-tree/

  1. 二叉树的最小深度

我去,审题啊,叶节点啊,[1,2]的时候,叶节点是2啊,1还不是叶节点。

https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/solution/li-jie-zhe-dao-ti-de-jie-shu-tiao-jian-by-user7208/

  1. 翻转二叉树

我去,面试google的那个牛人都没能写出来,我写出来了,额。。其实不就是每个结点都在翻转么。估摸着是那个哥们紧张了吧。

https://leetcode-cn.com/problems/invert-binary-tree/

  1. 对称二叉树

右边比左边,左边比右边

https://leetcode-cn.com/problems/symmetric-tree/

面试题 03.04. 化栈为队

别人的想法太巧妙了,pop 或者 peek 的时候,只要辅助栈的元素不空,就可以出去。为空就将原本栈的元素倒入到辅助栈再pop或者peek。

https://leetcode-cn.com/problems/implement-queue-using-stacks-lcci/solution/java-liang-ge-zhan-by-npe_tle/

  1. 宝石与石头

真心傻逼了,用 if(!set.add(charAt(i))) 去判断,应该只要 if(set.contains(charAt(i))) 即可。

https://leetcode-cn.com/problems/jewels-and-stones/

  1. 整数的各位积和之差

乘积的初始值应该为1啊,蛋疼,居然初始化为 0

https://leetcode-cn.com/problems/subtract-the-product-and-sum-of-digits-of-an-integer/

  1. 将数字变成 0 的操作次数

while循环到num == 0 为止

https://leetcode-cn.com/problems/number-of-steps-to-reduce-a-number-to-zero/

  1. 统计位数为偶数的数字

转成字符串看length不就好了,还要想着去除,唉

https://leetcode-cn.com/problems/find-numbers-with-even-number-of-digits/

  1. 有多少小于当前数字的数字

1、快排的条件有点忘了,最开始的判断条件 front > tail,while循环中要有 nums[j] >= pvoit 、num[i] <= pvoit ,一轮while 之后,要执行 nums[start] = nums[front]; nums[front] = pvoit;
2、对于数组,int[] nums = {1,4,8,50,2,44,2};可以执行 nums.clone() 能将数组复制一份

https://leetcode-cn.com/problems/how-many-numbers-are-smaller-than-the-current-number/

面试题 02.03. 删除中间节点

脑筋急转弯

https://leetcode-cn.com/problems/delete-middle-node-lcci/

注: 以下题目都是根据 LeetCode App上的顺序,按照由易到难排列(与其他列表有重复的题目就略过了)

350、两个数组的交集II

点击看答案
  1. hashmap 记录第一个数组每个元素出现次数,再遍历第二个数组 2. 先给两个数组排序,再遍历**

自己写的时候的问题:Integer 写错、HashMap<Integer,Integer> 在 int times = get(key) 的时候,要注意判空

LeetCode

412、Fizz Buzz,写一个程序,输出从 1 到 n 数字的字符串表示。1. 如果 n 是3的倍数,输出“Fizz”;2. 如果 n 是5的倍数,输出“Buzz”;3.如果 n 同时是3和5的倍数,输出 “FizzBuzz”。

点击看答案

计算 n%3==0 及 n%5==0 的 boolean 值。之后分别输出

自己写的时候的问题:canDivid3 的时候,应该使用遍历的下标i ,而不是传入的参数n %3 == 0

LeetCode

13、 罗马数字转整数

点击看答案

前一个数比后一个数小

自己写的时候的问题:map写成了 HashMap<String,Integer> ,应该是 HashMap<Character,Integer>,还有,throw 抛出异常的时候,应该要有new关键字,即 throw new IllegalArgumentException(“error input!”);

LeetCode

387、字符串中的第一个唯一字符,给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1

点击看答案

第一次遍历,次数存放在HashMap中,第二次遍历,找到第一个不重复的。

自己写的时候的问题:

LeetCode

28、实现 strStr(),给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例:

1
2
输入: haystack = "hello", needle = "ll"
输出: 2
点击看答案

只有第一个字符匹配上才需要比较

自己写的时候的问题:haystack = “” 且 needle = “” 时,我返回了 -1,应该是 0,还有计算String的长度应该是 str.length() ,而不是 str.leng ,不过数组倒是可以这样做 arr.length

LeetCode

38、外观数列

点击看答案

读懂题目很重要,递归

自己写的时候的问题:throw 要 new !

LeetCode

69、x的平方根,计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去,如8的平方根是 2.28…,则结果取2即可

点击看答案

我自己的写法,没有考虑数字越界:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int mySqrt(int x) {
if(x < 0){
throw new IllegalArgumentException("error input!");
}

if(x == 0 || x == 1){
return x;
}

int result = 0;
for(int i = 1;i * i <= x; i++){
System.out.println("i = " + i);
result = i;
}

return result;

}

其他数字都对,但是如果输入 2147395600 ,则在 i= 46340 时,i就已经是结果了,但是这里的循环条件还是会继续执行,这时候 i= 46341 时,就超出 int 的取值范围越界了,因此不对。如果非要用这种方法去实现,可以在 for 语句里面加个条件: i * i > 0 。因为Java 中的int型数据是有符号数,最高位表示符号,越界的时候,最高位变成1了,也就是变成负数,所以这个条件在刚发生越界的时候就不满足了,就能得到正确的结果

使用二分查找法要注意几点:首先 (a/2)² > a ,则可以解出来正整数解是 a > 4,即在 a > 4的情况下 a/2 > 根号a 是必然的,再综合 1,2,3,4 这几个特殊数字,我们可以得出,a开根号的值在 [1,a/2] 这种闭合区间。还有,开根号这里可能会有小数,但是我们只要返回正数,所以可以要求 front == tail ,最后是返回tail的值,要注意

LeetCode

371、两整数之和,不使用运算符 + 和 -,计算两个整数 a、b之和

点击看答案

异或求出普通位相加,与操作求出进位

自己写的时候的问题:a 要暂存 无进位结果,我写的时候忘了暂存了。因为无进位结果 + 进位 还有可能产生进位,所以要有 while 循环。

LeetCode

283、移动零,给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序

点击看答案

2个指针,1次遍历

自己写的时候的问题:

LeetCode

1、两数之和,给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

点击看答案

使用 HashMap 辅助

自己写的时候的问题:

LeetCode

101、对称二叉树,给定一个二叉树,检查它是否是镜像对称的

点击看答案

递归判断

自己写的时候的问题:要懂递归啊,这是乱写成功的。不会递归时,把条件列出来,估计就有思路了。

LeetCode

108(还未写)、将有序数组转换为二叉搜索树,将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树

点击看答案

二叉树遍历

LeetCode

118、杨辉三角,给定一个非负整数numRows,生成杨辉三角的前numRows行

点击看答案

按照描述的方法,暴力解就好

自己写的时候的问题:自己想的 for语句,依次生成前面的行

LeetCode

125、验证回文串,给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

点击看答案

自己写的时候的问题:自己写的感受还是使用双指针,效率高,转小写,然后去掉无关的符号这种操作效率太低。注意Character的方法 toLowerCase()、 isDigit() 和 isLetter() 就可以了

[LeetCode](https://leetcode-cn.com/problems/valid-palindrome/solution/java-da-dao-zhi-jian-6xing-by-rabbitzhao/)

268、缺失数字,给定一个包含 0, 1, 2, …, n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。

点击看答案
  1. 数组中所有数字异或,结果再与 0n 异或,得到的结果就是缺失的那个数字(因为相同的数字异或结果为0) 2. 0n 这些数字的和-数组所有数字的和 = 缺失的数字

自己写的时候的问题:自己写的时候,感觉使用 异或 操作简单方便

LeetCode

171、Excel 表序列号,给定一个Excel表格中的列名称,返回其相应的列序号。如:A -> 1,B -> 2,Z -> 26,AA -> 27,AB -> 28

点击看答案

我们常用的 10 进制数字,0~9 共 10 个数字。题目A-Z,26个字母,所以是26进制,故,AB= 26 + 2 = 28**

自己写的时候的问题:看做26进制的数字,从字符串尾部开始遍历,计算每个char所在位的 base ,要注意输入小写字母,所以要统一转为小写去比较

LeetCode

172、 阶乘后的零,给定一个整数 n,返回 n! 结果尾数中零的数量,你算法的时间复杂度应为 O(log n)

点击看答案

可以转换为n的质因数中5的个数

自己写的时候的问题:*自己写的时候,使用for循环(从 5 到 n,每次进5) 的方式来计算5的个数,结果超时,虽然放在 IntelliJ 中运行结果是对的。因此,应该使用更高效的方式, n /= 5; result += n; *

[LeetCode](https://leetcode-cn.com/problems/factorial-trailing-zeroes/solution/xiang-xi-tong-su-de-si-lu-fen-xi-by-windliang-3/)

189、旋转数组,给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

点击看答案

首先能想到的是:新建一个数组,然后将原数组中的数字按照旋转后的顺序放到新数组 ,其次就是:首先将整个数组旋转,再分别旋转前面部分和后面部分

自己写的时候的问题:写了3次,旋转3次是没问题,关键是处理好 k > nums.length 的情况。

LeetCode

190、(还未写)颠倒二进制位,颠倒给定的 32 位无符号整数的二进制位。

点击看答案

每次取1位反转

LeetCode

191、位1的个数,编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数

点击看答案

n&(n-1) 或者 一直往右移

自己写的时候的问题:无符号数有点难,还么有完全理解。while 条件写成 n > 0 ,应该要写成 n != 0。

LeetCode

198、打家劫舍,小偷计划偷窃沿街的房屋。每间房内都藏有一定的现金,但如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额

点击看答案

奇偶,然后选较大

自己写的时候的问题:求偶数和的时候,先计算当前偶数和,再与奇数和比较,计算当前偶数和的最终值

LeetCode

202、快乐数,判断一个数是不是快乐数。一个“快乐数”定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。

点击看答案

快慢指针用来终止无限循环

自己写的时候的问题:主要是要细心,自己写的时候写成 result = temp * temp ,应该是 result += temp * temp

LeetCode

204、计数质数,统计所有小于非负整数 n 的质数的数量。(质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。)

点击看答案

质数的判断:for (int i = 2; i * i <= n; i++)

自己写的时候的问题:最优解暂时有点难理解,还是先只要注意 i * i <= n 这样的优化方式吧

LeetCode

234、回文链表,判断链表是否是回文链表

点击看答案

1、反转链表,对比反转后和原链表是否相同 2、链表值复制到数组中,再采用前后双指针

自己写的时候的问题:写得很完美,第一步,快慢指针找到中间节点 第二步、反转中间节点之后的部分 第三步、对比前半部分和反转后的后半部分

LeetCode

326、3的幂,给定一个整数,写一个函数来判断它是否是 3 的幂次方。

点击看答案

1、不断求余,不断除以3,看结果 2、转换成3进制,看是不是前面1个1,后面全是0

自己写的时候的问题:老老实实一直除以 3 ,但是,要注意数字 0 ,否则 n % 3 == 0 会进入死循环

LeetCode

242、有效的字母异位词,给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

点击看答案

大小26的数组,两个字符串遍历每个字符,s遍历就在相应下标+1,而遍历t时在相应下标-1

自己写的时候的问题:只有小写字母,所以用数组实现了,不难

LeetCode

注: 以下题目都是根据 LeetCode App上的顺序,按照由易到难排列

237、删除链表中的节点

编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,函数将只会传给你要被删除的节点(坑在这里,注意:传给你的不是head节点和要删除的值,而是直接传给你要删除的那个节点)。

点击看答案

仅仅只是删除而已,简直是脑筋急转弯,被坑进去了

自己写的时候的问题:没啥,主要是审题

LeetCode

344、反转字符串

点击看答案

首尾双指针

自己写的时候的问题:尴尬,忘了 i++,j– 了

LeetCode

7、整数反转

点击看答案

每次反转一个数字,注意防止溢出(可以考虑result使用long类型)

自己写的时候的问题:要注意负数的最小值,和整数最大值一样处理

LeetCode

9、回文数,判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。你能不将整数转为字符串来解决这个问题吗?

点击看答案反转后一半数字

自己写的时候的问题:*通过不断除以10,搞清楚这个值到底是什么数量级,比如99,那 div 就是10级,比如 999,那 div 就是 100级,然后通过 n/div 就能得到第一位, n%10,得到最后一位,比较他们的值。依次往复 *

LeetCode

14、(还未做)最长公共前缀

点击看答案

直接看链接,这个不大会

LeetCode

88、合并两个有序数组nums1和nums2,将nums2合并到nums1中,使nums1成为一个有序数组(假设nums有足够空间)。

点击看答案

从后往前,注意 nums1 中剩余的元素

自己写的时候的问题:没问题,从尾到头合并即可

LeetCode

20、有效的括号:给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效(即括号的开闭符合规则)。

点击看答案

使用HashMap 建立映射关系,Stack 用于遍历

自己写的时候的问题:记住 Stack 的 isEmpty() 、pop 、push 方法即可

LeetCode

21、合并两个有序链表,将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的(假设链表元素顺序由小到大)

点击看答案

双指针,注意其中一个链表到表尾时,另一个链表整个接上去

LeetCode

292、Nim 游戏,桌上一堆石头,两人轮流拿1~3块石头,拿到最后一块石头的人获胜。现在给n块石头,你先拿,判定你是否能赢得游戏。你们是聪明人,每一步都是最优解。

点击看答案

当 n%4 != 0 时,就可以赢

LeetCode

26、删除排序数组中的重复项,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。你不需要考虑数组中超出新长度后面的元素。

点击看答案

后一个元素与当前重复,则直到不重复的元素,移过来,覆盖重复元素。

LeetCode

53、最大子序和

点击看答案

前面的和是否是正数,是正数就有增益

LeetCode

70、爬楼梯,需要爬n阶才能到楼顶,每次可以爬1阶或者2阶,有多少种方法爬到楼顶?

点击看答案

斐波那契数列

LeetCode

104、二叉树的最大深度

点击看答案

记住递归方法,好用的递归的方法

LeetCode

121、买卖股票的最佳时机,给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

点击看答案

遍历,记录当前最小的值,以及之后相对应的最大利润

LeetCode

122、买卖股票的最佳时机 II,给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票,注意你不能在买入股票前卖出股票)

点击看答案

简单的一次遍历,相邻依次减下去

LeetCode

136、只出现一次的数字,一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素

点击看答案

所有元素异或

LeetCode

141、环形链表,给定一个链表,判断链表中是否有环

点击看答案

快慢指针,并且快指针每次走两步,慢指针每次走一步

LeetCode

557、反转字符串中的单词 III

输入: “Let’s take LeetCode contest”
输出: “s’teL ekat edoCteeL tsetnoc”

点击看答案

使用辅助栈

LeetCode

155、最小栈,设计一个支持push、pop、top操作,并且能在常数时间内检索到最小元素的栈

点击看答案

两个栈,一个正常存放元素,一个不同步存放小元素

LeetCode

160、相交链表,编写一个程序,找到两个单链表相交的起始节点

点击看答案

两个指针分别遍历两个链表,到尾结点后切换到另一个链表的头结点,两个指针相交的地方就是第一个相交的节点

LeetCode

169、多数元素,给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素

点击看答案

排序,中间那个元素就是

LeetCode

206、反转链表

点击看答案

pre节点和current 节点

LeetCode

217、存在重复元素,给定一个整数数组,判断是否存在重复元素。

点击看答案

HashSet

LeetCode

231、2的幂,给定一个整数,编写一个函数来判断它是否是 2 的幂次方。

点击看答案

二进制数据中只有1个1

LeetCode

二叉搜索树的最近公共祖先,给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

点击看答案

直接看链接

LeetCode

二叉查找树(也称二叉搜索树)

  • 左子树上的节点值都小于等于根节点的值
  • 右子树上的节点值都大于等于根节点的值
  • 左右子树也是二叉搜索树

它是基于二分查找的思想,查找最大的次数为二叉树高度

查找代价

  • 当左右子树高度大致平衡时,时间复杂度在 O(logN)
  • 当先后插入的关键字有序时,退化成链表,查找的时间复杂度就在 O(N)了

插入代价

新节点插入到树的叶子节点上,因此,插入节点和查找一个不存在的数据的代价相同

删除代价

  • 如果被删除的节点左、右 有一个为null时,代价仅为 O(1)
  • 如果左右子树都存在,时间复杂度最大也不会超过O(logN)

缺陷:

极端情况可能退化成链表,时间复杂度为 n。这主要是由于树不平衡导致的

平衡二叉查找树(平衡二叉搜索树)

是严格的平衡二叉树,它是空树或者左右两个子树的高度差 小于等于1,同时,左右两个子树也是平衡二叉搜索树

查找代价

时间很稳定,查找效率最好最坏都是 O(logN)

插入代价

由于要保证严格的平衡,插入时可能要进行再平衡(最多旋转一次),因此插入的整体代价还在 O(logN)

删除代价

和插入一样,要考虑再平衡,但是最多需要O(logN)次旋转,所以时间复杂度为 O(2logN)

红黑树(Red-Black Tree)

它并不严格地平衡,最长路径长度不超过最短路径长度的2倍。它删除和插入引起平衡性改变的概率要远低于平衡二叉搜索树

查找代价

查找代价基本上维持在 O(logN) 级别,最差情况下肯定比平衡二叉搜索树要差,因为没有那么平衡

插入代价

不容易引起失衡,整体代价和平衡二叉搜索树差不多,也是 O(logN) 级别(虽然涉及变色,但是变色的代价很小)

删除代价

相对平衡二叉搜索树,不容易引起失衡,时间复杂度也在 O(logN) 级别

补充

平衡二叉搜索树由于插入和删除,会引起需要调整,可以通过 :变色、左旋转、右旋转 三种方式调整。是否需要调整要根据红黑树的特性:

  • 节点是红色或黑色
  • 根节点是黑色
  • 叶子节点都是黑色的空节点
  • 红色节点的两个子节点都是黑的(红节点不能连续出现)
  • 任一点到每个叶子节点的路径包含相同数目的黑节点

以上内容参考自zhihucsdn知乎

B-树和B+ 树

我们所谓的B-树,其实并不是B减树,中间是横线,不是减号;B + 就是 B加树了

如 os 的文件目录存储、数据库中的索引结构的存储,不可能在内存中建立查找结构,必须在磁盘中建立好结构。

在磁盘组织结构下,从任何一个节点指向其他节点都可能读取一次磁盘,再将数据写入内存比较。这回带来大量的IO操作,所以我们需要新的数据结构,即 B树和B+树。

B树是一种多路平衡查找树,每个节点最多包含k个孩子,k称为B树的阶。K大小取决于磁盘页的大小

一些算法上的概念

B树

理解平衡二叉树后,就会更好理解后续的B树。

平衡二叉树

平衡二叉树是一棵二叉树,每个节点的左边节点小于当前节点的值,右边节点的值大于当前节点的值。

因为二叉树的遍历性能和树的层级成反比,层级h越小查询越快,为了保证树的结构左右两端数据大致平衡以降低二叉树高度,一般会采用算法机制实现节点的平衡,如下图所示:

平衡树的高度

B 树

当上述的平衡二叉树深度很深无法存储所有的节点数据时,需要读取磁盘。从而树的深度越大,需要的I/O操作次数越多,因此效率也越低,因此我们需要想办法降低树的高度。

B 树的思路和平衡二叉树一样,但是采用了多叉的方式降低了高度。

B树的具体实现比较难描述,看下面的参考链接更清晰,就不赘述。

以上内容参考自: 勤劳的小手B树概念

字符串匹配

主要是利用KMP算法,这个很令人头大,这里贴出算法代码,如果需要详细了解,可以去查看July大神的这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static int kmp(String str, String dest,int[] next){//str文本串  dest 模式串
for(int i = 0, j = 0; i < str.length(); i++){
while(j > 0 && str.charAt(i) != dest.charAt(j)){
j = next[j - 1];
}
if(str.charAt(i) == dest.charAt(j)){
j++;
}
if(j == dest.length()){
return i-j+1;
}
}
return 0;
}
public static int[] kmpnext(String dest){
int[] next = new int[dest.length()];
next[0] = 0;
for(int i = 1,j = 0; i < dest.length(); i++){
while(j > 0 && dest.charAt(j) != dest.charAt(i)){
j = next[j - 1];
}
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}

next数组的计算主要跟模式串有关,与文本串并没有关系,因为,模式串前后公共最长子序列。这样才会让我们跳过大量的重复计算

数字排列

题目:用1,2,2,3,4,5 这6个数字,写一个方法,打印出所有不同的排列,如 512234、412235等,要求4不能再第三位,3与5不能相连。

思路:问题可以归结为图的遍历,实际上6个数字就是6个结点,把6个结点连成无向连通图,对于每个结点求这个图形的遍历路径,所有结点的遍历路径就是最后对这6个数字的排列组合结果集。当然,这样获取的结果集未达到题目要求:
(1)3与5不能相连,这个可以在构造图的时候就满足条件;
(2)不能重复,有两个2,明显会存在重复结果,得最后去重(可以放在treeSet中);
(3)4不能排在第三位,这个仍旧在结果集中排除即可。

具体代码略。

手写算法题。猫扑素数;1到n,求1的个数;单词反转。

算法:排序、二叉树遍历、红黑树了解。常见的数据结构主要有数组、链表、栈、队列、二叉堆、树、图等,红黑树和BL树的区别

快速排序底层原理简单描述。

背景

在项目中,用户登录后,需要为白名单中的 host 注入特定的cookies,在用户退出登录的时候,需要将注入的 cookies 清除(只清除app自己注入的cookies,h5 注入的cookies不要清除)。

实现

Android 的webview 并没有提供针对单个host 清除cookies的方法,只有全部清除,因此主要思路是将需要删除的cookies 设置为过期,再删除过期的即可,具体可以参考Android清除单个域名的cookie

当然,设置cookies过期有两种方法:

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

使用Cookie的: Expires 属性. 它可以设置cookie的过期时间. 下面的代码表示id这条cookie的过期时间是2015年10月21日早上7点28分;

Set-Cookie: id=a3fWa; Max-Age=86400

使用Cookie的: Max-Age 属性. 它可以指定从现在开始Cookie存在的秒数. 秒数过完则cookie过期

参考 aisowe

问题

但是在清理的时候,发现总是不能清除掉指定cookie,打印出来发现:同一个host,比如 baidu.com ,它的cookie里面有2个key是一样的,但是值不一样,清理的时候,指定host 为baidu.com ,但是只能清除一个,另一个怎么也清理不掉。

这就很奇怪了,能想到的就是可能在多个不同的host下都注入了这个key,于是想办法要把它存储cookies的文件取出来看下。

我们知道,cookies 文件存储在 /data/data//app_webview,文件名是Cookies ,但是这个路径是一个app内部空间,普通情况下是没办法将这个文件获取出来的,除非是在虚拟机或者root过后,安装特定的文件管理器。

解决方案

不过我们安装Debug 的包的情况下,可能会有解决办法。因为 PackageManager 会检查 AndroidManifest.xml 中 isDebuggable 是不是false,这个值在我们打包的时候会根据你是打release包还是debug包而是不同的值,debug情况下是 isDebuggable = true,release 情况下是 true。

这样,我们将手机通过Android Studio 安装上debug包,进入shell:

adb shell

进入app 空间:

run-as

这样就能进入到app的内部空间,当然,也可以参考不root情况下读取data数据 ,此时我们再通过命令:

cd app_webview/

就能进到存放cookies的目录了,通过 ls 命令就能看到 Cookies 这个文件,这个文件是个数据库文件,可以使用sqlite3 打开。此时我们不能直接将其copy到我们电脑上,只能将其复制到我们的 sdcard 中:

cp Cookies /sdcard

此时,Cookies 文件已经被赋值到 sdcard,通过两次 exit 命令,就能退出 shell 模式,再通过 adb pull 命令,就能将其复制到电脑上了。

数据分析

在命令行打开Cookies 文件:

sqlite3 /desktop/Cookies
.dump cookies

就能看到cookies的数据了,这时候才发现,原来两个host都注入了这个key的cookie,一个是 baidu.com 一个是 .baidu.com,而后者是h5页面自己注入的,接下来问题就很明了了,将需要清除的cookies 按照前面的方法添加过期时间就ok,再手动执行删除过期cookies的操作,任务就完成了。

该篇文章旨在记录问题解决过程,详细过程这里略过。

热修复框架的核心技术主要有3类:代码修复、资源修复和动态链接库修复。

资源修复

很多热修复的框架的资源修复参考了Instant Run的资源修复原理,因此我们首先了解下Instant Run 的原理。Instant Run 的资源修复核心逻辑在MonkeyPatcher 的monkeyPatchExistingResources 方法中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//com/android/tools/fd/runtime/MonkeyPatcher.java

public static void monkeyPatchExistingResources(Context context,String externalResourceFile,Collection<Activity> activityes){

...
try{
//创建一个新的 AssetManager
AssetManager newAssetManager = (newAssetManager)newAssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);//1

Method mAddAssetPath = AssetManager.class.getDeclareMethod("addAssetPath",new Class[] {String.class});//2

mAddAssetPath.setAccessible(true);
//通过反射调用 addAssetPath 方法加载外部的资源( SD 卡)
if (((Integer) mAddAssetPath.invoke(newAssetManager,new Object[]{ externalResourceFile})).intValue () == 0) {//3
throw new IllegalStateException("Could not create new AssetManager");

if{activities != null){
for (Activity activity : activities) {
Resources resources= activity.getResources() ;//4
try {
//反射得到 Resources 的 AssetManager 类型的 mAssets 字段
Field mAssets = Resources.class.getDeclareField("mAssets");//5
mAssets.setAccessible(true );

//将 mAssets 字段的引用替换为新创建的 AssetManager
mAssets.set(resources,newAssetManager) ;//6

//得到 Activity 的 Resources.Theme
Resources.Theme theme = activity.getTheme();
...
//反射得到 Resources.Theme 的 mAssets 字段
Field ma = Resources . Theme.class.getDeclaredField (” mAssets " ) ;
ma.setAccessible(true);

//将 Resources.Theme 的 mAssets 字段替换为 newAssetManager
ma.set(theme,newAssetManager);//7
} catch (Throwable ignore) {

}
}
}

可以看出,在注释1处创建了一个新的AssetManager,之后通过反射调用 addAssetPath 方法加载外部(SD卡)的资源。在注释4处遍历Activity 列表,得到每个Activity 的Resource ,在5处通过反射得到Resources 的AssetManager 类型的mAsset字段,并在注释6处改写mAssets字段的引用为新的 AssetManager。之后,将AssetManager 类型的mAssets 字段的引用全部替换为新创建的 AssetManager。所以,总共就是两个步骤:

  • 创建新的 AssetManager ,通过反射调用 addAssetPath 方法加载外部的资源,这样
    新创建的 AssetManager 就含有了外部资源。
  • 将 AssetManager 类型的 rnAssets 字段的引用全部替换为新创建的 AssetManager。

代码修复

先写到这,后续有空再来。。。

Context 的关联类

开发中经常使用的Context 的使用场景大体分为两类:

  • 使用Context 调用方法,比如启动 Activity、访问资源、调用系统服务等。
  • 调用方法时传入,比如弹出 Toast、创建dialog。

Activity、Service 与 Application 都间接继承 Context,因此可以说一个应用进程的Context 数量 = Activity 数量 + Service 数量 + 1,这个1就是Application数量。Context 的关联类的关系如下图所示:

Context关联类关系

可以看出,ContextWrapper 中包含有 Context 类型的 mBase 对象,mBase 具体指向 ContextImpl,此外,ContextThemeWrapper、Service 和 Application 都继承自 ContextWrapper,这样它们都能通过 mBase 来使用Context 的方法。同时它们也是装饰类,在 ContextWrapper 上又添加了不少功能。比如,ContextThemeWrapper 包含了主题相关的方法(getTheme之类),因此Activity 继承了ContextThemeWrapper,而Service 不需要主题,因此继承 ContextWrapper。Context 关联类的继承结构有以下优点:

  • 使用者能够方便使用Context 的功能。
  • 如果 ContextImpl 发生了变化,它的装饰类 ContextWrapper 无需做任何修改。
  • ContextImpl 的具体实现不会暴露给使用者。
  • 通过组合而不是继承,拓展 ContextImpl 的功能。运行时选择不同的装饰类,实现不同功能。

Application Context 的创建过程

我们通过 getApplicationContext 来获取应用程序的全局 Application Context,那么 Application Context 是如何创建的呢?在应用程序启动完成后,应用程序就有一个全局的 Application Context,那就从应用程序启动过程着手,Application Context 的创建过程时序图如下:

Application context 的创建时序图

应用程序进程的主线程管理类 ActivityThread 会调用内部类 ApplicationThread 的 scheduleLaunchActivity 方法来启动Activity,而在scheduleLaunchActivity 中会向 H 发送 LAUNCH_ACTIVITY 类型消息,目的是将启动Activity 的逻辑放在主线程中。在 H 的 handleMessage 方法中最终会调用到 LoadApk 类的 makeApplication 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//frameworks/base/core/java/android/app/LoadedApk.java

public Application makeApplication(boolean forceDefaultAppClass,Instrumentation nstrumentation) {
if (mApplication != null) {//l
return mApplication;
}
...
Application app = null;
String appClass = mApplicationinfo.className;
if (forceDefaultAppClass || (appClass == null) ) {
appClass = "android.app.Application”;
}
try {
java.lang.ClassLoader cl= getClassLoader();
if (!mPackageName.equals ("android")){
initializeJavaContextClassLoader ();
}

Contextimpl appContext = Contextimpl.createAppContext(mActivityThread,this); //2
app = mActivityThread.rninstrumentation.newApplication(cl, appClass, appContext);//3
appContext. setOUterContext(app) ;//4
}catch (Exception e){
...
}
...
mApplication = app;//5
...
return app;

}

注释1处,假设是第一次启动应用程序,因此 mApplication 为null,在注释2处通过 Contextimpl 的 createAppContext 方法创建 Contextimpl 的实例,注释3中创建了 Application 对象,注释4处将 Application 对象赋值给 Contextimpl 的成员变量 mOuterContext ,这样,ContextImpl 中也包含了 Application 的引用。注释5处的 mApplication 即 LoadedApk 的成员变量 mApplication。来看看注释 3 处Application 是如何创建的(最终调用到如下代码的方法):

1
2
3
4
5
6
7
//frameworks/base/core/java/android/app/lnstrumentation.java

static public Application newApplication(xxxx){
Application app = (Application) clazz.newinstance ();
app.attach(context) ; //l
return app ;
}

注释1处通过反射来创建Application,并调用其 attach 方法,并且将 ContextImpl 类型的对象传进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//frameworks/base/core/java/android/app/Application.java

final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = Contextimpl.getimpl(context).mPackageinfo;
}


protected void attachBaseContext(Context base) {
if(mBase != null ) {
throw new IllegalStateException ("Base context already set");
}
mBase = base;
}

最终,把一路传过来的 ContextImpl 类型的 base 赋值给 Application 的 mBase 。前面讲过,这个歌mBase 是ContextWrapper 的成员变量,因为Application 继承 ContextWrapper ,所以才有这个变量。因此,Application 的attach 方法的作用就是使 Application 可以使用 Context 的方法,这样,Application 才可以用来代表 Application Context。

Application Context 的获取过程

我们通过 getApplicationContext 来获取Application Context,这个方法在 ContextWrapper 中实现:

1
2
3
4
5
//frameworks/base/core/java/android/content/ContextWrapper.java

public Context getApplicationContext()(
return mBase.getApplicationContext( );
}

从前面我们可知,mBase 指的是 ComtextImpl,具体代码:

1
2
3
4
5
//frameworks/base/core/java/android/app/ContextImpl.java

public Context getApplicationContext () {
return (mPackageinfo != null ) ? mPackageinfo.getApplication() : mMainThread.getApplication();
}

如果 loadedApk 类型的mPackageinfo 不为 null,则调用其 getApplication 方法,否则调用 ActivityThread 的 getApplication 方法:

1
2
3
4
5
//frameworks/base/core/java/android/app/LoadedApk.java

Application getApplication() {
return mApplication ;
}

这个 mApplication 应该熟悉,是在前面提到的 LoadedApk 的 makeApplication 方法中注释 5 处被赋值的,是个Application 对象。就这样,我们获取到 Application Context。

Activity 的Context 创建过程

Activity的context创建过程时序图

应用程序进程的主线程管理类 ActivityThread 会调用内部类 ApplicationThread 的 scheduleLaunchActivity 方法来启动Activity,最终通过 H 类在主线程中处理启动事项,最终调用到 ActivityThread 的 performLaunchActivity 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//frameworks/base/core/java/android/app/ActivityThread.java

private Activity perfomLaunchActivity(ActivityClientRecord r , Intent customintent) {
...
ContextImpl appContext = createBaseContextForActivity(r);//l
Activity activity = null ;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(cl , component getClassName() , r.intent) ; //2
...
}catch(Exception e){
...
}

try {
...
if(activity != null) {
appContext.setOuterC nte t(activity) ; //3
//4
activity.attach(xxxx,xxxx);
...
}catch(Exception e){
...
}

在注释2处创建了Activity 的实例,注释1处通过 createBaseContextForActivity 方法创建 Activity 的 ContextImpl,并在注释4 处将 ContextImpl 对象传给activity 的attach方法,在注释3处调用了 ContextImpl 的 setOuterContext 方法,将 Activity 的实例赋值给 ContextImpl 的成员变量 mOuterContext ,这样,ContextImpl 也可以访问 Activity 的变量和方法。createBaseContextForActivity 方法中,最终也会调用 ContextWrapper 的 attachBaseContext ,将Activity 中的 ContextImpl 对象赋值给 ContextWrapper 的成员变量 mBase,这样,ContextWrapper 的功能就可以交由 ContextImpl 来处理。

Service 的 Context 创建过程

Service 的Context 创建过程与 Activity 的Context 创建过程类似,这里先略过,后续有时间再记录。

热修复和插件化是目前比较热门的技术,想要更好地掌握它们需要先了解ClassLoader。从第11章可知,DVM 和 ART 加载的是dex文件,JVM 加载的是class文件,因此它们的类加载器 ClassLoader 是肯定有区别的。

Java中的ClassLoader

虚拟机章节提到 类加载子系统,它的主要作用就是通过多种类加载器来查找和加载Class文件到Java虚拟机中。Java系统中的类加载器主要包括以下:

  • Bootstrap ClassLoader(引导类加载器):C/C++实现的,用于加载指定的JDK核心类库,比如 java.lang、java.uti等系统类。
  • Extensions ClassLoader(拓展类加载器): Java中的实现类为 ExtClassLoader ,用于加载Java的拓展类,主要包括 $JAVA_HOME/jre/lib/ext 、java.ext.dir 等目录。
  • Application ClassLoader(应用程序类加载器):Java中的实现类为 AppClassLoader,用来加载 1、当前程序的 Classpath 目录 ;2、系统属性 java.class.path指定的目录。

ClassLoader 继承关系

以下代码可以验证 运行一个Java程序需要用到哪些类加载器:

1
2
3
4
5
6
7
8
9
public class ClassLoaderTest {
public static void main(String[] args){
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while (loader != null){
System.out.println(loader);//1
loader = loader.getParent();
}
}
}

上述代码可以获得当前类 ClassLoaderTest 的类加载器,接着打印出当前类加载器的父加载器,直到没有父加载器,打印的结果如:

sun misc Launcher AppClassLoader@75b84c92
sun .misc .Launcher$ExtClassLoader@lb6d3586

可以看出,加载 ClassLoaderTest 的加载器是 AppClassLoader,并且AppClassLoader 的父加载器是 ExtClassLoader。但是这里没有打印出 ExtClassLoader 的父加载器 Bootstrap ClassLoader ,是因为Bootstrap ClassLoader 由 C/C++ 编写,并不是一个Java类,因此我们无法在Java代码中获取。

双亲委派模型

所谓的双亲委派模型就是首先判断该Class是否已经加载,如果未加载,则当前加载器委托父加载器进行查找,这样依次地柜,直到委托到最顶层的 Bootstrap ClassLoader,如果Bootstrap ClassLoader 找到了该Class,就直接返回,否则,依次向下查找,如果当前加载器之上的所有加载器都未能加载,则当前加载器自身去查找。

如果要加载一个位于D盘的Class文件,系统所提供的类加载器就不能满足,这时候需要自定义类加载器 CustomClassLoader 继承java.lang.ClassLoader 并覆写findClass方法,加载D盘的Class文件步骤如下:

  1. CustomClassLoader 首先从缓存中查找Class文件是否已经加载,已经加载就返回,没有加载就委托给父加载器(AppClassLoader)
  2. 按照双亲委派模型递归。
  3. 一直委托到 Bootstrap ClassLoader ,如果 Bootstrap ClassLoader 也没能加载,则交给子加载器(ExtClassLoader),以此类推。

综合以上,ClassLoader的父子关系不是使用继承来实现的,二是使用组合来实现代码复用。

双亲委派模型的好处:

  1. 避免重复加载。如果Class已经加载过,就不需要加载,二是直接读取。
  2. 更加安全。如果不使用双亲委派模型,就可以自定义一个String类来替代系统的String类,显然会造成安全隐患。或者自定义一个Object类,有可能会动摇java基础,因为java里面所有类都要继承java的Object(这段是我自己理解添加的)。采用双亲委派模型似的系统的类在Java虚拟机启动时就被加载,也就无法自定义系统类来替代系统。

自定义类加载器的代码如下:
自定义类加载器1
自定义类加载器2

Android 中的ClassLoader

ClassLoader 的类型

Android中系统类加载器也主要包括3种:

  • BootClassLoader: 由Java代码实现,类的访问修饰符是默认的,只有在同一个包中才能访问,用户无法直接调用。Android系统启动时,会使用BootClassLoader 预加载常用类。
  • DexClassLoader:可以加在dex文件以及包含dex的压缩文件(apk和jar),不管加载哪种文件,最终都加载dex文件。
  • PathClassLoader:Android使用它来加载系统类和应用程序的类,通常用来加载已经安装的apk的dex文件。

ClassLoader的继承关系

通过前面用于验证java类继承关系的代码,在这里同样可以验证Android中类加载器的继承关系。

ClassLoader 的加载过程

Android 的 ClassLoader 同样遵循了双亲委派模型,ClassLoader 的加载方法为 loadClass方法,这个方法定义在抽象类 ClassLoader中。ClassLoader的查找流程如下图所示:

ClassLoader的查找流程

BootClassLoader的创建

在ZygoteInit的main方法中,调用了Zygote的 preload 方法,preload方法中又调用了 ZygoteInit 的 preloadClasses 方法,preloadClasses用于预加载常用的类,这个预加载属于拿空间换时间的策略。在preloadClasses方法中会创建 BootClassLoader 。

根Activity的启动过程

根Activity 是应用程序第一个Activity,相比普通的Activity的启动过程,一般用根Activity 的启动过程来指代应用程序的启动过程,更具有参考意义。根Activity 的启动过程比较复杂,这里分为3个部分来讲:Launcher 请求AMS 过程、AMS 到ApplicaitonThread 的调用过程 以及 ActivityThread 启动Activity

Launcher 请求AMS 过程

当我们点击桌面上某个应用的快捷图标时,就会通过Launcher 请求AMS 来启动该应用程序,过程的时序图如下:

Launcher调用AMS启动Activity时序图

点击桌面图标,会调用 Launcher 的startActivitySafely方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//  packages/apps/Launcher3/src/com/android/launcher3/Launcher.java 

public boolean startActivitySafely(View v ,Intent intent, Itemlnfo item){
...
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //l
try{
if (xxxx){
...
}else if(user == null || user.equals(Process.myUserHandle())){
startActivity(intent, optsBundle); //2
}else {
...
}
return true;
} catch (ActivityNotFoundExceptionlSecurityException e) {
...
}
return false;
}

因为是启动新的应用,所以注释1处将根Activity 在新的任务栈启动,应用启动会执行到注释2处的startActivity 方法,最终会在Activity 中调用 startActivityForResult 方法:

1
2
3
4
5
6
7
8
9
//  frameworks/base/core/java/and oid/app/Activity.java

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
if (mParent == null) {//1
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar = minstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToke,this,intent, requestCode , option);
} else {
...
}

注释1的mParent 是Activity 类型,表示当前Activity 的父类(个人理解,这里应该说是当前Activity的前一个Activity),因此mParent == null 成立,最后由 Instrumentation 的execStartActivity方法来执行启动操作。 Instrumentation 主要用于监控应用程序和系统的交互。主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//  frameworks/base/core/java/android/app/Instrumentation.java

public ActivityResult execStartActivity(Context who , IBinder contextThread, IBinder token , Activity target, Intent intent ,int requestCode ,Bundle options){
...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who );
int result = ActivityManager.getService().startActivity(xxx,xxx,xxxx));
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;

由代码可知,首先通过 ActivityManager 获取 AMS 的代理对象,接着调用代理对象的 startActivity 方法。AMS 的代理对象是一个 IActivityManager(该类由AIDL在工具编译时自动生成的)对象,这个对象封装了 IBinder 类型的 AMS 的引用。通过一系列进程间通信,最终调用 AMS 的 startActivity 方法。

AMS 到 ApplicationThread 的调用过程

Launcher 请求进入AMS 后,接着是AMS 到 ApplicationThread 调用流程,时序图如下所示:

Launcher调用AMS启动Activity时序图

在AMS 的startActivity 会使用 startActivityAsUser 实现功能,并获取UserHandle.getCallingUserld() 即 调用者的UserId 作为参数传入。之后,startActivityAsUser 中会判断调用者进程是否被隔离,如果隔离则抛出SecurityException异常;接着,根据UserId 等参数检查调用者权限,如果没权限也抛出 SecurityException 异常。

AMS 中最终调用ActivityStater 的 startActivityLocked 方法,并且如果有 TaskRecord(代表启动的Activity所在的栈),则将其也作为参数传入;startActivityLocked 中会收集所有逻辑来决定如何将Intent 和Flags 转换为Activity(生成用于描述Activity 的 ActivityRecord 对象),并且将Activity 与Task 及 Stack 关联。

TaskRecord 用于描述一个 Activity 任务栈,Activity 任务栈其实是一个假想模型,并不真实存在。

最终调用到 ActivityStackSupervisor 的 startSpecificActivityLocked 方法时,会判断要启动的Activity 所在的应用程序进程是否已经运行,已经运行则调用 realStartActivityLocked 方法,并传入代表应用程序进程的 ProcessRecord。之后会调用 ApplicationThread 的 scheduleLaunchActivity 方法。当前代码逻辑运行在AMS所在进程(即SystemServer进程)中,通过 ApplicationThread 进程间通信,将程序执行到应用程序进程,ApplicationThread是AMS 进程与应用程序进程的通信桥梁,如下图所示:

AMS与应用程序进程通信

ActivityThread 启动Activity 的过程

由前面的知识可知,目前的代码逻辑已经运行到应用程序进程中,先查看下ActivityThread 启动Activity 的时序图:

ActivityThread启动Activity过程的时序图

ApplicationThread 是 ActivityThread 的内部类,前面讲过应用程序进程创建完成后,会运行代表主线程的实例 ActivityThread 。接着上一节的内容查看 ApplicationThread.scheduleLaunchActivity 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//  frameworks/base/core/java/android/app/ActivityThread.java

public final void scheduleLaunchActivity(xxx,xx,xxx) {//参数太多,这里省略了参数
updateProcessState(procState ,false);
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident =ident;
r.intent = intent;
r.referrer = referrer;
...
updatePendingCoηfiguration (curConfig);
sendMessage(H.LAUNCH_ACTIVITY ,r);
}

在把启动Activity 必要的数据封装成 ActivityClientRecord 后,通过 sendMessage 方法将封装的数据以 H.LAUNCH_ACTIVITY 类型发送了出去,这里可以大胆地猜测sendMessage方法是通过handler的 sendMessage 执行的,果不其然,sendMessage 有多个重载方法,最终调用到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
//  frameworks/base/core/java/android/app/ActivityThread.java

private void sendMessage(int what ,Object obj ,int argl ,int arg2 ,boolean async){
Message msg = Message obta ();
msg.what = what;
msg.ob]= obj;
msg.argl = argl;
msg.arg2 = arg2;
if (async) {
msg .setAsynchronous(true);
}
mH.sendMessage(msg);

这里的mH指的是 ActivityThread 的内部类 H,前面讲过,这个H是集成Handler,是应用进程中主线程的消息管理类,因为ApplicationThread 是一个Binder,它的调用逻辑都运行在Binder 线程池中,所以这里需要使用H将代码的逻辑切换到主线程中。这样一来,我们只需要看 H 的handleMessage 方法即可知道具体的执行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
private class H extends Handler {
...
public void handleMessage (Message msg ) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,"activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;//1
r.packageinfo = getPackageinfoNoCheck(r.activityinfo.applicationinfo, r.compatlnfo); //2
handleLaunchActivity(r,null ,"LAUNCH ACTIVITY"); //3
Trace.traceEnd (Trace . TRACE TAG ACTIVITY MANAGER);
break;
...
}

在注释1处将传过来的 msg 的成员变量 obj 还原成 ActivityClientRecord,注释2获得LoadApk 类型的对象。应用程序进程要启动Activity时需要将该Activity 所属的APK 加载进来,而LoadApk 就是用来描述已经加载的APK 文件的。注释3处调用 handleLaunchActivity 方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  frameworks/base/core/java/android/app/ActivityThread.java

private void handleLaunchActivity(ActivityClientRecord r ,Intent customintent, String reason) {
...
WindowManagerGlobal.initialize ();
//启动 Activity
Activity a = performLaunchActivity(r, customintent);//1
if(a != null){
...
//将 Activity 的状态置为 Resume
handleResumeActivity(r.token, false, r.isForward,!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);//2
}else{
...
//停止 Activity 启动
ActivityManager getServ ce () .finishActivity(r . token , Activity . RESULT CANCELED , null , Activity . DONT_FINISH_TASK_WITH_ACTIVITY) ;
}
}

注释1处performLaunchActivity 方法启动了 Activity,注释2处将Activity的状态设置为 Resume ,如果该Activity 为null,则会通知AMS 停止启动Activity。我们来看看 performLaunchActivity 方法做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//  frameworks/base/core/java/android/app/ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r , Intent customintent) {
//获取 Activityinfo
Activityinfo ainfo = r.activityinfo;//l
if (r.packageinfo == null) {
//获取 APK 文件的描述类 LoadedApk
r.packageinfo = getPackageinfo(ainfo.applicationinfo ,r.compatinfo,Context.CONTEXT_INCLUDE_CODE);//2
}
ComponentName component= r.intent.getComponent();//3
...
//创建要启动 Activity 的上下文环境
Contextlmpl appContext = createBaseContextForActivity(r) ; //4
Activity activity = null;
try {
java.lang .ClassLoader cl= appContext.getClassLoader();
//用类加载器来创建该 Activity 的实例
activity = mInstrumentation newActivity(cl ,component.getClassName() ,r.intent) ;//5
...
} catch (Exception e) {
...
}

try {
//创建 Application
Application app = r.packageinfo.makeApplication(false ,minstrumentation); //6
...
if (activity != null) {
//初始化Activity,参数太多,省略
activity.attach(....) ;
...
if (r.isPersistable()) {
minstrumentation.callActivityOnCreate(activity,r.state ,r.persistentState); //8
} else {
minstrumentat on callActivityOnCreate(activ ty r .state) ;
}
...
}
r.paused = true ;
mActivities.put(r.token,r);
} catch (SuperNotCalledException e) {
throw e ;
}catch (Exception e) {
...
}

return activity;

注释1处获取 Activityinfo,用于存储AndroidManifest 以及 代码中设置的Activity 和 Receiver 节点的信息,比如Activity 的theme 和launchMode 。注释3中获取要启动的Activity 的 ComponentName 对象,该对象中保存了该Activity 的包名和类名。注释4中启动了Activity的上下文,注释5根据 Activity 的类名,用类加载器创建该 Activity 的实例。之后,注释6中创建了Application ,并且会调用 Application 的 onCreate方法。注释7中调用 Activity 的attach 方法初始化Activity,并且创建Window 对象(PhoneWindow)与Activity 自身关联。注释8中正式启动Activity,并调用Activity 的onCreate 方法。

至此,根Activity 就启动了,即应用程序启动了。

根Activity 启动过程中涉及的进程

根Activity 启动过程中涉及的4个进程之间关系如下:

根Activity启动过程中涉及的进程关系

首先Launcher 进程向 AMS 请求创建根 Activity ,AMS 会判断根Activity 所需要的应用程序进程是否存在,不存在就请求 Zygote 进程创建应用程序进程;之后,AMS 请求创建根Activity。上图中步骤 2 采用Socket 通信,步骤 1 和4采用Binder 通信。

读完书本,虽然各个点清晰,但是未能完整总结,以下 桌面点击图标 启动流程总结参考自他人博客

  1. 点击桌面图标,Launcher 采用Binder IPC 方式向system_server 发起startActivity 请求。
  2. system_server 进程接收到请求后,向 zygote 进程发送创建进程请求。
  3. zygote 进程fork 出新进程,即App进程。
  4. App 进程通过 Binder IPC 向 system_server 发起 attachApplication 请求。
  5. system_server收到请求做一系列准备后,通过 Binder IPC 向App 进程发送 scheduleLauncherActivity请求。
  6. App 进程的Binder 线程(ApplicationThread)收到请求后,通过Handler 向主线程发送 LAUNCH_ACTIVITY 消息。
  7. 主线程收到Message 后,通过反射机制创建目标Activity ,并回调Activity.onCreate 等方法。
  8. 至此,App启动,开始Activity 生命周期。

这个过程示意图如下所示:

Activity启动流程图

Service 启动过程

Service 的启动过程和根Activity 的启动过程有部分相似知识点。Service 的启动过程可以分为两个部分讲解:分别是ContextImpl 到ActivityManageService 的调用过程,以及 ActivityThread 启动 Service。

ContextImpl 到 AMS 的调用过程

首先上时序图:

ContextImpl到AMS的调用过程

调用startService方法启动service,这个方法在 ContextWrapper 中实现