您的位置:首页 > 产品设计 > UI/UE

Android:带你玩转Servie,子线程,与UI线程通信[导入导出]

2015-10-25 01:52 337 查看
如我所知:在Android中耗时操作不能放在主线程。这很好理解,因为UI线程需要刷新UI。如果因为你的一个耗时操作,而不能及时响应其他的交互,如按钮点击等等,就会导致UI卡顿。这样用户体验当然不好了,所以Android不允许我们在主线程做耗时操作。

但是这样就给开发者带来一个必须面对的问题:如何处理子线程与UI线程的通信问题。当然,Android也给我提供了这样的机制,如
AsyncTask
Handler
。(貌似也只提供了这两种)。不过话说回来,这种线程间通信,与直接在一个线程完成所有逻辑是不一样的。这实际上大大增加了开发的难度,也是一个非常考验开发者功底的问题。当然了,现在的,无所不能的github以及各大开源网站上面也提供了很多这样已经封装了线程间通信的各种框架。比如
xutils
,
imageLoader
等等。但是,是不是我们的每种需求,都能找到对应的开源框架?

所以,我们必须要根据自身的需求,去定制属于自己的通信。

好了,废话说的太多了。下面就说说这个Demo里是如何做到子线程与UI线程的通信的。

首先,这个Demo的功能是:将一个目录中的音乐文件,拷贝到另外一个目录当中。然而为何使用拷贝文件而不是直接重命名过去?因为,我要做的是,通过拷贝这样一个迟缓的过程,去在UI上面显示我们的拷贝进度。同时,在拷贝完成,删除源文件,对应的UI上面也会有Item的移除刷新。功能就是这样,相对实际需求而言,还是非常简单的。(不过就这么一个简单的功能,也是花费了我5个小时的时间才给他完全搞定)。

其次,简单介绍一下这个Demo所用到的一些知识点:

AsyncTask
的使用;

Handler
的使用;(为何有了
AsyncTask
还要使用
Handler
?见代码,解释起来很繁琐…)

Service
组件的使用;

文件IO流的操作;

Executors
线程池的使用;

Fragment
的使用;(复用)

Adapter
的使用;(复用以及缓存导致的UI错乱的解决)

JavaEE经典DAO模式的使用;(可能有点四不像,将就将就)

OK,知识点应该就这些了。说起来比较唬人。其实这些知识点大家在实际项目中都有使用到。

下面就是项目介绍了,这次不准备贴代码上去,到时候直接放出Demo的下载地址。

首先,我们来分析一下,如果要做这样的一个导入导出,并且在UI上,显示进度更新,显示导入完成的更新。要如何去做呢?

必须要有一个
Activity
,不然也就没有UI一说。

然后既然有导入导出两个功能,也就是说有两个UI,那么我们既可以选择使用两个
Activity
,也可以使用一个
Activity
+两个
Fragment
来做。这里,我使用的是第二种方式。

既然是要在UI上面显示一个目录中的文件,进行导入导出,那么一定是需要考虑,目录中有多个文件的情况,也就是说UI上面,有多个Item显示的状况。既然是多个Item,那么我们可以使用
ListView
或者
GridView
来做,这里我使用的是
GridView


既然使用到了
GridView
,那么必然要去使用
BaseAdapter
。是使用系统提供的
ArrayAdapter
SimpleAdapter
来做还是自己重写一下
BaseAdapter
?这里,我使用的是后者。

UI上面应该就是以上4种需要考虑。然后就是逻辑方面的问题了。

那么,第一,如何去获取指定目录下的文件?这个就不用说了。大家都明白的。不过需要注意一点就是,如果我们相应进行操作的是SDcard上面的目录,那么就要判断一下SDcard的状态先。

第二,如何将获取到的文件信息显示到UI 上面?这个也不用说的。不过,依然是需要注意几点:1.获取文件信息可能是一个耗时操作,应该放进子线程去做,然后是
Handler
还是
AsyncTask
到UI上去显示,都可以。这里我使用的是
AsyncTask


既然数据已经在UI上面显示了。那么我们如果给导入导出添加一个入口,让用户可以通过一个操作来触发文件的导入导出呢?这里,我就是通过
GridView
ItemClick
事件去触发的。

那么这个触发,会发生什么,或者说,这个事件里面,我们需要做什么逻辑操作呢?这里,我的做法是,通过这样的点击触发事件,我就去
start
一下
Service
。然后,
Service
收到意图之后,就去执行文件IO操作。

那么现在,重点来了:文件的IO,必然是一个耗时的操作。必然是放在子线程去弄。那么,我们是开一个匿名子线程去完成还是使用线程池去管理,还是使用
AsyncTask
去完成呢?都可以。这里我使用的是线程池去管理。

既然IO操作是放在子线程路面的,那么我们又如何去更新UI的进度呢?这里,因为我没有去使用
AsyncTask
,所以,我就使用了
Handler
去做这样的操作。逻辑就是,在文件IO的使用,通过
Handler
去将当前进度发送到UI线程。这样UI线程至少是先拿到了进度信息。不过这块又有一个新的问题出现,那就是:进度信息现在在
Service
中获取到了,但是我们的UI必然是在
Activity
中刷新的。我又如何将在
Service
中获取的信息让
Activity
拿到,然后去更新UI呢?这里,有两种思路,一种,是将需要更新的控件,比如
ProgressBar
传递给
Service
,直接在
Service
中进行这样的UI更新。(这种方式广泛的用于一些知名的开源框架中。比如
xutils的BitmapUtils
imageLoader
等。他们的做法就是,将你需要进行刷新的控件传递给他,他给你完成刷新。)另外一种,就是将
Service
获取的进度信息,传递给
Activity
,然后,在
Activity
中,去给相应的控件去更新UI。这种思路,一般又有两种解决方式:一种是
Android
提供的广播机制,这是可以完成的,另外一种就是Java的回调机制。也是可以完成的,这里,我使用的是回调的方式。

