使用lsof查看端口占用

0x81 lsof的作用

我们都知道ls命令是Linux发行版系统最常用的命令之一,它用于列出指定目录的文件系统信息,帮助我们快速定位文件。而lsof命令的作用是什么,根据manual的解释,lsof是list open files的意思,它的作用就是列出那些被操作系统进程打开的文件,由于Linux上一切皆文件,我们可以通过这个工具找出占用某个文件的进程。

0x82 react-native的packager启动失败

一年多以前在学习RN的跨平台开发的时候,那个时候RN还是0.2x的版本,现如今都到0.44了依然没有发布正式版,fresco都1.3.0了。当然1.0也就是个版本号没什么实际意义,无非就是开发人员认为bug少到了可以放出一个正式版了而已。React-Native自退出之时受到了很多开发者的青睐,但是它对iOS的支持要远优于Android是事实,当初抱着复习js并进一步学习跨平台开发的想法,也踩了不少的坑(最大的坑就是之前写的demo由于后来有了太多的broken changes,在新版本上直接编不过,一气之下就删掉了工程)。

RN的调试状态是通过一个本地的packager服务推送新的jsbundle到设备并渲染运行的,曾经packager没有使用一个单独的shell起,就出现了packager启动失败的现象,原因就是默认的8081被占用。端口占用其实时很常见的一件事,我们在电脑上跑一个tomcat很有可能就占用了8080,如果你想再起一个使用8080端口的程序,就会提示你端口正在被使用。而packager的8081端口我可以确信时没有其他程序占用的,也就是说是它自己占用了,另一个实例起不来。其实发生这种状况的原因就是我用run-android命令调试RN应用,而这个命令会判断是否启动了packager,如果没有就会启动,但是它可能会判断失误,现在我不清楚是否有这个问题,而我后来都是手动起一个packager,就算出了问题也可以直接关掉重启。

那如果端口被占用了,我们还不知道是谁占用的怎么办?RN的文档也提到了解决办法,一是修改packager端口,这种适合8081是必须被其他程序占用的情况;另一种就是lsof了,命令很简单,输入sudo lsof -n -i4TCP:8081 | grep LISTEN就可以找到占用8081端口的进程,kill掉进程id即可。

Read More

安装weex-toolkit小记

0x81 权限问题

其实这个问题由来已久,在很久之前第一次尝试使用Weex的时候遇到过这个问题,当时按照提示解决了之后便抛于脑后,直到前一段时间我在Home目录下执行了清理命令(rm -rf * :P),当我意识到部分工具坏掉了,不得不重新进行一些工具的重装,所以我再一次遇到了权限问题。

0x82 解决方案

其实权限的问题是最容易解决的,但也是最容易忽略和出问题的。我们使用npm的-g参数全局安装weex-toolkit的时候,如果不使用类似sudo的命令提权,后出现npm没有权限像/usr/lib/node_modules目录写入文件的问题。通常的处理办法都是使用sudo再去执行命令,但是这样有一个问题,npm命令执行写入的文件的权限信息是nobody组user用户的:

npm全局安装的权限信息

而安装weex-toolkit的时候,有一个脚本会向/root目录写入.xtoolkit文件夹,而/root目录的权限是550,显然会写入失败。于是我尝试手动创建了.xtoolkit文件夹,这简直异想天开,这个错误我犯了两次,原因就是我对Linux的权限控制仍然理解不透彻。不得已我放开了/root的权限到777,安装完之后再改回550,这样weex-toolkit就可以安装成功,/root目录下npm相关的权限信息是user组和user用户,这样.xtoolkit就能写入文件了。

Java Algorithm

Example

括号匹配

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
import java.util.Scanner;
import java.util.Stack;

