欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Kotlin入门(30)多线程交互

程序员文章站 2022-03-13 19:20:47
Android开发时常会遇到一些耗时的业务场景,比如后台批量处理数据、访问后端服务器接口等等,此时为了保证界面交互的及时响应,必须通过线程单独运行这些耗时任务。简单的线程可使用Thread类来启动,无论Java还是Kotlin都一样,该方式首先要声明一个自定义线程类,对应的Java代码如下所示: 自 ......

android开发时常会遇到一些耗时的业务场景,比如后台批量处理数据、访问后端服务器接口等等,此时为了保证界面交互的及时响应,必须通过线程单独运行这些耗时任务。简单的线程可使用thread类来启动,无论java还是kotlin都一样,该方式首先要声明一个自定义线程类,对应的java代码如下所示:

    private class playthread extends thread {
        @override
        public void run() {
            //此处省略具体的线程内部代码
        }
    }

 

自定义线程的kotlin代码与java大同小异,具体见下:

    private inner class playthread : thread() {
        override fun run() {
            //此处省略具体的线程内部代码
        }
    }

 

线程类声明完毕,接着要启动线程处理任务,在java中调用一行代码“new playthread().start();”即可,至于kotlin则更简单了,只要“playthread().start()”就行。如此看来,java的线程处理代码跟kotlin差不了多少,没发觉kotlin比java有什么优势。倘使这样,真是小瞧了kotlin,它身怀多项绝技,单单是匿名函数这招,之前在介绍任务runnabe时便领教过了,线程thread同样也能运用匿名函数化繁为简。注意到自定义线程类均需由thread派生而来,然后必须且仅需重写run方法,所以像类继承、函数重载这些代码都是走过场,完全没必要每次都依样画葫芦,编译器真正关心的是run方法内部的具体代码。于是,借助于匿名函数,kotlin的线程执行代码可以简写成下面这般:

    thread {
        //此处省略具体的线程内部代码
    }.start()

 

以上代码段看似无理,实则有规,不但指明这是个线程,而且命令启动该线程,可谓是简洁明了。

线程代码在运行过程中,通常还要根据实际情况来更新界面,以达到动态刷新的效果。可是android规定了只有主线程才能操作界面控件,分线程是无法直接调用控件对象的,只能通过android提供的处理器handler才能间接操纵控件。这意味着,要想让分线程持续刷新界面,仍需完成传统android开发的下面几项工作:
1、声明一个自定义的处理器类handler,并重写该类的handlemessage方法,根据不同的消息类型进行相应的控件操作;
2、线程内部针对各种运行状况,调用处理器对象的sendemptymessage或者sendmessage方法,发送事先约定好的消息类型;
举个具体的业务例子,现在有一个新闻版块,每隔两秒在界面上滚动播报新闻,其中便联合运用了线程和处理器,先由线程根据情况发出消息指令,再由处理器按照消息指令轮播新闻。详细的业务代码示例如下:

class messageactivity : appcompatactivity() {
    private var bplay = false
    private val begin = 0 //开始播放新闻
    private val scroll = 1 //持续滚动新闻
    private val end = 2 //结束播放新闻
    private val news = arrayof("北斗三号卫星发射成功,定位精度媲美gps", "美国赌城拉斯维加斯发生重大枪击事件", "日本在越南承建的跨海大桥未建完已下沉", "南水北调功在当代,近亿人喝上长江水", "德国外长要求中国尊重“一个欧洲”政策")

    override fun oncreate(savedinstancestate: bundle?) {
        super.oncreate(savedinstancestate)
        setcontentview(r.layout.activity_message)
        tv_message.gravity = gravity.left or gravity.bottom
        tv_message.setlines(8)
        tv_message.maxlines = 8
        tv_message.movementmethod = scrollingmovementmethod()
        btn_start_message.setonclicklistener {
            if (!bplay) {
                bplay = true
                //线程第一种写法的调用方式,通过具体的线程类进行构造。
                //注意每个线程实例只能启动一次,不能重复启动。
                //若要多次执行该线程的任务,则需每次都构造新的线程实例。
                //playthread().start()
                //线程的第二种写法,采用匿名类的形式。第二种写法无需显式构造
                thread {
                    //发送“开始播放新闻”的消息类型
                    handler.sendemptymessage(begin)
                    while (bplay) {
                        //休眠两秒,模拟获取突发新闻的网络延迟
                        thread.sleep(2000)
                        val message = message.obtain()
                        message.what = scroll
                        message.obj = news[(math.random() * 30 % 5).toint()]
                        //发送“持续滚动新闻”的消息类型
                        handler.sendmessage(message)
                    }
                    bplay = true
                    thread.sleep(2000)
                    //发送“结束播放新闻”的消息类型
                    handler.sendemptymessage(end)
                    bplay = false
                }.start()
            }
        }
        btn_stop_message.setonclicklistener { bplay = false }
    }

    //自定义的处理器类,区分三种消息类型,给tv_message显示不同的文本内容
    private val handler = object : handler() {
        override fun handlemessage(msg: message) {
            val desc = tv_message.text.tostring()
            tv_message.text = when (msg.what) {
                begin -> "$desc\n${dateutil.nowtime} 下面开始播放新闻"
                scroll -> "$desc\n${dateutil.nowtime} ${msg.obj}"
                else -> "$desc\n${dateutil.nowtime} 新闻播放结束,谢谢观看"
            }
        }
    }

}

