技术标签: PendingResult goAsync Android App 异步任务 广播 android
【需求】:监听Android系统中某个广播,在广播onReceive方法中收集和处理系统的信息,(比如设备型号,IMEI, 手机内存大小等信息),然后上报给服务器端。
要完成该功能的话,要在广播中处理事件上报,涉及到往服务器上报数据,又与网络请求挂钩,肯定不能在主线程中做太过耗时的任务,这样子阻塞主线程,容易引起ANR.
为了把功能做的稳定一点,我们得先理一理下面的概念:
1. 广播生命周期
从开始执行回调方法onReceiver() 到 结束;
前台广播默认的生命周期时间为10秒, 后台广播默认的生命周期为60秒;
// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;
在发送广播时,可以通过设置Intent.FLAG_RECEIVER_FOREGROUND属性来将广播定义 为前台广播,如果未定义,默认为后台广播。
2. 广播对所在进程的影响
BroadcastReceiver 的状态(无论它是否在运行)会影响其所在进程的状态,而其所在进程的状态又会影响它被系统终结的可能性。例如,当进程执行接收器(即当前在运行其 onReceive() 方法中的代码)时,它被认为是前台进程。除非遇到极大的内存压力,否则系统会保持该进程运行。
但是,一旦从 onReceive() 返回代码,BroadcastReceiver 就不再活跃。接收器的宿主进程变得与在其中运行的其他应用组件一样重要。如果该进程仅托管清单声明的接收器(这对于用户从未与之互动或最近没有与之互动的应用很常见),则从 onReceive() 返回时,系统会将其进程视为低优先级进程,并可能会将其终止,以便将资源提供给其他更重要的进程使用。
因此,您不应在广播接收器启动长时间运行的后台线程。onReceive() 完成后,系统可以随时终止进程来回收内存,在此过程中,也会终止进程中运行的派生线程 。意思就是说:如果在onReceive所在的主线程,在new一个子线程去做任务的话,最好保证子线程在10秒内完成并返回请求结果。
3. 如何给广播所在的进程续命
那有没有方法可以延长广播所在进程生命的方法呢? 这也是本篇文章重点要讨论的,我们可以从进程和应用生命周期这个角度先去理解一下。
在大多数情况下,每个 Android 应用都在各自的 Linux 进程中运行。当需要运行应用的一些代码时,系统会为应用创建此进程,并使其保持运行,直到不再需要它且系统需要回收其内存以供其他应用使用。
如果执行完onReceive方法,则系统会认为 BroadcastReceiver 不再处于活动状态,因此不再需要保留此进程,除非其中有其他应用组件(比如service)处于活动状态,也可以理解为广播进程中还存有前台活动,否则系统可能会随时终止进程以回收内存,当然也会终止在进程中运行的衍生线程。
如何理解上一句话呢? 我们先在讲一下前台进程这个概念:
前台进程是用户目前执行操作所需的进程。如果以下任一条件成立,则进程会被认为位于前台:
3.1 它正在用户的互动屏幕上运行一个 Activity(其 onResume() 方法已被调用)。
3.2 它有一个 BroadcastReceiver 目前正在运行(其 BroadcastReceiver.onReceive() 方法正在行)。
3.3 它有一个 Service 目前正在执行其某个回调(Service.onCreate()、Service.onStart() 或 Service.onDestroy())中的代码。
上述进程的生命周期基本能善始善终,只有在系统中内存过低,导致连这些进程都无法继续运行,才会在最后一步终止这些进程。
好了,说了这么多,就是为了使 广播接收者所在进程 在生命周期时间范围(10/60秒)内,尽量存活久一点,我们也往这个方向去给解决方案。
[第一种方案] : 使用 goAsync() 方法,得到PendingResult 对象,它允许实现将与其相关的工作转移到另一个线程,以避免UI 线程ANR。任务完成后,调用PendingResult.finish()方法,其实目的也是为了是广播进程存在时间更久一点,好让异步线程把任务做完。
广播发送端:
//发送后台广播 生命周期为60s
Intent broadcastIntent = new Intent(MainActivity.this, MyBroadcastReceiver.class);
broadcastIntent.setAction("com.android.action.test_broadcast"); //自定义的Action
sendBroadcast(broadcastIntent);
//发送前台广播 生命周期为10s
Intent broadcastIntent = new Intent(MainActivity.this, MyBroadcastReceiver.class);
broadcastIntent.setAction("com.android.action.test_broadcast"); //自定义的Action
broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); //设置前台广播的标志
sendBroadcast(broadcastIntent);
广播接收端 实例代码如下:
public class MyBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "MyBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
final PendingResult pendingResult = goAsync(); //开始
//AsyncTask,其内部原理也是新启一个线程去执行任务
Task asyncTask = new Task(pendingResult, intent);
asyncTask.execute();
Log.e(TAG, "====线程==="+ Thread.currentThread().getName() + "===" +
"线程ID===="+ Thread.currentThread().getId()) ;
}
private static class Task extends AsyncTask<String, Integer, String> {
private final PendingResult pendingResult;
private final Intent intent;
private Task(PendingResult pendingResult, Intent intent) {
this.pendingResult = pendingResult;
this.intent = intent;
}
@Override
protected String doInBackground(String... strings) {
StringBuilder sb = new StringBuilder();
sb.append("Action: " + intent.getAction() + "\n");
String log = sb.toString();
Log.d(TAG, log);
// 模拟耗时任务
try {
Log.e(TAG, "====线程开始睡眠===="+ Thread.currentThread().getName() + "===" +"线程ID===="+ Thread.currentThread().getId()) ;
Thread.sleep(10 * 1000); // 前台广播的生命极限值
Thread.sleep(60 * 1000); // 后台广播的生命极限值
} catch (InterruptedException e) {
e.printStackTrace();
}
return log;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
Log.e(TAG, "====这里是什么线程??==="+ Thread.currentThread().getName() + "===" +"线程ID===="+ Thread.currentThread().getId()) ;
// Must call finish() so the BroadcastReceiver can be recycled.
pendingResult.finish(); //任务结束一定要调用
}
}
}
打印log如下:
E MyBroadcastReceiver: ====线程===main===线程ID====2
E MyBroadcastReceiver: ====线程开始睡眠====AsyncTask #1===线程ID====372
E MyBroadcastReceiver: ====这里是什么线程??===main===线程ID====2
这里可以看出:
1. 执行onReceive方法是在主线程, AsyncTask的 doInBackground方法是在子线程执行, onPostExecute方法就是把子线程中的任务结果返回给主线程,然后UI显示出来。
2. 当发送前台广播时,我们用Thread.sleep(10s)模拟耗时任务,则必报ANR。
3. 当发送后台广播时,我们用Thread.sleep(60s)模拟耗时任务,则必要ANR。
所以在广播onReceive方法中,开启一个线程做异步任务然后配合 PendingResult 的使用,一般情况下,从服务器下载数据和上报数据给服务器,数据量不是很大的话,此方案是可以满足需求的。
【第二种方案】: onReceive 中使用 IntentService ,IntentService也是异步任务,当任务完成后,把数据回传给onReceive ,网上有很多这样子的例子,大家可以去参考,
其实原理在上面也描述过,就是我在广播接收者进程中,再启动了一个服务,服务它肯定会回调Service.onCreate()等方法,相当于多加入了一个前台活动,那么进程也就不会那么快被回收,这样子就尽量保证你的任务能正常执行完。
【第三种方案】: onReceive 中使用 HandlerThread + PendingResult 联动 代码如下:
广播接收类:
public class MyTestReceiver extends BroadcastReceiver {
private AsyncHandler asyncHandler;
//子线程的Handler
private Handler threadHandler;
private Handler uiHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Bundle object = msg.getData();
Log.d("TEST", "====信息===: "+object.getString("MSG_KEY_MAIN"));
}
};
@Override
public void onReceive(Context context, Intent intent) {
//获取AsyncHandler类的单例对象
asyncHandler = AsyncHandler.getInstance();
//把主线程中的uiHandler传入到子线程中
asyncHandler.setuiHandlerCallBack(uiHandler);
//获取子线程的threadHandler
threadHandler = asyncHandler.getThreadHandlerCallBack();
//通过子线程的handler发送消息
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putString("MSG_KEY_THREAD", "我是通过threadHandler从主线程 发送到 子线程的消息");
msg.setData(bundle);
threadHandler.sendMessage(msg);
final PendingResult pendingResult = goAsync();
AsyncHandler.post(new Runnable() {
@Override
public void run() {
handleIntent(context, intent);//耗时操作
// Must call finish() so the BroadcastReceiver can be recycled.
pendingResult.finish();
Log.d("TEST", "调用 pendingResult.finish() 广播可以回收了");
}
});
}
private void handleIntent(Context context, Intent intent) {
try {
Log.d("TEST", "===模拟耗时任务=开始===");
Thread.sleep(8 * 1000);
Log.d("TEST", "===模拟耗时任务=结束===");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
HandlerThread 类:
public class AsyncHandler {
//此类的核心原理还是利用 HandlerThread 去做异步任务
private static final HandlerThread sHandlerThread = new HandlerThread("AsyncHandler");
private static final Handler sthreadHandler;
private static Handler uiHandler; //主线程中的Handler
/*饿汉单例模式 begin*/
private AsyncHandler() {}
public static final AsyncHandler asyncHandler = new AsyncHandler();
public static AsyncHandler getInstance() {
return asyncHandler;
}
/*饿汉单例模式 end*/
//把主线程中的uiHandler传递过来
public void setuiHandlerCallBack(Handler uiHandler) {
this.uiHandler = uiHandler;
}
//提供获取子线程中threadHandler的方法
public Handler getThreadHandlerCallBack() {
return sthreadHandler;
}
static {
sHandlerThread.start(); //启动
sthreadHandler = new Handler(sHandlerThread.getLooper()){
@Override
public void handleMessage(@NonNull Message message) {
super.handleMessage(message);
Bundle object = message.getData();
Log.d("TEST", "====信息===: "+object.getString("MSG_KEY_THREAD"));
//用主线程的Handler与主线程通信
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putString("MSG_KEY_MAIN", "我是通过uiHandler从子线程 发送到 主线程的消息");
msg.setData(bundle);
uiHandler.sendMessage(msg);
}
};
}
public static void post(Runnable r) {
sthreadHandler.post(r);
}
public static void postDelayed(Runnable r, long delayedMills) {
sthreadHandler.postDelayed(r, delayedMills);
}
}
发送广播类:
public class MainTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_test);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//发送前台广播
Intent broadcastIntent = new Intent();
broadcastIntent.setAction("com.android.action.broadcast_test");
broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
sendBroadcast(broadcastIntent);
Log.d("TEST", "=====启动 MyTestReceiver=====");
}
});
}
}
打印log如下:
14:15:04.236 3385 3385 D TEST : =====启动 MyTestReceiver=====
14:15:04.249 3385 3465 D TEST : ====信息===: 我是通过threadHandler从主线程 发送到 子线程的消息
14:15:04.249 3385 3465 D TEST : ===模拟耗时任务=开始===
14:15:04.260 3385 3385 D TEST : ====信息===: 我是通过uiHandler从子线程 发送到 主线程的消息
14:15:12.249 3385 3465 D TEST : ===模拟耗时任务=结束===
14:15:12.250 3385 3465 D TEST : 调用 pendingResult.finish() 广播可以回收了
此Demo 中还包含了主线程中的uiHandler 和 子线程中的 threadHandler 发送消息的代码,写在这个demo中,也是为了更好的理解两个不同的线程可以通过Handler传递和处理简单的消息。
【第四种方案】:如果把第三种方案理解清楚了,上一个简洁版本的代码,如下:
public class TestBroadcast extends BroadcastReceiver {
private static Handler mAsyncHandler;
static {
HandlerThread thr = new HandlerThread("thread async");
thr.start();
mAsyncHandler = new Handler(thr.getLooper());
}
@Override
public void onReceive(Context context, Intent intent) {
final PendingResult result = goAsync(); //开始
Runnable worker = new Runnable() {
@Override
public void run() {
onReceiveAsync(context, intent);
result.finish(); // 结束
}
};
mAsyncHandler.post(worker);
}
private void onReceiveAsync(Context context, Intent intent) {
//做任务的代码
}
}
文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态
文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境
文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn
文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker
文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机
文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk
文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入
文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。 Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。
文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动
文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计
文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;gt;Jni-&amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图
文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法