Android 进程与线程:主线程阻塞问题、Handler、WorkThread、线程池工控实践
工控设备卡成PPT?大概率是你在主线程干了"重活",这篇把线程那点事儿掰碎了讲清楚。
一、Android 进程与线程模型
Android 应用启动时,系统会为其创建一个进程,并在该进程中启动一个主线程(Main Thread)。主线程也叫UI 线程,负责处理界面绘制、用户事件分发、组件生命周期回调等。
为什么不能在主线程做耗时操作?
核心原因一句话:主线程承担着每 16ms 左右刷新一次屏幕(60fps)的重任。如果某个操作阻塞了主线程超过阈值,系统就会弹出一个让所有开发者闻风丧胆的东西——ANR(Application Not Responding)。
常见的耗时操作包括:
- 网络请求(HTTP / Socket 通信)
- 文件 I/O(读写本地大文件)
- 数据库操作(批量增删改查)
- 串口数据读取(工控场景最常见的坑)
- 大量 CPU 计算(图像处理、数据解析)
在工控板子上,串口读取如果放在主线程,数据量大的时候画面直接冻住,按钮点不动,这就是典型的主线程阻塞。
二、ANR 机制详解
ANR 不是随机触发的,它有明确的超时阈值:
| ANR 类型 | 触发场景 | 超时阈值 |
|---|---|---|
| Service Timeout | Service 的 onCreate / onStartCommand 执行超时 | 前台 20s / 后台 200s |
| Broadcast Timeout | BroadcastReceiver.onReceive 执行超时 | 前台 10s / 后台 60s |
| Input Dispatch Timeout | 输入事件(触摸/按键)无响应 | 5s |
| ContentProvider Timeout | 内容提供者发布超时 | 10s |
注意一个常见误区:很多人以为"主线程耗时 5 秒就 ANR",其实5 秒是输入事件派发超时。如果没有触摸事件进来,主线程就算卡 10 秒也不一定弹 ANR——但界面肯定已经卡死了。真正让用户感知到的,是输入无响应那条。
工控设备往往是长时间无人触屏的,所以 ANR 不一定弹出来,但界面卡住一样致命——操作员按了停止按钮没反应,设备还在跑。
三、Handler 消息机制原理
Handler 是 Android 线程间通信的核心机制,涉及四个关键角色:
- Message:消息载体,携带
what(标识)、obj(数据)、arg1/arg2(整型参数) - MessageQueue:消息队列,按时间排序的单链表结构
- Looper:循环器,不断从 MessageQueue 中取出 Message 分发给 Handler
- Handler:发送和处理消息的入口
整个流程可以用一句话概括:
Handler 发送 Message → MessageQueue 入队 → Looper 循环取出 → Handler.handleMessage() 处理
[子线程] Handler.sendMessage(msg) ↓ [MessageQueue] 按 when 排序入队 ↓ [Looper.loop()] 死循环轮询取出 ↓ [Handler.handleMessage()] 在 Handler 创建所在线程执行关键点:Handler 的 handleMessage 在哪个线程执行,取决于 Handler 在哪个线程创建。在主线程创建的 Handler,handleMessage 就跑在主线程,天然可以更新 UI。
四、工作线程创建方式
4.1 普通 Thread
最原始的方式,直接 new 一个线程:
newThread(()->{// 子线程执行耗时操作Stringdata=readSerialPort();// 不能直接更新UI!需要Handler切换}).start();缺点:每次 new Thread 开销大,无法管理线程数量,容易造成 OOM。
4.2 HandlerThread
自带 Looper 的线程,适合需要在该线程上创建 Handler 的场景:
HandlerThreadhandlerThread=newHandlerThread("serial-worker");handlerThread.start();HandlerserialHandler=newHandler(handlerThread.getLooper()){@OverridepublicvoidhandleMessage(@NonNullMessagemsg){// 在HandlerThread线程中执行}};4.3 IntentService(已废弃,了解即可)
Android 8.0 后官方推荐用JobIntentService / WorkManager替代,但在老项目里还能见到。它的特点是串行执行任务,执行完自动停止。
五、线程池使用
线程池是工控开发中管理线程的最佳实践。核心类是ThreadPoolExecutor。
5.1 参数详解
ThreadPoolExecutorexecutor=newThreadPoolExecutor(intcorePoolSize,// 核心线程数:即使空闲也保留的线程数intmaximumPoolSize,// 最大线程数:队列满后能扩展的最大线程数longkeepAliveTime,// 非核心线程空闲存活时间TimeUnitunit,// 时间单位BlockingQueue<Runnable>workQueue,// 任务队列ThreadFactorythreadFactory,// 线程工厂(可自定义线程名)RejectedExecutionHandlerhandler// 拒绝策略);四种拒绝策略:
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException |
| CallerRunsPolicy | 由调用线程执行该任务 |
| DiscardPolicy | 直接丢弃 |
| DiscardOldestPolicy | 丢弃队列最老的任务,再尝试提交 |
5.2 工控场景线程池配置
工控设备通常资源有限(ARM Cortex-A 系列,可能只有 1~4 核),线程池不能无脑开大。推荐配置:
// 工控场景线程池:核心2线程,最大4线程,队列128privatestaticfinalintCPU_COUNT=Runtime.getRuntime().availableProcessors();privatestaticfinalintCORE_POOL_SIZE=Math.max(2,Math.min(CPU_COUNT-1,4));privatestaticfinalintMAX_POOL_SIZE=CORE_POOL_SIZE*2;privatestaticfinalintQUEUE_CAPACITY=128;publicstaticfinalThreadPoolExecutorWORK_EXECUTOR=newThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,30L,TimeUnit.SECONDS,newLinkedBlockingQueue<>(QUEUE_CAPACITY),newThreadFactory(){privatefinalAtomicIntegermCount=newAtomicInteger(1);@OverridepublicThreadnewThread(Runnabler){returnnewThread(r,"control-worker-"+mCount.getAndIncrement());}},newThreadPoolExecutor.CallerRunsPolicy()// 队列满了让调用线程自己跑,不丢任务);工控场景拒绝策略推荐CallerRunsPolicy——宁可让提交任务的线程自己执行,也不能丢任务。丢一个继电器控制指令可能就是事故。
六、工控实践:串口数据读取用子线程 + Handler 更新 UI
下面是一个完整的串口通信线程 + Handler 更新 UI 的实现:
publicclassSerialPortManager{privatestaticfinalintMSG_DATA_RECEIVED=0x01;privatestaticfinalintMSG_ERROR=0x02;// 主线程Handler,用于更新UIprivatefinalHandlermUiHandler;// 串口工作线程privateHandlerThreadmSerialThread;privateHandlermSerialHandler;// 串口运行标志privatevolatilebooleanmRunning=false;publicSerialPortManager(HandleruiHandler){this.mUiHandler=uiHandler;}/** * 初始化串口读取线程 */publicvoidstart(){mSerialThread=newHandlerThread("serial-read-thread");mSerialThread.start();mSerialHandler=newHandler(mSerialThread.getLooper()){@OverridepublicvoidhandleMessage(@NonNullMessagemsg){if(msg.what==MSG_DATA_RECEIVED){byte[]data=(byte[])msg.obj;parseAndNotify(data);}}};mRunning=true;// 启动读取循环mSerialHandler.post(this::readLoop);}/** * 串口读取循环(在子线程执行) */privatevoidreadLoop(){while(mRunning){try{byte[]buffer=newbyte[1024];intsize=mInputStream.read(buffer);// 阻塞读取if(size>0){byte[]data=newbyte[size];System.arraycopy(buffer,0,data,0,size);// 通过Handler切到主线程Messagemsg=mUiHandler.obtainMessage(MSG_DATA_RECEIVED,data);mUiHandler.sendMessage(msg);}}catch(IOExceptione){mUiHandler.obtainMessage(MSG_ERROR,e.getMessage()).sendToTarget();break;}}}/** * 解析数据并通知UI */privatevoidparseAndNotify(byte[]data){// 协议帧解析:帧头0xAA + 长度 + 数据 + 校验if(data.length<4)return;if(data[0]!=(byte)0xAA)return;intlen=data[1]&0xFF;// ...解析逻辑}publicvoidstop(){mRunning=false;if(mSerialThread!=null){mSerialThread.quitSafely();}}}在 Activity 中使用:
publicclassMainActivityextendsAppCompatActivity{privateTextViewtvTempData;privateSerialPortManagermSerialManager;// UI Handler:处理子线程发来的消息privatefinalHandlermUiHandler=newHandler(Looper.getMainLooper()){@OverridepublicvoidhandleMessage(@NonNullMessagemsg){switch(msg.what){caseSerialPortManager.MSG_DATA_RECEIVED:byte[]data=(byte[])msg.obj;StringhexStr=bytesToHex(data);tvTempData.setText("收到数据: "+hexStr);break;caseSerialPortManager.MSG_ERROR:Toast.makeText(MainActivity.this,"串口错误: "+msg.obj,Toast.LENGTH_SHORT).show();break;}}};@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tvTempData=findViewById(R.id.tv_temp_data);mSerialManager=newSerialPortManager(mUiHandler);mSerialManager.start();}@OverrideprotectedvoidonDestroy(){super.onDestroy();mSerialManager.stop();}}6.1 核心要点总结
| 要点 | 说明 |
|---|---|
| 串口读取必须在子线程 | InputStream.read()是阻塞调用,放主线程必卡 |
| UI 更新必须切回主线程 | 子线程直接操作 View 会抛异常 |
| Handler 是线程切换的标准方式 | 轻量、可控、生命周期清晰 |
| HandlerThread 适合持续轮询场景 | 自带 Looper,省去手动 prepare/loop |
| volatile 控制线程退出 | 保证可见性,避免死循环 |
工控开发中,线程管理是基本功。串口、网络、定时采集,每一路数据流都应该有独立的工作线程,通过 Handler 或回调将结果送到主线程刷新界面。把线程用好,设备才能跑得稳。