0%

继承图

Collections

Map

概述

Map类型 插入是否有序 顺序特点
HashMap 无序 -
LinkedHashMap 有序 记录插入顺序
TreeMap 有序 默认升序

结构

HashMap

Map结构

  • HashMap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null。在JDK1.8中,链表⻓度⼤于8的时候,链表会转成红⿊树。
  • DEFAULT_INITIAL_CAPACITY: Table数组的初始化长度 = 16。
  • DEFAULT_LOAD_FACTOR: 负载因子默认值为 0.75。当 元素的总个数>当前数组的长度 * 负载因子 时数组会扩容为原来的两倍。
  • TREEIFY_THRESHOLD: 链表树化阙值默认值为 8 。表示在一个node(Table)节点下的值的个数大于8时候,会将链表转换成为红黑树。
  • UNTREEIFY_THRESHOLD: 红黑树链化阙值:默认值为 6 。表示在进行扩容期间,单个Node节点下的红黑树节点的个数小于6时候,会将红黑树转化成为链表。
  • MIN_TREEIFY_CAPACITY: 64 最小树化阈值,当Table所有元素超过该值时才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)。
  • HashMap中存储的是泛型,因此不能是基础类型,可能会产生自动装箱的消耗,创建新的装箱对象。

为什么是2次幂的初始容量,为什么扩容也是2倍?

因为计算下标时采用的是 (n - 1) & hash 方式,位运算特别高效,按位与 & 的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当 HashMap 的容量是 2 的 n 次幂时,(n-1) 的 2 进制也就是 1111111***111 这样形式的,这样与添加元素的 hash 值进行位运算时,能够充分的散列,使得添加的元素均匀分布在 HashMap 的每个位置上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!

为什么使用链表+数组?⽤ LinkedList/ArrayList 代替数组结构可以吗?

在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。数组的查找效率比 LinkedList 大,所以使用数组。另外 ArrayList 底层虽然也是数组,但它是 1.5 倍扩容机制。

put方法

  1. 对 key 做 hash 运算,计算 index;
  2. 如果没碰撞直接放到数组⾥;如果碰撞了则以链表的形式存在数组后;
  3. 如果碰撞导致链表过⻓(⼤于等于TREEIFY_THRESHOLD),就把链表转换成红⿊树(JDK1.8中的改动);
  4. 如果节点已经存在就替换 old value(保证key的唯⼀性);
  5. 如果达到了扩容条件则扩容。

resise方法

HashMap 的扩容实现机制是将原 table 数组中所有的 Entry 取出来,重新对其 Hashcode 做 Hash 散列到扩容后新的 Table 中。

get方法

  1. 对 key 做 hash 运算,计算 index;
  2. 如果与数组⾥的第⼀个节点直接命中,则直接返回;
  3. 如果有冲突,则去查找对应的 Entry;若为树则为 O(logn);若为链表则为 O(n)。

ArrayMap

ArrayMap是一个通用的key-value映射数据结构,它相比HashMap会占用更少的内存空间。

数据结构

在ArrayMap内部有两个比较重要的数组,一个是mHashes,另一个是mArray。

  • mHashes用来存放key的hashcode值
  • mArray用来存储key与value的值,它是一个Object数组。依旧存在自动装箱的消耗。

其中这两个数组的索引对应关系是:

1
2
3
4
5
6
int[] mHashes;
Object[] mArray;

mHashes[index] = hash;
mArray[index<<1] = key; //等同于 mArray[index * 2] = key;
mArray[(index<<1)+1] = value; //等同于 mArray[index * 2 + 1] = value;

注:向左移一位的效率要比 乘以2倍 高一些。

查找数据

查找数据分成两步:

  • 根据key的hashcode找到在mHashes数组中的索引值
  • 根据上一步的索引值去查找key所对应的value值

mHashes是一个有序数组,查找元素的时候使用的是二分查找。

插入数据

  1. 新数据位置确定
    • 根据key的hashcode在mHashes表中二分查找确定合适的位置。
    • 如果新添加的数据的索引不是最后位置,在需要对这个索引之后的全部数据向后移动
  2. 当key为null时,其实和其他正常的key差不多,只是对应的hashcode会默认成0来处理。
  3. 数组扩容问题
    • 首先数组的容量会扩充到BASE_SIZE
    • 如果BASE_SIZE无法容纳,则扩大到2 * BASE_SIZE
    • 如果2 * BASE_SIZE仍然无法容纳,则每次扩容为当前容量的1.5倍。

删除数据

  • 如果当前ArrayMap只有一项数据,则删除操作将mHashes,mArray置为空数组,mSize置为0.
  • 如果当前ArrayMap容量过大(大于BASE_SIZE*2)并且持有的数据量过小(不足1/3)则降低ArrayMap容量,减少内存占用
  • 如果不符合上面的情况,则从mHashes删除对应的值,将mArray中对应的索引置为null

ArrayMap的缓存优化

在元素的新增和删除过程中,会频繁出现多个容量为BASE_SIZE和2 * BASE_SIZE的int数组和Object数组(put方法增加数据,扩大容量;remove方法删除数据,减小容量)。ArrayMap设计者为了避免创建不必要的对象,减少GC的压力,采用了类似对象池的优化设计。

SparseArray

1
2
private int[] mKeys;
private Object[] mValues;

SparseArray 比 HashMap 更省内存,某些条件下性能更好,因为它避免了对key的自动装箱。ArrayMap 与 SparseArray 最大的一点不同就是 ArrayMap 的 key 可以为任意的类型,而 SparseAraay 的 key 只能是整型。它也对 key 使用二分法进行排序。

ConcurrentHashMap

  • 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

ConcurrentSkipListMap

  • 它是一个有序的Map,相当于TreeMap。
  • TreeMap采用红黑树实现排序,而ConcurrentHashMap采用跳表实现有序。

HashTable

HashTable与 HashMap类似,它继承自Dictionary类,不同的是:

  • 它不允许记录的键或者值为空。
  • 它支持线程的同步,实现线程安全的方式是在修改数据时锁住整个HashTable,因此也导致了 HashTable在写入时会比较慢。
  • 初始size为11,扩容:newsize = olesize*2+1

LinkedHashMap

LinkedHashMap 直接继承自 HashMap, 内部维护了一个 LinkedList 双向链表。LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录是先插入的,它还可以在此基础上再根据访问顺序(get,put)来排序,可以用来实现 Lru 算法。

在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和容量有关。

初始化

1
2
3
4
5
6
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder; // 是否按照访问顺序排序
}
}

LinkedHashMapEntry

1
2
3
4
5
6
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

LinkedHashMap 的 Entry 新增了 before 和 after 两个指针,before 在每次添加元素的时候将会指向上一次添加的元素,而上一次添加的元素的 after 指针将指向本次添加的元素,来形成双向链表。

put元素

LinkedHashMap 没有重写 put 方法,只是重写了 newNode 方法生成新的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMapEntry<K,V> p = new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}

transient LinkedHashMapEntry<K,V> head;
transient LinkedHashMapEntry<K,V> tail;

// 将 p 放到链表尾部
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
if (last == null) head = p;
else {
p.before = last;
last.after = p;
}
}

remove元素

LinkedHashMap 没有重写 remove 方法,只是重写了 afterNodeRemoval 方法处理删除的节点。

1
2
3
4
5
6
7
// 从双向链表中删除节点 e
void afterNodeRemoval(Node<K,V> e) {
LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null) head = a; else b.after = a;
if (a == null) tail = b; else a.before = b;
}

维护节点访问顺序

accessOrder 参数默认为 false, 当设置为 true 时, LinkedHashMap 会维护节点访问顺序。在 putVal/get/repalce 方法最后都调用了 afterNodeAccess 方法,LinkedHashMap 重写了这个方法:

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
// 将被访问节点移动到链表最后
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}

迭代器

LinkedHashMap 实现了自己的迭代器,其迭代是通过双向链表实现的。

1
2
3
4
5
6
7
8
9
10
11
12
abstract class LinkedHashIterator {
// ...

final LinkedHashMapEntry<K,V> nextNode() {
LinkedHashMapEntry<K,V> e = next;
if (modCount != expectedModCount) throw new ConcurrentModificationException();
if (e == null) throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
}

其 containsValue 方法也是通过遍历双向链表实现的:

1
2
3
4
5
6
7
8
public boolean containsValue(Object value) {
for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}

TreeMap

  • TreeMap实现SortMap接口,内部是红黑树,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历TreeMap时,得到的记录是排过序的。

比较

  • 一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap 是最好的选择。
  • TreeMap取出来的是排序后的键值对。但如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
  • LinkedHashMap是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。
  • HashMap : 内存占用较大,增、删的效率较高,改、查的效率一般
  • ThreadLocal.Values : 内存占用一般,当数据量比较小时,增删改查的效率高;数据量大时,增删改查效率一般
  • ArrayMap: 内存占用较小,改、查的效率高,增、删的效率较低
  • SparseArray : 内存占用较小,改、查的效率高,增、删的效率低,且主键是数字

List

  • 有序,可重复。

Vector

  • 底层维护一个Object数组。
  • 查询速度快,增删慢。
  • 线程安全,操作效率低。

ArrayList

  • 底层维护一个Object数组。
  • 查询速度快,增删慢。
  • 1.5 倍扩容。

LinkedList

LinkedList是List接口的实现类,因此它可以是一个集合,可以根据索引来随机访问集合中的元素。此外,它还是Duque接口的实现类,因此也可以作为一个双端队列,或者栈来使用。

  • LinkedList是一个双向链表.
  • 查询速度慢,增删快

CopyOnWriteArrayList

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}

由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:

  1. 如果写操作未完成,那么直接读取原数组的数据;
  2. 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
  3. 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

CopyOnWriteArrayList表达的一些思想:

  1. 读写分离,读和写分开
  2. 最终一致性
  3. 使用另外开辟空间的思路,来解决并发冲突

Stack

继承自Vector.

  • boolean empty():测试堆栈是否为空。
  • Object peek():查看堆栈顶部的对象,但不从堆栈中移除它。
  • Object pop():移除堆栈顶部的对象,并作为此函数的值返回该对象。
  • Object push(Object element):把项压入堆栈顶部。
  • int search(Object element):返回对象在堆栈中的位置,从栈顶往下,以1为基数。

Set

  • 无序,不可重复。

HashSet

HashSet 底层用的是 HashMap:

1
2
3
4
5
6
7
8
9
10
11
12
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
}

HashSet导致的内存泄漏:把一个对象存储进hashSet集合后,修改这个对象中参与计算hash的变量的值,这时这个对象的hash值也会随之改变,那么这么对象不可以正常地被删除。

TreeSet

TreeSet采用的数据结构是红黑树,我们可以让它按指定规则对其中的元素进行排序。它又是如何判断两个元素是否相同呢?除了用equals方法检查两个元素是否相同外,还要检查compareTo方法是否返回为0。所以如果对象要存放到Tree集合里,需要在重写compareTo时,把相同的对象的比较值定为0,防止相同的元素被重复添加进集合中。

ConcurrentSkipListSet

  • 它是一个有序的、线程安全的Set,相当于线程安全的TreeSet。
  • 它内部拥有ConcurrentSkipListMap实例,本质上就是一个ConcurrentSkipListMap,只不过仅使用了Map中的key。

Queue

  • void add(Object e):将指定元素插入到队列的尾部。

  • Object remove():获取队列头部的元素,并删除该元素。

  • object element():获取队列头部的元素,但是不删除该元素。

  • boolean offer(Object e):将指定的元素插入此队列的尾部。当使用容量有限的队列时,此方法通常比add(Object e)有效。 

  • Object poll():返回队列头部的元素,并删除该元素。如果队列为空,则返回null。

  • Object peek():返回队列头部的元素,但是不删除该元素。如果队列为空,则返回null。

Queue

1
Queue<T> queue = new LinkedList<T>();

PriorityQueue

PriorityQueue 默认是小根堆,容量没有界限,会在指定的初始容量基础上扩容,默认排序是自然排序,队头元素是最小元素。可以这样实现大根堆:

1
2
3
4
5
6
7
8
val queue = PriorityQueue<Int>(10) { o1, o2 -> o2.compareTo(o1) }
for (i in 0..9) {
queue.add(Random.nextInt(50))
}
println(queue)
for (i in 0..9) {
println("${queue.remove()} - $queue")
}

输出:

1
2
3
4
5
6
7
8
9
10
11
[47, 41, 28, 38, 25, 11, 19, 11, 23, 21]
47 - [41, 38, 28, 23, 25, 11, 19, 11, 21]
41 - [38, 25, 28, 23, 21, 11, 19, 11]
38 - [28, 25, 19, 23, 21, 11, 11]
28 - [25, 23, 19, 11, 21, 11]
25 - [23, 21, 19, 11, 11]
23 - [21, 11, 19, 11]
21 - [19, 11, 11]
19 - [11, 11]
11 - [11]
11 - []

Deque

Deque接口是Queue接口的子接口,它代表一个双端队列,Deque定义了一些方法:

  • void addFirst(Object e):   将指定元素添加到双端队列的头部。
  • void addLast(Object e):  将指定元素添加到双端队列的尾部。
  • Iteratord descendingItrator():  返回该双端队列对应的迭代器,该迭代器以逆向顺序来迭代队列中的元素。
  • Object getFirst():  获取但不删除双端队列的第一个元素。
  • Object getLast():  获取但不删除双端队列的最后一个元素。
  • boolean offFirst(Object e):  将指定元素添加到双端队列的头部。
  • boolean offLast(OBject e):  将指定元素添加到双端队列的尾部。
  • Object peekFirst():  获取但不删除双端队列的第一个元素;如果双端队列为空,则返回null。
  • Object PeekLast():  获取但不删除双端队列的最后一个元素;如果双端队列为空,则返回null。
  • Object pollFirst():  获取并删除双端队列的第一个元素;如果双端队列为空,则返回null。
  • Object pollLast():  获取并删除双端队列的最后一个元素;如果双端队列为空,则返回null。
  • Object pop()(栈方法):  pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。
  • void push(Object e)(栈方法):  将一个元素push进该双端队列所表示的栈的栈顶。相当于addFirst()。
  • Object removeFirst():  获取并删除该双端队列的第一个元素。
  • Object removeFirstOccurence(Object o):  删除该双端队列的第一次出现的元素o。
  • Object removeLast():  获取并删除该双端队列的最后一个元素o。
  • Object removeLastOccurence(Object o):  删除该双端队列的最后一次出现的元素o。

BlockingQueue

操作 可能报异常 返回布尔值 可能阻塞 设定等待时间
入队 add(e) offer(e) put(e) offer(e, timeout, unit)
出队 remove() poll() take() poll(timeout, unit)
查看 element() peek()

ArrayBlockingQueue

此队列创建时必须指定大小.

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

SynchronousQueue

SynchronousQueue是一个没有数据缓冲的BlockingQueue(队列只能存储一个元素),生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样,消费者移除数据操作必须等待生产者的插入。

ConcurrentLinkedQueue

ConcurrentLinkedQueue 是非阻塞队列,它是一个基于链接节点的、无界的、线程安全。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部是队列中时间最长的元素。队列的尾部是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许 null 元素。

使用非阻塞队列,虽然能即时返回结果(消费结果),但必须自行编码解决返回为空的情况处理(以及消费重试等问题)。

并发容器

CopyOnWrite容器

包括:CopyOnWriteArrayList和CopyOnWriteArraySet。

  • 适用于读操作远远多于写操作,并且数据量较小的情况。
  • 修改容器的代价是昂贵的,因此建议批量增加addAll、批量删除removeAll。

概述

  • 运行时注解:通过反射在运行时动态处理注解的逻辑
  • 编译时注解:通过注解处理器在编译期动态处理相关逻辑

使用代码自动生成,一是为了提高编码的效率,二是避免在运行期大量使用反射,通过在编译期利用反射生成辅助类和方法以供运行时使用。

阅读全文 »

gcc,make,CMake

  1. gcc是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。
  2. 程序只有一个源文件时,可以直接用gcc命令编译它。
  3. 当程序包含很多个源文件时,用gcc命令逐个去编译时,很容易混乱而且工作量大。
  4. make工具可以看成是一个智能的批处理工具,它本身并没有编译和链接的功能,而是用类似于批处理的方式:通过调用makefile文件中用户指定的命令来进行编译和链接的。
  5. makefile就像一首歌的乐谱,make工具就像指挥家,指挥家根据乐谱指挥整个乐团怎么样演奏,make工具就根据makefile中的命令进行编译和链接的。makefile命令中就包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。
  6. makefile在一些简单的工程中可以人工编写,但是当工程非常大的时候,手写makefile也是非常麻烦的,如果换了个平台makefile又要重新修改。
  7. cmake 是一个跨平台、开源的构建系统。它是一个集软件构建、测试、打包于一身的软件。它使用与平台和编译器独立的配置文件来对软件编译过程进行控制。cmake可以生成makefile文件,并且可以跨平台生成对应平台能用的makefile。
  8. cmake根据CMakeLists.txt文件去生成makefile。

