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.
Related
I’m playing with the new VNGeneratePersonSegmentationRequest Vision API to make a simple background removal filter
I made a small project to test it, works great, but I’m running into issues with memory. After executing the request the app’s memory consumption adds 300MBs that are never freed.
I’ve cycled through a bunch of images and requests in a test run and thankfully memory consumption remains constant even when filtering more images, but I worry about that initial memory that is never freed, even when inducing a memory warning. I suspect the Vision Framework needs that memory after being called, but my app doesn’t handle video frames or anything, so it’s memory going to waste
//Autorelease pool doesn't helps
autoreleasepool {
// Create request
let request = VNGeneratePersonSegmentationRequest()
request.revision = VNGeneratePersonSegmentationRequestRevision1
request.qualityLevel = .accurate
request.outputPixelFormat = kCVPixelFormatType_OneComponent8
let handler:VNImageRequestHandler = VNImageRequestHandler(ciImage: inputs.ciImage,
options: [:])
//A jump in memory usage after running the handler
//Subsequent calls don't add to memory usage
do{
try handler.perform([request])
}
catch{
return
}
//Even if I delete this chunk of code, memory consumption remains high
let mask = request.results?.first!
if let maskBuffer = mask?.pixelBuffer{
self.personMask = CIImage(cvPixelBuffer: maskBuffer)
let maskScaleX = inputs.ciImage.extent.width / personMask!.extent.width
let maskScaleY = inputs.ciImage.extent.height / personMask!.extent.height
self.personMask = personMask!.transformed(by: __CGAffineTransformMake(
maskScaleX, 0, 0, maskScaleY, 0, 0))
}
}
I have a navigation and location tracking app. While a user is tracking his trip, coordinates, speed, timestamp (and a few more) are logged with each coordinate that comes in. I don't want to store this in memory as that would make the app memory grow as the user moves along, eventually leading to a didReceiveMemoryWarning and even an app crash. (At least that's been my experience so far)
What would be the most efficient way to do this? Thinking of battery consumption, CPU usage and also memory usage if that comes into play.
I can think of two options to do this:
Log in a file (what I'm currently doing, using this code snipped):
let newLine: String = "bla bla bla"
let url: URL = URL(string: "someLocalPath")
newLine.appendToURL(url)
extension String {
func appendToURL(_ fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.appendToURL(fileURL)
}
}
extension Data {
func appendToURL(_ fileURL: URL) throws {
if let fileHandle = try? FileHandle(forWritingTo: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
fileHandle.closeFile()
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
Using Core Data
I have not tried this yet, but it would be relatively easy I believe. Just creating an entity with all the fields required, and then adding a managedObject for each received coordinate.
So, to repeat my question: which of these two options would be more efficient from a battery, CPU and memory perspective?
Additional question: Is there perhaps another way to do this. One that I didn't think of?
I found another option that works best in my case. Should have thought of that in the first place 🤦🏻♂️.
So, I did not want to log to memory because it could make iOS kill my app due to excessive memory usage. But, iOS sends a didReceiveMemoryWarning first, upon which you can reduce the memory footprint of the app, preventing it from being killed.
So, what I do now:
I added a instance variable:
fileprivate var logString: String = ""
And log to it when a new coordinate is received:
logString += newLine
If / when I get a didReceiveMemoryWarning I write the entire logString to the file on disk and empty it afterwards:
override func didReceiveMemoryWarning() {
logString.appendToURL(url)
logString = ""
}
This way I only write to disk when necessary (and not with each coordinate received), which is a lot more energy efficient. (Writing to disk costs more energy than writing to memory. Source: https://developer.apple.com/videos/play/wwdc2016/719/)
Plus, my app is prevented from being killed by iOS due to high memory usage, because I clear up memory on time.
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 have a function below that changes the text of a text view by getting the contents of a file stored in the main bundle:
func setUpText() {
let path = Bundle.main.path(forResource: book, ofType: "txt")
var rawText = ""
do {
rawText = try String(contentsOfFile: path!, encoding: String.Encoding.utf8)
} catch {
print("Contents of File could not be retrieved")
}
myTextView.text = rawText
}
When the user changes the value of book, this function is called and the text view is populated with the new text. Everything works fine, but I've noticed in the Debug Navigator which shows CPU, Memory, Disk, and Network usage information, that Memory keeps going up every time I repopulate the text. How can I clear the memory before changing the text?
Here's how setUpText is called in viewDidLoad:
aiv.startAnimating()
myTextView.text = ""
DispatchQueue.main.async {
self.setUpText()
self.aiv.stopAnimating()
self.aiv.isHidden = true
}
I'm also calling setUpText in the function below in the dropdownMenu.didSelectItemAtIndexHandler closure. This is the source of my problem, when I add this, the memory climbs without ever going down as verified through Instrumenting.
func createDropdownMenu(title: String, items: [AnyObject]) -> BTNavigationDropdownMenu {
let dropdownMenu = BTNavigationDropdownMenu(navigationController: self.navigationController,
containerView: self.navigationController!.view,
title: title,
items: items)
dropdownMenu.didSelectItemAtIndexHandler = { (indexPath: Int) -> () in
self.chapterIndex = indexPath + 1
self.setUpText()
}
return dropdownMenu
}
I don't see anything that would cause a retain cycle...self doesn't have a strong reference to the closure, so you're fine there.
You can't 'clear memory', per se, under ARC. It won't allow you to manually deallocate an object (nor do you need to--that's the point). My best guess is that because you're reading a string from disk, which is a relatively slow/expensive operation, (emphasis on 'relatively') the system might be caching it at least temporarily. This happens in certain cases such [UIImage imageNamed:].
Is this actually causing a problem for you? Have you instrumented it at all?
I'm appending uiimages from core data to an array in order to create a gif of them.
I'm creating the gif and then emptying the array; however, I still have an indefinite 50 mb memory allocation from the moment I create the gif.
I tried looking at instruments and this is what I'm getting in the call tree:
Call tree image
This is the code:
do{
let objects = try managedObjectContext.executeFetchRequest(request)
let results = objects
if results.count > 0 {
for var i = 0; i < results.count; i += 1{
let match = results[i] as! cFW
date.append(match.date)
let image = match.image
fetchedImage.append(UIImage(data: image)!)
}
} else {
}
}
catch{}
Even after deleting the gif and the array already being deleted, the app stays at 50mb of memory usage.
Thank you
edit: The issue that I'm having has to do with how I'm displaying the gif (in a webview). I will update the question with my solution asap
The issue was happening due to presenting the gif in a webview.
Clearing the cache of web view did not resolve the allocation of memory.
I ended up using a library to FLAnimatedImage to present my GIF.
No more unreleased memory.
Thank you guys