可恢复子程序 (Resumable Subs) 是 B4A v7.00 / B4i v4.00 / B4J v5.50 中添加的一项新功能。它大大简化了异步任务的处理。可恢复子程序的特殊功能是它们可以暂停,稍后再恢复,而无需暂停执行主线程。程序不会等待可恢复子程序继续。其他事件将照常引发。
任何具有一个或多个 Sleep 或 Wait For 调用的子程序都是可恢复子程序。
一、Sleep
1、语法及释义
语法:Sleep (Milliseconds As Int)
暂停当前例程执行并在指定时间Milliseconds毫秒后恢复。
Milliseconds:时间延迟的毫秒数。
例如:Sleep (1000) ' 当前例程将暂停 1000 毫秒,然后恢复。
2、刷新 UI
您可以调用 Sleep(0) 来实现最短的暂停。这可用于刷新 UI。它是 DoEvents 的一个很好的替代方案(B4J 和 B4i 中不存在 DoEvents,在 B4A 中应避免使用DoEvents)。
示例:
在程序主界面添加两个按钮Button1、Button2和一个文本标签Label1,添加两个按钮点击事件如下:
Private Sub Button1_Click
For i = 1 To 5000000
Label1.Text=i
Next
Log("Button1_Click finished!")
End Sub
Private Sub Button2_Click
For i = 1 To 5000000
Label1.Text=i
If i Mod 1000 = 0 Then Sleep(0) '允许 UI 每 1000 次迭代刷新一次。
Next
Log("Button2_Click finished!")
End Sub
当点击Button1后,Label1只在循环完成后才显示最终结果5000000。
当点击Button2后,Label1在循环期间每隔1000次刷新显示一次i的数值。
二、Wait For
B4X 编程语言是事件驱动的。异步任务在后台运行,并在任务完成时引发事件。使用 Wait For 关键字,您可以在当前子程序中处理事件。
1、等待地图加载示例
例如,此代码将等待 GoogleMap Ready 事件(B4J 示例):
Sub AppStart (Form1 As Form, Args() As String)
MainForm = Form1
MainForm.RootPane.LoadLayout("1") 'Load the layout file.
gmap.Initialize("gmap")
Pane1.AddNode(gmap.AsPane, 0, 0, Pane1.Width, Pane1.Height)
MainForm.Show
Wait For gmap_Ready '<----------------
gmap.AddMarker(10, 10, "Marker")
End Sub
2、等待文件下载示例
使用 FTP 的稍微复杂一点的示例:列出远程文件夹中的所有文件,然后下载所有文件:
Sub DownloadFolder (ServerFolder As String)
FTP.List(ServerFolder)
Wait For FTP_ListCompleted (ServerPath As String, Success As Boolean, Folders() As FTPEntry, Files() As FTPEntry) '<----
If Success Then
For Each f As FTPEntry In Files
FTP.DownloadFile(ServerPath & f.Name, False, File.DirApp, f.Name)
Wait For FTP_DownloadCompleted (ServerPath2 As String, Success As Boolean)
Log($"File ${ServerPath2} downloaded. Success = ${Success}"$)
Next
End If
Log("Finish")
End Sub
当调用 Wait For 关键字时,子程序会暂停,内部事件调度程序会在等待的事件完成后将其恢复。如果等待的事件一直未完成,则子程序将永远不会恢复。但程序仍将完全响应其它事件。
如果稍后使用同一事件调用 Wait For,则新的子程序实例将取代前一个。
3、等待下载图像示例
假设我们要创建一个下载图像并将其设置为 ImageView 的子程序:
'第一个示例:不正确的示例。
Sub DownloadImage(Link As String, iv As ImageView)
Dim job As HttpJob
job.Initialize("", Me) '请注意,已不再需要名称参数。
job.Download(Link)
Wait For JobDone(job As HttpJob)
If job.Success Then
iv.SetImage (job.GetBitmap) '在B4A/B4i中用iv.Bitmap=job.GetBitmap替换
End If
job.Release
End Sub
如果我们只调用它一次,它会正常工作(更准确地说,如果我们在前一次调用完成之前不再调用它)。如果我们像这样调用它:
DownloadImage("https://www.b4x.com/images3/android.png", ImageView1)
DownloadImage("https://www.b4x.com/images3/apple.png", ImageView2)
这样的话只会显示第二幅图像,因为第二次调用 Wait For JobDone 将覆盖前一个图像。为了解决这个问题,Wait For 可以根据事件发送者区分事件。
这是通过一个可选参数完成的:Wait For (<sender>) <event signature>
'第二个示例:正确的示例。
Sub DownloadImage(Link As String, iv As ImageView)
Dim job As HttpJob
job.Initialize("", Me) '请注意,已不再需要名称参数。
job.Download(Link)
Wait For (job) JobDone(job As HttpJob)
If job.Success Then
iv.SetImage (job.GetBitmap) '在B4A/B4i中用iv.Bitmap=job.GetBitmap替换
End If
job.Release
End Sub
通过上述代码,每个可恢复的子实例将等待不同的事件,并且不会受到其他调用的影响。
不同之处在于 Wait For 行:
错误的: Wait For JobDone(job As HttpJob)
正确的: Wait For (job) JobDone(job As HttpJob)
三、使用Sleep/Wait For的程序的代码流
1、先看下面两个子程序,体会一下使用Sleep时代码的执行顺序:
Sub S1
Log("S1: A")
S2
Log("S1: B")
End Sub
Sub S2
Log("S2: A")
Sleep(0)
Log("S2: B")
End Sub
我们执行S1时将输出下面结果:
S1: A
S2: A
S1: B
S2: B
说明每当调用 Sleep 时,当前子程序都会暂停。这相当于调用 Return。
2、再看下面两个子程序,体会一下使用Sleep和Wait For时代码的执行顺序:
当一个子例程调用第二个可恢复的子例程时,第一个子例程中的代码将在第一个Sleep 或 Wait For调用之后继续向下执行。如果您想等待第二个子例程完成,那么您可以从第二个子例程中引发一个事件,并在第一个子例程中等待它:
Sub FirstSub
Log("FirstSub started")
SecondSub
Wait For SecondSub_Complete
Log("FirstSub completed")
End Sub
Sub SecondSub
Log("SecondSub started")
Sleep(1000)
Log("SecondSub completed")
CallSubDelayed(Me, "SecondSub_Complete")
End Sub
我们执行FirstSub时日志输出结果:
FirstSub started
SecondSub started
SecondSub completed
FirstSub completed
注意:
- 使用 CallSubDelayed 比使用 CallSub 更安全。如果第二个子程序从未暂停(例如,如果仅根据某些条件调用 Sleep),CallSub 将失败。
- 这里有一个假设,即 FirstSub 在完成之前不会再次被调用。
四、可恢复子程序返回值
可恢复子程序可以返回一个 ResumableSub 值。
示例:
Sub Button1_Click
Sum(1, 2)
Log("after sum")
End Sub
Sub Sum(a As Int, b As Int)
Sleep(100) '这将导致代码流返回到父级
Log(a + b)
End Sub
输出:
after sum
3
这就是为什么不能简单地返回一个值的原因。
解决方案:
可恢复的子程序可以返回一个名为 ResumableSub 的新类型。其他子程序可以使用此值等待子程序完成并获取所需的返回值。
Sub Button1_Click
Wait For(Sum(1, 2)) Complete (Result As Int)
Log("result: " & Result)
Log("after sum")
End Sub
Sub Sum(a As Int, b As Int) As ResumableSub
Sleep(100)
Log(a + b)
Return a + b
End Sub
输出:
3
result: 3
after sum
上述 Button1_Click 代码等效于:
Sub Button1_Click
Dim rs As ResumableSub = Sum(1, 2)
Wait For(rs) Complete (Result As Int)
Log("result: " & Result)
Log("after sum")
End Sub
使用可恢复子程序返回值所需步骤如下:
1、将 As ResumableSub 添加到可恢复子程序签名中。
2、使用您想要返回的值调用 Return。
3、在调用子程序中,使用 Wait For (<sub here>) Complete (Result As <matching type>) 调用可恢复子程序。
注意事项和提示:
如果您不需要返回值但仍想等待可恢复子程序完成,则从可恢复子程序返回 Null,并将调用子程序中的类型设置为 Object。
多个子程序可以安全地调用可恢复子程序。完成事件将到达正确的父级。
您可以在其他模块中等待可恢复子程序(在 B4A 中,它仅与类相关)。
可以更改结果参数名称。
五、 KeyPress 和 Wait For MsgBox2Async(仅B4A)
在B4A中,经常检查Back键,以防止用户无意中退出程序。您可以使用以下代码:
Sub Activity_KeyPress (KeyCode As Int) As Boolean '返回True以销毁该事件
Select KeyCode
Case KeyCodes.KEYCODE_BACK
OpenMsgBox
Return True
Case Else
Return False
End Select
End Sub
Sub OpenMsgBox
Private Answ As Int
Msgbox2Async("Do you want to exit?", "Exit", "Yes", "", "No", Null, False)
Wait For Msgbox_Result (Answ As Int)
If Answ = DialogResponse .POSITIVE Then
Activity.Finish
End If
End Sub
六、对话框
模态对话框 = 保持主线程直到对话框关闭的对话框。
如上所述, 模态对话框与DoEvents实现方式相同。 因此,建议切换到新的异步对话框。使用Wait For是一个简单不错的选择:
你应该使用如下代码:
Msgbox2Async("Delete?", "Title", "Yes", "Cancel", "No", Null, False)
Wait For Msgbox_Result (Result As Int)
If Result = DialogResponse .POSITIVE Then
' ...
End If
而不是下面的代码:
Dim res As Int = Msgbox2("Delete?", "Title", "Yes", "Cancel", "No", Null)
If res = DialogResponse.POSITIVE Then
'...
End If
Wait For不会占用主线程,相反,它保存当前的子例程状态并释放它。 当用户单击其中一个对话框按钮时,代码将会恢复。其他类似的新方法有:MsgboxAsync、InputListAsync 和 InputMapAsync。
除了MsgboxAsync外,新方法还添加了一个新的可取消参数。 如果条件为真,则可以通过单击后退键或对话框外部取关闭话框。 这也是旧方法的默认行为。
由于其他代码可以在异步对话框可见时运行,因此有可能会同时出现多个对话框。如果这种情况与你的应用程序相关,那么你应该在Wait For中设置发送者过滤器参数:
Dim sf As Object = Msgbox2Async("Delete?", "Title", "Yes", "Cancel", "No", Null, False)
Wait For (sf) Msgbox_Result (Result As Int)
If Result = DialogResponse .POSITIVE Then
'...
End If
这允许显示多个消息,并会正确处理结果事件。
六、SQL 使用 Wait For
可恢复子例程的新特性,使处理大型数据集变得更简单,且对程序响应性的影响最小。
插入数据的新标准方法是:
For i = 1 To 1000
SQL1.AddNonQueryToBatch("INSERT INTO table1 VALUES (?)", Array(Rnd(0, 100000)))
Next
Dim SenderFilter As Object = SQL1.ExecNonQueryBatch("SQL")
Wait For (SenderFilter) SQL_NonQueryComplete (Success As Boolean)
Log("NonQuery: " & Success)
插入数据的新标准方法步骤步骤如下:
- 对每个应该发出的命令调用 AddNonQueryToBatch 。
- 使用 ExecNonQueryBatch执行命令,这是一种异步方法。这个命令将在后台执行,完成后将引发NonQueryComplete事件。
- 此调用返回一个可用作发送方过滤器参数的对象。 这一点很重要,因为可能会运行多个后台批处理程序。 使用过滤器参数,在所有情况下,正确的 Wait For 调用都会捕获事件。
- 注意:SQL1.ExecNonQueryBatch 在内部开始和结束事务。
对于SQL查询:
在大多数情况下,查询是很快的,因此一般使用同步查询SQL1.ExecQuery2。但是,如果有一个缓慢的查询,那么您应该使用SQL1.ExecQueryAsync:
Dim SenderFilter As Object = SQL1.ExecQueryAsync("SQL", "SELECT * FROM table1", Null)
Wait For (SenderFilter) SQL_QueryComplete (Success As Boolean, rs As ResultSet)
If Success Then
Do While rs.NextRow
Log(rs.GetInt2(0))
Loop
rs.Close
Else
Log(LastException)
End If
与前面的情况一样,ExecQueryAsync方法返回一个用作发送者过滤器参数的对象。
提示:
1、B4A中的结果集类型扩展了cursor类型。如果您愿意,您可以将其更改为cursor。使用结果集的优点是它与B4J和B4i兼容。
2、如果从查询返回的行数很多,那么在调试模式执行循环将会很慢。你可以通过把它放在一个不同的子例程中和清理项目(Ctrl + P)来加快查询速度:
Wait For (SenderFilter) SQL_QueryComplete (Success As Boolean, rs As ResultSet)
If Success Then
WorkWithResultSet(rs)
Else
Log(LastException)
End If
End Sub
Private Sub WorkWithResultSet(rs As ResultSet)
Do While rs.NextRow
Log(rs.GetInt2(0))
Loop
rs.Close
End Sub
这与当前在可恢复子例程中禁用的调试器优化有关。在发布模式下,两种解决方案的性能将相同。
注意事项和提示:
在大多数情况下,可恢复子例程在发布模式下的性能开销应该是很小的。 在调试模式下,开销可能会大些。 (如果这成为一个问题,那么可以将执行慢的代码放到能从可恢复子例程调用的其它子例程中)。
• Wait For 事件处理程序先于常规的事件处理程序。
• 可恢复子例程不会创建其他线程。 该代码由服务器解决方案中的主线程或处理程序线程执行。
• 在B4J中的SQL使用Wait For,需要jSQL v1.50+,且建议将日志模式设置为WAL。