问题
The issue is now resolved. See the last edit for details!
I have an UIViewController with an audio visualisation and a button. When a button is pressed, the following function is fired:
func use(sender:UIButton!) {
// Analyse the audio
let analysisQueue = dispatch_queue_create("analysis", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(analysisQueue, {
// Initialise the analysis controller
let analysisController = AnalysisController()
analysisController.analyseAudio(global_result.filePath, completion: {
// When analysis is complete, navigate to another VC
dispatch_async(dispatch_get_main_queue(), {
let mainStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
let navigationController = self.navigationController
// Pop the current VC before pushing the new one
navigationController?.popViewControllerAnimated(false)
navigationController?.pushViewController(vc, animated: false)
})
})
})
}
Here, I create a background queue and start a very lengthy signal processing operation. Once the processing is complete, I perform navigation to another view controller using the main queue.
This causes the ResultDetailViewController
to appear on the screen with all the relevant data and visualisation fully loaded.
However, for the first 2-3 seconds after the VC has been loaded, none of the buttons work! If I click any button within this initial period, the action would be fired once that initial period is over.
When I perform this transition from any other VC, the ResultDetailViewController
is loaded smoothly, and everything works.
What am I doing wrong? Any help would be much appreciated!
EDIT 1
I will add more details about my set-up:
In AnalysisController
, I am doing the following:
- Process the signal using FFT's and such
- Update a the properties of a
global_result
- Trigger a method of
global_result
, which stores the result in CoreData and uses Alamofire to POST the data to my server - Once the first POST succeeds, the callback updates
global_result
's id, and fires few more POST requests. - Completion handler is triggered, which then causes the transition
global_result
, is my custom global Object which is initialised as a public var
.
In theory, the completion handler should be triggered once the processing completes, results are saved in CoreData, and the first POST request is dispatched.
In ResultDetailViewController
's viewDidLoad
function, I am copying the global_result
into a local variable and creating the UI elements using data from global_result
.
Now, I suspected that the lag occurs due to background thread using global_result
when ResultDetailViewController
is already loaded, so I tried to create a new instance of Result
class, instead of copying the global_result
, but that also didn't help.
And here's the Result
class:
import Foundation
import CoreData
import Alamofire
import CryptoSwift
public class Result {
var localID: Int
var id: Int
var filePath: NSURL!
var result: Double
var origSound: [Double]
init(localID: Int, id: Int, filePath: NSURL, result: Double, origSound: [Double]) {
// Initialize stored properties.
self.localID = localID
self.id = id
self.filePath = filePath
self.result = result
self.origSound = origSound
}
func store() {
self.storeLocal()
// Serialise data
let parameters = [
"localID": self.localID,
"id": self.id,
"result": self.result
]
// Prepare request
let request = NSMutableURLRequest(URL: NSURL(string: "my_server/script.php")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: parameters)
// POST the data to server
Alamofire.request(request)
.responseJSON { response in
if let JSON = response.result.value {
if let myID = JSON as? Int {
self.id = myID
// Store the global ID in CoreData
updateIDLocal(self.localID, id: self.id)
// Serialise and POST array
let param = [
"origSound": self.origSound
]
// Prepare request
var request = NSMutableURLRequest(URL: NSURL(string: "my_server/script2.php?id=\(self.id)")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: param)
// POST the data to server
Alamofire.request(request)
// Upload the file
let upURL = "my_server/script3.php?id=\(self.id)"
// Prepare request
request = NSMutableURLRequest(URL: NSURL(string: upURL)!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encrypt the file
request.HTTPBody = fileToEncryptedData(rsaKey, filePath: self.filePath)
// POST the data to server
Alamofire.request(request)
}
}
}
}
// Store the object in CoreData
func storeLocal() {
// Create a local id
if let oldID = NSUserDefaults.standardUserDefaults().integerForKey("localID") as Int? {
// Increment the ID
NSUserDefaults.standardUserDefaults().setInteger(oldID + 1, forKey: "localID")
self.localID = oldID + 1
}
else {
// First object in CoreData
NSUserDefaults.standardUserDefaults().setInteger(0, forKey: "localID")
}
// Store data in CoreData
var resultDatas = [NSManagedObject]()
//1
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let entity = NSEntityDescription.entityForName("Result",
inManagedObjectContext:managedContext)
let resultData = NSManagedObject(entity: entity!,
insertIntoManagedObjectContext: managedContext)
// Store data
resultData.setValue(localID, forKey: "localID")
resultData.setValue(id, forKey: "id")
resultData.setValue(filePath.path!, forKey: "url")
resultData.setValue(result, forKey: "result")
// Store the array
var data = NSData(bytes: origSound, length: origSound.count * sizeof(Double))
resultData.setValue(data, forKey: "origSound")
//4
do {
try managedContext.save()
//5
resultDatas.append(resultData)
} catch _ {
print("Could not save")
}
}
}
Within the AnalysisController
, I am calling global_result.store()
EDIT 2
What I thought was happening, was this:
- Created a background thread
- Done heavy processing on that thread
- Sent a POST request on that thread
- HTTP response was processed, and lots of data was encrypted on that background thread
- Jumped to the main thread
- Performed the transition on the main thread
In reality, this happened:
- Created a background thread
- Done heavy processing on that thread
- Sent a POST request on that thread
- Jumped to main thread
- Performed the transition on the main thread
- HTTP responses suddenly came back to the main thread, and it became blocked until tons of data finished encrypting.
Thanks to alexcurylo's suggestion, and this SO thread, I realised that the Alamofire's response handling occurs on the main thread, thus it was necessary to use one cool Alamofire feature to push the response handling onto a concurrent thread, so not to block the main queue.
For anyone's future reference, I implemented it like so:
// Prepare request
let request = NSMutableURLRequest(URL: NSURL(string: "my_server/script.php")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: parameters)
// Create a concurrent queue
let queue = dispatch_queue_create("DT.response-queue", DISPATCH_QUEUE_CONCURRENT)
// POST the data to server
Alamofire.request(request)
.response(
queue: queue,
responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments),
completionHandler: { response in
// Perform response-handling HERE
})
Thanks everyone for your help! Even though it didn't directly solve this problem, Sandeep Bhandari's background queue creation tip, and Igor B's method for Storyboard referencing represent a better coding practice, which should be adopted instead of my original code.
回答1:
The actions firing after a delay is a dead giveaway that some processing is blocking your main thread.
Watchdog will help you suss out exactly what that processing is.
Class for logging excessive blocking on the main thread. It watches the main thread and checks if it doesn’t get blocked for more than defined threshold. You can also inspect which part of your code is blocking the main thread.
Simply, just instantiate Watchdog with number of seconds that must pass to consider the main thread blocked. Additionally you can enable strictMode that stops the execution whenever the threshold is reached. This way, you can inspect which part of your code is blocking the main thread.
Presumably the problem will be obvious from that inspection!
回答2:
try changing the way you have created a background thread it might help :)
func use(sender:UIButton!) {
// Analyse the audio
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
// Initialise the analysis controller
let analysisController = AnalysisController()
analysisController.analyseAudio(global_result.filePath, completion: {
// When analysis is complete, navigate to another VC
dispatch_async(dispatch_get_main_queue(), {
let mainStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
let navigationController = self.navigationController
// Pop the current VC before pushing the new one
navigationController?.popViewControllerAnimated(false)
navigationController?.pushViewController(vc, animated: false)
})
})
})
}
回答3:
My point instantiation of UIStoryboard could be consuming task so that I would recommend to try something like that:
func use(sender:UIButton!) {
// Analyse the audio
let analysisQueue = dispatch_queue_create("analysis", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(analysisQueue, {
// Initialise the analysis controller
let analysisController = AnalysisController()
analysisController.analyseAudio(global_result.filePath, completion: {
// When analysis is complete, navigate to another VC
dispatch_async(dispatch_get_main_queue(), {
let mainStoryboard = self.navigationController!.storyboard
let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
let navigationController = self.navigationController
// Pop the current VC before pushing the new one
navigationController?.popViewControllerAnimated(false)
navigationController?.pushViewController(vc, animated: false)
})
})
})
}
来源:https://stackoverflow.com/questions/37526616/long-delay-when-pushing-a-view-controller-from-gcd