/**
* 括号匹配方案
* 合法的括号匹配序列被定义为:
* 1. 空串""是合法的括号序列
* 2. 如果"X"和"Y"是合法的序列,那么"XY"也是一个合法的括号序列
* 3. 如果"X"是一个合法的序列,那么"(X)"也是一个合法的括号序列
* 4. 每个合法的括号序列都可以由上面的规则生成
* 例如"", "()", "()()()", "(()())", "(((())))"都是合法的。 东东现在有一个合法的括号序列s,
* 一次移除操作分为两步:
* 1. 移除序列s中第一个左括号
* 2. 移除序列s中任意一个右括号.保证操作之后s还是一个合法的括号序列
* 东东现在想知道使用上述的移除操作有多少种方案可以把序列s变为空
* 如果两个方案中有一次移除操作移除的是不同的右括号就认为是不同的方案。
* 例如: s = "()()()()()",输出1, 因为每次都只能选择被移除的左括号所相邻的右括号.
* s = "(((())))",输出24, 第一次有4种情况, 第二次有3种情况, ... ,依次类推, 4 * 3 * 2 * 1 = 24
* 输入描述:
* 输入包括一行,一个合法的括号序列s,序列长度length(2 ≤ length ≤ 20).
* 输出描述:
* 输出一个整数,表示方案数
* 输入例子1:
* (((())))
* 输出例子1:
* 24
*
* 思路:遍历字符串,每次把左括号都压入栈,每次遇到右括号,先统计栈中有几个左括号,统计数与上次统计数相乘
* 接着弹出栈中的一个左括号
* 直到遍历结束,结果即为方案数
*/
public class BracketMatch {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String s = sc.next();

Stack<Character> stack = new Stack<>();
int result = 1;
char c;

for (int i = 0; i < s.length(); i++) {
c = s.charAt(i);
if (c == '(') {
stack.push(c);

}
if (c == ')') {
int size = stack.size();
result *= size;
stack.pop();
}
}

System.out.println(result);
}
}

魔数

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
import java.util.Arrays;
import java.util.Scanner;

/**
* 神奇数
* 东东在一本古籍上看到有一种神奇数,如果能够将一个数的数字分成两组,其中一组数字的和等于另一组数字的和,
* 我们就将这个数称为神奇数。例如242就是一个神奇数,我们能够将这个数的数字分成两组,分别是{2,2}以及{4},
* 而且这两组数的和都是4.东东现在需要统计给定区间中有多少个神奇数,即给定区间[l, r],统计这个区间中有多
* 少个神奇数,请你来帮助他。
* 输入描述:
* 输入包括一行,一行中两个整数l和r(1 ≤ l, r ≤ 10^9, 0 ≤ r - l ≤ 10^6),以空格分割
* <p>
* <p>
* 输出描述:
* 输出一个整数,即区间内的神奇数个数
* <p>
* 输入例子1:
* 1 50
* <p>
* 输出例子1:
* 4
*/
public class MagicNumber {
/**
* 首先判断数组能否被平分,即数组分割问题,
* dp[i][j]
* 表示数组前 i
* 个数字能否求和得到 j
* 则
* dp[i][j]=dp[i−1][j]||dp[i−1][j−array[i]]
* 其中||是逻辑或运算。
* 优化:
* 1、若sum(array)为奇数,直接返回false
* 2、使用逆序循环将dp数组简化为一维数组
*/

public static boolean isMagic(int[] nums, int sum) {
int len = nums.length;

if (sum % 2 != 0)
return false;

int mid = sum / 2;

int[] dp = new int[mid + 1];
dp[0] = 1;
for (int i = 0; i < len; i++) {
for (int j = mid; j > 0; j--) {
if (j >= nums[i] && nums[i] != -1)
dp[j] = Math.max(dp[j], dp[j - nums[i]]);
}
}
if (dp[mid] > 0)
return true;
else
return false;
}

public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int l = sc.nextInt();
int r = sc.nextInt();

int result = 0;

for (int i = l; i <= r; i++) {

int num = i;
int[] nums = new int[10];
int sum = 0;
Arrays.fill(nums, -1);
int index = 0;
while (num > 0) {
int temp = num % 10;
nums[index++] = temp;
sum += temp;
num = num / 10;
}

if (isMagic(nums, sum)) {
result++;
}
}

System.out.println(result);
}
}

Java Core

ArrayList

早期的ArrayList是使用数组完成的列表功能,内部关键数组 Object[] elementData和计数值size。

ArrayList的容量是动态扩充的,newCapacity = oldCapacity + (oldCapacity >> 1),即增加50%。

容量扩充使用copyarray的方式,向index位置添加元素也是同一道理,只是需要将对应位置元素全部后移。

添加新元素都要进行容量检查,容量不足时进行容量扩充,再进行具体的增加元素操作。