CMakeLists.txt语法

常用命令

指定 cmake 的最小版本

1
cmake_minimum_required(VERSION 3.4.1)

这行命令是可选的,在有些情况下,如果 CMakeLists.txt 文件中使用了一些高版本 cmake 特有的一些命令的时候,就需要加上这样一行,提醒用户升级到该版本之后再执行 cmake。

设置项目名称

1
project(demo)

这个命令不是强制性的,但最好都加上。它会引入两个变量 demo_BINARY_DIR 和 demo_SOURCE_DIR,同时,cmake 自动定义了两个等价的变量 PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR。

设置编译类型

1
2
3
add_executable(demo demo.cpp) # 生成可执行文件
add_library(common STATIC util.cpp) # 生成静态库
add_library(common SHARED util.cpp) # 生成动态库或共享库

add_library 默认生成是静态库,通过以上命令生成文件名字,在 Linux 下是:

  • demo
  • libcommon.a
  • libcommon.so

在 Windows 下是:

  • demo.exe
  • common.lib
  • common.dll

指定编译包含的源文件

明确指定包含哪些源文件

1
add_library(demo demo.cpp test.cpp util.cpp)

搜索所有的 cpp 文件

aux_source_directory(dir VAR) 发现一个目录下所有的源代码文件并将列表存储在一个变量中。

1
2
aux_source_directory(. SRC_LIST) # 搜索当前目录下的所有.cpp文件
add_library(demo ${SRC_LIST})

自定义搜索规则

1
2
3
4
5
6
7
8
9
10
file(GLOB SRC_LIST "*.cpp" "protocol/*.cpp")
add_library(demo ${SRC_LIST})
# 或者
file(GLOB SRC_LIST "*.cpp")
file(GLOB SRC_PROTOCOL_LIST "protocol/*.cpp")
add_library(demo ${SRC_LIST} ${SRC_PROTOCOL_LIST})
# 或者
aux_source_directory(. SRC_LIST)
aux_source_directory(protocol SRC_PROTOCOL_LIST)
add_library(demo ${SRC_LIST} ${SRC_PROTOCOL_LIST})

查找指定的库文件

find_library(VAR name path)查找到指定的预编译库,并将它的路径存储在变量中。默认的搜索路径为 cmake 包含的系统库,因此如果是 NDK 的公共库只需要指定库的 name 即可。

1
2
3
4
5
6
find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log )

类似的命令还有 find_file()、find_path()、find_program()、find_package()。

设置包含的目录

1
2
3
4
5
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
)

Linux 下还可以通过如下方式设置包含的目录

1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_CURRENT_SOURCE_DIR}")

设置链接库搜索目录

1
2
3
link_directories(
${CMAKE_CURRENT_SOURCE_DIR}/libs
)

Linux 下还可以通过如下方式设置包含的目录

1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_CURRENT_SOURCE_DIR}/libs")

设置 target 需要链接的库

1
2
3
4
5
6
target_link_libraries( # 目标库
demo

# 目标库需要链接的库
# log-lib 是上面 find_library 指定的变量名
${log-lib} )

在 Windows 下,系统会根据链接库目录,搜索xxx.lib 文件,Linux 下会搜索 xxx.so 或者 xxx.a 文件,如果都存在会优先链接动态库(so 后缀)。

指定链接动态库或静态库

1
2
target_link_libraries(demo libface.a) # 链接libface.a
target_link_libraries(demo libface.so) # 链接libface.so

指定全路径

1
2
target_link_libraries(demo ${CMAKE_CURRENT_SOURCE_DIR}/libs/libface.a)
target_link_libraries(demo ${CMAKE_CURRENT_SOURCE_DIR}/libs/libface.so)

指定链接多个库

1
2
3
4
5
target_link_libraries(demo
${CMAKE_CURRENT_SOURCE_DIR}/libs/libface.a
boost_system.a
boost_thread
pthread)

设置变量

set 直接设置变量的值

1
2
set(SRC_LIST main.cpp test.cpp)
add_executable(demo ${SRC_LIST})

set 追加设置变量的值

1
2
3
set(SRC_LIST main.cpp)
set(SRC_LIST ${SRC_LIST} test.cpp)
add_executable(demo ${SRC_LIST})

list 追加或者删除变量的值

1
2
3
4
set(SRC_LIST main.cpp)
list(APPEND SRC_LIST test.cpp)
list(REMOVE_ITEM SRC_LIST main.cpp)
add_executable(demo ${SRC_LIST})

条件控制

if…elseif…else…endif

逻辑判断和比较:

  • if (expression):expression 不为空(0,N,NO,OFF,FALSE,NOTFOUND)时为真
  • if (not exp):与上面相反
  • if (var1 AND var2)
  • if (var1 OR var2)
  • if (COMMAND cmd):如果 cmd 确实是命令并可调用为真
  • if (EXISTS dir) if (EXISTS file):如果目录或文件存在为真
  • if (file1 IS_NEWER_THAN file2):当 file1 比 file2 新,或 file1/file2 中有一个不存在时为真,文件名需使用全路径
  • if (IS_DIRECTORY dir):当 dir 是目录时为真
  • if (DEFINED var):如果变量被定义为真
  • if (var MATCHES regex):给定的变量或者字符串能够匹配正则表达式 regex 时为真,此处 var 可以用 var 名,也可以用 ${var}
  • if (string MATCHES regex)

数字比较:

  • if (variable LESS number):LESS 小于
  • if (string LESS number)
  • if (variable GREATER number):GREATER 大于
  • if (string GREATER number)
  • if (variable EQUAL number):EQUAL 等于
  • if (string EQUAL number)

字母表顺序比较:

  • if (variable STRLESS string)
  • if (string STRLESS string)
  • if (variable STRGREATER string)
  • if (string STRGREATER string)
  • if (variable STREQUAL string)
  • if (string STREQUAL string)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if(MSVC)
set(LINK_LIBS common)
else()
set(boost_thread boost_log.a boost_system.a)
endif()
target_link_libraries(demo ${LINK_LIBS})
# 或者
if(UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fpermissive -g")
else()
add_definitions(-D_SCL_SECURE_NO_WARNINGS
D_CRT_SECURE_NO_WARNINGS
-D_WIN32_WINNT=0x601
-D_WINSOCK_DEPRECATED_NO_WARNINGS)
endif()

if(${CMAKE_BUILD_TYPE} MATCHES "debug")
...
else()
...
endif()

while…endwhile

1
2
3
while(condition)
...
endwhile()

foreach…endforeach

1
2
3
foreach(loop_var RANGE start stop [step])
...
endforeach(loop_var)

start 表示起始数,stop 表示终止数,step 表示步长,示例:

1
2
3
4
foreach(i RANGE 1 9 2)
message(${i})
endforeach(i)
# 输出:13579

打印信息

1
2
3
4
message(${PROJECT_SOURCE_DIR})
message("build with debug mode")
message(WARNING "this is warnning message")
message(FATAL_ERROR "this build has many error") # FATAL_ERROR 会导致编译失败

包含其它 cmake 文件

1
2
3
include(./common.cmake) # 指定包含文件的全路径
include(def) # 在搜索路径中搜索def.cmake文件
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # 设置include的搜索路径

常用变量

预定义变量

  • PROJECT_SOURCE_DIR:工程的根目录
  • PROJECT_BINARY_DIR:运行 cmake 命令的目录,通常是 ${PROJECT_SOURCE_DIR}/build
  • PROJECT_NAME:返回通过 project 命令定义的项目名称
  • CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 所在的路径
  • CMAKE_CURRENT_BINARY_DIR:target 编译目录
  • CMAKE_CURRENT_LIST_DIR:CMakeLists.txt 的完整路径
  • CMAKE_CURRENT_LIST_LINE:当前所在的行
  • CMAKE_MODULE_PATH:定义自己的 cmake 模块所在的路径,SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake),然后可以用INCLUDE命令来调用自己的模块
  • EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
  • LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置

环境变量

  • 使用环境变量: $ENV{Name}
  • 写入环境变量: set(ENV{Name} value)(这里没有“$”符号)

系统信息

  • CMAKE_MAJOR_VERSION:cmake 主版本号,比如 3.4.1 中的 3
  • CMAKE_MINOR_VERSION:cmake 次版本号,比如 3.4.1 中的 4
  • CMAKE_PATCH_VERSION:cmake 补丁等级,比如 3.4.1 中的 1
  • CMAKE_SYSTEM:系统名称,比如 Linux-­2.6.22
  • CMAKE_SYSTEM_NAME:不包含版本的系统名,比如 Linux
  • CMAKE_SYSTEM_VERSION:系统版本,比如 2.6.22
  • CMAKE_SYSTEM_PROCESSOR:处理器名称,比如 i686
  • UNIX:在所有的类 UNIX 平台下该值为 TRUE,包括 OS X 和 cygwin
  • WIN32:在所有的 win32 平台下该值为 TRUE,包括 cygwin

主要开关选项

  • BUILD_SHARED_LIBS:这个开关用来控制默认的库编译方式,如果不进行设置,使用 add_library 又没有指定库类型的情况下,默认编译生成的库都是静态库。如果 set(BUILD_SHARED_LIBS ON) 后,默认生成的为动态库
  • CMAKE_C_FLAGS:设置 C 编译选项,也可以通过指令 add_definitions() 添加
  • CMAKE_CXX_FLAGS:设置 C++ 编译选项,也可以通过指令 add_definitions() 添加
  • add_definitions(-DENABLE_DEBUG -DABC) 参数之间用空格分隔

项目示例

简单项目(单个源文件)

新建cpp文件main.cpp

新建CMakeList.txt

1
2
project(HELLO)
add_executable(hello main.c)

编译运行

一般采用 cmake 的 out-of-source 方式来构建(即生成的中间产物和源代码分离),这样做可以让生成的文件和源文件不会弄混,且目录结构看起来也会清晰明了。所以推荐使用这种方式,至于这个文件夹的命名并无限制,习惯命名为 build。在build目录下依次执行以下命令:

1
2
cd build
cmake ..

然后执行make命令即可生成可执行文件。

复杂项目

目录结构如下:

1
2
3
4
5
6
7
8
.
├── build
├── CMakeLists.txt
├── main.cpp
└── util
├── CMakeLists.txt
├── util.cpp
└── util.hpp

demo 根目录下的 CMakeLists.txt 文件如下:

1
2
3
4
5
6
7
8
9
cmake_minimum_required (VERSION 2.8)
project(demo)
aux_source_directory(. DIR_SRCS)
# 添加math子目录
add_subdirectory(util)
# 指定生成目标
add_executable(demo ${DIR_SRCS})
# 添加链接库
target_link_libraries(demo Util)

math 目录下的 CMakeLists.txt 文件如下:

1
2
3
aux_source_directory(. DIR_LIB_SRCS)
# 生成链接库
add_library(Util ${DIR_LIB_SRCS})

CI:持续集成(CONTINUOUS INTEGRATION)

CI的全称是Continuous Integration,表示持续集成。

在CI环境中,开发人员将会频繁地向主干提交代码。这些新提交的代码在最终合并到主干前,需要经过编译和自动化测试流进行验证。持续集成过程中很重视自动化测试验证结果,以保障所有的提交在合并主线之后的质量问题,对可能出现的一些问题进行预警。

CD:持续部署(CONTINUOUS DEPLOYMENT)

CD的全称是Continuous Deployment,表示持续部署。

在CD环境中,通过自动化的构建、测试和部署循环来快速交付高质量的产品。某种程度上代表了一个开发团队工程化的程度,任何修改通过了所有已有的工作流就会直接和客户见面,只有当一个修改在工作流中构建失败才能阻止它部署到产品线。

持续部署是一个很优秀的方式,可以加速与客户的反馈循环,但是会给团队带来压力,因为不再有“发布日”了。开发人员可以专注于构建软件,他们看到他们的修改在他们完成工作后几分钟就上线了。

基本上,当开发人员在主分支中合并一个提交时,这个分支将被构建、测试,如果一切顺利,则部署到生产环境中。

CD:持续交付(CONTINUOUS DELIVERY)

持续交付的英文全称是:Continuous delivery,缩写也是CD,它是一种软件工程手法。

它可以让软件产品的产出过程在一个短周期内完成,以保证软件可以稳定、持续的保持在随时可以释出的状况。它的目标在于让软件的建置、测试与释出变得更快以及更频繁。这种方式可以减少软件开发的成本与时间,减少风险。

有时候,持续交付也与持续部署混淆。持续部署意味着所有的变更都会被自动部署到生产环境中。持续交付意味着所有的变更都可以被部署到生产环境中,但是出于业务考虑,可以选择不部署。如果要实施持续部署,必须先实施持续交付。

DevOps

DevOps是Development和Operations的组合,是一种方法论,是一组过程、方法与系统的统称,用于促进应用开发、应用运维和质量保障(QA)部门之间的沟通、协作与整合。以期打破传统开发和运营之间的壁垒和鸿沟。

DevOps是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。通过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。具体来说,就是在软件交付和部署过程中提高沟通与协作的效率,旨在更快、更可靠的的发布更高质量的产品。

也就是说DevOps是一组过程和方法的统称,并不指代某一特定的软件工具或软件工具组合。各种工具软件或软件组合可以实现DevOps的概念方法。其本质是一整套的方法论,而不是指某种或某些工具集合,与软件开发中设计到的OOP、AOP、IOC(或DI)等类似,是一种理论或过程或方法的抽象或代称。

概述

模块

模块指的是独立的业务模块,比如直播模块会员模块等。

组件

组件指的是单一的功能组件,如登录组件上报组件等,每个组件都可以以一个单独的module开发,并且可以单独抽出来作为SDK对外发布使用。

模块组件间最明显的区别就是模块相对与组件来说粒度更大,一个模块中可能包含多个组件。并且两种方式的本质思想是一样的,都是为了代码重用和业务解耦。在划分的时候,模块化是业务导向,组件化是功能导向。

组件化

一个可用的组件化架构图从上向下分别为APP壳,业务层、组件层和基础层:

  • 基础层:基础层包含的是一些基础库以及对基础库的封装,比如常用的图片加载,网络请求,数据上报操作等等,其他模块或者组件甚至App应用都可以引用同一套基础库,因此这些基础库最好在另一个项目/git仓库中维护。在基础库上面可以再增加一个Common组件,它位于App应用所在的项目中,用来添加一些都需要引用到的依赖以及本App应用独有的共有方法,UI控件等。
  • 组件层:基础层往上是组件层,组件层包含一些功能组件,比如分享,登录,下载,播放等等。
  • 业务层:组件层往上是业务层,一个具体的业务模块会按需引用不同的组件,最终实现业务功能。
  • APP壳:在APP模块中根据需求统筹各个业务组件,最终输出为一个完整的应用。

组件单独调试

Android Gradle提供了三种插件,在开发中可以通过配置不同的插件来配置不同的工程。

  • App插件:com.android.application
  • Library插件:com.android.libraay
  • Test插件:com.android.test

可以在gradle.properties配置文件中声明一个boolean变量:isAlone,在build.gradle中通过判断这个变量来动态加载不同的插件,ApplicationId以及AndroidManifest文件:

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
<!-- main/manifest/AndroidManifest.xml 单独调试 -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hearing.share">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".ShareActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

<!-- main/AndroidManifest.xml 集成调试 -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hearing.share">

<application android:theme="@style/AppTheme">
<activity android:name=".ShareActivity"/>
</application>
</manifest>