通过线程加上处理器固然可以实现滚动播放的功能,可是想必大家也看到了,这种交互方式依旧很突兀,还有好几个难以克服的缺点:

1、自定义的处理器仍然存在类继承和函数重载的冗余写法;
2、每次操作界面都得经过发送消息、接收消息两道工序,繁琐且拖沓;
3、线程和处理器均需在指定的activity代码中声明,无法在别处重用;
有鉴于此,android早已提供了异步任务asynctask这个模版类,专门用于耗时任务的分线程处理。然而asynctask的用法着实不简单,首先它是个模板类,初学者瞅着模板就发慌;其次它区分了好几种运行状态,包括未运行、正在运行、取消运行、运行结束等等,一堆的概念叫人头痛;再次为了各种状况都能与界面交互,又得定义事件监听器及其事件处理方法;末了还得在activity代码中实现监听器的相应方法,才能正常调用定义好的asynctask类。
初步看了下自定义asynctask要做的事情,直让人倒吸一口冷气,看起来很高深的样子,确实每个android开发者刚接触asynctask之时都费了不少脑细胞。为了说明asynctask是多么的与众不同,下面来个异步加载书籍任务的完整java代码,温习一下那些年虐过开发者的asynctask:

//模板类的第一个参数表示外部调用execute方法的输入参数类型,第二个参数表示运行过程中与界面交互的数据类型,第三个参数表示运行结束后返回的输出参数类型
public class progressasynctask extends asynctask<string, integer, string> {
    private string mbook;
    //构造函数,初始化数据
    public progressasynctask(string title) {
        super();
        mbook = title;
    }

    //在后台运行的任务代码,注意此处不可与界面交互
    @override
    protected string doinbackground(string... params) {
        int ratio = 0;
        for (; ratio <= 100; ratio += 5) {
            // 睡眠200毫秒模拟网络通信处理
            try {
                thread.sleep(200);
            } catch (interruptedexception e) {
                e.printstacktrace();
            }
            //刷新进度,该函数会触发调用onprogressupdate方法
            publishprogress(ratio);
        }
        return params[0];
    }

    //在任务开始前调用,即先于doinbackground执行
    @override
    protected void onpreexecute() {
        mlistener.onbegin(mbook);
    }

    //刷新进度时调用,由publishprogress函数触发
    @override
    protected void onprogressupdate(integer... values) {
        mlistener.onupdate(mbook, values[0], 0);
    }

    //在任务结束后调用,即后于doinbackground执行
    @override
    protected void onpostexecute(string result) {
        mlistener.onfinish(result);
    }

    //在任务取消时调用
    @override
    protected void oncancelled(string result) {
        mlistener.oncancel(result);
    }

    //声明监听器对象
    private onprogresslistener mlistener;
    public void setonprogresslistener(onprogresslistener listener) {
        mlistener = listener;
    }

    //定义该任务的事件监听器及其事件处理方法
    public static interface onprogresslistener {
        public abstract void onfinish(string result);
        public abstract void oncancel(string result);
        public abstract void onupdate(string request, int progress, int sub_progress);
        public abstract void onbegin(string request);
    }

}

见识过了asynctask的惊涛骇浪,不禁喟叹开发者的心灵有多么地强大。多线程是如此的令人望而却步,直到kotlin与anko的搭档出现,因为它俩在线程方面带来了革命性的思维,即编程理应是面向产品,而非面向机器。对于分线程与界面之间的交互问题,它俩给出了堪称完美的解决方案,所有的线程处理逻辑都被归结为两点:其一是如何标识这种牵涉界面交互的分线程,该点由关键字“doasync”阐明;其二是如何在分线程中传递消息给主线程,该点由关键字“uithread”界定。有了这两个关键字,分线程的编码异乎寻常地简单,即使加上activity的响应代码也只有以下寥寥数行:

    //圆圈进度对话框
    private fun dialogcircle(book: string) {
        dialog = indeterminateprogressdialog("${book}页面加载中……", "稍等")
        doasync {
            // 睡眠200毫秒模拟网络通信处理
            for (ratio in 0..20) thread.sleep(200)
            //处理完成,回到主线程在界面上显示书籍加载结果
            uithread { finishload(book) }
        }
    }

    private fun finishload(book: string) {
        tv_async.text = "您要阅读的《$book》已经加载完毕"
        if (dialog.isshowing) dialog.dismiss()
    }

以上代码被doasync括号圈起来的代码段,就是分线程要执行的全部代码;至于uithread括号圈起来的代码,则为通知主线程要完成的工作。倘若在分线程运行过程中,要不断刷新当前进度,也只需在待刷新的地方添加一行uithread便成,下面是添加了进度刷新的代码例子:

    //长条进度对话框
    private fun dialogbar(book: string) {
        dialog = progressdialog("${book}页面加载中……", "稍等")
        doasync {
            for (ratio in 0..20) {
                thread.sleep(200)
                //处理过程中,实时通知主线程当前的处理进度
                uithread { dialog.progress = ratio*100/20 }
            }
            uithread { finishload(book) }
        }
    }