We have an iOS app that has a UITableView containing map snapshots that are generated when the cells appear. The sample list we are using is simply showing the map snapshots based on a lat/long provided in a Model class. I started noticing memory crashes, so I reduced the code to its bare minimum. The crashes still occur, when we are only doing the snapshot with nothing even being done to the results. See below for the code, which is contained in our custom cell and called via the cellForItemAtIndexPath method:
private func testMapSnapshot(viewModel: StreamViewModel)
{
let latDelta:CLLocationDegrees = 0.005
let lonDelta:CLLocationDegrees = 0.005
let span:MKCoordinateSpan = MKCoordinateSpanMake(latDelta, lonDelta)
let location:CLLocationCoordinate2D = CLLocationCoordinate2DMake(viewModel.coordinate.latitude, viewModel.coordinate.longitude)
let region:MKCoordinateRegion = MKCoordinateRegionMake(location, span)
let options = MKMapSnapshotOptions()
options.region = region
options.size = mapImageView.frame.size
options.scale = UIScreen.mainScreen().scale
viewModel.mapSnapshotter = MKMapSnapshotter(options: options)
viewModel.mapSnapshotter!.startWithCompletionHandler() { snapshot, error in
// do nothing
}
}
In didEndDisplayingCell, I am making sure to cancel the mapSnapshotter. See for reference (we keep the list of models in our main VC class that contains the tableview):
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath)
let model = viewModel?[indexPath.item] {
model.mapSnapshotter?.cancel()
model.mapSnapshotter = nil
}
}
Note that before doing this last step, it was crashing way earlier. But now, if you start scrolling down the list fast, it starts stuttering and won't stop stuttering. If you go up and down a list of about 150 rows, it'll take less than 30 seconds before we start seeing memory warnings and then a crash.
I ran this through Instruments, but it wasn't very helpful. It looks as if the Heap and Anonymous VM allocations are gradually going up, possibly causing the crash. See for reference:
I saw this post out there:
MKMapSnapshotter uses incredible amounts of CPU & RAM But it's unanswered and doesn't really address why the memory wouldn't be released.
Any thoughts on where to go on this? Thank you in advance, and please let me know if I can provide more info.
Although I was not able to find a fix to this particular issue, I was able to resolve it by calling the map snapshotter only when the scrollView stops - it then grabs all the visible cells and loads only for those. That way it minimizes the number of calls to this API and prevents memory issues, as opposed to it constantly calling it as you scroll down a list via the cellForRow method.
Related
So I couldn't find a good answer that works for my problem. I am creating a SpriteKit game on iOS. Im just going to give you guys a quick overview of my setup and then go over the problem. I think that's the best way to explain this.
The Setup:
Basically in my didMove(to view:) method I call two helper functions: setupNodes() and setupLevel(). What these functions do is load the different scenes and SKSpriteNodes into memory and then add them to the scene. Here is the code I use to do this:
func setupNodes(){
doubleVent = (loadForegroundOverlayTemplate("DoubleVent").copy() as! SKSpriteNode)
}
func loadForegroundOverlayTemplate(_ fileName: String) -> SKSpriteNode{
let overlayScene = SKScene(fileNamed: fileName)!
let overlayTemplate = overlayScene.childNode(withName: "ForegroundOverlay")!.copy() as! SKSpriteNode
return overlayTemplate
}
This now stores the scene which is a child of an SKNode called "ForegroundOverlay". I then pass this to a function called "createForegroundOverlay()" like so:
let overlaySprite = doubleVent
createForegroundOverlay(doubleVent)
and then "createForegroundOverlay" adds the SKSpriteNode stored in doubleVent to the scene like so:
func createForegroundOverlay(_ overlayTemplate: SKSpriteNode) {
let foregroundOverlay = overlayTemplate.copy() as? SKSpriteNode
lastOverlayPosition.x = lastOverlayPosition.x + (lastOverlayWidth + ((foregroundOverlay?.size.width)!)/2)
lastOverlayWidth = (foregroundOverlay?.size.width)!/2.0
foregroundOverlay?.position = lastOverlayPosition
//TESTING PURPOSES
if TESTING_NODE_POS{
print("The last overlay position x:\(lastOverlayPosition.x), y:\(lastOverlayPosition.y)")
print("The last overlay width is \(lastOverlayWidth)")
print("Adding a foreground overlay \(count)")
count += 1
}
//TESTING PURPOSES
if TESTING_BOX_OUTLINE {
let foregroundPos = foregroundOverlay?.position
let boxLocation = foregroundOverlay?.childNode(withName: "Box1")?.position
if boxLocation != nil {
print("The location of the box \(String(describing: boxLocation))")
print("The foregroundLocation is \(String(describing: foregroundPos))")
print("last overlay position is \(lastOverlayPosition)")
}
}
//add the foreground overlay as a child of the foreground node
fgNode.addChild(foregroundOverlay!)
}
the variables for positioning - lastOverlayPosition, lastOverlayWidth etc.. - are just properties of my GameScene class used to know where and when to add the overlay that was passed.
The fgNode is a node that I stored from my GameScene.sks file like so:
let worldNode = self.childNode(withName: "World")!
bgNode = worldNode.childNode(withName: "Background")!
bgCityNode = worldNode.childNode(withName: "BackgroundCity")
cityBGOverlayTemplate = (bgCityNode.childNode(withName: "Overlay")!.copy() as! SKNode)
cityOverlayWidth = bgCityNode.calculateAccumulatedFrame().width
backgroundOverlayTemplate = (bgNode.childNode(withName: "Overlay")!.copy() as! SKNode)
backgroundOverlayWidth = backgroundOverlayTemplate.calculateAccumulatedFrame().width
fgNode = worldNode.childNode(withName: "Foreground")!
This was also done in my setupNodes() method.
The problem:
So maybe some of you guys have already seen the problem, but the problem is that when I launch my game it crashes and I get the message:
"Thread 1: EXC_BAD_ACCESS: (code = 1, address = 0x78)"
It is the exact same message every single time a crash occurs. I think I understand what the error is saying. Basically there is a pointer pointing to some location in memory (0x78) that has nothing there. So dereferencing that pointer obviously causes a fault. Here is where I get confused... This only happens about 50% of the time. when I run and build the project it builds successfully every time and then crashes 50% of the time with that error message. Secondly, this occurs at the very beginning. This is odd to me because how can some memory already be freed resulting in a bad pointer at the very beginning of the game. Also if the crash doesn't occur at launch then a crash never occurs except for when I reload the scene after the game over scene is displayed, which is basically the same as the game being relaunched.
With some time I narrowed the problem to one line of code:
fgNode.addChild(foregroundOverlay!)
if I comment this line out, no crash ever occurs (I tried building and running 50 times). The problem must be with the foregroundOverlay variable, which was setup using the code in the setup section of this discussion. So I have no idea how to fix this... Any Ideas??
P.S. it might also be worth noting that adding a child to the scene in this way only noticeably became a problem when I upgraded to xCode 10.0 and that I have considered using zombies, but I don't think that would help since the crash only happens at the launch of the game.
If I was unclear anywhere please let me know.
I'm looping through all pages in a PDFDocument (200+ pages) but app crashes with
Message from debugger: Terminated due to memory issue
The pdf is approx 4mb in size yet each iteration of the loop jumps the memory up approx 30mb. Which doesn't seem right to me. I have managed to locate where in my code the memory is being used just not sure how to claim it back. Tried setting variables to nil but no effect. Tried code in the for loop in an autoreleaspool{} but no effect.
#objc func scrapePDF(){
let documentURL = self.documentDisplayWebView!.url!
let document = PDFDocument(url: documentURL)
let numberOfPages = document!.pageCount
DispatchQueue.global().async {
for pageNumber in 1...numberOfPages {
print(document?.page(at: pageNumber)!.string!)
}
}
}
UPDATE: solved ..... kind of
Playing around a bit I found that rather than passing a reference to the PDFDocument inside the loop, if instead I create a new instance for each loop this strangely solves the memory issue. I don't quite understand why though. PDFDocument is a Class not a Struct so is passed by reference. Meaning it is only created once and then referenced to inside my loop. So why would it cause a memory issue?
#objc func scrapePDF(){
let documentURL = self.documentDisplayWebView!.url!
let document = PDFDocument(url: documentURL)
let numberOfPages = document!.pageCount
DispatchQueue.global().async {
for pageNumber in 1...numberOfPages {
let doc = PDFDocument(url: documentURL)
print(doc?.page(at: pageNumber)!.string!)
}
}
}
Though the above code clears the memory issue the problem with it is that its too slow. Each loop takes 0.5 seconds and with 300+ pages I can't accept that. Any tips on speeding it up? Or why it doesn't give the memory back if referencing the PDFDocument from outside the loop
Further UPDATE.
It seems that it’s calling the .string method of the PDFPage that is increases the memory to the point of crashing.
So I have an app that pulls up movie results when I type in a search. (Like IMDb.) I use a free API from themoviedb.org to load the results. I load them in a TableViewController. I load the posters for the results using a mod on the .dataTaskWithRequest method. to make it synchronous. Other than that, it's just basic API sending and receiving for the titles, genres, and years of the movies or TV Shows.
Now my app lags when I type too fast, this isn't completely because of the synchronous loading, because it still happens when I don't load images at all, but image loading makes the app lag, too. Now this is an issue in and of itself, but the problem is that when the app loads the words on to the screen, and is done with the lag, the results are the results of part of the word I have on screen. For example, if I type "The Simpsons" too fast, I get results for "The Sim", but if I backspace once, and retype "The Simpsons", the results reload correctly. Something that complicates things even more, is that sometimes I get the top result only being one of the old, partial results, and the rest are normal and loaded underneath.
Here is a video explaining the situation. The first time i type down "the simpsons", you can see the lag. I typed it all really fast, but it lags past the word "the". When it is done loading, it loads up a beowulf result that shouldn't even be there. I have no idea what's going on and it's driving me nuts. Even when I don't load images, and the typing doesn't lag, the results still don't update.
Here are the relevant code snippets, if you want any more, feel free to ask. I just don't want to bombard you with too much code at once:
This updates search results when text is typed in search bar:
extension SearchTable : UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
//To Handle nils
var searchBarText = searchController.searchBar.text
if (searchBarText == nil) {
searchBarText = ""
}
searchBarText! = searchBarText!.condenseWhitespace()
//To Handle Disallowed Characters
searchBarText = searchBarText!.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
//Find Results from themoviedb
let urlString = "https://api.themoviedb.org/3/search/multi?query=" + searchBarText! + "&api_key= (I can't post the api key publicly online, sorry)"
let results = NSURL(string: urlString)
if (results == nil) {
//Server Error
}
//Wire Up Results with matchingItems Array
let task = NSURLSession.sharedSession().dataTaskWithURL(results!) { (data, response, error) -> Void in
if let jsonData = data {
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)
if var results = jsonData["results"] as? [NSDictionary] {
if results.count > 0 {
//Clean out non-english results:
//I wrote the function, it shouldn't be the source of the lag, but I can still provide it.
self.cleanArray(&results)
self.matchingItems = results
} else {
self.matchingItems = []
}
}
} catch {
//JSON Serialization Error
}
}
}
task.resume()
self.tableView.reloadData()
}
}
Then, after I get the results, I reload the table using the two required methods from a TableViewDataSource:
//Table Data Source
extension SearchTable {
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matchingItems.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as! CustomCell
//Safe-Guard. This shouldn't be needed if I understood what I was doing
if (indexPath.row < matchingItems.count) {
cell.entry = matchingItems[indexPath.row] //404
//Name & Type & Year
//This is only for TV Shows, I removed the rest for simplicity
cell.title.text = matchingItems[indexPath.row]["name"] as? String
cell.icon.image = UIImage(named: "tv.png")
let date = (matchingItems[indexPath.row]["first_air_date"] as? String)
cell.year.text = date == nil ? "" : "(" + date!.substringToIndex(date!.startIndex.advancedBy(4)) + ")"
//Genre
//Code here removed for simplicity
//Poster
cell.poster.image = UIImage(named: "Placeholder.jpg")
if let imagePath = matchingItems[indexPath.row]["poster_path"] as? String {
let url = NSURL(string: "http://image.tmdb.org/t/p/w185" + imagePath)
let urlRequest = NSURLRequest(URL: url!)
let session = NSURLSession.sharedSession()
//Synchronous Request
let semaphore = dispatch_semaphore_create(0)
let task = session.dataTaskWithRequest(urlRequest) { data, response, error in
if let poster = UIImage(data: data!) {
cell.poster.image = poster
}
dispatch_semaphore_signal(semaphore)
}
task.resume()
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
return cell
}
}
Thanks!
First of all, I strongly recommend you don't use synchronous request, mainly because it blocks your UI until it finish and this is a very bad responsiveness for an app.
In your case you can place a placeholder for the UIImage and when the request finish substitute it for the correct image.
Regarding your issue of typing faster, it's called throttle or debounce, Apple recommends:
Performance issues. If search operations can be carried out very rapidly, it is possible to update the search results as the user is typing by implementing the searchBar:textDidChange: method on the delegate object. However, if a search operation takes more time, you should wait until the user taps the Search button before beginning the search in the searchBarSearchButtonClicked: method. Always perform search operations a background thread to avoid blocking the main thread. This keeps your app responsive to the user while the search is running and provides a better user experience.
But if you until want it to handle yourself you can see this two good answers explaining how to handle it correctly:
How to throttle search (based on typing speed) in iOS UISearchBar?
How can I debounce a method call?
I recommend you handle it as Apple recommends or you can change your philosophy and adopt some libraries that handle it for your automatically like:
Bond
RxSwift
The first one in more easy to learn, the second one needs to learn Reactive Programming and concepts of Functional Programming, It's up to you.
I hope this help you.
Just for people who may be struggling in the future with this same issue. First of all, read my comment to Victor Sigler's answer.
Here were the issues:
1 - I searched for the results online using .dataTaskWithURL() This is an asynchronous method which ran in the background while the code continued. So on occasion, the table would reload before the new results were in. See this thread for more information. I highly recommend checking this tutorial on concurrency if you are serious about learning Swift:
www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1
2 - The images lagged because of the search being synchronous, as Victor said. His answer pretty much handles the rest, so read it!
I have a UITableView with A lot of UITableViewCell, I use this code to load content and images to the UITableCell but it takes long time for the image to show for each UITableViewCell.
var loadOnce : [String] = []
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("ProfileActivities", forIndexPath: indexPath) as! ProfileActivitiesTableViewCell
if !loadOnce.contains("\(indexPath.row)") {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let LocationImage = self.postmap[indexPath.row]
let LocationImageUrl = NSURL(string: LocationImage)
let LocationImageData = NSData(contentsOfURL: LocationImageUrl!)
let ProfileImage = self.postimg[indexPath.row]
let ProfileImageUrl = NSURL(string: ProfileImage)
let ProfileImageData = NSData(contentsOfURL: ProfileImageUrl!)
dispatch_async(dispatch_get_main_queue(), {
cell.locationImg.image = UIImage(data: LocationImageData!)
cell.activitesProfileImg.image = UIImage(data: ProfileImageData!)
});
});
loadOnce.append("\(indexPath.row)")
}
return cell
}
Putting the code in dispatch_async made scrolling smother but its still takes time to load images.
how to make it load images faster considering I have to many images something like Facebook or Instagram.
Thanks.
There will be issue because of Async call and ReUsability. Lets say your Image at IndexPath 1 is loading image but it is still taking time to download and meanwhile user scrolled down at some another index i.e. 15 and Internally the same cell of Index 1 is alloted to Index 15 and Image of cell 1 will be displayed in Cell 15 if you have not managed to handle previous calls for same instance of cell. Thats why its Better to use some caching library like SDWebImage(As #SeanLintern88 said) or AFNetworking's UIImageView Category
AlamoFire's UIImageView (For Swift)
Event if you wanted to do this, then better is to go with subclassing, it will help you to manage the Calls and Its termination.
In your case the Loading of Image might be because of Size of actual image that you are loading from server. CDN kind of service provide some easy to implement techniques to serve different size of images as per the request. So even If User has uploaded 1200x1200 sized image but on list screen you can fetch its 120x120 sized image, This helps is faster loading as well as it will save Physical Memories.
Use one of the following libraries to asynchronously load images to you view :
1) Asyncimageview https://github.com/nicklockwood/AsyncImageView
2) SDWebImage https://github.com/rs/SDWebImage
It sounds like your image sizes could be large as the code looks fine, usually apps like FB/Insta would use low res renditions and then load in higher res after/when needed.
Most people use an image fetching library such as SDWebImage as this will async fetch the images and cache them so you don't have to save them on your model and consume the memory.
https://github.com/rs/SDWebImage
I'm new into programming in Swift and so far I'm downloading Json data as Strings and populate a UITableView. The images inside are url links. The problem is that the data are many, 53, so 53 url calls to get the images as well. At first, I was doing everything in the main thread so it was lagging a lot. Now I've put the code that does the http calls in an async method. The app does not lag but
1) The images don't download in order (I don't mind about that much although it would be nicer)
2) The download is slow, the memory hits around 250-270mb and the cpu around 40-50% while the network is around 500kb/s.
I don't own an iPhone to do a real check but with those numbers I see that the app uses a lot of resources. I wonder why the network is so slow though. Using 3g-4g must be faster and less stressing in my opinion and I don't know what the emulator is using.
So my question, is there any way for my app to go any faster or use less resources?
Below the code that puts the data on the TableView. It takes below 2 seconds to fill the table with the strings and a lot of time to download all the images.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "GameTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! GameTableViewCell
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
cell.nameLabel?.text = games[indexPath.row].name
cell.releaseDateLabel?.text = games[indexPath.row].releaseDate
dispatch_async(backgroundQueue, {
for _ in self.games{
if let url = NSURL(string: self.games[indexPath.row].gameImage!) {
if let data = NSData(contentsOfURL: url){
cell.photoImageView?.contentMode = UIViewContentMode.ScaleAspectFit
cell.photoImageView?.image = UIImage(data: data)
}
}
}
})
return cell
}
You're downloading the images every time you want to show a cell. You need to use NSCache or any other third-party solution or something as simple as NSMutableDictionary for caching the images that downloads successfully so you don't download them every time.
You can also use the existing third-party solutions like AlamofireImage and SDWebImage which provide async downloading of images, showing placeholder image, and caching
I was skeptical because I have never used an outside library but erasing 5-10 lines of code to just type the code below.. well, it saves a lot of time and trouble.
if let url = NSURL(string: self.games[indexPath.row].gameImage!) {
cell.photoImageView?.hnk_setImageFromURL(url)
}