在组件的build.gradle中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
android {
defaultConfig {
if (isAlone.toBoolean()) {
// 单独调试时添加 applicationId ,集成调试时移除
applicationId "com.hearing.login"
}
// ...
}

sourceSets {
main {
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (isAlone.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}

另外为了使组件化项目中的所有组件的compileSdkVersion、buildToolsVersion以及开源库版本等都能保持统一,也为了方便修改版本号,统一在Android工程根目录下的build.gradle中定义这些版本号,当然也可以在项目根目录添加一个单独的gradle文件定义这些常量,然后由根build.gradle引入。

组件混淆方案

组件化项目的代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。

组件Application

组件化开发中每一个组件可能都会自定义一个Application类,当所有组件要打包合并在一起的时候,由于程序只能有一个Application,组件中自己定义的Application无法使用。因此需要想办法在任何一个业务组件中都能获取到一个可用的全局Context,而且这个Context不管是在组件开发模式还是在集成开发模式都是生效的。

在基础库组件中封装了项目中用到的各种Base类(BaseActivity,BaseFragment等),这些基类中有一个BaseApplication类,它主要作用是使各个业务组件和app壳工程中都能访问到全局Context,且在其中可以存储一些全局对象:

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
object BaseApplication {
private val mGlobalObjects = mutableMapOf<Any, Any>()
private var mApplication: Application? = null
private var mContext: Context? = null

fun init(application: Application) {
mApplication = application
mContext = application.baseContext
}

fun getContext(): Context? {
return mContext ?: mApplication
}

fun getApplication(): Application? {
return mApplication
}

@Synchronized
fun putGlobalObject(key: Any, obj: Any) {
mGlobalObjects[key] = obj
}

@Synchronized
fun getGlobalObject(key: Any): Any? {
return mGlobalObjects[key]
}

@Synchronized
fun removeGlobalObject(key: Any): Any? {
return mGlobalObjects.remove(key)
}
}

然后可以在本应用的Common组件中定义一个公用的Application基类,然后在各自组件中定义一个Application继承这个基类,这样就可以通过BaseApplication来获取全局的Context了:

1
2
3
4
5
6
open class MyBaseApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
BaseApplication.init(this)
}
}

另外需要在项目从组件模式转换到集成模式后将组件的Application剔除出项目,可以在组件的Java文件夹下创建一个debug文件夹,用于存放不会在业务组件中引用的类,比如说Application类:

1
2
3
4
5
6
7
8
9
10
11
12
13
sourceSets {
main {
if (isAlone.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
// 集成开发模式下排除debug文件夹中的所有Java文件
java {
exclude 'debug/**'
}
}
}
}

组件间数据传递与方法调用

接下来模拟一个组件间数据传递和方法调用的场景:分享组件分享时需要根据用户是否登录来判断分享的动作。

base_component

base_component组件用来提供组件间数据传递和方法调用的功能。

ILoginService

1
2
3
4
interface ILoginService {
fun isLogin(): Boolean
fun getUserId(): String
}

空实现:EmptyLoginService

1
2
3
4
5
6
7
8
9
class EmptyLoginService : ILoginService {
override fun isLogin(): Boolean {
return false
}

override fun getUserId(): String {
return ""
}
}

Service工厂:ServiceFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
object ServiceFactory {
private val mServices = CopyOnWriteArrayList<Any>()

fun register(service: Any) {
if (!mServices.contains(service)) {
mServices.add(service)
}
}

private fun <T> getService(cls: Class<T>): Any? {
mServices.forEach {
if (cls.isInstance(it)) {
return it
}
}
return null
}

fun getLoginService(): ILoginService {
val service = getService(ILoginService::class.java)
return service as? ILoginService ?: EmptyLoginService()
}
}

component_login

登录组件中提供ILoginService的具体实现:

1
2
3
4
5
6
7
8
9
class LoginService : ILoginService {
override fun isLogin(): Boolean {
return true
}

override fun getUserId(): String {
return "1024"
}
}

component_share

分享组件中进行分享的逻辑判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object ShareHelper {
fun share() {
val loginService = ServiceFactory.getLoginService()
if (loginService.isLogin()) {
Toast.makeText(
BaseApplication.getContext(),
"Share from ${loginService.getUserId()}", Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
BaseApplication.getContext(),
"Not login", Toast.LENGTH_SHORT
).show()
}
}
}

app

在App主Module中进行ILoginService服务的注册:

1
2
3
4
5
6
class MainApplication : MyBaseApplication() {
override fun onCreate() {
super.onCreate()
ServiceFactory.register(LoginService())
}
}

然后根据业务逻辑调用分享组件的功能:

1
2
3
4
5
6
7
8
9
10
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.text_view).setOnClickListener {
ShareHelper.share()
}
}
}

组件间界面跳转

可以使用开源框架:ARouter或者CC等。

总结

以上组件化的内容是在参考了网络上一些关于Android组件化的解析后,结合了自己的理解与思考得到的一些想法,在实际场景里可能会有问题,期待在以后的开发中有更多的实践与思考!

Activity

生命周期

看一下以下操作:

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
// 程序启动
09-25 22:05:34.687 13204-13204/com.hearing.demo D/LLL: onCreate
09-25 22:05:34.718 13204-13204/com.hearing.demo D/LLL: MainActivity-onCreate
09-25 22:05:34.819 13204-13204/com.hearing.demo D/LLL: MainActivity-onStart
09-25 22:05:34.823 13204-13204/com.hearing.demo D/LLL: MainActivity-onResume
// 进入SecondActivity
09-25 22:05:53.802 13204-13204/com.hearing.demo D/LLL: MainActivity-onPause
09-25 22:05:53.815 13204-13204/com.hearing.demo D/LLL: SecondActivity-onCreate
09-25 22:05:53.822 13204-13204/com.hearing.demo D/LLL: SecondActivity-onStart
09-25 22:05:53.824 13204-13204/com.hearing.demo D/LLL: SecondActivity-onResume
09-25 22:05:54.156 13204-13204/com.hearing.demo D/LLL: MainActivity-onStop
// 按下back键
09-25 22:06:04.672 13204-13204/com.hearing.demo D/LLL: SecondActivity-onPause
09-25 22:06:04.716 13204-13204/com.hearing.demo D/LLL: MainActivity-onRestart
09-25 22:06:04.717 13204-13204/com.hearing.demo D/LLL: MainActivity-onStart
09-25 22:06:04.718 13204-13204/com.hearing.demo D/LLL: MainActivity-onResume
09-25 22:06:05.071 13204-13204/com.hearing.demo D/LLL: SecondActivity-onStop
09-25 22:06:05.071 13204-13204/com.hearing.demo D/LLL: SecondActivity-onDestroy
// 按下home键
09-25 22:06:14.703 13204-13204/com.hearing.demo D/LLL: MainActivity-onPause
09-25 22:06:14.722 13204-13204/com.hearing.demo D/LLL: MainActivity-onStop
09-25 22:06:14.723 13204-13204/com.hearing.demo D/LLL: onTrimMemory
// 返回应用
09-25 22:06:18.968 13204-13204/com.hearing.demo D/LLL: MainActivity-onRestart
09-25 22:06:18.970 13204-13204/com.hearing.demo D/LLL: MainActivity-onStart
09-25 22:06:18.970 13204-13204/com.hearing.demo D/LLL: MainActivity-onResume
// 按下back键
09-25 22:06:20.822 13204-13204/com.hearing.demo D/LLL: MainActivity-onPause
09-25 22:06:21.274 13204-13204/com.hearing.demo D/LLL: onTrimMemory
09-25 22:06:21.275 13204-13204/com.hearing.demo D/LLL: MainActivity-onStop
09-25 22:06:21.275 13204-13204/com.hearing.demo D/LLL: MainActivity-onDestroy
// 进入应用
09-25 22:06:39.925 13204-13204/com.hearing.demo D/LLL: MainActivity-onCreate
09-25 22:06:39.959 13204-13204/com.hearing.demo D/LLL: MainActivity-onStart
09-25 22:06:39.963 13204-13204/com.hearing.demo D/LLL: MainActivity-onResume
// 进入task
09-25 22:06:47.056 13204-13204/com.hearing.demo D/LLL: MainActivity-onPause
09-25 22:06:47.068 13204-13204/com.hearing.demo D/LLL: MainActivity-onStop
09-25 22:06:47.088 13204-13204/com.hearing.demo D/LLL: onTrimMemory
// 后台终止
09-25 22:06:51.310 13204-13204/com.hearing.demo D/LLL: MainActivity-onDestroy

各个生命周期的作用:

  • onCreate: Activity 已创建 状态,在 Activity 的整个生命周期中只发生一次,用来进行 Activity 的一些初始化工作。
  • onStart: 为 Activity 可见做准备工作,此时还不在前台。
  • onResume: Activity 进入前台,对用户可见了。
  • onPause: 将要离开 Activity, 此时 Activity 依旧可见。可以做一些轻量级数据存储任务,因为在跳转 Activity 时只有当一个 Activity 执行完了 onPause 方法后另一个 Activity 才会启动。
  • onStop: 此时 Activity 已经不可见了,但是 Activity 对象还在内存中,在此可以做一些资源回收操作。
  • onDestroy: Activity 被销毁。

关于 onRestoreInstanceState 和 onSaveInstanceState 方法:

  • onSaveInstanceState 在 onStop 后被调用;
  • onRestoreInstanceState 在 onStart 后被调用;

在 Activity 中有一个属性 — View mDecor, 在 PhoneWindow 中有一个属性 — DecorView mDecor,在 handleResumeActivity 方法中,会把 PhoneWindow 中的 mDecor 赋值给 Activity.mDecor。

启动方式

standard

standard 是 Activity 默认的启动模式。在 standard 模式下,每启动一个 Activity 都会创建一个新的实例进入任务栈栈顶。

singleTop

singleTop 模式与 standard 类似,不同的是当启动的 Activity 已经位于栈顶时,则直接使用它不创建新的实例,此时栈顶的 Activity 实例会调 onNewIntent 方法。如果启动的 Activity 没有位于栈顶时,则创建一个新的实例位于栈顶。

singleTask

当 Activity 的启动模式指定为 singleTask,每次启动该 Activity 时,系统首先会检查栈中是否存在该 Activity 的实例,如果发现已经存在则直接使用该实例,此时栈顶的 Activity 实例会调 onNewIntent 方法,并将当前 Activity 之上的所有 Activity 出栈,如果没有发现则创建一个新的实例。

singleInstance

在程序开发过程中,如果需要 Activity 在整个系统中都只有一个实例,这时就需要用到 singleInstance 模式。指定为 singleInstance 模式的 Activity 会启动一个新的任务栈来管理这个 Activity。

singleInstance模式加载Activity时,无论从哪个任务栈中启动该Activity,只会创建一个Activity实例,并且会使用一个全新的任务栈来装载该Activity实例。采用这种模式启动Activity会分为一下两种情况:

  • 如果要启动的Activity不存在,系统会创建一个新的任务栈,在创建该Activity的实例,并把该Activity加入栈顶.
  • 如果要启动的Activity已经存在,无论位于哪个应用程序或者哪个任务栈中,系统都会把该Activity所在的任务栈转到前台,从而使该Activity显示出来。

taskAffinity

概述

每个Activity都有taskAffinity属性,这个属性指出了它希望进入的Task。如果一个Activity没有显式的指明该Activity的taskAffinity,那么它的这个属性就等于Application指明的taskAffinity,如果Application也没有指明,那么该taskAffinity的值就等于包名。而Task也有自己的affinity属性,它的值等于它的根Activity的taskAffinity的值。

allowTaskReparenting

如果该Activity的allowTaskReparenting设置为true,它进入后台,当一个和它有相同affinity的Task进入前台时,它会重新宿主,进入到该前台的task中。

Application Activity taskAffinity allowTaskReparenting
application1 Activity1 com.winuxxan.affinity true
application2 Activity2 com.winuxxan.affinity false

创建两个工程:application1和application2,分别含有Activity1和Activity2,它们的taskAffinity相同,Activity1的allowTaskReparenting为true。

首先,我们启动application1,加载Activity1,然后按Home键,使该task(假设为task1)进入后台。然后启动application2,默认加载Activity2。本来应该是显示Activity2,但是我们却看到了Activity1。实际上Activity2也被加载了,只是Activity1重新宿主,所以看到了Activity1。

FLAG_ACTIVITY_NEW_TASK

如果加载某个Activity的intent,Flag被设置成FLAG_ACTIVITY_NEW_TASK时,它会首先检查是否存在与自己taskAffinity相同的Task,如果存在,那么它会直接宿主到该Task中,如果不存在则重新创建Task。

写一个应用,包含两个Activity:Activity1的taskAffinity为com.hearing.task,Activity2为入口,且点击Activity2会以FLAG_ACTIVITY_NEW_TASK启动Activity1。再写一个应用MyActivity,它包含一个Activity(MyActivity),其taskAffinity为com.hearing.task

首先启动MyActivity,然后Home回桌面,然后打开Activity2,点击Activity2,进入Activity1。然后按返回键。进入Activity的顺序为Activity2->Activity1,而返回时顺序为Activity1->MyActivity。这就说明了一个问题,Activity1在启动时,重新宿主到了MyActivity所在的Task中去了。

launchMode

  • 当一个应用程序加载一个singleTask模式的Activity时,首先该Activity会检查是否存在与它的taskAffinity相同的Task。如果存在,那么检查是否实例化,如果已经实例化,那么销毁在该Activity以上的Activity并调用onNewIntent。如果没有实例化,那么该Activity实例化并入栈。如果不存在,那么就重新创建Task,并入栈。
  • 当一个应用程序加载一个singleInstance模式的Activity时,如果该Activity没有被实例化,那么就重新创建一个Task,并入栈,如果已经被实例化,那么就调用该Activity的onNewIntent.singleInstance的Activity所在的Task不允许存在其他Activity,任何从该Activity加载的其它Activity(假设为Activity2)都会被放入其它的Task中,如果存在与Activity2相同affinity的Task,则在该Task内创建Activity2。如果不存在,则重新生成新的Task并入栈。

启动方式的问题

MainActivity是SingleTask或者SingleInstance模式,启动TestActivity,TestActivity马上跳转到MainActivity,这种情况下,MainActivity的onResume会回调两次。日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
com.hearing.activitytest.MainActivity@2c5d2eb onCreate
com.hearing.activitytest.MainActivity@2c5d2eb onStart
com.hearing.activitytest.MainActivity@2c5d2eb onResume
com.hearing.activitytest.MainActivity@2c5d2eb onPause
TestActivity onCreate
TestActivity onStart
TestActivity onResume
TestActivity onPause
com.hearing.activitytest.MainActivity@2c5d2eb onNewIntent
com.hearing.activitytest.MainActivity@2c5d2eb onResume
com.hearing.activitytest.MainActivity@2c5d2eb onPause
com.hearing.activitytest.MainActivity@2c5d2eb onResume
TestActivity onStop

适当延时500ms再跳转回MainActivity可以解决这个问题(可能由于业务场景不同,会带来新的问题)。

stackoverflow上也有人发现过类似的问题。

BroadcastReceiver

分类

  • 普通广播(Normal Broadcast)
  • 系统广播(System Broadcast)
  • 有序广播(Ordered Broadcast)
  • 粘性广播(Sticky Broadcast)(已弃用)
  • App应用内广播(Local Broadcast)

使用步骤

BroadcastReceiver

  • 继承BroadcastReceivre基类
  • 复写抽象方法onReceive()方法

注册

静态注册

  • 常驻:不受任何组件声明周期影响
  • 耗电,占内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>

动态注册

  • 非常驻,灵活
  • 手动释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onResume(){
super.onResume();
mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
registerReceiver(mBroadcastReceiver, intentFilter);
}

@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mBroadcastReceiver);
}

Service

概述

生命周期:

  1. startService()

    • 作用:启动Service服务
    • 手动调用startService()后,自动调用内部方法:onCreate()、onStartCommand()
    • 如果一个service被startService多次启动,onCreate()只会调用一次
    • onStartCommand()调用次数=startService()次数
  2. stopService()

    • 作用:关闭Service服务
    • 手动调用stopService()后,自动调用内部方法:onDestory()
    • 如果一个service被启动且被绑定,如果没有在绑定的前提下stopService()是无法停止服务的。
  3. bindService()

    • 作用:绑定Service服务
    • 手动调用bindService()后,自动调用内部方法:onCreate()、onBind()
  4. unbindService()

    • 作用:解绑Service服务
    • 手动调用unbindService()后,自动调用内部方法:onCreate()、onBind()、onDestory()

接口函数:

  • onStartCommand():当其他组件调用startService()方法请求启动Service时,该方法被回调。一旦Service启动,它会在后台独立运行。当Service执行完以后,需调用stopSelf() 或 stopService()方法停止Service。(若您只希望bind Service,则无需调用这些方法)
  • onBind():当其他组件调用bindService()方法请求绑定Service时,该方法被回调。该方法返回一个IBinder接口,该接口是Service与绑定的组件进行交互的桥梁。若Service未绑定其他组件,该方法应返回null。
  • onCreate():当Service第一次创建时,回调该方法。该方法只被回调一次,并在onStartCommand() 或 onBind()方法被回调之前执行。若Service处于运行状态,该方法不会回调。
  • onDestroy():当Service被销毁时回调,在该方法中应清除一些占用的资源,如停止线程、接触绑定注册的监听器或broadcast receiver 等。该方法是Service中的最后一个回调。