序列化反序列化自行实现writeObject和readObject方法,不以容量数组践行序列化,而是以实际数组进行序列化,序列化writeObject先写入size,同理readObject先读出size,再进行对应的序列化反序列化操作。该操作可能导致并发异常,比如序列化时添加内容导致size不一致。

Vector与ArrayList类似,对应操作方法由synchronized修饰保证线程安全,因此效率相对较低。

LinkedList

双向链表:HEAD NULL <- prev E next <-> prev E next <-> prev E -> NULL END

新增add实际调用linkLast,元素叫做Node,linkLast做三件事——生成新的Node、关联prev、关联next。LinkedList有一个last变量用于记录最后一个元素,如果为空,则新增的元素为第一个元素first = newNode,否则关联原本的last.next为新的Node。

查询采用折半,index小于size的1/2从HEAD开始,否则从END开始,因为没有明显的顺序数组关系,只能遍历,复杂度O(n/2)。

HashMap-JDK7

数组和链表,容量和负载因子确定了扩容策略,默认容量16~0.75

新增Element调用put方法,key通过hash得到HashCode,根据数组长度取模作为数组index。

位运算效率通常高于除模运算,因此HashMap内的数组长度通常为2^n,然后使用2^n - 1做位运算,运算方式是hashcode & (2^n - 1) = hashcode % 2^n

至于hash后取模冲突,则通过链表处理,table[index]为链表。

获取Element与put类似,key通过hash算出index,如果连标志有一个元素则就是这个元素,否则遍历链表。。

红黑树-JDK8,遍历链表是一个极为低效的行为,红黑树能将复杂度从O(n)提升到O(logn)

红黑树仅仅在某个index下元素个数达到阈值(8)才会替代链表。

建议单线程使用,否则可能死循环,多线程并发环境使用ConcurrentHashMap。

HashSet

里面有一个transient的HashMap和一个dummy Object PRESENT,add方法以E为key、PRESENT为value写入HashMap。

key的不重复性保证了HashSet中Element的不重复性。

LinkedHashMap

HashMap是无序的,LinkedHashMap是有序的。

与LinkedList有相似的顺序记录功能,通过双向链表实现。

根据写入/访问顺序排序。

存储方式仍是HashMap,通过继承HashMap的Entry添加before和after两个变量来记录顺序。

重写了主要方法以支持顺序。

MultiThread

单核心分配时间片切换线程需要做上下文切换,即保存现场,十分低效,但远比等待一个IO高效。

安全的多线程有两个条件:互相独立(没有共享变量,数据分离)和加锁。

尽量避免创建过的线程,线程池是一个很好的方案。

一个线程只获取一个锁(占用一个资源)来避免死锁。

原子性、可见性(volatile)、顺序性。

synchronized

同步锁,用于多线程安全,修饰实例方法锁对象,修饰静态方法锁Class,修饰代码块需指定锁对象。

JVM 进入退出Monitor实现同步,monitor.enter/exit,锁的获取和释放是低效的。

JDK1.6引入偏向锁,使用Compare And Swap更新锁对象的Mark Word为ThreadID,解锁时会暂停锁线程判断锁对象Mark Word设置无锁或降级轻量锁,这一时刻发生在全局安全点(无字节码运行)。

ReentrantLock

重入锁,线程获得锁可以重新加锁,使用加锁计数确定锁状态,自己不会阻塞自己,基于AQS(抽象队列同步)。

非公平锁不会像公平锁一样判断队列是否有其他线程,而是直接获取锁。

ConcurrentHashMap-JDK7

rehash的HashMap线程不安全,可能在get时死循环。

并发安全的原因是数组使用Segment,Segment继承ReentrantLock,通过锁控制。HashEntry中的value使用volatile保证可见性。

提前扩容,HashMap插入数据后扩容。

JDK8中使用CAS和synchronized保证并发安全,HashEntry直接改为Node。

内存划分

方法区、堆、虚拟机栈、本地方法栈、程序计数器,前两者线程共享。

虚拟机栈由栈帧组成,每调用一次方法都会产生。

堆,垃圾回收器工作区域,分代回收。

方法区(永久代)主要是常量、静态变量等。

类加载双亲委派模型:Bootstrap - Extension - Application - Custom,类加载先交由父加载器,无法加载再降级。这有利于像Object这种类在加载中唯一。

