Android 广播 goAsync PendingResult 异步任务-程序员宅基地

技术标签: 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) {

        //做任务的代码

    }

}

    

        

        

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u012514113/article/details/123449084

智能推荐

注册登陆业务逻辑实现_利用面向对象思路,设计和完成“手机号注册校验”业务逻辑。如果涉及db存储逻辑,你-程序员宅基地

文章浏览阅读4.1k次,点赞4次,收藏18次。注册前端 ajax发起请求 -&amp;gt;调用注册接口后端 设置路由 &amp;lt;—&amp;gt; controller层后端 注册接口的逻辑实现 a. database连接数据库 b. model层负责数据库增删改查 c. controller层导入模块-&amp;gt;model d. model-&amp;gt;查询用户名是否存在 f. 有/提..._利用面向对象思路,设计和完成“手机号注册校验”业务逻辑。如果涉及db存储逻辑,你

Google Colab 环境现已无需手动登录 HF 账号-程序员宅基地

文章浏览阅读487次,点赞9次,收藏7次。前段时间我们介绍了一些 HF 核心库在 Google Colab 环境中的内置,包括可以使用 hf:// 资源标识符来调用 Hub 上的数据集等等,详情可以回顾文章:Google Colab 现已支持直接使用 ???? transformers 库。今天,随着 huggingface_hub 库的更新,在 Google Colab 环境里,你可以使用其 Secrets (密钥) 功能,将 HF_TOKE...

搭建hdfs分布式集群_hdfs集群地址-程序员宅基地

文章浏览阅读1.4k次。搭建hdfs分布式集群_hdfs集群地址

外挂学习之路(8)--- 释放技能call-程序员宅基地

文章浏览阅读8.6k次,点赞5次,收藏29次。首先以寻找 所有对怪物释放技能的call为主题首先理一下编程者如何编写释放技能的流程,1. 游戏玩家释放技能2. 获取当前选中怪物ID或者指针或者标识一类的总之能标识释放对象3. 进行释放技能的校验(如技能CD,施法对象是否正确)4. 向服务器发送封包数据一般是这么写的不排除特殊写法,按照这个思路我们从步骤2下手回溯找_技能call

Android获取当前网速质量——分四个等级_connectionclassmanager.getinstance().getcurrentban-程序员宅基地

文章浏览阅读2.9k次,点赞2次,收藏6次。在开发中,有时候常常需要根据用户当前的网速来做一些操作,比如图片的加载,当网速非常好的时候,比如连接的是wifi,我们就会下载高分辨率的图片,反之,当用户使用的是2g网时,我们则给他下载低分辨率的小图,从而节省用户流量。而Facebook其实已经给我们提供了这么一个库,详见network-connection-class。使用其实超级简单,先加入依赖compile 'com.f_connectionclassmanager.getinstance().getcurrentbandwidthquality 无效

20180601_Eclipse自动生成mybatis映射文件-程序员宅基地

文章浏览阅读215次。Eclipse自动生成mybatis映射文件1.安装MyBatis Generator插件打开Eclipse,找到Help--Eclipse Marketplace。搜索MyBatis Generator,Install安装即可。2.新建generatorConfig.xml文件,填写配置信息generatorConfig.xml ..._eclipse生成mybatis映射文件

随便推点

Web前端开发(七)--正则表达式,jQ选择器,操作元素,尺寸_前端正则表达式 长宽 高 取规则字符-程序员宅基地

文章浏览阅读303次。一、正则表达式使用单个字符,区间所符合某个规则的字符串。声明正则对象1.new RegExp();使用这个方法时需要注意,如果有转义字符,需要进行两次转义。2.var reg=/…/;内置方法1.text()返回匹配结果,成功为true,不成功为false。2.exec()匹配成功返回时数组,不成功返回null。转义字符\w 单个的数字字母下划..._前端正则表达式 长宽 高 取规则字符

华为硬件笔试 通用器件知识2_华为硬件笔试题(最新版)-程序员宅基地

文章浏览阅读2.7k次。《华为硬件笔试题(最新版)》由会员分享,可在线阅读,更多相关《华为硬件笔试题(最新版)(3页珍藏版)》请在人人文库网上搜索。1、一 选择 13 个题目,没有全部抄下来,涉及的课程有电路,模拟电路,数字电路,信号与系统,微机原理 ,网络,数字信号处理 有关于 1.微分电路 2.CISC,RISC 3.数据链路层 二 填空 10 个题目,没有全部抄下来,涉及的课程有电路,模拟电路,数字电路,信号与系统..._华为器件机考

jieba结巴分词--关键词抽取(核心词抽取)_jieba分词提取关键词-程序员宅基地

文章浏览阅读5.3k次,点赞3次,收藏23次。转自:http://www.cnblogs.com/zhbzz2007 欢迎转载,也请保留这段声明。谢谢!1 简介关键词抽取就是从文本里面把跟这篇文档意义最相关的一些词抽取出来。这个可以追溯到文献检索初期,当时还不支持全文搜索的时候,关键词就可以作为搜索这篇论文的词语。因此,目前依然可以在论文中看到关键词这一项。除了这些,关键词还可以在文本聚类、分类、自动摘要等领域中有着重_jieba分词提取关键词

source insight怎样恢复默认界面设置!_sourceinsight 窗口这么回复默认-程序员宅基地

文章浏览阅读6.2w次,点赞13次,收藏29次。郁闷,文件搜索的窗口让我弄没了,界面被我弄乱了,。最终的想法恢复一下默认就好了。解决方案有一下几种:1)快捷键:CTRL+O2)视图选择,项目窗口打勾就行了3)这个配置文件保存在 我的文档里面Source Insight\Settings文件夹里面关闭SI,把settings里边的配置文件删掉,重新打开SI,就恢复默认的全部设置了4)关闭source insig_sourceinsight 窗口这么回复默认

DWT数字水印算法(Python)-程序员宅基地

文章浏览阅读7.3k次,点赞19次,收藏117次。DWT数字水印算法的基本原理 结合Arnold变换的基于DWT的数字水印的嵌入。充分利用了小波变换的特点,采用Haar小波,把原始图像及水印图像进行三级小波分解,然后在多分辨率分解后的频段嵌入水印信号,得到嵌入水印的图像。 数字水印最重要的性质就是鲁棒性和不可感知性,两者存在一定的矛盾。鲁棒性要求加大水印信息的强度,而水印信息强度的增大反过来_dwt数字水印

syzkaller配置小结_libncurses5-dev : depends: libtinfo5 (= 5.9+201401-程序员宅基地

文章浏览阅读1.9k次。记录syzkaller的配置过程及遇到的一些问题_libncurses5-dev : depends: libtinfo5 (= 5.9+20140118-1ubuntu1) but 6.1-1ubun

推荐文章

热门文章

相关标签