启动方式:

  • Started:其他组件调用startService()方法启动一个Service。一旦启动,Service将一直运行在后台(run in the background indefinitely)即便启动Service的组件已被destroy。通常,一个被start的Service会在后台执行单独的操作,也并不给启动它的组件返回结果。比如说,一个start的Service执行在后台下载或上传一个文件的操作,完成之后,Service应自己停止。
  • Bound:其他组件调用bindService()方法绑定一个Service。通过绑定方式启动的Service是一个client-server结构,该Service可以与绑定它的组件进行交互。一个bound service仅在有组件与其绑定时才会运行(A bound service runs only as long as another application component is bound to it),多个组件可与一个service绑定,service不再与任何组件绑定时,该service会被destroy。

注意:

  • Service运行在主线程中(A service runs in the main thread of its hosting process),Service并不是一个新的线程,也不是新的进程。也就是说,若您需要在Service中执行较为耗时的操作(如播放音乐、执行网络请求等),需要在Service中创建一个新的线程。这可以防止ANR的发生,同时主线程可以执行正常的UI操作。
  • 如果某个组件通过调用startService()启动了Service(系统会回调onStartCommand()方法),那么直到在Service中手动调用stopSelf()方法、或在其他组件中手动调用stopService()方法,该Service才会停止。
  • 如果某个组件通过调用bindService()绑定了Service(系统不会回调onStartCommand()方法),只要该组件与Service处于绑定状态,Service就会一直运行,当Service不再与组件绑定时,该Service将被destroy。
  • 当系统内存低时,系统将强制停止Service的运行;若Service绑定了正在与用户交互的activity,那么该Service将不大可能被系统kill( less likely to be killed)。如果创建的是前台Service,那么该Service几乎不会被kill(almost never be killed)。否则,当创建了一个长时间在后台运行的Service后,系统会降低该Service在后台任务栈中的级别——这意味着它容易被kill(lower its position in the list of background tasks over time and the service will become highly susceptible to killing),所以在开发Service时,需要使Service变得容易被restart,因为一旦Service被kill,再restart它需要其资源可用时才行

注册

1
<service android:name="com.example.servicetest.MyService"/>

继承IntentService类

  • 默认在子线程中处理回传到onStartCommand()方法中的Intent;
  • 在重写的onHandleIntent()方法中处理按时间排序的Intent队列,所以不用担心多线程带来的问题。
  • 当所有请求处理完成后,自动停止service,无需手动调用stopSelf()方法;
  • 默认实现了onBind()方法,并返回null;
  • 默认实现了onStartCommand()方法,并将回传的Intent以序列的形式发送给onHandleIntent(),只需重写该方法并处理Intent即可。

示例:

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
class MyService : IntentService("MyService") {

override fun onCreate() {
Log.d("LLL", "onCreate")
super.onCreate()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("LLL", "onStartCommand: $startId")
return super.onStartCommand(intent, flags, startId)
}

override fun onDestroy() {
Log.d("LLL", "onDestroy")
super.onDestroy()
}

override fun onHandleIntent(intent: Intent?) {
Log.d("LLL", "Current thread: ${Thread.currentThread().name}")
Log.d("LLL", "Intent: ${intent?.getStringExtra("name")}")
Thread.sleep(1000)
}
}

// 使用
val intent = Intent(this, MyService::class.java)
intent.putExtra("name", "hearing-1")
startService(intent)
Thread.sleep(20)
intent.putExtra("name", "hearing-2")
startService(intent)
Thread.sleep(20)
intent.putExtra("name", "hearing-3")
startService(intent)

// 日志
D/LLL: onCreate
D/LLL: onStartCommand: 1
D/LLL: onStartCommand: 2
D/LLL: onStartCommand: 3
D/LLL: Current thread: IntentService[MyService]
D/LLL: Intent: hearing-1
D/LLL: Current thread: IntentService[MyService]
D/LLL: Intent: hearing-2
D/LLL: Current thread: IntentService[MyService]
D/LLL: Intent: hearing-3
D/LLL: onDestroy

如果启动的 Service 存在则不会再次创,而会回调 onStartCommand 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
// 会检查 id 是否为最后一个 id,是则停止服务
stopSelf(msg.arg1);
}
}

继承Service类

如果需要在Service中执行多线程而不是处理一个请求队列,那么需要继承Service类,分别处理每个Intent。

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
public class MyService extends Service {

public static final String TAG = "MyService";

private MyBinder mBinder = new MyBinder();

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate() executed");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand() executed");
return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy() executed");
}

@Override
public IBinder onBind(Intent intent) {
return mBinder;
}

class MyBinder extends Binder {
public void startDownload() {
Log.d("TAG", "startDownload() executed");
// 执行具体的下载任务
}
}
}

onStartCommand()返回一个整形变量,该变量必须是下列常量之一:

  • START_NOT_STICKY:若执行完onStartCommand()方法后,系统就kill了service,不要再重新创建service,除非系统回传了一个pending intent。这避免了在不必要的时候运行service,您的应用也可以restart任何未完成的操作。
  • START_STICKY:若系统在onStartCommand()执行并返回后kill了service,那么service会被recreate并回调onStartCommand()。dangerous不要重新传递最后一个Intent(do not redeliver the last intent)。相反,系统回调onStartCommand()时回传一个空的Intent,除非有 pending intents传递,否则Intent将为null。该模式适合做一些类似播放音乐的操作。
  • START_REDELIVER_INTENT:若系统在onStartCommand()执行并返回后kill了service,那么service会被recreate并回调onStartCommand()并将最后一个Intent回传至该方法。任何 pending intents都会被轮流传递。该模式适合做一些类似下载文件的操作。

启动服务

startService(intent)方法将立即返回,并回调onStartCommand()(请不要手动调用该方法),若该Service未处于运行状态,系统将首先回调onCreate(),接着再回调onStartCommand()。若您希望Service可以返回结果,那么需要通过调用getBroadcast 返回的PendingIntent启动Service(将PendingIntent包装为Intent),service可使用broadcast 传递结果。

多个启动Service的请求可能导致onStartCommand()多次调用,但只需调用stopSelf() 、 stopService()这两个方法之一,就可停止该服务。

绑定Service

  • 通过其他组件调用bindService()方法可以绑定一个Service以保持长连接(long-standing connection),这时一般不允许其他组件调用startService()启动Service。
  • 当其他组件需要与Service交互或者需要跨进程通信时,可以创建一个bound Service。
  • 为创建一个bound Service,必须重写onBind()回调,该方法返回一个IBinder接口。该接口时组件与Service通信的桥梁。组件调用bindService()与Service绑定,该组件可获取IBinder接口,一旦获取该接口,就可以调用Service中的方法。一旦没有组件与Service绑定,系统将destroy它,您不必手动停止它。
  • 为创建一个bound Service,必须定义一个接口 ,该接口指定组件与Service如何通信。定义的接口在组件与Service之间,且必须实现IBinder接口。这正是onBind()的返回值。一旦组件接收了IBinder,组件与Service便可以开始通信。
  • 多个组件可同时与Service绑定,当组件与Service交互结束后,可调用unbindService()方法解绑。bound Service比start Service要复杂,故我将在后续单独翻译。
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
public class MainActivity extends Activity implements OnClickListener {

private MyService.MyBinder myBinder;

private ServiceConnection connection = new ServiceConnection() {

@Override
public void onServiceDisconnected(ComponentName name) {
}

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myBinder = (MyService.MyBinder) service;
myBinder.startDownload();
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent, connection, BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
unbindService(connection);
break;
default:
break;
}
}
}

运行前台Service

  • 前台Service用于动态通知消息,如天气预报。该Service不易被kill。前台Service必须提供status bar,只有前台Service被destroy后,status bar才能消失。
  • 举例来说,一个播放音乐的Service必须是前台Service,只有这样用户才能确知其运行状态。为前台Service提供的status bar可以显示当前音乐的播放状态,并可以启动播放音乐的Activity。
  • 调用startForeground()可以启动前台Service。该方法接收两个参数,参数一是一个int型变量,用户指定该通知的唯一性标识,而参数而是一个Notification用于配置status bar

ContentProvider

概述

ContentProvider 需要实现的接口:

1
2
3
4
5
6
public Uri insert(Uri uri, ContentValues values) 
public int delete(Uri uri, String selection, String[] selectionArgs)
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 
public boolean onCreate()
public String getType(Uri uri)

参数含义:

  • projection: 返回内容(conlumn名),null 表示返回所有
  • selection: 设置条件
  • selectionArgs: selection 参数条件的内容
  • sortOrder: ASC(升序),DESC(降序),默认升序

例如:

1
Cursor cursor = contentResolver.query(uri, new String[]{"name"}, "name=?", new String[]{"hearing"}, "id DESC");

可以通过 ContentResolver 类与 ContentProvider 统一进行交互;可以通过 ContentObserver 类监听 ContentProvider 中指定 Uri 的数据变化。

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
class MyObserver extends ContentObserver {

public MyObserver(Handler handler) {
super(handler);
}

@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
}
}

// 步骤1:注册内容观察者ContentObserver
getContentResolver().registerContentObserver(uri, true, myOberver);

// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界
public class UserContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("user", "userid", values);
getContext().getContentResolver().notifyChange(uri, null);
}
}

// 步骤3:解除观察者
getContentResolver().unregisterContentObserver(myOberver);

Uri

Uri 的四个组成部分:content://contacts/people/5

  • schema:已由Android固定设置为content://
  • authority:ContentProvider权限,在AndroidMenifest中设置权限
  • path:要操作的数据库表
  • id:查询的关键字(可选字段)

Uri匹配模式:Uri的匹配表示要查询的数据,对于单个数据查询,可直接使用Uri定位具体的资源位置,但当范围查询时就需要结合通配符的使用,Uri提供以下两种通配符:

  • *:匹配由任意长度的任何有效字符组成的字符串
  • #:匹配由任意长度的数字字符组成的字符串

示例:

1
2
3
content://com.example.app.provider/table2/*  //多数据查询
content://com.example.app.provider/table3/#
content://com.example.app.provider/table3/6 //单数据查询

ContentUris

核心方法有两个:

  • withAppendedId():向Uri追加一个id
  • parseId():从Uri中获取id
1
2
3
4
5
6
7
Uri uri = Uri.parse("content://com.hearing.provider/user")
// 生成的Uri为:content://com.hearing.provider/user/7
Uri resultUri = ContentUris.withAppendedId(uri, 7);

Uri uri = Uri.parse("content://com.hearing.provider/user/7")
//获取的结果为:7
long personid = ContentUris.parseId(uri);

UriMatcher

UriMatcher的作用:

  • 在 ContentProvider 中注册Uri
  • 根据 Uri 匹配 ContentProvider 中对应的数据表

使用步骤:

  1. 初始化UriMatcher对象
  2. 在ContentProvider 中注册URI(addURI())
  3. 根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 步骤1:初始化UriMatcher对象
// 常量UriMatcher.NO_MATCH:不匹配任何路径的返回码
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

// 步骤2:在ContentProvider 中注册URI
int URI_CODE_a = 1
int URI_CODE_b = 2
matcher.addURI("com.hearing.provider", "user1", URI_CODE_a);
matcher.addURI("com.hearing.provider", "user2", URI_CODE_b);
// 若URI资源路径 = content://com.hearing.provider/user1 ,则返回注册码URI_CODE_a
// 若URI资源路径 = content://com.hearing.provider/user2 ,则返回注册码URI_CODE_b

// 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源
private String getTableName(Uri uri) {
Uri uri = Uri.parse("content://com.hearing.provider/user1");

switch(matcher.match(uri)) {
case URI_CODE_a:
return tableNameUser1;
case URI_CODE_b:
return tableNameUser2;
}
}

权限

概述

指定其他应用访问提供程序的数据所必须具备权限的属性。

grantUriPermssions

android:grantUriPermssions:表示是否可以通过临时权限访问数据,默认为false,在开发中可以只对限定的内容提供临时权限,如对照片的内容 URI 设置临时权限。

1
2
3
4
5
android:grantUriPermissions="false"