OK,既然
Activity
拿到进度了。那么就可以去刷新UI了。这里,有可以用多种的方式,一般,可以直接在
BaseAdapter
getView
方法里面去刷新,另外一种就是,不在这里刷新,而是在外面的任何地方去刷新。比如,在
ItemClick
的回调方法里面去通过
findViewById()
或者
findViewWithTag()
等方式去找到对应的控件,然后拿到控件进行刷新。 我之前就是这么做的。但是后来我一想,这样的每次都去寻找控件其实是比较消耗资源的,还不如直接在
getView
方法里面去刷新。

说的
getView
肯定是使用经典的复用模式。不过,在这里,这个复用模式倒是有点问题。因为复用会导致,上一个
Item
的UI跑到下一个
Item
上面去,也就是说,你上一个Item更新到100%,然后移除掉了。那么,你的下一个Item会沿用上一个Item的View,也就导致了你的Progress==100.这样就导致UI错乱。这个解决就比较简单了,就是在
getView
方法里面,在
ProgressBar
控件完成初始化之后,立即将进度设置为0.在进行更新的时候,再去更新。这样就可以将复用的
ProgressBar
进度设置为0。而避免UI的错乱。这是很简单的一种错乱,也很好解决。

但是,还有一种错乱,就不好解决了。就是,如果你同时对多个文件进行IO操作的时候,你的UI上面,也就是有多个Item在进行进度的更新。这时候,你的UI上面,就会出现进度条乱跳的情况。这就不好弄了。因为这些Item确实是需要进行UI刷新的,而现在也确实在进行UI的刷新。不过就是会将A的进度给B,B的也给A。所以,进度就一下前一下后。这样跳来跳去,直到只剩下最后一个正在刷新的UI,否则会一直这样跳下去。

这个跳到的错乱如何解决?这个我们就要去分析一下了,为什么会出现这样的错乱呢?因为,在拿到
Service
传递过来的进度的时候,我们没有办法判断这个进度到底是给哪个Item去更新UI的。于是他们就会给所有的更新的Item的进度都去赋值,这样,因为每个Item的当前进度不同,就导致了进度的跳来跳去。那么,既然我们已经明白了为何如此,那么我们就去对症下药。既然
Service
传递过来的进度,我们不知道是应该给谁。那么我们就要
Service
去明确这一点,要他告诉我,我到底应该将进度给谁去更新,而不是现在这样给大家。所以,我们在
Service
进行传递进度的时候,同时传递一个唯一的flag.这个flag只要是唯一,且和UI上面的Item是一一对应的就可以了。这里,我的做法是,再传递一个当前Item对应的文件的绝对路径,这个恰好就是唯一,且和Item一一对应的flag。既然,我们已经拿到了对应的Item的进度,那么我们在做进度更新的时候,就针对对应的Item去更新对应的进度。也就是说,我们在更新Item的进度的时候,需要先判断一下,如果传递过来的进度的flag是当前Item的对应的flag,那么我们才去更新该Item的进度,否则,就不去更新该item的进度。

到此为止,我们的Demo已经完成了95%。但是如果你在文件IO没有结束之前,去点击界面的按钮,或者去点击返回键等。你就会发现,UI变得非常卡顿。你的点击,几乎没有任何的响应。这又是为何?需要我们再次去分析一下。因为我们的UI一直在更新进度,也就是说UI线程,一直在运行。而你的点击事件,是在UI正在运行时候触发的。而他当前的任务并没有执行完。所以,他就没有机会去响应我们的点击,直到文件IO完成,也就是进度刷新完成。这也就造成了UI的卡顿。既然如此,那么我们就对症下药。既然我们在点击的时候有UI的卡顿,也就是进度的刷新没有结束导致的。那么,我们就做一个操作,就是在我们点击返回的时候,去移除进度刷新的回调。如果是广播,那就反注册掉这个广播。当然,移除监听也是会带来新的问题的。那就是进度不再刷新了。这也是不好的用户体验。所以,移除的操作,应该放在返回这样的退出当前UI的操作里面,而不是任何的交互事件中。不过这么一来,就出现了一个两难的境地:就是,如果移除,是不卡顿了,但是进度不刷新了;如果不移除,那么,交互的时候,卡顿怎么办?

其实这个问题,我也没有好的解决方案。不过从我自己去点击来看,好像除了返回键这样的关闭当前UI的操作会因为没有移除而异常卡顿。而界面中的,一般的交互操作,倒是没有出现卡顿的情况。这个问题,留给大家去解决。

以上就是项目的整个逻辑流程以及对应技术点的使用。就不再贴代码了,给一个完整项目Demo完整项目Demo。里面有本地git,如果想看卡顿的时候,也可以回滚到前面的提交记录去运行查看。

因为我的Demo是基于平板来做的,如果大家是用手机去运行UI可能不太好看,将就将就。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: