最近结合自身项目来读了下<<阿里巴巴Android开发手册>>,有了些思绪,写成此文将一些感想记录下来,以备后用.
线程
-
子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用。
这属于常规操作,不多谈. -
主线程不进行耗时操作
例如,读写一个文件,都知道需要去子线程中操作.但通常,文件读写会封装在某个类或者方法里面,外面调用者有可能不清楚方法的具体实现,直接在主线程调用. 这种坑埋下去,如果代码可读性还比较差,没有
review
出来,也没引起ANR,会长期发现不了.解决这个问题可以:
a. 给方法加上注解,编译期间,错误的调用方式IDE会提示
@WorkerThread public void saveImage2Storage() { // save } @UiThread public void refreshMainView() { // 这里编译器错误提示 saveImage2Storage(); }
b. 防御式编程,在测试环境抛出异常
public void saveImage2Storage() { ThreadUtils.assertIsNotMainThread(); // save } public static void assertIsNotMainThread() { if (BuildConfig.DEBUG) { Assert.assertFalse("Call cannot be made on the main thread", isMainThread()); } }
-
新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查.
例如, 表明线程来自于Pandora(游戏模块)的调用,在
Debug
的时候,清晰的知道自己现在在哪个功能里面.使用SystemTrace
,以及看ANR
堆栈信息的时候也会很有用.private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); @Override public Thread newThread(Runnable runnable) { return new Thread(runnable, "Pandora_TID#" + mCount.getAndIncrement()); } };
线程池
-
新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池),不允许在应用中自行显式创建线程.
错误的使用方式:new Thread() { @Override public void run() { // do } }.start();
项目中这样的代码目前还比较多,粗略搜索了下有60+处
明确问题后, 如果已有的代码改动起来风险和困难比较大,至少之后不要再错误的使用了.
-
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方 式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
a.
FixedThreadPool
和SingleThreadPool
:线程队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
.源码中,线程队列
FixedThreadPool
使用默认的构造方法LinkedBlockingQueue()
,其默认长度就是Integer.MAX_VALUE
// 源码 package java.util.concurrent; // 构造方法使用LinkedBlockingQueue的默认无参数构造 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
b.
CachedThreadPool
和ScheduledThreadPool
: 允 许 的 创 建 线 程 数 量 为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM
源码中
CachedThreadPool
默认使用Integer.MAX_VALUE
为最大线程池数目// 源码 package java.util.concurrent; // 默认构造方法使用直接传递Integer.MAX_VALUE为线程数 public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
c. 我们项目中的多处调用,基本也是直接采用默认参数来使用:
// OpenWnnSimeji.class if (mInitService == null) { mInitService = Executors.newFixedThreadPool(3); } // App.class private ExecutorService executorService = Executors.newFixedThreadPool(2);
之前在崩溃平台看到一个Case,一个华为开辟了手机400+个线程,就Crash了,根本不可能会有
Integer.MAX_VALUE
的情况.参考一个比较优秀的线程管理库Facebook-Android-Bolts
的backgound线程池配置方式:private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); // 据说核心数+1是最优参数 /* package */ static final int CORE_POOL_SIZE = CPU_COUNT + 1; /* package */ static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1; /* package */ static final long KEEP_ALIVE_TIME = 1L; public static ExecutorService newCachedThreadPool() { ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); allowCoreThreadTimeout(executor, true); return executor; }
-
ThreadPoolExecutor
设置线程存活时间(setKeepAliveTime
),确保空闲时 线程能被释放。同样这里需要明确ThreadPoolExecutor构造函数各个参数的意义,为什么是这个参数值而不是其他什么值.
线程模块的设计
- 内部调用设计
目前项目除了一些游离线程(new出来的),线程管理类有3个
WorkerThreadPool
SimejiTask
ThreadManager
这里先不讨论3个管理类的重复设计.
SimejiTask
底层是借助第三方库,模仿系统工具类AsyncTask
的外观,实现了一个功能接口一模一样的
WorkerThreadPool
和ThreadManager
两个类都是单例化ThreadPoolExecutor
后的简单封装,但是在实际使用中发现用起来有些问题.
例如,项目中有一个这样的业务场景:
从服务器获取一个图片地址 -> 将图片保存到SD卡->弹出Toast提示保存成功.
保存图片借助Glide
实现,Glide
必须运行在主线程,所以保存图片会有两步先切换到主线程使用Glide,然后在子线程保存图片
{
WorkerThreadPool.getInstance().execute(new Runnable() {
@Override
public void run() {
// 网络请求获取图片URL地址
final String iconPath = StampImageHelper.getStampPath();
PlusManager.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
// Glide第三方库 必须在主线程执行
Glide.with(App.instance).load(iconPath).downloadOnly(new SimpleTarget<File>() {
@Override
public void onResourceReady(final File resource,
GlideAnimation<? super File> glideAnimation) {
WorkerThreadPool.getInstance().execute(new Runnable() {
@Override
public void run() {
// 保存图片到本地文件
FileUtils.copyFile(resource.getPath(), filePath);
PlusManager.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
ToastShowHandler.getInstance().showToast("Save Icon Success!");
}
});
}
}, false);
}
});
}
});
}
}, false);
}
这里涉及到4次线程切换,导致代码复杂度增大,为了降低复杂度,我把这个业务逻辑拆分成了3个函数,就是简单抽取,具体代码这里就不贴出来了.
但是现在回过头来看,代码可读性仍然比较差,原因是因为无法从代码调用看见事件传递的顺序(就像小学1年级写作文,老师会特别强调按故事发展的先后顺序写).
这时候想如果用到RxJava
的链式调用岂不美哉,但是RxJava
集成进来不太现实,一个是因为其学习曲线很陡,第二是因为这个库太大.
我用Bolts
重写这一块(Bolts
2016年已经引入,Facebook生产的轻量级线程库),使用方式很像Rxjava
,代码可读性大大增强.
Task.callInBackground(new Callable<String>() {
@Override
public String call() throws Exception {
return StampImageHelper.getStampPath();
}
}).continueWith(new Continuation<String, File>() {
@Override
public File then(Task<String> task) throws Exception {
if (!TextUtils.isEmpty(task.getResult())) {
return Glide.with(App.instance).load(task.getResult()).downloadOnly(mWidth, mHeight).get();
}
return null;
}
}, Task.UI_THREAD_EXECUTOR).continueWith(new Continuation<File, Boolean>() {
@Override
public Boolean then(Task<File> task) throws Exception {
if (task.getResult() != null) {
return FileUtils.copyFile(task.getResult().getAbsolutePath(), filePath);
}
return false;
}
}, Task.BACKGROUND_EXECUTOR).continueWith(new Continuation<Boolean, Void>() {
@Override
public Void then(Task<Boolean> task) throws Exception {
if (task.getResult()) {
ToastShowHandler.getInstance().showToast("Save Icon Success!");
}
}
}, Task.UI_THREAD_EXECUTOR);
结合IDE的自动收缩功能,代码看起来会更简洁
这一段要表达的主题就是: 作为框架设计者,除了要考虑安全,性能,易用性上的问题外,代码可读性,可维护性也是一个重要话题. 尤其长期来看,由于代码可读性,可维护性下降引发的问题数量不会少于安全,性能.
- 线程外部设计
在看一些第三方库实现代码的时候,会发现第三方库一般会其在内部实现一个线程池.自身项目的各个模块,为了方便模块拆分,减少依赖,也会这样干.
从模块化的角度来看这样做是没有问题的,但是设想我们引入10个第三方库,从而引入10个线程池,10个线程池之间的调度成本真的会低于直接new Thread()
?
参考系统提供的AsyncTask
,除了默认实现的线程池外,他还提供了一个可指定线程池的运行方法.
// 1
new AsyncTask<String, Void, Boolean>() {
@Override
protected Boolean doInBackground(String... strings) {
// todo
return null;
}
}.execute(filePath);
// 2
new AsyncTask<String, Void, Boolean>() {
@Override
protected Boolean doInBackground(String... strings) {
// todo
return null;
}
// 指定调用线程池
}.executeOnExecutor(Task.BACKGROUND_EXECUTOR, filePath);
回到上面的问题,为了避免创建过多的线程池,如果我是一个第三方类库的设计者,我会考虑对外部暴露一个配置方法,使用者可以选择指定线程池给库使用