<grant-uri-permission android:path="string"
android:pathPattern="string"
android:pathPrefix="string" />
  • true:系统会向整个系统授予临时权限,并替代其他设置的权限。
  • false:需添加<grant-uri-permission>并表明可以授权临时权限所对应的URI
    • path:表示绝对路径Uri
    • pathPattern:表示限定完整的路径但可以使用./*通配符匹配
    • pathPrefix:限定路径的初始部分后面可以变化,只要初始部分符合即可授权

permission

  • android:permission:统一提供程序范围读取/写入权限
  • android:readPermission:提供程序范围读取权限,优先于permission权限
  • android:writePermission:提供程序范围写入权限,优先于permission权限
1
2
3
android:readPermission="com.hearing.provider.permission.READ_PERMISSION"
android:writePermission="com.hearing.provider.permission.WRITE_PERMISSION"
android:permission="com.hearing.provider.permission.PERMISSION"

使用

  1. 创建两个程序A和B,在程序A中使用ContentProvider保存数据,在程序B中进行查询,在开始A程序中不设置任何权限,B程序进行访问数据会报错;
  2. 修改A程序清单文件添加android:exported=”true”,再次访问数据访问成功;
  3. 在A程序的清单文件中,为Provider添加两个读写权限,添加完权限后再次在B程序中获取数据,还是会报错,也很正常因为已经对数据的访问设置了门槛,所以在B程序中声明读写权限即可。
1
2
3
4
5
6
7
<!-- A程序 -->
android:writePermission="com.hearing.provider.provider.WRITE"
android:readPermission="com.hearing.provider.provider.READ"

<!-- B程序 -->
<uses-permission android:name="com.hearing.provider.provider.READ"/>
<uses-permission android:name="com.hearing.provider.provider.WRITE"/>

实例:IPC

进程一

创建数据库类:

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
public class DBHelper extends SQLiteOpenHelper {

// 数据库名
private static final String DATABASE_NAME = "finch.db";

// 表名
public static final String USER_TABLE_NAME = "user";
public static final String JOB_TABLE_NAME = "job";

// 数据库版本号
private static final int DATABASE_VERSION = 1;

public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
// 创建两个表格:用户表 和职业表
db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");
db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}

自定义ContentProvider:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class MyProvider extends ContentProvider {

private Context mContext;
DBHelper mDbHelper = null;
SQLiteDatabase db = null;

// 设置ContentProvider的唯一标识
public static final String AUTOHORITY = "com.hearing.provider";

public static final int User_Code = 1;
public static final int Job_Code = 2;

// UriMatcher类使用:在ContentProvider 中注册URI
private static final UriMatcher mMatcher;

static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTOHORITY, "user", User_Code);
mMatcher.addURI(AUTOHORITY, "job", Job_Code);
}

@Override
public boolean onCreate() {
mContext = getContext();
// 在ContentProvider创建时对数据库进行初始化,不能做耗时操作,此处仅作展示
mDbHelper = new DBHelper(getContext());
db = mDbHelper.getWritableDatabase();

db.execSQL("delete from user");
db.execSQL("insert into user values(1,'Carson');");
db.execSQL("insert into user values(2,'Kobe');");

db.execSQL("delete from job");
db.execSQL("insert into job values(1,'Android');");
db.execSQL("insert into job values(2,'iOS');");

return true;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
String table = getTableName(uri);
db.insert(table, null, values);
mContext.getContentResolver().notifyChange(uri, null);
return uri;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String table = getTableName(uri);
return db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
}

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// 此处不作展开
return 0;
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 此处不作展开
return 0;
}

@Override
public String getType(Uri uri) {
// 此处不作展开
return null;
}

private String getTableName(Uri uri) {
String tableName = null;
switch (mMatcher.match(uri)) {
case User_Code:
tableName = DBHelper.USER_TABLE_NAME;
break;
case Job_Code:
tableName = DBHelper.JOB_TABLE_NAME;
break;
}
return tableName;
}
}

注册ContentProvider:

1
2
3
4
5
6
7
8
9
10
11
<provider
android:name="MyProvider"
android:authorities="com.hearing.provider.myprovider"
android:permission="com.hearing.provider.PROVIDER"
android:readPermission = "com.hearing.provider.Read"
android:writePermission = "com.hearing.provider.Write"
android:exported="true"/>

<permission android:name="com.hearing.provider.Read" android:protectionLevel="normal"/>
<permission android:name="com.hearing.provider.Write" android:protectionLevel="normal"/>
<permission android:name="com.hearing.provider.PROVIDER" android:protectionLevel="normal"/>

进程二

声明权限:

1
2
3
4
<uses-permission android:name="com.hearing.provider.PROVIDER"/>

<uses-permission android:name="com.hearing.provider.Read"/>
<uses-permission android:name="com.hearing.provider.Write"/>

访问ContentProvider:

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
48
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 设置URI
Uri uri_user = Uri.parse("content://com.hearing.provider.myprovider/user");

// 插入表中数据
ContentValues values = new ContentValues();
values.put("_id", 4);
values.put("name", "Jordan");

// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 通过ContentResolver 根据URI 向ContentProvider中插入数据
resolver.insert(uri_user, values);

// 通过ContentResolver 向ContentProvider中查询数据
Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
while (cursor.moveToNext()) {
System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
}
cursor.close();

// 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
Uri uri_job = Uri.parse("content://com.hearing.provider.myprovider/job");

// 插入表中数据
ContentValues values2 = new ContentValues();
values2.put("_id", 4);
values2.put("job", "NBA Player");

// 获取ContentResolver
ContentResolver resolver2 = getContentResolver();
// 通过ContentResolver 根据URI 向ContentProvider中插入数据
resolver2.insert(uri_job,values2);

// 通过ContentResolver 向ContentProvider中查询数据
Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
while (cursor2.moveToNext()) {
System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
}
cursor2.close();
}
}

FileProvider

概述

对于面向Android 7.0及以上的应用,Android禁止在应用外部公开file://url。如果一项包含文件URI的intent离开应用,则应用会抛出FileUriExposedException异常。

解决方案:要在应用间共享文件,应发送一项content://URI,并授予URI临时访问权限。进行此授权的最简单方式是使用FileProvider类。FileProvider是ContentProvider的一个特殊的子类,它让应用间共享文件变得更加容易,其通过创建一个Content URI来代替File URI。

注册FileProvider

由于FileProvider中已经包含了为file生成Content URI的基本代码了,所以开发者不必再去定义一个FileProvider的子类。你可以在XML文件中指定一个FileProvider:在manifest中使用<provider>标签来指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.hearing.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>
  • name的值一般都固定为android.support.v4.content.FileProvider。如果开发者继承了FileProvider,则可以写上其绝对路径。
  • authorities字段的值用来表明使用的使用者,在FileProvider的函数getUriForFile需要传入该参数。
  • exported 的值为false,表示该FileProvider只能本应用使用,不是public的。
  • grantUriPermissions 的值为true,表示允许赋予临时权限。

xml配置

只有事先指定了目录,一个FileProvider才可以为文件生成一个对应的Content URI。要指定一个路径,需要在XML文件中指定其存储的路径。使用<paths>标签。例如:

1
2
3
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
</paths>
  • <root-path name="name" path="path" />: /
  • <files-path name="name" path="path" />: /data/data/<package-name>/files/path/
  • <cache-path name="name" path="path" />: /data/data/<package-name>/cache/path/
  • <external-files-path name="name" path="path" />: Context.getExternalFilesDir(null) + "/path/"/storage/emulated/0/Android/data/<package_name>/files/path/
  • <external-cache-path name="name" path="path" />: Context.getExternalCacheDir() + "/path/",即/storage/emulated/0/Android/data/<package-name>/cache/path/
  • <external-media-path name="name" path="path" />: Context.getExternalMediaDirs() + "/path/",即/storage/emulated/0/Android/media/<package-name>/path/
  • <external-path name="name" path="path" />: Environment.getExternalStorageDirectory() + "/path/",即/storage/emulated/0/path/

在res目录下新建xml目录,然后新建文件file_paths.xml,根据上述内容编写。

获取Content Uri

1
2
3
4
5
6
7
File file = new File(mContext.getFilesDir() + "/text", "hello.txt");
Uri data;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
data = FileProvider.getUriForFile(mContext, "com.hearing.fileprovider", file);
} else {
data = Uri.fromFile(file);
}

赋予临时权限

两种方法:(通常使用第2种)

  1. Context.grantUriPermission(package, Uri, mode_flags)
  2. Intent.setFlags():intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION);

Flag意义如下:

  • FLAG_GRANT_READ_URI_PERMISSION:表示读取权限;
  • FLAG_GRANT_WRITE_URI_PERMISSION:表示写入权限。

分享文件URI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void shareFile() {
Log.d(TAG, "shareFile: ");
Intent intent = new Intent();
ComponentName componentName = new ComponentName("com.hearing.fileproviderclient",
"com.hearing.fileproviderclient.MainActivity");
intent.setComponent(componentName);
File file = new File(mContext.getFilesDir() + "/text", "hello.txt");
Uri data;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
data = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITIES, file);
// 给目标应用一个临时授权
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
data = Uri.fromFile(file);
}
intent.setData(data);
startActivity(intent);
}

Mime Type

概述

MIME:多用途互联网邮件扩展(Multipurpose Internet Mail Extensions)是一个互联网标准,它扩展了电子邮件标准,使其能够支持非ASCII字符、二进制格式附件等多种格式的邮件消息。

MIME TYPE一般以这种形式出现:[type]/[subtype]

type标识内容type,有下面的形式:

  • Text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的;
  • Multipart:用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的数据;
  • Application:用于传输应用程序数据或者二进制数据;
  • Message:用于包装一个E-mail消息;
  • Image:用于传输静态图片数据;
  • Audio:用于传输音频或者音声数据;
  • Video:用于传输动态影像数据,可以是与音频编辑在一起的视频数据格式。

subtype用于指定type的详细形式。content-type/subtype配对的集合和与此相关的参数,将随着时间而增长。为了确保这些值在一个有序而且公开的状态下开发,MIME使用Internet Assigned Numbers Authority (IANA)作为中心的注册机制来管理这些值。常用的subtype值如下所示:

  • text/plain(纯文本)
  • text/html(HTML文档)
  • application/xhtml+xml(XHTML文档)
  • image/gif(GIF图像)
  • image/jpeg(JPEG图像)
  • image/png(PNG图像)
  • video/mpeg(MPEG动画)
  • application/octet-stream(任意的二进制数据)
  • application/pdf(PDF文档)
  • application/msword(Microsoft Word文件)
  • message/rfc822(RFC 822形式)
  • multipart/alternative(HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示)
  • application/x-www-form-urlencoded(使用HTTP的POST方法提交的表单)
  • multipart/form-data(同上,但主要用于表单提交时伴随文件上传的场合)

使用

当Android系统接收到一个隐式Intent要启动一个Activity(或其他组件)时,Android会根据以下三个信息比较Intent的信息与注册的组件的intent-filter的信息,从而为该Intent选择出最匹配的Activity(或其他组件):

  • intent中的action
  • intent中的category
  • intent中的data(包含Uri以及data的MIME类型)

也就是隐式intent对象要满足要启动的目标组件中注册的intent-filter中的<action/><category/><data/>三个标签中的信息,即要分别通过action测试、category测试以及data测试。

MINI类型即在data中指定。

Intent和Intent-filter

概述

  • Intent 是一个消息传递对象,可以用来从其他应用组件请求操作, Intent 包括显式和隐式。
  • Intent-filter 是 Manifest 中的一个表达式,用于指定该组件要接收的 Intent 类型。

为了确保应用的安全性,启动 Service 时应该始终使用显式 Intent,且不要为服务声明 Intent-filter。

构建Intent

Component name

可选项,如果没有组件名称,则 Intent 为隐式,如需在应用中启动特定的组件,则应指定该组件的名称。可以使用 setComponent(), setClass(), setClassName() 或 Intent 构造函数设置组件名称。

Action

可以指定自己的 action。以下是一些用于启动 Activity 的常见操作:

  1. ACTION_VIEW: 拥有一些可向用户显示的信息(查看照片 或 观看视频等)。
  2. ACTION_SEND:拥有一些用户可通过其他应用(例如电子邮件应用 或 社交共享应用)共享的数据。

可以使用 setAction() 或 Intent 构造函数为 Intent 指定操作。

Data

指定数据 MIME 类型的 URI,提供的数据类型通常由 Intent 的 Action 决定,例如如果操作是 ACTION_EDIT 则数据应包含待编辑文档的 URI。

可以使用 setData() 设置数据 URI;可以使用 setType() 设置 MIME 类型;可以使用 setDataAndType() 同时显式设置二者。注意:若要同时设置 URI 和 MIME 类型,请勿调用 setData() 和 setType(),因为它们会互相抵消彼此的值。

Category

一个包含应处理 Intent 组件类型的附加信息的字符串。可以将任意数量的Category放入一个 Intent 中,但大多数 Intent 均不需要 Category。以下是一些常见category:

  • CATEGORY_BROWSABLE:目标 Activity 允许本身通过网络浏览器启动,以显示链接引用的数据,如图像或电子邮件。
  • CATEGORY_LAUNCHER:该 Activity 是任务的初始 Activity,在系统的应用启动器中列出。
  • CATEGORY_HOME:桌面应用需要声明。
  • CATEGORY_DEFAULT:通过隐式启动Activity时,Android会默认加上一个CATEGORY_DEFAULT,所以如果Activity要支持隐式启动的话,除了默认LaunchActivity,其余都需要加上CATEGORY_DEFAULT。

可以使用 addCategory() 指定 category。

Extras

携带完成请求操作所需的附加信息的键值对,可以使用各种 putExtra() 方法添加 extra 数据。

Flags

可以使用 setFlags() 方法添加Flags。

接收隐式 Intent

应用组件应当为自身可执行的每个独特作业声明单独的filter。例如,图像库应用中的一个 Activity 可能会有两个filter,分别用于查看图像和编辑图像。

<intent-filter> 内部可以使用以下三个元素中的一个或多个指定要接受的 Intent 类型:

  • <action>:在 name 属性中,声明接受的 Intent 操作。
  • <data>:使用一个或多个指定数据 URI(scheme、host、port、path)各个方面和 MIME 类型的属性,声明接受的数据类型。
  • <category>:在 name 属性中,声明接受的 Intent category。

请注意:要接收隐式 Intent,必须将 CATEGORY_DEFAULT category 包括在 Intent-filter中。方法 startActivity() 和 startActivityForResult() 将按照其声明 CATEGORY_DEFAULT category的方式处理所有 Intent。如果未在 Intent-filter中声明此category,则隐式 Intent 不会解析为您的 Activity。

可以创建一个包括多个 <action><data><category> 实例的filter,创建时,需确定组件能够处理这些filter元素的任何及所有组合。系统通过将 Intent 与所有这三个元素进行比较,根据filter测试隐式 Intent。隐式 Intent 若要传递给组件,必须通过所有这三项测试。

Intent 解析

当收到隐式 Intent 以启动 Activity 时,系统会根据以下三个方面将该 Intent 与 Intent-filter进行比较,搜索该 Intent 的最佳 Activity:

  • Action
  • Data(URI 和数据类型)。
  • Category

Action测试

要指定接受的 Intent action,Intent-filter既可以不声明任何 <action> 元素,也可以声明多个此类元素。要通过此filter,Intent 中指定的action必须与filter中列出的某一action匹配。

Category测试

要指定接受的 Intent category,Intent-filter既可以不声明任何 <category> 元素,也可以声明多个此类元素。要使 Intent 通过category测试,则 Intent 中的每个category均必须与filter中的category匹配。Intent-filter声明的category可以超出 Intent 中指定的数量,因此不含category的 Intent 应当始终会通过此测试,无论filter中声明何种category均是如此。

请注意:Android 会自动将 CATEGORY_DEFAULT category应用于传递给 startActivity() 和 startActivityForResult() 的所有隐式 Intent。如需 Activity 接收隐式 Intent,则必须将 “android.intent.category.DEFAULT” 的category包括在其 Intent-filter中。

Data测试

要指定接受的 Intent 数据,Intent-filter既可以不声明任何 <data> 元素,也可以声明多个此类元素,如下例所示:

1
2
3
4
5
<intent-filter>
<data android:mimeType="video/mpeg" android:scheme="http" ... />
<data android:mimeType="audio/mpeg" android:scheme="http" ... />
...
</intent-filter>

每个 <data> 元素均可指定 URI 结构和数据类型(MIME 媒体类型)。URI 的每个部分都是一个单独的属性:scheme、host、port 和 path:<scheme>://<host>:<port>/<path>

<data> 元素中,上述每个属性均为可选,但存在线性依赖关系:

  • 如果未指定scheme,则会忽略host。
  • 如果未指定host,则会忽略port。
  • 如果未指定scheme和host,则会忽略path。

将 Intent 中的 URI 与filter中的 URI 规范进行比较时,它仅与filter中包含的部分 URI 进行比较。例如:

  • 如果filter仅指定scheme,则具有该scheme的所有 URI 均与该filter匹配。
  • 如果filter指定scheme和host,但未指定path,则具有相同scheme和host的所有 URI 都会通过filter,无论其path如何均是如此。
  • 如果filter指定scheme、host和path,则仅具有相同scheme、host和path的 URI 才会通过filter。

请注意:path规范可以包含星号通配符 (*),因此仅需部分匹配路径名即可。

数据测试会将 Intent 中的 URI 和 MIME 类型与filter中指定的 URI 和 MIME 类型进行比较。规则如下:

  • 仅当filter未指定任何 URI 或 MIME 类型时,不含 URI 和 MIME 类型的 Intent 才会通过测试。
  • 对于包含 URI 但不含 MIME 类型(既未显式声明,也无法通过 URI 推断得出)的 Intent,仅当其 URI 与filter的 URI 格式匹配、且filter同样未指定 MIME 类型时,才会通过测试。
  • 仅当filter列出相同的 MIME 类型且未指定 URI 格式时,包含 MIME 类型但不含 URI 的 Intent 才会通过测试。
  • 仅当 MIME 类型与filter中列出的类型匹配时,同时包含 URI 类型和 MIME 类型(通过显式声明,或可以通过 URI 推断得出)的 Intent 才会通过测试的 MIME 类型部分。如果 Intent 的 URI 与filter中的 URI 匹配,或者如果 Intent 具有 content: 或 file: URI 且filter未指定 URI,则 Intent 会通过测试的 URI 部分。换言之,如果filter只是列出 MIME 类型,则假定组件支持 content: 和 file: 数据。

请注意:如果 Intent 指定 URI 或 MIME 类型,则数据测试会在 <intent-filter> 中没有 <data> 元素时失败。

最后一条规则反映出对组件能够从文件中或内容提供程序处获得本地数据的预期。因此,其filter只能列出数据类型,不需要显式命名 content: 和 file: scheme。以下是一个典型示例,说明 <data> 元素向 Android 指出,组件可从内容提供程序处获得并显示图像数据:

1
2
3
4
<intent-filter>
<data android:mimeType="image/*" />
...
</intent-filter>

由于大部分可用数据均由内容提供程序分发,因此指定数据类型(而非 URI)的filter也许最为常见。

另一常见的配置是具有scheme和数据类型的filter。例如,下文中的 <data> 元素向 Android 指出,组件可从网络中检索视频数据以执行操作:

1
2
3
4
<intent-filter>
<data android:scheme="http" android:mimeType="video/*" />
...
</intent-filter>

View动画(补间动画)

XML Java 效果
alph AlphaAnimation 渐变透明度动画效果
scale ScaleAnimation 渐变尺寸伸缩动画效果
translate TranslateAnimation 画面转换位置移动动画效果
rotate RotateAnimation 画面转移旋转动画效果
阅读全文 »

常用命令

多条命令执行结果不输出到终端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用;分隔的多条命令执行结果之间互不干扰
$ ls;lls;ls
Downloads termux-packages
zsh: command not found: lls
Downloads termux-packages

# 使用&&分割的多条命令后一条会在前一条命令执行成功后才执行
$ ls&&lls&&ls
Downloads termux-packages
zsh: command not found: lls

# 使用cmd > /dev/null 2>&1将输出重定向到null,即不输出到终端
# 使用if [ $? -ne 0 ]; then echo 1; else echo 0;fi;可以得到上一条命令的执行结果($?表示上一条命令的执行结果)
$ ls > /dev/null 2>&1;if [ $? -ne 0 ]; then echo 1; else echo 0;fi;
0

# 使用{}包裹起来的多条命令
$ {ls&&ls&&ls} > /dev/null 2>&1;if [ $? -ne 0 ]; then echo 1; else echo 0;fi;
0

# echo指定-e参数可开启转移,添加/c可不换行输出
$ echo -e "please input a value:\c";

后台运行

当用户注销(logout)或者网络断开时,终端会收到 HUP(hangup)信号从而关闭其所有子进程。因此,解决办法有两种途径:

  • 让进程忽略 HUP 信号;
  • 让进程运行在新的会话里从而成为不属于此终端的子进程。

nohup

nohup 的用途就是让提交的命令忽略 hangup 信号,一般我们可在结尾加上”&”来将命令同时放入后台运行,也可用”>filename 2>&1”来更改缺省的重定向文件名。

1
2
3
4
5
6
$ nohup ping www.baidu.com &
[1] 3059
nohup: appending output to 'nohup.out'
$ ps -ef |grep 3059
root 3059 984 0 21:06 pts/3 00:00:00 ping www.baidu.com
root 3067 984 0 21:06 pts/3 00:00:00 grep 3059

setsid

换个角度思考,如果我们的进程不属于接受 HUP 信号的终端的子进程,那么自然也就不会受到 HUP 信号的影响了。setsid 就能做到这一点。

1
2
3
4
$ setsid ping www.baidu.com
$ ps -ef |grep www.baidu.com
root 31094 1 0 07:28 ? 00:00:00 ping www.baidu.com
root 31102 29217 0 07:29 pts/4 00:00:00 grep www.baidu.com

进程 ID(PID)为31094,而它的父 ID(PPID)为1(即为 init 进程 ID),并不是当前终端的进程 ID。

&

将一个或多个命名包含在()中就能让这些命令在子 shell 中运行中,从而扩展出很多有趣的功能。

当我们将&也放入()内之后,就会发现所提交的作业并不在作业列表中,也就是说,是无法通过jobs来查看的。

1
2
3
4
$ (ping www.baidu.com &)
$ ps -ef |grep www.baidu.com
root 16270 1 0 14:13 pts/4 00:00:00 ping www.baidu.com
root 16278 15362 0 14:13 pts/4 00:00:00 grep www.baidu.com

从上例中可以看出,新提交的进程的父 ID(PPID)为1(init 进程的 PID),并不是当前终端的进程 ID。因此并不属于当前终端的子进程,从而也就不会受到当前终端的 HUP 信号的影响了。

查看进程

概述

  • VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存),一个进程总共可访问的地址空间。其大小还包括了可能不在RAM中的内存(比如虽然malloc分配了空间,但尚未写入)。 VSS 很少被用于判断一个进程的真实内存使用量。
  • RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存),一个进程在RAM中真实存储的总内存。但是RSS还是可能会造成误导,因为它仅仅表示该进程所使用的所有共享库的大小,它不管有多少个进程使用该共享库,该共享库仅被加载到内存一次。所以RSS并不能准确反映单进程的内存占用情况。
  • PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存),按比例表示使用的共享库, 例如:如果有三个进程都使用了一个共享库,共占用了30页内存。那么PSS将认为每个进程分别占用该共享库10页的大小。 PSS是非常有用的数据,因为系统中所有进程的PSS都相加的话,就刚好反映了系统中的总共占用的内存。 而当一个进程被销毁之后, 其占用的共享库那部分比例的PSS,将会再次按比例分配给余下使用该库的进程。这样PSS可能会造成一点的误导,因为当一个进程被销毁后,PSS不能准确地表示返回给全局系统的内存(the memory returned to the overall system)。
  • USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存),一个进程所占用的私有内存。即该进程独占的内存。 USS是非常非常有用的数据,因为它反映了运行一个特定进程真实的边际成本(增量成本)。当一个进程被销毁后,USS是真实返回给系统的内存。当进程中存在一个可疑的内存泄露时,USS是最佳观察数据。

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

free

1
2
3
4
$ free -h
total used free shared buff/cache available
Mem: 7.9G 6.6G 1.0G 17M 223M 1.1G
Swap: 19G 566M 19G

top

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 得到进程的pid
$ pidof sleep
602

# top指定查看PID
$ top -p 602
top - 11:01:36 up 23 min, 0 users, load average: 0.52, 0.58, 0.59
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 0.9 sy, 0.0 ni, 98.2 id, 0.0 wa, 0.2 hi, 0.0 si, 0.0 st
KiB Mem : 8266588 total, 2224708 free, 5812528 used, 229352 buff/cache
KiB Swap: 20680852 total, 19784620 free, 896232 used. 2320328 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
602 hearing 15 42+ 13956 808 688 S 0.0 0.0 0:00.01 sleep

# top查看某些指定的PID
$ top -p 602,666,1001
  • 键入 P 按照CPU排序输出
  • 键入 M 按照内存排序输出

ps

ps是显示瞬间进程的状态,并不动态连续;如果想对进程进行实时监控应该用top命令。如果输出较多可以结合less命令和管道来使用,即 ps -ef | less

  • -A:所有的进程均显示出来,与 -e 具有同样的效用
  • -a:显示现行终端机下的所有进程,包括其他用户的进程
  • -u:以用户为主的进程状态/以针对用户的格式输出
  • -x:通常与 a 这个参数一起使用,可列出较完整信息
  • -l:较长、较详细的将该PID 的的信息列出;
  • -j:工作的格式 (jobs format)
  • -f:做一个更为完整的输出。
1
2
3
4
5
6
7
8
# 根据CPU使用率来升序排序
$ ps -aux --sort -pcpu | less

# 根据内存使用率来升序排序
$ ps -aux --sort -pmem | less

# 合并命令,并通过管道显示前10个结果
$ ps -aux --sort -pcpu,+pmem | head -n 10

可以使用 -C 参数查看指定进程的信息:

1
2
3
$ ps -f -C sleep
UID PID PPID C STIME TTY TIME CMD
hearing 348 7 0 10:45 tty1 00:00:00 sleep 555

也可以结合watch命令实时查看信息:

1
2
# 每秒执行一次ps -aux --sort -pmem, -pcpu
$ watch -n 1 'ps -aux --sort -pmem, -pcpu'

proc

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
$ cat /proc/$pid/status
Name: main
State: S (sleeping)
Tgid: 24154
Pid: 24154
PPid: 1
TracerPid: 0
Uid: 2000 2000 2000 2000
Gid: 2000 2000 2000 2000
FDSize: 32
Groups: 1004 1007 1011 1015 1028 3001 3002 3003 3006
VmPeak: 777484 kB
VmSize: 777484 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 26960 kB
VmRSS: 26960 kB
VmData: 11680 kB
VmStk: 8192 kB
VmExe: 12 kB
VmLib: 52812 kB
VmPTE: 134 kB
VmSwap: 0 kB
Threads: 13
SigQ: 0/6947
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000001204
SigIgn: 0000000000000001
SigCgt: 00000002000094f8
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000000000c0
Seccomp: 0
Cpus_allowed: f
Cpus_allowed_list: 0-3
voluntary_ctxt_switches: 18
nonvoluntary_ctxt_switches: 76

Android

procrank

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rocrank
PID Vss Rss Pss Uss cmdline
1078 59840K 59708K 42125K 39344K com.csr.BTApp
2683 59124K 59040K 37960K 33032K com.android.launcher
1042 51572K 51488K 35686K 33604K android.process.acore
782 32808K 32748K 16775K 14716K system_server
667 20560K 17560K 12739K 8940K /system/bin/surfaceflinger
851 30124K 30036K 12085K 7996K com.android.systemui
2999 27680K 27596K 9929K 7040K com.baidu.input
959 20764K 20676K 5522K 3788K com.android.phone
3468 21892K 21800K 4591K 1920K com.apical.dreamthemetime
982 19880K 19792K 4438K 2644K com.csr.csrservices
668 19592K 19480K 3525K 1360K zygote
670 2960K 2960K 2407K 2356K /system/bin/mediaserver
663 1784K 1784K 1209K 1116K /system/bin/synergy_service
756 3404K 1348K 1133K 1124K /usr/bin/gpsexe
669 1468K 1468K 959K 928K /system/bin/drmserver
675 692K 692K 692K 692K /bin/sh
3482 656K 652K 456K 444K procrank
1 164K 164K 144K 144K /init
------ ------ ------
195031K 163724K TOTAL

RAM: 480380K total, 3624K free, 732K buffers, 299788K cached, 264844K shmem, 7632K slab

dumpsys

1
2
3
4
5
6
7
dumpsys [options]
meminfo 显示内存信息
cpuinfo 显示CPU信息
account 显示accounts信息
activity 显示所有的activities的信息
window 显示键盘,窗口和它们的关系
wifi 显示wifi信息

可以在其后通过包名或者进程pid展示指定进程的信息。

UNIX与Linux

Linux 是一个类似 Unix 的操作系统(类UNIX系统),Unix 要早于 Linux,Linux 的初衷就是要替代 UNIX,并在功能和用户体验上进行优化,所以 Linux 模仿了 UNIX(但并没有抄袭 UNIX 的源码),使得 Linux 在外观和交互上与 UNIX 非常类似。

二者的区别:

  • UNIX 系统大多是与硬件配套的,也就是说,大多数 UNIX 系统如 AIX、HP-UX 等是无法安装在 x86 服务器和个人计算机上的,而 Linux 则可以运行在多种硬件平台上;
  • UNIX 是商业软件,而 Linux 是开源软件,是免费、公开源代码的。

UNIX/Linux系统结构:可以粗糙地抽象为 3 个层次:底层是 UNIX/Linux 操作系统,即系统内核(Kernel);中间层是 Shell 层,即命令解释层;高层则是应用层。

  1. 内核层:内核层是 UNIX/Linux 系统的核心和基础,它直接附着在硬件平台之上,控制和管理系统内各种资源(硬件资源和软件资源),有效地组织进程的运行,从而扩展硬件的功能,提高资源的利用效率,为用户提供方便、高效、安全、可靠的应用环境。Linux 内核由如下几部分组成:内存管理、进程管理、设备驱动程序、文件系统和网络管理等。
  2. Shell层:Shell 层是与用户直接交互的界面。用户可以在提示符下输入命令行,由 Shell 解释执行并输出相应结果或者有关信息,所以我们也把 Shell 称作命令解释器,利用系统提供的丰富命令可以快捷而简便地完成许多工作。目前主要有下列版本的shell:
    • Bourne Shell(sh):贝尔实验室开发的  
    • Bourne Again Shell(bash):GNU操作系统上默认的shell,大部分linux的发行套件使用的都是这种shell
    • Korn Shell:是对Bourne SHell的发展,在大部分内容上与Bourne Shell兼容
    • C Shell:是SUN公司Shell的BSD版本
  3. 应用层:应用层提供基于 X Window 协议的图形环境。X Window 协议定义了一个系统所必须具备的功能,可系统能满足此协议及符合 X 协会其他的规范,便可称为 X Window。

Linux内核与发行版

Linux内核是计算机操作系统的核心。一个完整的 Linux发行版包括了内核与一些其他与文件相关的操作,用户管理系统,和软件包管理器等一系列软件。每个工具都是整个系统的一小部分。这些工具通常都是一个个独立的项目,有相应的开发者来开发及维护。

Linux的众多发行版可能是基于不同的内核版本的。例如:流行的 RHEL6发行版是基于很老但是很稳定的 2.6.32 版本的Linux内核的。其他的一些发行版可能会很快的更新以适应最新的内核版本。需要特别注意的一点是,内核并不是一个非此即彼的命题,例如RHEL6就在2.6.32的内核中引进了新版本内核的许多改进。

各发行版提供的其他基本工具和组成部分还有包括以下的内容:C/C++编译器,gdbdebugger 调试工具,核心系统库应用程序,用于在屏幕上绘图的底层接口以及高级的桌面环境,以及供安装和更新包括内核在内的众多组建的系统

Debian是包括Ubuntu在内许多发行版的上游,而Ubuntu又是Linux Mint及其他发行版的上游。Debian在服务器和桌面电脑领域都有着广泛的应用。Debian是一个纯开源计划并着重在一个关键点上,稳定性。它同时也提供了最大的和完整的软件仓库给用户。

Android与Linux

  • Android采用Linux作为内核
  • Android对Linux内核做了修改,目的是适应在移动设备上使用
  • Android开始作为Linux的一个分支,后来由于无法并入Linux的主开发树,已被Linux Kernel小组从开发树中删除

Android是在Linux内核基础上运行的,提供的核心系统服务包括安全、内存管理、进程管理、网络组和驱动模型等内容。在硬件层和系统中其他软件之间添加了硬件抽象层(HAL),严格上来说Android不算是Linux系统

Android内核是有标准的Linux内核修改而来的,继承了Linux内核的诸多优点,保留了Linux内核的主题框架,同时Android按照移动设备的要求,在文件系统、内存管理、进程间通信机智和电源管理方面进行了修改,添加了相关的驱动程序和必要的新功能。Android在很大程度上保留了Linux的基本架构。

GNU

GPL:GNU通用公共许可协议(英语:GNU General Public License,缩写GNU GPL 或 GPL),是被广泛使用的自由软件许可证,给予了终端用户运行、学习、共享和修改软件的自由。

GNU是一个自由的操作系统,其内容软件完全以GPL方式发布。这个操作系统是GNU计划的主要目标,名称来自GNU’s Not Unix!的递归缩写,因为GNU的设计类似Unix,但它不包含具著作权的Unix代码。GNU的创始人,理查德·马修·斯托曼,将GNU视为“达成社会目的技术方法”。

作为操作系统,GNU的发展仍未完成,其中最大的问题是具有完备功能的内核尚未被开发成功。GNU的内核,称为Hurd,是自由软件基金会发展的重点,但是其发展尚未成熟。在实际使用上,多半使用Linux内核、FreeBSD等替代方案,作为系统核心,其中主要的操作系统是Linux的发行版。Linux操作系统包涵了Linux内核与其他自由软件项目中的GNU组件和软件,可以被称为GNU/Linux。

GNU/Linux命名争议,是在自由及开放源代码软件社群成员内的,关于是应该把使用GNU软件与Linux内核组合之操作系统称为“GNU/Linux”还是“Linux”的争议。

GNU/Linux这一名称是由自由软件基金会的创立者与GNU计划的发起人理查德·斯托曼所提出的。GNU的开发者与其支持者,希望以该名称来作为此操作系统的正式名称。他们认为,此操作系统,包括了GNU系统软件包与Linux kernel,使用GNU/Linux这个名称,可以良好概括它的主要内容。况且,GNU项目原本就是以发展一个自由的操作系统为远程项目,但迟迟没有完成。而Linux kernel的出现刚好可以补足这个缺口。

Linux内核本身并不是GNU计划的一部分,GNU/Linux这个名称在Linux社群中并没有得到一致认同。一些发行版社群例如Debian采用了GNU/Linux这一名称,但许多Linux社群中的成员认为使用Linux这一名称是更好的,为此提出了数项理由,主张Linux这个名称朗朗上口,且在公众与媒体中更为通用。Linux内核项目的发起人林纳斯·托瓦兹(Linus Torvalds)偏好于使用Linux,但对于GNU/Linux这个名字并不强烈反感。

一切皆文件

描述

linux/unix下的哲学核心思想是一切皆文件。它指的是,对所有文件(目录、字符设备、块设备、套接字、打印机、进程、线程、管道等)操作,读写都可用fopen()/fclose()/fwrite()/fread()等函数进行处理,屏蔽了硬件的区别,所有设备都抽象成文件,提供统一的接口给用户,虽然类型各不相同,但是对其提供的却是同一套操作界面,更进一步,对文件的操作也可以跨文件系统执行。

操作一个已经打开的文件:使用文件描述符(file descriptor),简称fd,它是一个对应某个已经打开的文件的索引(非负整数)。

Windows的内部实现也近似于“一切皆文件”的思想,当然,这一切都只在内核里才有,下载一个WinObj这软件就可以看到,Windows上各种设备、分区、虚拟对象都是挂载到根“\”下的,通过这个树可以访问各种设备、驱动、文件系统等。

Windows与Linux不同的就是把这些对象又重新封装了一层WindowsAPI,对外以设备、盘符、文件等等表现出来,重新封装WindowsAPI的目的是为了兼容性,而设备、盘符、文件这些是为了让普通用户更好理解。

文件类型

Linux文件类型

Inode

概述

inode是一个重要概念,是理解Unix/Linux文件系统和硬盘储存的基础。

文件储存在硬盘上,硬盘的最小存储单位叫做”扇区”(Sector)。每个扇区储存512字节(相当于0.5KB)。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”(block)。这种由多个扇区组成的”块”,是文件存取的最小单位。”块”的大小,最常见的是4KB,即连续八个 sector组成一个 block。

文件数据都储存在”块”中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为”索引节点”。每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

inode的内容

inode包含文件的元信息,具体来说有以下内容:

  • 文件的字节数
  • 文件拥有者的User ID
  • 文件的Group ID
  • 文件的读、写、执行权限
  • 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
  • 链接数,即有多少文件名指向这个inode
  • 文件数据block的位置

可以用stat命令,查看某个文件的inode信息:

1
2
3
4
5
6
7
8
9
$ stat README.md
File: README.md
Size: 15 Blocks: 0 IO Block: 4096 regular file
Device: fh/15d Inode: 30680772461790950 Links: 1
Access: (0777/-rwxrwxrwx) Uid: ( 1000/ hearing) Gid: ( 1000/ hearing)
Access: 2019-08-16 20:56:58.121431000 +0800
Modify: 2019-08-16 20:56:58.121431000 +0800
Change: 2019-08-16 20:56:58.121431000 +0800
Birth: -

总之,除了文件名以外的所有文件信息,都存在inode之中。至于为什么没有文件名,下文会有详细解释。

inode的大小

inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。

每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。

查看每个硬盘分区的inode总数和已经使用的数量,可以使用df -i命令。

查看每个inode节点的大小,可以用如下命令:sudo dumpe2fs -h /dev/hda | grep "Inode size"

由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。

inode号码

每个inode都有一个号码,操作系统用inode号码来识别不同的文件。Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。

表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

使用ls -i命令,可以看到文件名对应的inode号码:

1
2
$ ls -i README.md
30680772461790950 README.md

目录文件

Unix/Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ls命令只列出目录文件中的所有文件名:
$ ls
HybridAccessory README.md _config.yml db.json gulpfile.js node_modules package-lock.json package.json scaffolds source themes

# ls -i命令列出整个目录文件,即文件名和inode号码:
$ ls -i
281474976845551 HybridAccessory 5348024557503872 db.json 281474976858064 package-lock.json 281474976858853 source
30680772461790950 README.md 281474976845560 gulpfile.js 281474976858065 package.json 281474976859117 themes
1125899906905575 _config.yml 281474976845561 node_modules 281474976858849 scaffolds

# ls -l命令列出文件的详细信息:
$ ls -l
total 280
drwxrwxrwx 1 hearing hearing 4096 Jul 22 14:06 HybridAccessory
-rwxrwxrwx 1 hearing hearing 15 Aug 16 20:56 README.md
-rwxrwxrwx 1 hearing hearing 2189 Aug 19 12:34 _config.yml
-rwxrwxrwx 1 hearing hearing 174 Aug 27 17:55 db.json
-rwxrwxrwx 1 hearing hearing 1596 Jul 22 14:06 gulpfile.js
drwxrwxrwx 1 hearing hearing 4096 Jul 22 14:06 node_modules
-rwxrwxrwx 1 hearing hearing 277830 Jul 22 14:06 package-lock.json
-rwxrwxrwx 1 hearing hearing 686 Jul 22 14:06 package.json
drwxrwxrwx 1 hearing hearing 4096 Jul 22 14:06 scaffolds
drwxrwxrwx 1 hearing hearing 4096 Jul 29 11:35 source
drwxrwxrwx 1 hearing hearing 4096 Aug 16 20:51 themes

目录文件的读权限(r)和写权限(w),都是针对目录文件本身。由于目录文件内只有文件名和inode号码,所以如果只有读权限,只能获取文件名,无法获取其他信息,因为其他信息都储存在inode节点中,而读取inode节点内的信息需要目录文件的执行权限(x)。

查看目录大小:

1
2
$ du -h --max-depth=1
$ du -h -d 1

硬链接

一般情况下,文件名和inode号码是”一一对应”关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。

这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为”硬链接”(hard link)。

ln命令可以创建硬链接:ln 源文件 目标文件

1
2
3
4
5
6
7
8
9
10
11
12
$ ls -li
total 0
16044073672577795 drwxrwxrwx 1 hearing hearing 4096 Aug 27 18:07 test
14918173765727074 -rw-rw-rw- 1 hearing hearing 0 Aug 27 18:06 test.txt

$ ln test.txt test1

$ ls -li
total 0
16044073672577795 drwxrwxrwx 1 hearing hearing 4096 Aug 27 18:07 test
14918173765727074 -rw-rw-rw- 2 hearing hearing 0 Aug 27 18:06 test.txt
14918173765727074 -rw-rw-rw- 2 hearing hearing 0 Aug 27 18:06 test1

运行上面这条命令以后,源文件与目标文件的inode号码相同,都指向同一个inode。inode信息中有一项叫做”链接数”,记录指向该inode的文件名总数,这时就会增加1。反过来,删除一个文件名,就会使得inode节点中的”链接数”减1。当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。

创建目录时,默认会生成两个目录项:”.”和”..”。前者的inode号码就是当前目录的inode号码,等同于当前目录的”硬链接”;后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的”硬链接”。

1
2
3
4
5
$ ls -ai
9570149208327005 . 1970324837227736 .. 16044073672577795 test 4785074604151560 test2

$ ls -i ../
9570149208327005 Downloads

目录不能创建硬链接(hard link not allowed for directory)。

软链接(快捷方式)

文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的”软链接”(soft link)或者”符号链接(symbolic link)。

这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:”No such file or directory”。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode”链接数”不会因此发生变化。

ln -s命令可以创建软链接:ln -s 源文文件或目录 目标文件或目录

1
2
3
4
5
6
7
$ ln -s test2 test1

$ ls -l
total 0
drwxrwxrwx 1 hearing hearing 4096 Aug 27 18:07 test
lrwxrwxrwx 1 hearing hearing 5 Aug 27 18:17 test1 -> test2
-rw-rw-rw- 1 hearing hearing 5 Aug 27 18:09 test2

inode的特殊作用

由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。

  1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
  2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。
  3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。

第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。

文件描述符

概念

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

文件描述符、文件、进程间的关系

1.描述:

  • 每个文件描述符会与一个打开的文件相对应
  • 不同的文件描述符也可能指向同一个文件
  • 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开

2.系统为维护文件描述符,建立了三个表

  • 进程级的文件描述符表
  • 系统级的文件描述符表
  • 文件系统的i-node表
  • 在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(#23),这可能是该进程多次对执行打开操作
  • 进程A中的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(#73),这种情况有几种可能,1.进程A和进程B可能是父子进程关系;2.进程A和进程B打开了同一个文件,且文件描述符相同(低概率事件=_=);3.A、B中某个进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程。
  • 进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(#1936),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了打开请求。同一个进程两次打开同一个文件,也会发生类似情况。

文件描述符限制

“文件描述符”也是一种资源,系统中的每个进程都需要有“文件描述符”才能进行一些操作。

永久修改用户级限制时有三种设置类型:

  • soft 指的是当前系统生效的设置值
  • hard 指的是系统中所能设定的最大值
  • - 指的是同时设置了 soft 和 hard 的值

检查某个进程的文件描述符相关内容

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
# 查看指定进程的限制:在 Max open files 那一行,可以看到当前设置中最大文件描述符的数量为1024
$ cat /proc/803/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 8041 8041 processes
Max open files 1024 4096 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 8041 8041 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 40 40
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us

# 查看该进程占用了多少个文件描述符
$ ll /proc/803/fd | wc -l
5

$ ll /proc/803/fd
total 0
lrwx------ 1 hearing hearing 0 Aug 27 20:14 0 -> /dev/tty3
lrwx------ 1 hearing hearing 0 Aug 27 20:14 1 -> /dev/tty3
lrwx------ 1 hearing hearing 0 Aug 27 20:14 10 -> /dev/tty3
lrwx------ 1 hearing hearing 0 Aug 27 20:14 2 -> /dev/tty3

总结

实际应用过程中,如果出现“Too many open files” , 可以通过增大进程可用的文件描述符数量来解决,但往往故事不会这样结束,很多时候,并不是因为进程可用的文件描述符过少,而是因为程序bug,打开了大量的文件连接(web连接也会占用文件描述符)而没有释放。程序申请的资源在用完后及时释放,才是解决“Too many open files”的根本之道。

证书&CA

  • 证书(“digital certificate”或“public key certificate”):它是用来证明某某东西确实是某某东西的东西
  • CA(Certificate Authority):证书授权中心,负责管理和签发证书的第三方机构
  • CA 证书:就是CA颁发的证书

apt&dpkg

概述

在Linux平台下使用源代码进行软件编译可以具有定制化的设置,但对于Linux distribution的发行商来说,毕竟不是每个人都会进行源代码编译的,这个问题将会严重的影响linux平台上软件的发行与推广。

为了解决上述的问题,厂商先在他们的系统上面编译好了用户所需要的软件,然后将这个编译好并可执行的软件直接发布给用户安装。不同的 Linux 发行版使用不同的打包系统,一般而言,大多数发行版分别属于两大包管理技术阵营:Debian 的”.deb”,和 Red Hat的”.rpm”。

dpkg

简介

dpkg - package manager for Debian。dpkg是Debian的一个底层包管理工具,主要用于对已下载到本地和已安装的软件包进行管理。

通过dpkg的机制,Debian提供的软件就能够简单的安装起来,同时能提供安装后的软件信息,只要派生于Debian的其它Linux distributions大多使用dpkg这个机制来管理,包括B2D,Ubuntu,Deepin等。

deb软件包名规则

格式为:Package_Version-Build_Architecture.deb,如:nano_1.3.10-2_i386.deb:

  • 软件包名称(Package Name): nano
  • 版本(Version Number):1.3.10
  • 修订号(Build Number):2
  • 平台(Architecture):i386

dpkg软件包相关文件

  • /etc/dpkg/dpkg.cfg:dpkg包管理软件的配置文件【Configuration file with default options】
  • /var/log/dpkg.log:dpkg包管理软件的日志文件【Default log file (see /etc/dpkg/dpkg.cfg(5) and option –log)】
  • /var/lib/dpkg/available:存放系统所有安装过的软件包信息【List of available packages.】
  • /var/lib/dpkg/status:存放系统现在所有安装软件的状态信息
  • /var/lib/dpkg/info:记安装软件包控制目录的控制信息文件

dpkg数据库

dpkg 使用文本文件作为数据库来维护系统中软件,包括文件清单, 依赖关系, 软件状态, 等等详细的内容,通常在 /var/lib/dpkg 目录下。通常在 status 文件中存储软件状态和控制信息。在 info/ 目录下备份控制文件,并在其下的 .list 文件中记录安装文件清单,其下的 .mdasums 保存文件的 MD5 编码。

使用手册

1
2
3
4
5
6
7
8
9
10
11
# 安装软件包,必须是deb包的完整名称。(软件的安装可被拆分为两个对立的过程“解包”和“配置”)
dpkg -i package-name.deb

# “解包”:解开软件包到系统目录但不配置,如果和-R一起使用,参数可以是一个目录
dpkg --unpack package-name.deb

# “配置”:配置软件包
dpkg --configure package-name.deb

# 列出 deb 包的内容
dpkg -c package-name.deb

安装相关选项:

  • -R:递归地指向特定目录的所有安装包,可以结合-i, -A, –install, –unpack 与 –avail一起使用
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
# --remove,移除软件包,但保留其配置文件
dpkg -r package-name

# --purge,清除软件包的所有文件(removes everything,including conffiles)
dpkg -P package-name

# --list, 查看系统中软件包名符合pattern模式的软件包
dpkg -l package-name-pattern

# --listfiles, 查看package-name对应的软件包安装的文件及目录
dpkg -L package-name

# --print-avail, 显示包的具体信息
dpkg -p package-name

# --status, 查看package-name(已安装)对应的软件包信息
dpkg -s package-name

# --search,从已经安装的软件包中查找包含filename的软件包名称 (Tip:也可使用子命令dpkg-query来进行查询操作)
dpkg -S filename-search-pattern

# 解包
dpkg -X pkg.deb

# 例:列出系统上安装的与dpkg相关的软件包
dpkg -l \*dpkg*

# 例:查看dpkg软件包安装到系统中的文件
dpkg -L dpkg

apt

apt简介

虽然我们在使用dpkg时,已经解决掉了软件安装过程中的大量问题,但是当依赖关系不满足时,仍然需要手动解决,而apt这个工具解决了这样的问题,linux distribution 先将软件放置到对应的服务器中,然后分析软件的依赖关系,并且记录下来,然后当客户端有安装软件需求时,通过清单列表与本地的dpkg以存在的软件数据相比较,就能从网络端获取所有需要的具有依赖属性的软件了。

工作原理

Debian 采用集中式的软件仓库机制,将各式各样的软件包分门别类地存放在软件仓库中,进行有效地组织和管理。然后,将软件仓库置于许许多多的镜像服务器中,并保持基本一致。这样,所有的 Debian 用户随时都能获得最新版本的安装软件包。因此,对于用户,这些镜像服务器就是他们的软件源(Reposity)

然而,由于每位用户所处的网络环境不同,不可能随意地访问各镜像站点。为了能够有选择地访问,在 Debian 系统中,使用软件源配置文件/etc/apt/sources.list列出最合适访问的镜像站点地址。

apt udpate过程:

  1. 执行apt-get update
  2. 程序分析/etc/apt/sources.list
  3. 自动连网寻找list中对应的Packages/Sources/Release列表文件,如果有更新则下载之,存入/var/lib/apt/lists/目录
  4. 然后 apt-get install 相应的包,下载并安装。

用户可以使用“apt-get update”命令刷新软件源,建立更新软件包列表。它会扫描每一个软件源服务器,并为该服务器所具有软件包资源建立索引文件,存放在本地的/var/lib/apt/lists/目录中。使用apt执行安装、更新操作时,都将依据这些索引文件,向软件源服务器申请资源。

apt相关文件

  • var/lib/dpkg/available 文件的内容是软件包的描述信息, 该软件包括当前系统所使用的Debian 安装源中的所有软件包,其中包括当前系统中已安装的和未安装的软件包.
  • /etc/apt/sources.list 记录软件源的地址(当你执行 sudo apt-get install xxx 时,Ubuntu 就去这些站点下载软件包到本地并执行安装)
  • /var/cache/apt/archives 已经下载到的软件包都放在这里(用 apt-get install 安装软件时,软件包的临时存放路径)
  • /var/lib/apt/lists 使用apt-get update命令会从/etc/apt/sources.list中下载软件列表,并保存到该目录

创建apt软件源

概述

以下是我搭建的软件源目录结构:

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
hearing/
├── bootstraps
│   ├── bootstrap-aarch64
│   ├── bootstrap-aarch64.zip
│   ├── bootstrap-arm.zip
│   ├── bootstrap-i686.zip
│   └── bootstrap-x86_64.zip
├── repository
│   ├── dists
│ | └── stable
│ | ├── InRelease
│ | ├── main
│ | │   ├── binary-aarch64
│ | │   │   └── Packages
│ | │   ├── binary-all
│ | │   │   └── Packages
│ | │   ├── binary-arm
│ | │   │   └── Packages
│ | │   ├── binary-i686
│ | │   │   └── Packages
│ | │   └── binary-x86_64
│ | │   └── Packages
│ | ├── Release
| | ├── Release.gpg
| | └── repo.asc
│   └── pool
│   ├── aarch64
│    | ├── xxx.deb
│   ├── all
│    | ├── xxx.deb
│   ├── arm
│    | ├── xxx.deb
│   ├── i686
│    | ├── xxx.deb
│   └── x86_64
│    ├── xxx.deb
└── tmp
├── xxx.tar.gz
  • bootstraps目录下放的是最终编译成功的zip文件
  • repository是自己制作的软件仓库
  • tmp放置的是上一步编译中下载失败的一些依赖

引用

根据https://wiki.debian.org/DebianRepository/Setup中的说法,仓库分为两种,一种比较简单的是trivial archive,而另外一种复杂的仓库称为official archive。在一个official archive中,典型特征是顶层有个 dists 目录和 pool 目录。这样的好处是:

  • 将所有类型CPU的包列表(Packages或者Packages.gz文件)放在一个文件里面,这样每个机器要获取的包列表就比较小。
  • 不同套件/不同CPU可共用的deb包(主要是那些 _all.deb)和源代码包,也只在 pool/all目录下存放一份。
  • 源代码包(.dsc,orig.tar.xz)有路径存放,这样 dget / apt source 可以取到源代码包。

对应的/etc/apt/sources.list配置如下:

1
deb http://192.168.56.47:80/hearing/repository stable main

配置软件源的格式为:

1
deb|deb-src uri distribution [component1] [component2] [...]

生成Packages

Packages文件包括每个deb包的位置,描述,版本等信息,生成命令:

1
2
dpkg-scanpackages all/ /dev/null| gzip -9c > dists/stable/main/binary-all/Packages.gz
dpkg-scanpackages all/ /dev/null| > dists/stable/main/binary-all/Packages

生成Release

Release文件里面包含了 Packages 等文件的大小和校验和(包含MD5/SHA1/SHA256/SHA512 多种值),如果这个文件里面所描述的 Packages 大小与校验和与实际读取到的文件不一致,apt 会拒绝这个仓库。生成命令:

1
apt-ftparchive release $dir > Release

生成Release.gpg 和 InRelease 文件

Release.gpg 是一个签名文件,随同 Release 一起出现的,比较老的客户端只认这两个文件,而 InRelease 是内嵌签名的(也就是说,将原来 Release 的内容和 Release.gpg 的内容揉到一起了,注意这里不是简单地拼到一起),新的客户端才支持这个这个文件,观察一下 Debian 和 Ubuntu 的仓库 ( http://mirrors.ustc.edu.cn/debian/dists/jessie/, http://mirrors.ustc.edu.cn/ubuntu/dists/xenial/ ) ,可以看到 Debian 的仓库只有 Release 和 Release.gpg 这两个文件,而 Ubuntu 仓库里面这三个文件都有。

如何生成这两个文件:

  • 生成自己的gpg key: gpg --list-keys || gpg --gen-key
  • 生成Release.gpg: gpg --armor --detach-sign --sign -o Release.gpg Release
  • 生成InRelease: gpg --clearsign -o InRelease Release

导入公钥

当运行apt update的时候,会出现警告:The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 722D2AFAD8BAD548

也就是说 InRelease / Release.gpg 虽然签名了,但由于这个签名所用的公钥没有被接受,因此还是不能正常使用,有三种解决方法:

  1. 服务端将公钥导出,然后提供给客户端导入:

    1
    2
    3
    4
    5
    # 导出
    gpg --export --armor 722D2AFAD8BAD548 -o my-repo.gpg-key.asc

    # 导入
    sudo apt-key add my-repo.gpg-key.asc
  2. 客户端在执行apt update的时候,添加--allow-insecure-repositories选项;在执行apt install pkg的时候,添加--allow-unauthenticated选项。

  3. 用户修改仓库的配置,改为deb [trusted=yes] http://192.168.56.47:80/hearing/repository stable main

使用手册

  1. apt-get update 更新源
  2. apt-get dist-upgrade 升级系统到相应的发行版(根据 source.list 的配置)
  3. apt-get upgrade 更新所有已经安装的软件包
  4. apt-get install package_name 安装软件包(加上 –reinstall重新安装),apt-get install package_name=version 安装指定版本的软件包
  5. apt-get remove package_name 卸载一个已安装的软件包(保留配置文件)
  6. apt-get purge package_name 移除软件包(删除配置信息)或apt-get –purge remove packagename
  7. apt-get check 检查是否有损坏的依赖
  8. apt-get autoclean:删除你已经删掉的软件(定期运行这个命令来清除那些已经卸载的软件包的.deb文件。通过这种方式,可以释放大量的磁盘空间。可以使用apt-get
    clean以释放更多空间。这个命令会将已安装软件包裹的.deb文件一并删除)
  9. apt-get clean 把安装的软件的备份也删除,不过这样不会影响软件的使用

apt-cache对APT的程序包缓存执行各种操作。apt-cache不会操纵系统状态,但会提供操作以从包元数据中搜索并生成输出。

  1. apt-cache depends packagename 了解使用依赖
  2. apt-cache rdepends packagename 是查看该包被哪些包依赖
  3. apt-cache search packagename 搜索包
  4. apt-cache show packagename 获取包的相关信息,如说明、大小、版本等
  5. apt-cache showpkg packagename 显示软件包的大致信息

概述

Termux是一个Android下一个高级的终端模拟器,开源且不需要root,支持apt管理软件包,拥有自己的apt仓库源,可以十分方便地安装软件包,可以实现支持Python,PHP,Ruby,Go,Nodejs,MySQL等功能环境。

仓库地址
修改包名后台开启终端的仓库地址

一切皆文件

linux/unix下的哲学核心思想是一切皆文件。它指的是,对所有文件(目录、字符设备、块设备、套接字、打印机、进程、线程、管道等)操作,读写都可用fopen()/fclose()/fwrite()/fread()等函数进行处理,屏蔽了硬件的区别,所有设备都抽象成文件,提供统一的接口给用户,虽然类型各不相同,但是对其提供的却是同一套操作界面,更进一步,对文件的操作也可以跨文件系统执行。

操作一个已经打开的文件:使用文件描述符(file descriptor),简称fd,它是一个对应某个已经打开的文件的索引(非负整数)。

文件描述符

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

修改包名

主要是指定目录的前缀.

Termux修改包名

源码解读

流程图:

Termux流程图

setupIfNeeded

bootstraps.zip目录结构:

1
2
3
4
5
6
7
8
9
10
11
bootstrap-aarch64
├── SYMLINKS.txt
├── bin
├── etc
├── include
├── lib
├── libexec
├── repo.asc
├── share
├── tmp
└── var
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that:
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) {
Termux.mInstance.setInstalled(false);
Termux.mInstance.getTermuxHandle().initFail();
return;
}

final File PREFIX_FILE = new File(Termux.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) {
whenDone.run();
return;
}

new Thread() {
@Override
public void run() {
try {
final String STAGING_PREFIX_PATH = Termux.FILES_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);

if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE);
}

final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);

final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line;
while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split("←");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));

ensureDirectoryExists(new File(newPath).getParentFile());
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();

ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());

if (!isDirectory) {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
}
}
}
}

if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
Os.symlink(symlink.first, symlink.second);
}

if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder");
}
activity.runOnUiThread(whenDone);
} catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
Termux.mInstance.setInstalled(false);
Termux.mInstance.getTermuxHandle().initFail();
}
}
}.start();
}

private static void ensureDirectoryExists(File directory) {
if (!directory.isDirectory() && !directory.mkdirs()) {
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
}
}

/**
* Get bootstrap zip url for this systems cpu architecture.
*/
private static URL determineZipUrl() throws MalformedURLException {
String archName = determineTermuxArchName();
String url = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? "https://termux.org/bootstrap-" + archName + ".zip"
: "https://termux.net/bootstrap/bootstrap-" + archName + ".zip";
return new URL(url);
}

