My app working with Geojson
file. I use MapBox SDK to add MGLPolyline
to map. But the problem is my file too large, so that the app crash and got the e
The problem here is related to effective memory management. You are loading a lot of data via your json file. You realized that you needed to do the majority of the work on a background queue (thread) however the issue is how you are updating the UI via DispatchQueue.main.async
function. In the current version of the drawPolyline()
method you are switching between the background queue and the main queue 66234 times, given the number of objects in your first loop. Also you were creating the same number of CLLocationCoordinate2D
arrays.
This leads to a huge memory footprint. You do not mention any requirements in regards to how you render the lines. So if we restructure your drawPolyline()
method to use a instance variable for the CLLocationCoordinate2D
array so we only use one and then we process all of the json file before we update the UI. The memory usage dropped down to a some what more manageable 664.6 MB.
Of course the rendering may not be exactly what you are looking for and if that's the case you might want to restructure your CLLocationCoordinate2D
array into a more suitable data structure.
Below is your ViewController
class with the rewritten drawPolyline()
as drawPolyline2()
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
@IBOutlet var mapboxView: MGLMapView!
fileprivate var coordinates = [[CLLocationCoordinate2D]]()
fileprivate var jsonData: NSData?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
mapboxView = MGLMapView(frame: view.bounds)
mapboxView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// mapboxView.setCenter(CLLocationCoordinate2D(latitude: 45.5076, longitude: -122.6736),
// zoomLevel: 11, animated: false)
mapboxView.setCenter(CLLocationCoordinate2D(latitude: 1.290270, longitude: 103.851959),
zoomLevel: 11, animated: false)
view.addSubview(self.mapboxView)
mapboxView.delegate = self
mapboxView.allowsZooming = true
drawPolyline2()
//newWay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func drawPolyline2() {
DispatchQueue.global(qos: .background).async {
if let path = Bundle.main.path(forResource: "KMLMAPNew", ofType: "json") {
let fileURL = URL(fileURLWithPath: path)
if let data = try? Data(contentsOf: fileURL) {
do {
let dictionary = try JSONSerialization.jsonObject(with: data as Data, options: []) as? Dictionary<String, AnyObject>
if let features = dictionary?["features"] as? Array<AnyObject> {
print("** START **")
for feature in features {
guard let feature = feature as? Dictionary<String, AnyObject>, let geometry = feature["geometry"] as? Dictionary<String, AnyObject> else { continue }
if geometry["type"] as? String == "LineString" {
// Create an array to hold the formatted coordinates for our line
if let locations = geometry["coordinates"] as? Array<AnyObject> {
// Iterate over line coordinates, stored in GeoJSON as many lng, lat arrays
var featureCoordinates = [CLLocationCoordinate2D]()
for location in locations {
// Make a CLLocationCoordinate2D with the lat, lng
if let location = location as? Array<AnyObject>{
let coordinate = CLLocationCoordinate2DMake(location[1].doubleValue, location[0].doubleValue)
// Add coordinate to coordinates array
featureCoordinates.append(coordinate)
}
}
// Uncomment if you need to store for later use.
//self.coordinates.append(featureCoordinates)
DispatchQueue.main.async {
let line = MGLPolyline(coordinates: &featureCoordinates, count: UInt(featureCoordinates.count))
// Optionally set the title of the polyline, which can be used for:
// - Callout view
// - Object identification
line.title = "Crema to Council Crest"
self.mapboxView.addAnnotation(line)
}
}
}
}
print("** FINISH **")
}
} catch {
print("GeoJSON parsing failed")
}
}
}
}
}
func drawSmallListObj(list: [Dictionary<String, AnyObject>]){
for obj in list{
// print(obj)
if let feature = obj as? Dictionary<String, AnyObject> {
if let geometry = feature["geometry"] as? Dictionary<String, AnyObject> {
if geometry["type"] as? String == "LineString" {
// Create an array to hold the formatted coordinates for our line
var coordinates: [CLLocationCoordinate2D] = []
if let locations = geometry["coordinates"] as? Array<AnyObject> {
// Iterate over line coordinates, stored in GeoJSON as many lng, lat arrays
for location in locations {
// Make a CLLocationCoordinate2D with the lat, lng
if let location = location as? Array<AnyObject>{
let coordinate = CLLocationCoordinate2DMake(location[1].doubleValue, location[0].doubleValue)
// Add coordinate to coordinates array
coordinates.append(coordinate)
}
}
}
let line = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
// Optionally set the title of the polyline, which can be used for:
// - Callout view
// - Object identification
line.title = "Crema to Council Crest"
// Add the annotation on the main thread
DispatchQueue.main.async {
// Unowned reference to self to prevent retain cycle
[unowned self] in
self.mapboxView.addAnnotation(line)
}
}
}
}
}
}
func mapView(_ mapView: MGLMapView, alphaForShapeAnnotation annotation: MGLShape) -> CGFloat {
// Set the alpha for all shape annotations to 1 (full opacity)
return 1
}
func mapView(_ mapView: MGLMapView, lineWidthForPolylineAnnotation annotation: MGLPolyline) -> CGFloat {
// Set the line width for polyline annotations
return 2.0
}
func mapView(_ mapView: MGLMapView, strokeColorForShapeAnnotation annotation: MGLShape) -> UIColor {
// Give our polyline a unique color by checking for its `title` property
if (annotation.title == "Crema to Council Crest" && annotation is MGLPolyline) {
// Mapbox cyan
return UIColor(red: 59/255, green:178/255, blue:208/255, alpha:1)
}
else
{
return UIColor.red
}
}
}
I had some problems to test your project with pods, so I've directly remove pods and use Mapbox framework directly from here.
I've no problems for the first launches both in simulator and real iPad (my iPad 4 gen.) but after a while I've your same error, so I've correct this code with:
DispatchQueue.main.async {
// weaked reference to self to prevent retain cycle
[weak self] in
guard let strongSelf = self else { return }
strongSelf.mapboxView.addAnnotation(line)
}
because unowned
it's not enough to prevent retain cycle.
Now seems it works well.
Hope it helps.
P.S. (I've used the latest available Mapbox v3.3.6)
Update (after comments):
So, first of all I make all my test with Mapbox framework inserted as "embedded framework".
I've make some corrections to your github project only to ViewController.swift
to avoid retain cycles.
P.S. I remove comments lines to make easy reading:
func drawPolyline() {
DispatchQueue.global(qos: .background).async {
[weak self] in
guard let strongSelf = self else { return }
let jsonPath = Bundle.main.path(forResource: "KMLMAPNew", ofType: "json")
let jsonData = NSData(contentsOfFile: jsonPath!)
do {
guard let jsonDict = try JSONSerialization.jsonObject(with: jsonData! as Data, options: []) as? Dictionary<String, AnyObject>, let features = jsonDict["features"] as? Array<AnyObject> else{return}
for feature in features {
guard let feature = feature as? Dictionary<String, AnyObject>, let geometry = feature["geometry"] as? Dictionary<String, AnyObject> else{ continue }
if geometry["type"] as? String == "LineString" {
var coordinates: [CLLocationCoordinate2D] = []
if let locations = geometry["coordinates"] as? Array<AnyObject> {
for location in locations {
if let location = location as? Array<AnyObject>{
let coordinate = CLLocationCoordinate2DMake(location[1].doubleValue, location[0].doubleValue)
coordinates.append(coordinate)
}
}
}
let line = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
line.title = "Crema to Council Crest"
print(feature) // Added this line just for debug to see the flow..
DispatchQueue.main.async {
strongSelf.mapboxView.addAnnotation(line)
}
}
}
}
catch
{
print("GeoJSON parsing failed")
}
}
}
func newWay(){
DispatchQueue.global(qos: .background).async {
[weak self] in
guard let strongSelf = self else { return }
let jsonPath = Bundle.main.path(forResource: "KMLMAPNew", ofType: "json")
let jsonData = NSData(contentsOfFile: jsonPath!)
do {
if let jsonDict = try JSONSerialization.jsonObject(with: jsonData! as Data, options: []) as? Dictionary<String, AnyObject> {
if let features = jsonDict["features"] as? Array<AnyObject> {
let chunks = stride(from: 0, to: features.count, by: 2).map {
Array(features[$0..<min($0 + 2, features.count)])
}
for obj in chunks{
strongSelf.drawSmallListObj(list: obj as! [Dictionary<String, AnyObject>])
}
}
}
}
catch
{
print("GeoJSON parsing failed")
}
}
}
func drawSmallListObj(list: [Dictionary<String, AnyObject>]){
for obj in list{
if let feature = obj as? Dictionary<String, AnyObject> {
if let geometry = feature["geometry"] as? Dictionary<String, AnyObject> {
if geometry["type"] as? String == "LineString" {
var coordinates: [CLLocationCoordinate2D] = []
if let locations = geometry["coordinates"] as? Array<AnyObject> {
for location in locations {
if let location = location as? Array<AnyObject>{
let coordinate = CLLocationCoordinate2DMake(location[1].doubleValue, location[0].doubleValue)
coordinates.append(coordinate)
}
}
}
let line = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
line.title = "Crema to Council Crest"
DispatchQueue.main.async {
[weak self] in
guard let strongSelf = self else { return }
strongSelf.mapboxView.addAnnotation(line)
}
}
}
}
}
}
make you stuff on callout this means execute polyne only when click on the pin func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)
One thing I have learnt from creating memory intensive apps is that you have to use autoreleasepool
every time you create variables inside loops, if these loops are long
Review all your code and transform things like
func loopALot() {
for _ in 0 ..< 5000 {
let image = NSImage(contentsOfFile: filename)
}
}
into
func loopALot() {
for _ in 0 ..< 5000 {
autoreleasepool {
let image = NSImage(contentsOfFile: filename)
}
}
}
Review all kinds of loops for
, while
, etc.
This will force of iOS to release the variable and its correspondent memory usage at the end of every turn of the loop, instead of holding the variable and its memory usage until the function ends. That will reduce dramatically your memory usage.
Will share my experience with this strange issue.
For me the app crashed with "Message from debugger: Terminated due to memory issue" and instruments didn't help a lot. As well the Memory - was within the green limits. So I was not sure what is causing that. And it was not possible to debug, and single-device specific issue.
Just restarted the iPhone 6 - and the issue disappeared for now.
First Solution
Maybe your for loop is running infinitely and allocating memory to an array with nil value every time. It is using high amounts of memory, so it gives this error.
Please check by print something in the for loop.
Second Solution
Add this in didReceiveMemoryWarning
:
NSURLCache.sharedURLCache().removeAllCachedResponses()
NSURLCache.sharedURLCache().diskCapacity = 0
NSURLCache.sharedURLCache().memoryCapacity = 0
You can also change the cache policy of the NSURLRequest
:
let day_url = NSURL(string: "http://www.example.com")
let day_url_request = NSURLRequest(URL: day_url,
cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 10.0)
let day_webView = UIWebView()
day_webView.loadRequest(day_url_request)
More information on cache policies here.