本文微信公众号「AndroidTraveler」首发。java
最近部门有新入职员工,做为规划技术路线的导师,这边给新员工安排了学习路线。android
除了基本的学习路线以外,每次沟通,我都留了一个小问题,让小伙伴去思考。数据库
这些问题有些是刚接触 Android 开发的小伙伴所不熟悉的,有些则是部分初级工程师都没有注意到的。bash
所以这边纪录一下,但愿帮助刚毕业进入职场的 Android 小伙伴,或是对这些还不是很熟悉的 Android 开发工程师们。微信
若有补充或者交流,欢迎留言。app
Q: 你是否了解过 ANR?
A: 知道,但不是很了解。异步
Q: 什么状况下会出现 ANR?
假设这里回答的不是“在主线程执行耗时任务”的话,能够不继续追问,直接让小伙伴去了解 ANR,后期再讨论。
若是回答了是“在主线程执行耗时任务”的话,那么继续:async
Q: 多久算耗时?
A: 不要超过 5s。ide
Q: 那么假设我在 Activity sleep 20s,是否是就必定会 ANR?函数
上代码例子:
MainActivity.java 文件:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("zengyu","before sleep");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("zengyu","after sleep");
}
});
}
}
复制代码
activity_main.xml 文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
复制代码
代码功能很简单,就是一个按钮,点击以后会 sleep 20 秒。在 sleep 前和 sleep 后都会打印日志。
若是你只是点击按钮,而后什么都不动,是不会有 ANR 的。
可是你点击了按钮以后,你继续屡次点击按钮,那么就会有 ANR 了。
如下四个条件均可以形成ANR发生:
InputDispatching Timeout: 5秒内没法响应屏幕触摸事件或键盘输入事件 BroadcastQueue Timeout : 在执行前台广播(BroadcastReceiver)的onReceive()函数时10秒没有处理完成,后台为60秒。
Service Timeout : 前台服务20秒内,后台服务在200秒内没有执行完毕。
ContentProvider Timeout : ContentProvider的publish在10s内没进行完。
因此可能不少小伙伴会把上面四个条件的第一个和 Activity 直接挂钩,觉得是在主线程耗时超过 5s 就会 ANR。其实是 InputDispatching。
Q: 既然主线程不能作耗时任务,那么有耗时任务怎么办?
A: 经过 new Thread 启动一个子线程,在子线程处理。
Q: 考虑一个场景,好比相似微信这类 IM 软件收到消息。须要写数据库,这个时候须要启动线程。当收到消息 N 多的时候,若是都用 new Thread 启动线程的话,是否会有问题。场景模拟能够经过循环建立子线程模拟。
上代码例子:
MainActivity.java 文件:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
复制代码
这个部分手机厂商好比(华为)有对线程数目作限制的话,一运行就会 crash,Logcat 会看到下面信息:
pthread_create (1040KB stack) failed: Out of memory
复制代码
我这边一开始在三星 S7 上面运行,并无出现。后面换成华为 5x 手机就出现了。
Android 开发的小伙伴都知道兼容是硬伤,因此咱们不能抱有侥幸心理。
针对这种状况,咱们不能一遇到耗时任务,就很潇洒的一个 new Thread 所有搞定。
若是你当前界面只有一个耗时任务,并且只须要调用一次,那么你进入该界面用 new Thread 来处理没有问题。
可是假设像上面咱们描述的场景那样,须要调用屡次的时候。你就不能简单粗暴的使用 new Thread 了。
推荐方式是使用线程池。
一个缘由是避免一些厂商的线程数目限制。
另外一个缘由是减小线程的频繁建立和销毁。
Q: 上面咱们说到了,若是界面调用一次,并且须要启动线程的时候。可使用 new Thread 建立,那么直接使用 new Thread 可能还有什么问题吗?
这里想考察的点可能比较晦涩一点。
因为内部类会持有外部类的引用。假设在 Activity 里面经过匿名内部类的方式来启动线程作耗时任务。当用户退出界面时,因为内部类还持有 Activity 的引用,所以 Activity 无法获得释放。
就会存在内存泄漏问题。
解决方法也比较统一,那就是将内部类改成静态内部类。
因此修改后的代码对好比下:
修改前:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
//TODO
}
}).start();
}
}
复制代码
修改后:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new StaticThread().start();
}
private static class StaticThread extends Thread {
@Override
public void run() {
super.run();
//TODO
}
}
}
复制代码
Q: 用过 Handler 吗?
A: 用过。
Q: 写一下简单的 demo 我看下。
这个是紧接第三点。若是不涉及界面交互,只须要使用到静态内部类就能够解决。可是当 Handler 里面须要作界面更新处理时,那么须要使用弱引用。由于静态内部类的处理方式原本就是为了不 Activity 没法获得释放。你若是把 Activity 直接传进来,那么 Activity 的引用被静态内部类持用了,因此这个时候就须要使用到弱引用了。
直接上代码:
public class MainActivity extends AppCompatActivity {
private static class StaticHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public StaticHandler(MainActivity mainActivity) {
this.activityWeakReference = new WeakReference<>(mainActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//TODO
//use activityWeakReference.get() to get view
}
}
private StaticHandler mStaticHandler = new StaticHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStaticHandler.sendEmptyMessage(0);
}
}
复制代码
通常不少网上教程和例子在 SharedPreferences 的数据写入时,通常都会使用 Editor 的 commit 方法。
因为 commit 方法是同步写入的。而且写文件属于 I/O 操做,若是你有大量的数据须要写入,而且你是在主线程处理的,可能会致使流畅性受影响。极端状况下可能会出现 ANR。
咱们点进去源码看下:
If you don't care about the return value and you're using this from your application's main thread, consider using {@link #apply} instead. 复制代码
其实源码也说的很清楚了。若是你不关心返回值而且你是在应用的主线程使用的话,考虑使用 apply 替换 commit。
因为咱们通常不会处理返回值,所以建议使用 apply 替换 commit。
apply 会把变化立刻写进内存,而后经过异步方式去写入。
Unlike {@link #commit}, which writes its preferences out to persistent storage synchronously, {@link #apply} commits its changes to the in-memory {@link SharedPreferences} immediately but starts an asynchronous commit to disk and you won't be notified of any failures.
复制代码
固然源码还有一个注释以下:
If another editor on this {@link SharedPreferences} does a regular {@link #commit} while a {@link #apply} is still outstanding, the {@link #commit} will block until all async commits are completed as well as the commit itself.
复制代码
大概意思就是 apply 若是在处理中还未完成的状况下,commit 会阻塞直到全部异步操做完成才会去 commit。
所以若是要替换,建议将 commit 都替换为 apply。
这里主要是考虑相似微信 IM 登陆后拉取大量离线消息写入数据库的问题。
经过对比开启事务和不开启事务的耗时来进行说明。
好比不开启事务插入 10000 条纪录和开启事务插入 10000 条纪录耗时对比。
对于大量的数据库操做,建议开启事务的方式,速度的提高是很明显的。
参考连接:
Android ANR:原理分析及解决办法