private static String determineTermuxArchName() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (the ordering of the
// Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
// emulation is available.
for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
case "arm64-v8a":
return "aarch64";
case "armeabi-v7a":
return "arm";
case "x86_64":
return "x86_64";
case "x86":
return "i686";
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
}

createSubprocess

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
static int create_subprocess(JNIEnv* env,
char const* cmd,
char const* cwd,
char* const argv[],
char** envp,
int* pProcessId,
jint rows,
jint columns)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");

#ifdef LACKS_PTSNAME_R
char* devname;
#else
char devname[64];
#endif
if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL
#else
ptsname_r(ptm, devname, sizeof(devname))
#endif
) {
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}

// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);

/** Set initial winsize. */
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
ioctl(ptm, TIOCSWINSZ, &sz);

pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);

close(ptm);
setsid();

int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);

dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);

DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if(fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}

clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);

if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
}

操作terminal

1
2
3
4
5
6
7
8
9
10
/**
* A queue written to from a separate thread when the process outputs, and read by main thread to process by
* terminal emulator.
*/
private final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
/**
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
* writing to the {@link #mTerminalFileDescriptor}.
*/
private final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
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
48
49
50
public void initializeEmulator(int columns, int rows) {

int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];

final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);

new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override
public void run() {
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
final byte[] buffer = new byte[4096];
while (true) {
int read = termIn.read(buffer);
if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
}
} catch (Exception e) {
// Ignore, just shutting down.
}
}
}.start();