Mysql命令速记

0x80 前言

该篇文章主要用于记录我进行一系列mysql操作的命令笔记,会随着使用不断增加。

在Fedora上使用MariaDB进行测试。

0x81 命令

其中无前缀的是bash命令,以’:’开头的是mycli命令或全局SQL语句,以’[‘和’]’包裹的是术语,以’;’开始的为SQL语句或部分SQL语句。

systemctl start mariadb // 开启mariadb service
systemctl stop mariadb // 关闭mariadb service
mycli -u root -p {password} // 以管理员登录mysql

[ACID] // 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

:SHOW ENGINES // 显示支持的数据库引擎,InnoDB是MySql5.5.5后默认的支持事务的引擎,InnoDB支持行锁定和外键
:SHOW DATABASES // 显示所有的数据库
:USE {database} // 选择数据库
:SHOW CREATE DATABASE {database} // 查看数据库定义
:SHOW VARIABLES LIKE ‘storage_engine’ // 查看正在使用的数据库存储引擎
:SELECT @@GLOBAL.sql_mode ~ SELECT @@sql_mode // 查看sql_mode,比如only_full_group_by模式的的设置
:SET GLOBAL sql_mode=’ONLY_FULL_GROUP_BY’ // 直接指定sql_mode
[ONLY_FULL_GROUP_BY] // 新版本mysql添加的特性,用于和oracle特性相符,对于select target list 不出现在group by list的不具有单调性的元素,server拒绝查询

  1. Structure

:SHOW TABLES // 显示当前数据库下的表
;PRIMARY KEY(key,kye,eky) // 设定key,kye,eky为联合主键
[参照完整性、引用完整性] // 一个表的外键约束,其外键值可为空,若不为空则必然有对应约束表的主键与之相等
;CONSTRAINT {name} FOREIGN KEY(key) REFERENCES tb_main(id) // 外键约束
;DESC {table} // 查看表定义
;SHOW CREATE TABLE {table} // 查看表定义DSL描述
;SHOW CREATE TABLE {table}\G // 查看表定义DSL语句

Android DI利器:Dagger2 Scope

0x81 Singleton注解

在两个月前的一篇文章Android DI利器:Dagger2应用中,我书写了关于Module、Component的简单用法,其中有一个注解@Singleton用在了@Provide的模块和Component当中,表示它们是单例的。我们看下Singleton注解:

1
2
3
4
5
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
}

什么也没有,就是一个Scope注解,加了Singleton注解,并不是说它就单例了,实际上它规约了一种Scope,其表示的是同一Scope下它是单例的,因此如果我们只在一个地方进行初始化,那它就是单例的,而这个注解名也起到一定的标记作用。

0x82 Scope

Dagger使用Scope来规范对象的实例化,要做到单例的管理,有两个前提条件:

  1. Scope注解必须用在Module的provide方法上,否则并不能达到局部单例的效果。

  2. 如果Module的provide方法使用了Scope注解,那么Component就必须使用同一个注解,否则编译会失败。(从逻辑上也是不合理的)

Scope的原理明白了,使用起来就很容易了,它本身就是一个标记告诉Dagger如何维护实例,但是具体的情况还要看@Inject注入的状况。

Read More

Realm:使用Realm存储数据

0x81 Realm简单使用

昨天我写了如何在Android上启用Realm数据库在Android中使用Realm,并举了一个小例子来演示Realm操纵数据库最简单的办法,并且提到了我们的主角——事务。

0x82 Realm事务

先看昨天的例子:

1
2
3
4
5
6
7
8
Realm.init(context);
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
RealmResults<GankItem> items = realm.where(GankItem.class).findAll(); // 查询
realm.copyToRealmOrUpdate(gankItem); // 插入或更新
items.deleteAllFromRealm(); // 删除
realm.commitTransaction();
realm.close();

先初始化后获取了Realm的默认实例,之后启用一个事务,进行增删改查等操作。

通过上面的例子,我们会发现,Realm的事务机制和Fragment等事务的处理方式类似,启用事务进行操作,最后提交事务更新数据。这是事务的一种基本写法,当然这种开闭式的写法会让代码显得一块一块的,所以Realm提供一种类似回调式的写法:

