Android项目管理/开发规范探索-线程管理

本文探讨了Android应用中线程管理的重要性,包括避免主线程耗时操作、正确使用线程池、线程命名规范及线程池配置优化。通过对比分析不同线程池的使用场景,提出了一种更高效、安全的线程管理方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近结合自身项目来读了下<<阿里巴巴Android开发手册>>,有了些思绪,写成此文将一些感想记录下来,以备后用.

线程

  1. 子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用。
    这属于常规操作,不多谈.

  2. 主线程不进行耗时操作
    例如,读写一个文件,都知道需要去子线程中操作.

    但通常,文件读写会封装在某个类或者方法里面,外面调用者有可能不清楚方法的具体实现,直接在主线程调用. 这种坑埋下去,如果代码可读性还比较差,没有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());
    	        }
    	    }
    
  3. 新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查.

    例如, 表明线程来自于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());
        }
    };
    

线程池

  1. 新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池),不允许在应用中自行显式创建线程.
    错误的使用方式:

     new Thread() {
            @Override
            public void run() {
            // do
            }
        }.start();
    

    项目中这样的代码目前还比较多,粗略搜索了下有60+处

    明确问题后, 如果已有的代码改动起来风险和困难比较大,至少之后不要再错误的使用了.

  2. 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方 式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

    a. FixedThreadPoolSingleThreadPool :线程队列长度为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. CachedThreadPoolScheduledThreadPool : 允 许 的 创 建 线 程 数 量 为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;
      }
    
  3. ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时 线程能被释放。

    同样这里需要明确ThreadPoolExecutor构造函数各个参数的意义,为什么是这个参数值而不是其他什么值.

线程模块的设计

  1. 内部调用设计

目前项目除了一些游离线程(new出来的),线程管理类有3个

	WorkerThreadPool
	SimejiTask
	ThreadManager

这里先不讨论3个管理类的重复设计.
SimejiTask底层是借助第三方库,模仿系统工具类AsyncTask的外观,实现了一个功能接口一模一样的
WorkerThreadPoolThreadManager两个类都是单例化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重写这一块(Bolts2016年已经引入,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的自动收缩功能,代码看起来会更简洁
在这里插入图片描述

这一段要表达的主题就是: 作为框架设计者,除了要考虑安全,性能,易用性上的问题外,代码可读性,可维护性也是一个重要话题. 尤其长期来看,由于代码可读性,可维护性下降引发的问题数量不会少于安全,性能.

  1. 线程外部设计

在看一些第三方库实现代码的时候,会发现第三方库一般会其在内部实现一个线程池.自身项目的各个模块,为了方便模块拆分,减少依赖,也会这样干.
从模块化的角度来看这样做是没有问题的,但是设想我们引入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);

回到上面的问题,为了避免创建过多的线程池,如果我是一个第三方类库的设计者,我会考虑对外部暴露一个配置方法,使用者可以选择指定线程池给库使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值