new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
@Override
public void run() {
final byte[] buffer = new byte[4096];
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite);
}
} catch (IOException e) {
// Ignore.
}
}
}.start();

new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
@Override
public void run() {
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}.start();

}

参数

1
2
3
4
5
6
mShellPath: /data/data/$pkg/files/usr/bin/login
mCwd: /data/data/$pkg/files/home
mArgs: [-login]
mEnv: [TERM=xterm-256color, HOME=/data/data/$pkg/files/home, PREFIX=/data/data/$pkg/files/usr, BOOTCLASSPATH/system/framework/com.qualcomm.qti.camera.jar:/system/framework/QPerformance.jar:/system/framework/core-oj.jar:/system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/bouncycastle.jar:/system/framework/apache-xml.jar:/system/framework/legacy-test.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/org.apache.http.legacy.boot.jar:/system/framework/android.hidl.base-V1.0-java.jar:/system/framework/android.hidl.manager-V1.0-java.jar:/system/framework/tcmiface.jar:/system/framework/WfdCommon.jar:/system/framework/oem-services.jar:/system/framework/qcom.fmradio.jar:/system/framework/telephony-ext.jar:/system/app/miui/miui.apk:/system/app/miuisystem/miuisystem.apk, ANDROID_ROOT=/system, ANDROID_DATA=/data, EXTERNAL_STORAGE=/sdcard, LD_LIBRARY_PATH=/data/data/$pkg/files/usr/lib, LANG=en_US.UTF-8, PATH=/data/data/$pkg/files/usr/bin:/data/data/$pkg/files/usr/bin/applets, PWD=/data/data/$pkg/files/home, TMPDIR=/data/data/$pkg/files/usr/tmp]
mShellPid: 15381
mTerminalFileDescriptor: 54