1
2
3
4
5
6
7
Realm realm = Realm.getDefaultInstance();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
gankDayResults.cascadeDelete();
}
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 引用自官网文档
Realm realm = Realm.getDefaultInstance();
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
Dog dog = bgRealm.where(Dog.class).equals("age", 1).findFirst();
dog.setAge(3);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// Original queries and Realm objects are automatically updated.
puppies.size(); // => 0 because there are no more puppies younger than 2 years old
managedDog.getAge(); // => 3 the dogs age is updated
}
});

这两种方式分别代表同步事务和异步事务,后者不会阻塞线程并通过回调通知事务完成。

0x83 RealmObject的持久性

引用官方文档的说法RealmObject是“Auto-Updating Objects”,意为自动更新,其含义就是通过Realm查询出来的同一对象,比如它们的主键相同,对任何一个对象进行修改,所有的结果都会发生变化。既然这里提到了主键不得不说说Realm创建数据的方法,最简单的方法是使用Realm#createObject方法,它返回一个RealmObject对象,这个对象是自动更新的,还可以使用Realm#copyToRealm方法像Realm数据库表中插入数据,此时若RealmObject存在主键,还可以使用Realm#copyToRealmOrUpdate插入新数据或更新旧数据,当然如果没有主键,将会出现异常。

如果RealmObject存在主键,不推荐使用Realm#createObject方法创建数据对象,因为该方法使用默认值创建对象很有可能引起对象冲突,这种状况使用new对象并更新到Realm的方法创建数据。

关于数据存储的部分就先说这么多,后面写一下如何在Realm中完成一对多,多对多这种常用的数据库关系。

Realm:在Android中使用Realm

0x81 Realm是什么

Realm是一个移动数据库,它可运行于手机、平板等移动设备,它的目的是取代SQLite。正是因为Realm并不是SQLite的封装,所以它不同于GreenDao、Suger这些ORM框架。Realm为Android平台提供支持,使用Realm Java Api管理数据库。

0x82 安装Realm支持

根据官方文档,Realm为Gradle依赖构建提供完美的支持,我们只需要在Project级的build.gradle配置文件中配置Realm插件:

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.4.0-alpha7'
classpath 'io.realm:realm-gradle-plugin:3.1.4'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

然后在需要的Module的build.gradle文件中应用插件即可使用Realm Java Api:

1
2
apply plugin: 'com.android.application'
apply plugin: 'realm-android'

0x83 简单使用

