文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

关于Android中工作者线程的思考

2022-06-06 12:52

关注

  在Android中,我们或多或少使用了工作者线程,比如Thread,AsyncTask,HandlerThread,甚至是自己创建的线程池,使用工作者线程我们可以将耗时的操作从主线程中移走。然而在Android系统中为什么存在工作者线程呢,常用的工作者线程有哪些不易察觉的问题呢,关于工作者线程有哪些优化的方面呢,本文将一一解答这些问题。

  工作者线程的存在原因   因为Android的UI单线程模型,所有的UI相关的操作都需要在主线程(UI线程)执行   Android中各大组件的生命周期回调都是位于主线程中,使得主线程的职责更重   如果不使用工作者线程为主线程分担耗时的任务,会造成应用卡顿,严重时可能出现ANR(Application Not Responding),即程序未响应。   因而,在Android中使用工作者线程显得势在必行,如一开始提到那样,在Android中工作者线程有很多,接下来我们将围绕AsyncTask,HandlerThread等深入研究。   AsyncTask   AsyncTask是Android框架提供给开发者的一个辅助类,使用该类我们可以轻松的处理异步线程与主线程的交互,由于其便捷性,在Android工程中,AsyncTask被广泛使用。然而AsyncTask并非一个完美的方案,使用它往往会存在一些问题。接下来将逐一列举AsyncTask不容易被开发者察觉的问题。   AsyncTask与内存泄露   内存泄露是Android开发中常见的问题,只要开发者稍有不慎有可能导致程序产生内存泄露,严重时甚至可能导致OOM(OutOfMemory,即内存溢出错误)。AsyncTask也不例外,也有可能造成内存泄露。   以一个简单的场景为例:   在Activity中,通常我们这样使用AsyncTask   //In Activity   new AsyncTask<String, Void, Void>() {   @Override   protected Void doInBackground(String... params) {   //some code   return null;   }   }.execute("hello world");   上述代码使用的匿名内存类创建AsyncTask实例,然而在Java中,非静态内存类会隐式持有外部类的实例引用,上面例子AsyncTask创建于Activity中,因而会隐式持有Activity的实例引用。   而在AsyncTask内部实现中,mFuture同样使用匿名内部类创建对象,而mFuture会作为执行任务加入到任务执行器中。   private final WorkerRunnable<Params, Result> mWorker;   public AsyncTask() {   mFuture = new FutureTask<Result>(mWorker) {   @Override   protected void done() {   //some code   }   };   }   而mFuture加入任务执行器,实际上是放入了一个静态成员变量SERIAL_Executor指向的对象SerialExecutor的一个ArrayDeque类型的集合中。   public static final Executor SERIAL_EXECUTOR = new SerialExecutor();   private static class SerialExecutor implements Executor {   final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();   public synchronized void execute(final Runnable r) {   mTasks.offer(new Runnable() {   public void run() {   //fake code   r.run();   }   });   }   }   当任务处于排队状态,则Activity实例引用被静态常量SERIAL_EXECUTOR 间接持有。   在通常情况下,当设备发生屏幕旋转事件,当前的Activity被销毁,新的Activity被创建,以此完成对布局的重新加载。   而本例中,当屏幕旋转时,处于排队的AsyncTask由于其对Activity实例的引用关系,导致这个Activity不能被销毁,其对应的内存不能被GC回收,因而出现了内存泄露问题。   关于如何避免内存泄露,我们可以使用静态内部类 + 弱引用的形式解决。   cancel的问题   AsyncTask作为任务,是支持调用者取消任务的,即允许我们使用AsyncTask.canncel()方法取消提交的任务。然而其实cancel并非真正的起作用。   首先,我们看一下cancel方法:   public final boolean cancel(boolean mayInterruptIfRunning) {   mCancelled.set(true);   return mFuture.cancel(mayInterruptIfRunning);   }   cancel方法接受一个boolean类型的参数,名称为mayInterruptIfRunning,意思是是否可以打断正在执行的任务。   当我们调用cancel(false),不打断正在执行的任务,对应的结果是   处于doInBackground中的任务不受影响,继续执行   任务结束时不会去调用onPostExecute方法,而是执行onCancelled方法   当我们调用cancel(true),表示打断正在执行的任务,会出现如下情况:   如果doInBackground方法处于阻塞状态,如调用Thread.sleep,wait等方法,则会抛出InterruptedException。   对于某些情况下,有可能无法打断正在执行的任务   如下,是一个cancel方法无法打断正在执行的任务的例子   AsyncTask<String,Void,Void> task = new AsyncTask<String, Void, Void>() {   @Override   protected Void doInBackground(String... params) {   boolean loop = true;   while(loop) {   Log.i(LOGTAG, "doInBackground after interrupting the loop");   }   return null;   }   }   task.execute("hello world");   try {   Thread.sleep(2000);//确保AsyncTask任务执行   task.cancel(true);   } catch (InterruptedException e) {   e.printStackTrace();   }   上面的例子,如果想要使cancel正常工作需要在循环中,需要在循环条件里面同时检测isCancelled()才可以。   串行带来的问题   Android团队关于AsyncTask执行策略进行了多次修改,修改大致如下:   自初引入到Donut(1.6)之前,任务串行执行   从Donut到GINGERBREAD_MR1(2.3.4),任务被修改成了并行执行   从HONEYCOMB(3.0)至今,任务恢复至串行,但可以设置executeOnExecutor()实现并行执行。   然而AsyncTask的串行实际执行起来是这样的逻辑   由串行执行器控制任务的初始分发   并行执行器一次执行单个任务,并启动下一个   在AsyncTask中,并发执行器实际为ThreadPoolExecutor的实例,其CORE_POOL_SIZE为当前设备CPU数量+1,MAXIMUM_POOL_SIZE值为CPU数量的2倍 + 1。   以一个四核手机为例,当我们持续调用AsyncTask任务过程中   在AsyncTask线程数量小于CORE_POOL_SIZE(5个)时,会启动新的线程处理任务,不重用之前空闲的线程   当数量超过CORE_POOL_SIZE(5个),才开始重用之前的线程处理任务   但是由于AsyncTask属于默认线性执行任务,导致并发执行器总是处于某一个线程工作的状态,因而造成了ThreadPool中其他线程的浪费。同时由于AsyncTask中并不存在allowCoreThreadTimeOut(boolean)的调用,所以ThreadPool中的核心线程即使处于空闲状态也不会销毁掉。   Executors   Executors是Java API中一个快速创建线程池的工具类,然而在它里面也是存在问题的。   以Executors中获取一个固定大小的线程池方法为例   public static ExecutorService newFixedThreadPool(int nThreads) {   return new ThreadPoolExecutor(nThreads, nThreads,0L,   TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());   }   在上面代码实现中,CORE_POOL_SIZE和MAXIMUM_POOL_SIZE都是同样的值,如果把nThreads当成核心线程数,则无法保证大并发,而如果当做大并发线程数,则会造成线程的浪费。因而Executors这样的API导致了我们无法在大并发数和线程节省上做到平衡。   为了达到大并发数和线程节省的平衡,建议自行创建ThreadPoolExecutor,根据业务和设备信息确定CORE_POOL_SIZE和MAXIMUM_POOL_SIZE的合理值。   HandlerThread   HandlerThread是Android中提供特殊的线程类,使用这个类我们可以轻松创建一个带有Looper的线程,同时利用Looper我们可以结合Handler实现任务的控制与调度。以Handler的post方法为例,我们可以封装一个轻量级的任务处理器   private Handler mHandler;   private LightTaskManager() {   HandlerThread workerThread = new HandlerThread("LightTaskThread");   workerThread.start();   mHandler = new Handler(workerThread.getLooper());   }   public void post(Runnable run) {   mHandler.post(run);   }   public void postAtFrontOfQueue(Runnable runnable) {   mHandler.postAtFrontOfQueue(runnable);   }   public void postDelayed(Runnable runnable, long delay) {   mHandler.postDelayed(runnable, delay);   }   public void postAtTime(Runnable runnable, long time) {   mHandler.postAtTime(runnable, time);   }


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