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

  • 如我所知:在Android中耗时操作不能放在主线程。这很好理解,因为UI线程需要刷新UI。如果因为你的一个耗时操作,而不能及时响应其他的交互,如按钮点击等等,就会导致UI卡顿。这样用户体验当然不好了,所以Android不允许我们在主线程做耗时操作。
  • 但是这样就给开发者带来一个必须面对的问题:如何处理子线程与UI线程的通信问题。当然,Android也给我提供了这样的机制,如AsyncTaskHandler。(貌似也只提供了这两种)。不过话说回来,这种线程间通信,与直接在一个线程完成所有逻辑是不一样的。这实际上大大增加了开发的难度,也是一个非常考验开发者功底的问题。当然了,现在的,无所不能的github以及各大开源网站上面也提供了很多这样已经封装了线程间通信的各种框架。比如xutils,imageLoader 等等。但是,是不是我们的每种需求,都能找到对应的开源框架?
  • 所以,我们必须要根据自身的需求,去定制属于自己的通信。
  • 好了,废话说的太多了。下面就说说这个Demo里是如何做到子线程与UI线程的通信的。
  • 首先,这个Demo的功能是:将一个目录中的音乐文件,拷贝到另外一个目录当中。然而为何使用拷贝文件而不是直接重命名过去?因为,我要做的是,通过拷贝这样一个迟缓的过程,去在UI上面显示我们的拷贝进度。同时,在拷贝完成,删除源文件,对应的UI上面也会有Item的移除刷新。功能就是这样,相对实际需求而言,还是非常简单的。(不过就这么一个简单的功能,也是花费了我5个小时的时间才给他完全搞定)。
  • 其次,简单介绍一下这个Demo所用到的一些知识点:
    1. AsyncTask的使用;
    2. Handler的使用;(为何有了AsyncTask还要使用Handler?见代码,解释起来很繁琐…)
    3. Service组件的使用;
    4. 文件IO流的操作;
    5. Executors线程池的使用;
    6. Fragment的使用;(复用)
    7. Adapter的使用;(复用以及缓存导致的UI错乱的解决)
    8. JavaEE经典DAO模式的使用;(可能有点四不像,将就将就)
  • OK,知识点应该就这些了。说起来比较唬人。其实这些知识点大家在实际项目中都有使用到。
  • 下面就是项目介绍了,这次不准备贴代码上去,到时候直接放出Demo的下载地址。
  • 首先,我们来分析一下,如果要做这样的一个导入导出,并且在UI上,显示进度更新,显示导入完成的更新。要如何去做呢?
    1. 必须要有一个Activity,不然也就没有UI一说。
    2. 然后既然有导入导出两个功能,也就是说有两个UI,那么我们既可以选择使用两个Activity,也可以使用一个Activity+两个Fragment来做。这里,我使用的是第二种方式。
    3. 既然是要在UI上面显示一个目录中的文件,进行导入导出,那么一定是需要考虑,目录中有多个文件的情况,也就是说UI上面,有多个Item显示的状况。既然是多个Item,那么我们可以使用ListView或者GridView来做,这里我使用的是GridView
    4. 既然使用到了GridView,那么必然要去使用BaseAdapter。是使用系统提供的ArrayAdapterSimpleAdapter来做还是自己重写一下BaseAdapter?这里,我使用的是后者。
    5. UI上面应该就是以上4种需要考虑。然后就是逻辑方面的问题了。
    6. 那么,第一,如何去获取指定目录下的文件?这个就不用说了。大家都明白的。不过需要注意一点就是,如果我们相应进行操作的是SDcard上面的目录,那么就要判断一下SDcard的状态先。
    7. 第二,如何将获取到的文件信息显示到UI 上面?这个也不用说的。不过,依然是需要注意几点:1.获取文件信息可能是一个耗时操作,应该放进子线程去做,然后是Handler还是AsyncTask到UI上去显示,都可以。这里我使用的是AsyncTask
    8. 既然数据已经在UI上面显示了。那么我们如果给导入导出添加一个入口,让用户可以通过一个操作来触发文件的导入导出呢?这里,我就是通过GridViewItemClick事件去触发的。
    9. 那么这个触发,会发生什么,或者说,这个事件里面,我们需要做什么逻辑操作呢?这里,我的做法是,通过这样的点击触发事件,我就去start一下Service。然后,Service收到意图之后,就去执行文件IO操作。
    10. 那么现在,重点来了:文件的IO,必然是一个耗时的操作。必然是放在子线程去弄。那么,我们是开一个匿名子线程去完成还是使用线程池去管理,还是使用AsyncTask去完成呢?都可以。这里我使用的是线程池去管理。
    11. 既然IO操作是放在子线程路面的,那么我们又如何去更新UI的进度呢?这里,因为我没有去使用AsyncTask,所以,我就使用了Handler去做这样的操作。逻辑就是,在文件IO的使用,通过Handler去将当前进度发送到UI线程。这样UI线程至少是先拿到了进度信息。不过这块又有一个新的问题出现,那就是:进度信息现在在Service中获取到了,但是我们的UI必然是在Activity中刷新的。我又如何将在Service中获取的信息让Activity拿到,然后去更新UI呢?这里,有两种思路,一种,是将需要更新的控件,比如ProgressBar传递给Service,直接在Service中进行这样的UI更新。(这种方式广泛的用于一些知名的开源框架中。比如xutils的BitmapUtilsimageLoader等。他们的做法就是,将你需要进行刷新的控件传递给他,他给你完成刷新。)另外一种,就是将Service获取的进度信息,传递给Activity,然后,在Activity中,去给相应的控件去更新UI。这种思路,一般又有两种解决方式:一种是Android提供的广播机制,这是可以完成的,另外一种就是Java的回调机制。也是可以完成的,这里,我使用的是回调的方式。
    12. OK,既然Activity拿到进度了。那么就可以去刷新UI了。这里,有可以用多种的方式,一般,可以直接在BaseAdaptergetView方法里面去刷新,另外一种就是,不在这里刷新,而是在外面的任何地方去刷新。比如,在ItemClick的回调方法里面去通过findViewById()或者findViewWithTag()等方式去找到对应的控件,然后拿到控件进行刷新。 我之前就是这么做的。但是后来我一想,这样的每次都去寻找控件其实是比较消耗资源的,还不如直接在getView方法里面去刷新。
    13. 说的getView肯定是使用经典的复用模式。不过,在这里,这个复用模式倒是有点问题。因为复用会导致,上一个Item的UI跑到下一个Item上面去,也就是说,你上一个Item更新到100%,然后移除掉了。那么,你的下一个Item会沿用上一个Item的View,也就导致了你的Progress==100.这样就导致UI错乱。这个解决就比较简单了,就是在getView方法里面,在ProgressBar控件完成初始化之后,立即将进度设置为0.在进行更新的时候,再去更新。这样就可以将复用的ProgressBar进度设置为0。而避免UI的错乱。这是很简单的一种错乱,也很好解决。
    14. 但是,还有一种错乱,就不好解决了。就是,如果你同时对多个文件进行IO操作的时候,你的UI上面,也就是有多个Item在进行进度的更新。这时候,你的UI上面,就会出现进度条乱跳的情况。这就不好弄了。因为这些Item确实是需要进行UI刷新的,而现在也确实在进行UI的刷新。不过就是会将A的进度给B,B的也给A。所以,进度就一下前一下后。这样跳来跳去,直到只剩下最后一个正在刷新的UI,否则会一直这样跳下去。
    15. 这个跳到的错乱如何解决?这个我们就要去分析一下了,为什么会出现这样的错乱呢?因为,在拿到Service传递过来的进度的时候,我们没有办法判断这个进度到底是给哪个Item去更新UI的。于是他们就会给所有的更新的Item的进度都去赋值,这样,因为每个Item的当前进度不同,就导致了进度的跳来跳去。那么,既然我们已经明白了为何如此,那么我们就去对症下药。既然Service传递过来的进度,我们不知道是应该给谁。那么我们就要Service去明确这一点,要他告诉我,我到底应该将进度给谁去更新,而不是现在这样给大家。所以,我们在Service进行传递进度的时候,同时传递一个唯一的flag.这个flag只要是唯一,且和UI上面的Item是一一对应的就可以了。这里,我的做法是,再传递一个当前Item对应的文件的绝对路径,这个恰好就是唯一,且和Item一一对应的flag。既然,我们已经拿到了对应的Item的进度,那么我们在做进度更新的时候,就针对对应的Item去更新对应的进度。也就是说,我们在更新Item的进度的时候,需要先判断一下,如果传递过来的进度的flag是当前Item的对应的flag,那么我们才去更新该Item的进度,否则,就不去更新该item的进度。
    16. 到此为止,我们的Demo已经完成了95%。但是如果你在文件IO没有结束之前,去点击界面的按钮,或者去点击返回键等。你就会发现,UI变得非常卡顿。你的点击,几乎没有任何的响应。这又是为何?需要我们再次去分析一下。因为我们的UI一直在更新进度,也就是说UI线程,一直在运行。而你的点击事件,是在UI正在运行时候触发的。而他当前的任务并没有执行完。所以,他就没有机会去响应我们的点击,直到文件IO完成,也就是进度刷新完成。这也就造成了UI的卡顿。既然如此,那么我们就对症下药。既然我们在点击的时候有UI的卡顿,也就是进度的刷新没有结束导致的。那么,我们就做一个操作,就是在我们点击返回的时候,去移除进度刷新的回调。如果是广播,那就反注册掉这个广播。当然,移除监听也是会带来新的问题的。那就是进度不再刷新了。这也是不好的用户体验。所以,移除的操作,应该放在返回这样的退出当前UI的操作里面,而不是任何的交互事件中。不过这么一来,就出现了一个两难的境地:就是,如果移除,是不卡顿了,但是进度不刷新了;如果不移除,那么,交互的时候,卡顿怎么办?
    17. 其实这个问题,我也没有好的解决方案。不过从我自己去点击来看,好像除了返回键这样的关闭当前UI的操作会因为没有移除而异常卡顿。而界面中的,一般的交互操作,倒是没有出现卡顿的情况。这个问题,留给大家去解决。
  • 以上就是项目的整个逻辑流程以及对应技术点的使用。就不再贴代码了,给一个完整项目Demo完整项目Demo。里面有本地git,如果想看卡顿的时候,也可以回滚到前面的提交记录去运行查看。
  • 因为我的Demo是基于平板来做的,如果大家是用手机去运行UI可能不太好看,将就将就。。。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值