65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ThinkAlike 为 Android 和 Windows 构建空气质量指数小部件( 第二部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2015年7月24日

LGPL3

15分钟阅读

viewsIcon

30007

downloadIcon

299

展示如何使用 ThinkAlike 实现 Android/桌面跨平台开发的新示例,包括 helloworld、Web API 访问和小部件 UI。

目录

第 2 天 - 服务:跨平台 AsyncTask 执行器协调

您准备好迎接“末日空气”的探险了吗?参加过挑战的“护戒远征队”成员正在大厅集结,快点!

部署草图

白袍巫师甘道夫倚着他的白色魔法手杖,审视着墙上悬挂的手稿。“让我们从部署图开始。”巫师指示道。

“如右图所示,在污染严重或高风险区域部署了大量收集器,它们将定期获取空气质量指数(AQI)数据并发送到本地数据库。一个名为 PM2.5.in 的组织将整合这些公共数据,并通过 Web 服务(RESTful API)进行共享。

“一个灵活的代理服务,由Python符文驱动,将代表客户的请求连同授权的应用密钥每次转发到后端服务。

“然而,我们的职责,”甘道夫强调,“集中在客户端。正如你们年轻人可能听说过的那样,客户端采用MVVM架构设计,可以减轻开发人员进行跨平台迁移的工作量。”

身着绿褐色服装的精灵弓箭手莱戈拉斯举起了手。“亲爱的顾问,我听说已经派出另一支队伍来处理 Android 和 JavaFX 之间视图层的兼容性问题了。”

“是的,确实如此,我希望他们明天能取得杰出成就。”甘道夫承认。“在此之前,我们必须处理数据检索部分,该部分由ViewModel层调用,并最终由数据访问层(DAL)完成。”

关于 AsyncTask、Callback 的概念

“围绕这两个层,正如侦察兵所观察到的,必须建立一个AsyncTask机制,”巫师拿出另一卷磨损的卷轴放在桌上。“事实上,这是一个常规的通用机制,因为普遍需要让设备在耗时操作时保持响应。有关详情,请参阅这卷古老的卷轴。”

“一个活生生的 3D UML 图!”精灵惊叹道。“等等,这看起来像是一辆巨大的卡车正驶过隧道,可以通过触摸闪烁的青色珠子来查看它的载物。(太棒了,我甚至能在卡车里看到矮人金姆利)危险!它们在抵达时正遭到巨龙烈焰的攻击!”

“‘卡车里的金姆利’,我们眼尖的莱戈拉斯!”甘道夫笑着说。“事实上,这是一张序列图,描绘了Callback的方案。我不知道它来自哪里……可能源于函数指针仍用于寻址回调目标的旧时代。然而,Callback的概念对于理解AsyncTask至关重要。

“正如你所见,附有‘隧道’的块代表了两个不同的角色(或责任)及其生命线。当一个角色想请另一个角色承担其职责时,它将发送一个调用,即橙色的‘卡车’,以及所需的数据(‘矿石’,或参数),可以通过青色块进行检查,发送到另一方。相应地,消息接收者将处理请求。事实上,发送者应估计处理任务所需的时间,并选择是等待直到完成,还是继续进行其他业务,但会在工作完成后收到通知。

“对于前一种情况,发送者的‘卡车’通常会带着结果回来,这种模型称为同步调用。但对于后者,因为任务处理程序端需要一个新‘载具’来将结果带回调用者,‘巨龙烈焰’般的Callback方案就登场了;从调用者的角度来看,整个过程是几乎透明的(只需要额外的结果处理程序),就像完成了AsyncTask一样。

“在 10 分钟的霍比特蛋糕休息时间之前,有几个关键因素需要勘察,”

  • Callback 如何追踪目标——通常是通过一些预先的技巧,如‘监听器’/‘观察者’/‘订阅者’注册
     
  • 所以每个任务处理程序都需要一个Callback方案——错误。

    如前所述,一个专注于其核心职责的任务处理程序闻起来更好;如果需要异步调用,则可能由调用者负责实例化一个通用 Callback 方案并将其指定为特定用途的委托。魔法的艺术即将揭晓。

  • Callback 的‘载具’是什么颜色——它与AsyncTask 调用者的颜色不必要地不同……

    说得好。事实上,前面提到的通用 Callback 方案,目前通常实现为AsyncTask 执行器,它将为一个(或已经为)自己的用途启动一个空的工作线程。由于Executor是一个通用代理,因此AsyncTask 调用者也有责任覆盖其执行部分。一个设计完美的AsyncTask Executor可以这样使用:一个特定的任务(例如耗时的 HTTP 请求)将覆盖一个 `run()` 方法并在独立线程中执行;另一个覆盖的 `onSuccess(Results...)` 方法将在工作线程正常完成后被调用,将结果传回AsyncTask 调用者的原始线程。

  • (可选)金姆利为何如此热衷于激怒巨龙烈焰——为了点燃他自己很难够到的烈炉。

AsyncTask,Android 的实现,以及盗贼

即使在享用巴金斯的种子蛋糕时,远征队成员们也开始争论他们即将到来的任务的复杂性。

“Java 太难处理了,不管加上什么 FX(特效);我宁愿用 aXe-Sharp,”一个矮人咕哝着,大口喝了一杯咖啡。

“所有面向对象的技术都差不多,我的强项,”精灵弓箭手莱戈拉斯笑着回答。“在我看来,成为一个快速学习者对像你这样坚强的战士来说是加分项,如果不是乘数的话。”

一个大包裹被拖进大厅,打断了他们的谈话。“援军终于到了,”甘道夫兴奋地说。“我之前提到的所有关键因素,都已经实现在 Android 设备中了。我们的任务是分析其中的奥秘,并希望能像我们在 UI 组件上所做的那样,分离出平台无关的适配器

“这个包裹里的东西就是比尔博从 Android 核心代码库盗取的代码!”巫师宣布。矮人、霍比特人和精灵们跳跃、欢呼,甚至原地打转。“然而,Android 的制造商是我们的盟友,”甘道夫停顿了一下提醒道。又一次停顿,听到一声勺子落地的叮当声。“困难并非获取所有代码那么大,而是挖掘出必需的片段,甚至找出与其它 Java 平台兼容的通用本质。

“让我们来看看比尔博的AsyncTask 集合,以及从经典 Java 工具包中引用的那些。”

  • android.os.AsyncTask
  • android.os.Handler
  • android.os.Looper
  • android.os.Message
  • java.util.concurrent.Executors
  • java.util.concurrent.ExecutorService
  • java.util.concurrent.FutureTask
  • java.util.concurrent.ThreadPoolExecutor

甘道夫掏出一个短陶土烟斗,眼中闪烁着智慧的光芒。“前面有三个问题:参数传递、工作线程启动和结果反馈。”

任务 1. 参数传递

烟圈以各种神秘的形状升上天空。有些消散了,有些则以更显眼的颜色增强。一个小时过去了,又没有茶歇……一个矮人咳嗽了一声,又睡着了,一个声音随即响起,“我们明白了!”

概述


(点击放大)

“尽管它看起来很复杂,这张协作图可以阐明新的AsyncTask方案的所有调用流程和责任。设计的核心是平台无关的AsyncExecutor,位于黄色背景区域,它有一个 Android 实现(中上部)和一个 JavaFX 等效实现(中下部)。简而言之,调用者可以初始化一个平台无关的FutureTask对象,并将其传递给任一执行器以完成异步调用。”

“一个有十几个节点的图表让我不知所措,”莱戈拉斯深吸一口气说道。

“确实只有十几个,”甘道夫说。“黄色区域的三个节点已经描述过了,蓝色区域的五个节点引用自 Android 或 Java SDK,而其余四个是调用者类和辅助类。链接上方的数字标记了对象之间交换消息的顺序。”

任务讨论

“我们的第一个任务是,将任务处理程序所需的数据,用Callback卷轴中描绘的‘卡车’运载。问题是,有一个AsyncTask 执行器将作为任务调用者的委托,异步调用任务;你认为谁会产生数据,又该先向谁传递参数?”

“我认为AsyncTask 调用者,在我们这个场景下是ViewModel层,最了解数据;而AsyncTask 执行器作为任务的直接调用者,应该首先被通知参数,”莱戈拉斯分析道。“此外,Android SDK 使用 `android.os.AsyncTask<Params, Progress, Result>` 作为其通用AsyncTask 执行器。由于我们倾向于封装平台特定的实现而不是重新发明轮子,我们将不得不通过它来传递Params。”

“你注意到了 Android 中的习惯用法,这值得称赞,”甘道夫说。“但还不够。的确,他们调用 `AsyncTask.execute()` 来执行一个异步任务,但如果你查看`android.os.AsyncTask` 的源代码,你会发现他们内部使用了一个静态的 `java.util.concurrent.ThreadPoolExecutor` 来处理所有 `android.os.AsyncTask` 实例。”

“你的意思是……`android.os.AsyncTask` 的核心是一个 `java.util.concurrent.ThreadPoolExecutor`,它应该是我们封装的真正目标?”莱戈拉斯想了想,抓住了要点,问道。

解决方案

执行器就是执行器,”巫师总结道。“无论使用哪种平台特定的类。我们必须拥有自己的平台无关的AsyncTask类,命名为 `thinkalike.generic.concurrent.FutureTask<Params, Result>`,以指向预期的任务处理程序(例如,击败索伦),并传递所需的数据(例如,魔戒)。”

“一个执行器只知道执行一个指定的Callable,在一个新线程中调用它的 `call()` 方法,没有更多细节。而一个FutureTask,当它的 `call()` 方法被调用时,将联系相应的任务处理程序并将其参数传递给它。

Executor<AqiInfo> asynExec = Loader.getInstance().getPlatform().getFactory().createAsyncExecutor(...);
FutureTask<String, AqiInfo> task = new FutureTask<String, AqiInfo>(_aqiAreaCode){
	//Note that this is a protected method. A public call() method will be called by Executor
	//and eventually call this method with parameters which are initialized in constructor method.
	@Override
	protected AqiInfo call(String... params) {
		if(params==null || params.length<1)
			throw new NullPointerException();
		String aqiAreaCode = params[0];
		return AqiInfoLoader.loadAqiInfo(aqiAreaCode);
	}
	......
};
asynExec.execute(task);

“我亲爱的朋友,”甘道夫看着莱戈拉斯说。“你能识别出这里采用了哪种设计模式,并为我画一个图吗?”

这位精灵弓箭手盯着协作图看了一会儿,回答道:“适配器,一种结构型模式;以及两层嵌套,就像我刚吃过的黑莓苹果派一样。”

任务调用者要求任务处理程序,
不耐烦地等待,异步执行器已发送。
任务处理程序化身为 Callable 适配器,
调用接口,显然匹配。
Callable 适配器携带任务参数,
对一切无知,执行器已启动。

任务 2. 启动工作线程

“启动‘线程’?你不是说要在这里启动一个三头巨龙吧?”首席工匠,一个矮人,困惑地问道。

“如果用作执行任务的‘载具’,它们的意思是一样的,”精灵莱戈拉斯回答道,面前放着一张协作图。“事实上,白袍巫师甘道夫在向我解释完设计后就去忙别的事了;如果我们能尽快制作好这个机制……但愿能……”

“当然!把剩下的点说清楚,你就能得到我的支持,”工匠坚定地承诺道。

任务讨论

“第二个任务,正如甘道夫所指示的,是确定工作线程应该在哪里启动,”莱戈拉斯解释道。“工作线程是为任务处理程序分配空间和时间以完成其任务。”

“很抱歉,在我给你解释相对论之前不得不打断你……;我的意思是,我明白了概念。”首席工匠说。

“好吧,快速射击模式,”莱戈拉斯说。“很明显,在 Android 中,启动工作线程是 `android.os.AsyncTask` 的职责;然而,在 JavaFX 中,该类不再可用,我们必须自己维护一个 `java.util.concurrent.ThreadPoolExecutor`,尽管没有经验。通过查看这两个类的源代码,2.7K 行或更多,我们在 `ThreadPoolExecutor` 类中发现了一个 `addWorker()` 方法,它为每个 `Worker`(这是一个内部类,包装了 `Runnable` 任务以进行锁定/解锁控制)启动工作线程(`t.start()`)。

“然后我们转向 `javafx.concurrent.Task`,据说它最适合在 JavaFX 中进行异步任务编程。幸运的是,我们发现该类继承自 `FutureTask` 并具有一个 `Runnable` 接口。最终,我们设法模仿了 `android.os.AsyncTask` 的源代码来维护一个静态的 `ThreadPoolExecutor`,以便为每个 `javafx.concurrent.Task` 依次启动工作线程,相应地调用 `thinkalike.generic.concurrent.FutureTask`,并最终完成由任务调用者指定的实质性任务及其参数。”

“……嗯,干得不错,”矮人沉默了一会儿,挤出一丝笑容评论道。

解决方案

“我来确认一下,”矮人说。“从根本上说,`java.util.concurrent.ThreadPoolExecutor` 作为一个通用组件,将负责在 Android 和 JavaFX 平台上启动工作线程。由任务调用者在外部定义的 `thinkalike.generic.concurrent.FutureTask`,可以通过 `AsyncExecutor` 的外壳提交,并最终优雅地运行在已启动的工作线程上。”

任务 3. 结果返回(至原始线程)

“我得承认,”莱戈拉斯说,“这张协作图里藏着更复杂的东西。例如,Android SDK 的 `android.os.AsyncTask` 内部使用了其他平台特定的类,如 `Message`、`Loop` 和 `Handler`,以便通过消息机制发送回结果;而 `javafx.concurrent.Task` 则利用FX 应用线程进行此类传递。”

“幸运的是,所有这些细节都不需要我们担心,不是吗?”工匠说。“除此之外,我注意到 `thinkalike.generic.concurrent.FutureTask` 是一个 `Callable` 而不是 `Runnable`。但为什么呢?”

“`Callable` 接口与 `Runnable` 类似,”莱戈拉斯回答。“它们都设计用于实例可能由另一个线程执行的类。然而,`Runnable` 既不能返回结果(这是AsyncTask处理的最后环节),也不能抛出有用的异常。”

任务讨论

原始线程,即任务调用者所在的线程,将等待异步处理结果的返回,”莱戈拉斯分析道。“这种原始线程通常指的是(在大多数情况下它就是)‘UI 线程’(在 SWT、.NET、Android 中),‘事件分发线程’(EDT,在 AWT、Swing 中)或‘FX 应用线程’(在 JavaFX 中)。由于 UI 操作既不能被耗时任务阻塞,也不能在接收AsyncTask 结果时被渲染请求淹没,因此必须有一个绑定到原始线程的队列,以保证其稳健性和线程安全。”

“很好。如果我犯了错误,请原谅我,”矮人接过莱戈拉斯的话说。“总之,不同平台对这种AsyncTask 结果的传递和排队有不同的实现。封装得很好,但是,为了重用它,我们需要声明一个平台无关的 `FutureTask` 类,使其与 `Callable` 接口兼容,以便将结果委托给 `android.os.AsyncTask` 或 `javafx.concurrent.Task`。”

“你说对了,”莱戈拉斯说。“Callable 接口不关心任务参数,所以我们将重载构造函数来初始化它们,如你所记得的。参数不需要传递给平台特定的AsyncTask 执行器,它通过启动工作线程并返回任务结果来履行其职责。”

“为甘道夫大师欢呼三声!”矮人欢呼胜利。

“现在既然你已经掌握了所有要点,我可以揭开另一个棘手的问题吗?希望不是压垮你的最后一根稻草。”莱戈拉斯又笑了。“取消是另一种结果。对于耗时任务,用户必须有权中断它。此外,程序员可能还想停止不必要的作业——例如,渲染已经滑出边界的ListView单元格。对于资源受限的平台,一个可取消的异步计算是必须的……”

解决方案

“所以 `java.util.concurrent.FutureTask` 是关键,”矮人首席工匠站在椅子上,向他的船员解释道。“不是我们刚才讨论的 `FutureTask`,而是 JDK 中同名类。

“`java.util.concurrent.FutureTask` 实现 `RunnableFuture<V>` 接口,该接口扩展了 `Runnable` 和 `Future<V>`,因此对取消相关的方法具有具体实现:`cancel()`、`isCancelled()`、`isDone()` 和 `get()`,后者将返回结果或异常。事实上,`android.os.AsyncTask` 和 `javafx.concurrent.Task` 都在内部使用了这个类!唯一的区别是 Android 的 `AsyncTask` 嵌入了 `ThreadPoolExecutor` 并且可以单独使用,而 JavaFX 的 `Task` 没有。”

“由于可取消性是必需的,我们的 `FutureTask` 可以将相关请求委托给嵌入的 JDK `FutureTask`。这将是一个快速的解决方案。然而,一个更好的解决方案,正如我与莱戈拉斯讨论过的,不是在任务任务执行器中复制 `FutureTask`,而是通过工厂,让我们的 `FutureTask` 直接继承平台特定的AsyncTask类。换句话说,协作图的右半部分将简化为平台无关的 `ThreadPoolExecutor`,而左半部分将更繁重但更精确!”

“重构!重构!”工匠们挥舞着锤子、斧头和咖啡杯喊道。

休息时间

白袍巫师甘道夫坐在一个绿色营地里,读着一只鹰送来的剧本。“太棒了,”巫师喃喃自语,一半自言自语地笑着。“他们修改了我的设计,协作图。然而,核心组件保持不变:参数工作线程结果;接口也是如此。

“从 `android.os.AsyncTask` 的源代码,到如何通过 `AyncExecutor` 挂钩回指定任务,然后确认 `ThreadPoolExecutor` 的角色,接着是 JavaFX 中的任务类,然后是跨线程的结果传递。此外还有取消……好吧,跨平台 AsyncTask 处理的协调终于来了。”

皮平和梅利,两个年轻的霍比特人拿着营地里的设备,听到了这些话,互相看了一眼,笑了起来。

真正的和谐是在冲突的灰烬燃烧之前不会萌芽的。

待续...

历史

2015-07-25:第 1 版

许可证

本文及任何相关的源代码和文件,均根据 GNU 宽通用公共许可证(LGPLv3) 许可。

© . All rights reserved.