概述

Android SDK 自带了混淆工具Proguard。它位于SDK根目录\tools\proguard下面。如果开启了混淆,Proguard默认情况下会对所有代码,包括第三方包都进行混淆,可是有些代码或者第三方包是不能混淆的,这就需要我们手动编写混淆规则来保持不能被混淆的部分。

混淆有几个作用(默认开启):

  1. 【优化】它能优化java的字节码,使程序运行更快(-dontoptimize:关闭优化);
  2. 【压缩】最直观的就是减少App大小,在混淆过程中它会找出未被使用过的类和类成员并删除他们(-dontshrink,关闭压缩);
  3. 【混淆】这个功能使我们的java代码中的类、函数、变量名随机变成无意义的代号(-dontobfuscate:关闭混淆)。

在Android Studio新版本中启用了R8混淆:

开启混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android {
compileSdkVersion 29
buildToolsVersion "29.0.1"
defaultConfig {
}
buildTypes {
release {
zipAlignEnabled true // 能优化java字节码,提高运行效率
minifyEnabled true // 开启混淆
// 'proguard-android.txt' 是默认导入的规则,这个文件位于/tools/proguard/下
// 'proguard-rules.pro'是针对我们自己的项目需要特别定义混淆规则
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
....
}

混淆规则

实例

proguard-android.txt文件内容如下:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#混淆时不生成大小写混合的类名
-dontusemixedcaseclassnames
#不忽略非公共的类库
-dontskipnonpubliclibraryclasses
#混淆过程中打印详细信息
-verbose

#关闭优化
-dontoptimize
#不预校验
-dontpreverify

# Annotation注释不能混淆
-keepattributes *Annotation*
#对于NDK开发 本地的native方法不能被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#保持View的子类里面的set、get方法不被混淆(*代替任意字符)
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}

#保持Activity子类里面的参数类型为View的方法不被混淆,如被XML里面应用的onClick方法
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

#保持枚举类型values()、以及valueOf(java.lang.String)成员不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

#保持实现Parcelable接口的类里面的Creator成员不被混淆
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}

#保持R类静态成员不被混淆
-keepclassmembers class **.R$* {
public static <fields>;
}

#不警告support包中不使用的引用
-dontwarn android.support.**
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
#保持使用了Keep注解的方法以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
#保持使用了Keep注解的成员域以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}

上面默认的规则中指示了些需要保持不能别混淆的代码,包括:

  1. 继承至Android组件(Activity, Service…)的类。
  2. 自定义控件,继承至View的类(被xml文件引用到的,名字已经固定了的)
  3. enum 枚举
  4. 实现了 android.os.Parcelable 接口的
  5. Android R文件
  6. 数据库驱动…
  7. Android support 包等
  8. Android 的注释不能混淆
  9. 对于NDK开发 本地的native方法不能被混淆
  10. 对于特定的项目还有很多不能被混淆的,需要我们自己写规则来指示

自己编写proguard-rules.pro

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
48
49
#压缩级别0-7,Android一般为5(对代码迭代优化的次数)
-optimizationpasses 5

#不使用大小写混合类名
-dontusemixedcaseclassnames

#混淆时记录日志
-verbose

#不警告org.greenrobot.greendao.database包及其子包里面未引用的引用
-dontwarn org.greenrobot.greendao.database.**
-dontwarn rx.**
-dontwarn org.codehaus.jackson.**
......
#保持jackson包以及其子包的类和类成员不被混淆
-keep class org.codehaus.jackson.** {*;}
#--------重要说明-------
#-keep class 类名 {*;}
#-keepclassmembers class 类名{*;}
#一个*表示保持了该包下的类名不被混淆;
# -keep class org.codehaus.jackson.*
#二个**表示保持该包以及它包含的所有子包下的类名不被混淆
# -keep class org.codehaus.jackson.**
#------------------------
#保持类名、类里面的方法和变量不被混淆
-keep class org.codehaus.jackson.** {*;}
#不混淆类ClassTwoOne的类名以及类里面的public成员和方法
#public 可以换成其他java属性如private、public static 、final等
#还可以使<init>表示构造方法、<methods>表示方法、<fields>表示成员,
#这些前面也可以加public等java属性限定
-keep class com.dev.demo.two.ClassTwoOne {
public *;
}
#不混淆类名,以及里面的构造函数
-keep class com.dev.demo.ClassOne {
public <init>();
}
#不混淆类名,以及参数为int 的构造函数
-keep class com.dev.demo.two.ClassTwoTwo {
public <init>(int);
}
#不混淆类的public修饰的方法,和private修饰的变量
-keepclassmembers class com.dev.demo.two.ClassTwoThree {
public <methods>;
private <fields>;
}
#不混淆内部类,需要用$修饰
#不混淆内部类ClassTwoTwoInner以及里面的全部成员
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}

混淆规则

  • -keepattributes {name}:保护给定的属性不被混淆

  • -dontwarn {name}:不要警告指定库中找不到的引用。混淆在默认情况下会检查每个库的引用是否正确,但是有些第三方库里面会有用不到的类,有些没有正确引用,所以需要对第三方库取消警告,否则会报错,而且有可能混淆时间会很长

  • -keep {Modifier} {class_specification}:保留指定的类、类成员不被混淆

    1
    2
    3
    4
    #例如对一个可执行jar包来说,需要保护main的入口类,对一个类库来说需要保护它的所有public元素
    -keep public class MyMain{
    public static void main(java.lang.String[]);
    }
  • -keepclassmembers {modifier} {class_specification}:保留指定的类成员不被混淆

    1
    2
    3
    4
    5
    6
    7
    #例如指定继承了Serizalizable的类的如下成员不被混淆,成员属性和方法,非私有的属性非私有的方法.
    -keepclassmembers class * implements java.io.Serializable{
    static final long seriaVersionUID;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOuputStream);
    }
  • -keepclasseswithmembers {class_specification}:保护指定成员的类,根据成员确定一些将要被保护的类

    1
    2
    3
    4
    #保护含有main方法的类以及这个类的main方法
    -keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
    }
  • -keepnames {Modifier}{class_specification}:这个是-keep,allowshrinking {Modifier}{class_specification}的简写.意思是说允许压缩操作,如果在压缩过程中没有被删除,那么类名和该类中的成员的名字会被保护

  • -keepclassmembernames {Modifier}{class_specification}:如果在压缩过程中没有被删除在保护这个类中的成员

  • -keepclasseswithmembernames {Modifier}{class_specification}:如果在压缩过程中该类没有被删除,同样如果该类中有某个成员字段或者方法,那么保护这个类和这个方法

  • -printseeds {filename}将keep的成员输出到文件或者打印到标准输出

通配符

通配符 描述
<fields> 匹配类中的所有字段
<methods> 匹配类中的所有方法
<init> 匹配类中的所有构造函数
* 匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.,或者com.exmaple.都是无法匹配的,因为\无法匹配包名中的分隔符,正确的匹配方式是com.exmaple..*,或者com.exmaple.test.*,这些都是可以的。
** 匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。
*** 匹配任意参数类型。比如void set*(***),就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。

删除日志

1
2
3
4
5
6
7
8
9
10
11
# 删除日志 (一定要把 -dontoptimize 配置去掉,否则无法删除日志)
-assumenosideeffects class org.apache.log4j.** {*;}
-assumenosideeffects class de.mindpipe.android.logging.log4j.LogConfigurator {*;}
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}

混淆输出

成功打包后会在目录 app/build/outputs/mapping/release 下生成几个文件:

  • dump.txt 混淆后类的内部结构说明
  • mapping.txt 混淆前与混淆后名称对应关系
  • seeds.txt 经过了一系列keep语句的保持,没有被混淆的类,成员的名称列表文件
  • usage.txt 经过压缩后被删除的没有使用的代码,方法…等的名称的列表文件

ReTrace

可以使用retrace工具和mapping文件反推崩溃日志以及还原混淆代码,retrace工具在tools/proguard/bin 下,可以使用retrace命令行工具,也可以使用proguardgui工具。

1
retrace.sh -verbose mapping.txt stacktrace.txt > out.txt

有时为了Crash日志更容易定位可以在规则里面添加:

1
-keepattributes SourceFile, LineNumberTable