Realm的数据表默认对应一个POJO类,和GreenDao这些ORM框架的@Table注解类似,一个POJO类只要继承RealObject类即可在数据库中生成与之对应的表,各个成员变量作为表的列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GankItem
extends RealmObject {
@PrimaryKey
@SerializedName("_id")
private String id;
private String type;
private String desc;
private String who;
private String url;
@Ignore
private List<String> images;
private String createdAt;
private String publishedAt;

......

上述代码中,有几个常见的注解,其中@PrimaryKey注解表示该字段为主键,@SerializedName("_id")表示在序列化时的字段名,@Ignore表示不在表中生成该列,这三个注解比较常见。

PS:Realm默认支持基本类型及包装类型,boolean、byte、byte []、long(short、int、long都被映射为long)、float、double、String、Date以及RealmObject子类。

如何使用Realm查询数据呢?需要使用RealmInstance执行事务Transaction:

1
2
3
4
5
6
7
8
9
10
11
GankItem gankItem = new GankItem();
gankItem.setId("cz89a7s4ehsjkf");

Realm.init(context);
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
RealmResults<GankItem> items = realm.where(GankItem.class).findAll(); // 查询
realm.copyToRealmOrUpdate(gankItem); // 插入或更新
items.deleteAllFromRealm(); // 删除
realm.commitTransaction();
realm.close();

Realm简单使用就是这样,后面我会写写具体的操作以及事务。

Nexus5 Swap Boom

0x81 Android的Swap支持

所幸这一次没有那么糟糕了,唯一麻烦的就是去找一个支持Swap功能的内核。我们都知道交换分区是诸多Linux发行版上的一个常见的功能,绝大部分Linux在安装时都会让你创建Swap分区以提高操作系统运行性能,亦或者是提供一个swapfile来作为交换区域。交换分区可以理解为Windows上的虚拟内存的概念,它旨在提供足够的内存空间以弥补物理内存的不足,在Linux上可以使用swapiness参数来控制内核对交换分区使用的积极性,0为尽量不使用交换分区。Android运行在Linux内核之上,并且该内核的版本通常在出厂后就不会再更改了,比如我的老伴侣Nexus5的内核版本是3.4.0,既然Android运行在Linux内核之上,那应该也是支持Swap的,但是很遗憾,由于手机闪存的特殊性,持续性读写同一个位置会对闪存的寿命存在一定的影响,因此Android的内核通常是已除了这一特性的,在挂载swapfile时会告诉你内核没有对该功能进行实现,当然想要开启我们只需要找一个支持Swap的内核刷如就可以了。

0x82 开启Swap内存

得益于Nexus5这个不再亲的亲儿子,我在XDA上找了Franco Kernel,它支持的特性非常多,用TWRP刷入后正常开机,按照在Linux上开启的方式开启即可:

  1. 创建swapfile
    dd if=/dev/zero of=swapfile bs=4m count=100

    使用dd命令创建一个块大小是4m的400m交换文件。

  2. 转换swap格式
    mkswap swapfile

    使用mkswap命令将空文件转换成swap支持的组织格式,执行完毕会返回转换的基本信息。

  3. 挂载swapfile
    swapon swapfile

    这次使用swapon程序就不会提示没有实现了,成功启用后在free命令中就可以看到效果了

Nexus5用了快3年了,这个手机只有2G的内存,但是由于我使用力度不大,它现在仍然能满足我的使用需求,唯一不足的地方就是电池不耐用了。这款2013年10月发布的手机真的是物超所值,性价比也很高,用它我实验和学习了不少东西,不过这么高性价比的手机Google估计是不会再发布了,Pixel代替了Nexus产品线走向高端,而社区的热度仍旧不高,也许折腾Android的搞机之路也该停下了。

捕捉Android应用的崩溃

0x81 ForceClose

做过Android开发或Java开发的都知道,对于这样一个面向对象的语言,用null来表示一个对象为空再常见不过了,而空指针错误也变成了最让人诟病的错误。我每次在做完一个功能开始调试的时候,Android一个”ForceClose”的弹窗让我直接想骂娘,看StackTrace是NullPointerException更是让我想砸键盘。这种情况真的比较常见,缺少空判断可能会出现这种问题,而如果我们对代码中抛出的异常加以处理,那默认的异常处理器就是通知系统弹出FC框,在用户点击确定后退出进程。

0x82 以前的崩溃处理方法

出现”ForceClose”是非常烦人的,这种阻断式的弹窗提醒不但没什么卵用,还会增加用户的厌恶感,因此iOS系统通常是直接闪退,一些国产Android定制厂商的产品如MIUI、Bugme等也效仿去掉Android原生的错误弹窗。

当然了,事情都有两面性,iOS的调试功能我感觉非常强大,而Android可能受限于调试器的笨重(反正我是只有需要具体分析的时候才适用附加调试,通常APP崩溃都会保留栈跟踪信息),在程序崩溃时Logcat会打印StackTrace,但是Logcat为了避免log太多,重新启动进程时即使是同一个应用也会清除之前的栈追踪信息,给我带来很大的困扰,当然添加过滤器或者用一些第三方的Logcat工具可以很好的解决这个问题。

为了能更直接的控制应用崩溃,比如在崩溃时将信息写入文件然后在下次启动应用时上传服务器等等需求,我们可以自己实现异常处理器来完成这个事情:

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
public class CrashHandler
implements UncaughtExceptionHandler {
private UncaughtExceptionHandler mDefaultHandler;

private CrashHandler() {
}

public static CrashHandler getInstance() {
return new CrashHandler();
}

public void init() {
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}

@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
} else {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
}

private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}
ex.printStackTrace();
return true;
}
}

因为虚拟机一旦发生异常通常会有try-catch捕捉走异常,而像空指针这种异常就不一定了,因此我们实现异常处理器并设定为线程默认未捕捉异常处理器Thread.setDefaultUncaughtExceptionHandler(this),之后调用系统Api关闭进程android.os.Process.killProcess(android.os.Process.myPid()),这样也能实现MIUI等三方ROM的效果。System.exit(1)意为虚拟机以non-zero退出,表示不正常退出通常会自动尝试启动应用。

Read More