I'll try to create up a minimal repro tonight, but here are the snippets of code that seem to be causing problems for me. I'm using a Redux Thunk to trigger the initial map download, e.g.
// redux/modules/downloads.js
export function beginDownload(destinationId) {
return async dispatch => {
// ...
const progressListener = (offlinePack, status) => dispatch(onMapDownloadProgress(offlinePack, status))
const errorListener = (offlinePack, err) => console.warn(offlinePack, err)
await MapboxGL.offlineManager.createPack({
name: packName,
styleURL: MapboxGL.StyleURL.Street,
minZoom: 10,
maxZoom: 20,
bounds: [[neLng, neLat], [swLng, swLat]],
}, progressListener, errorListener) // one subscriber here
}
}
export function onMapDownloadProgress(offlinePack, status) {
return async dispatch => {
const {name, percentage, state} = status
const idStr = name.replace('pack_', '')
const id = parseInt(idStr, 10)
if (state === MapboxGL.OfflinePackDownloadState.Complete) {
dispatch({type: DOWNLOAD_COMPLETE, value: id})
return
}
dispatch({
type: MAP_DOWNLOAD_PROGRESS,
value: { destinationId: id, percentage},
})
}
}
then on a "download status" component subscribe to download updates again
// note #onMapDownloadProgress from above is bound using redux's #mapDispatchToProps
class DownloadableScreen extends React.Component {
constructor (props) {
super(props)
this.state = {
packs: [],
}
}
async componentWillMount() {
const {onMapDownloadProgress} = this.props
const errorListener = (offlinePack, err) => console.warn(offlinePack, err)
const offlinePacks = await MapboxGL.offlineManager.getPacks()
const packs = offlinePacks.map(pack => pack._metadata.name)
console.log('packs====', packs)
this.setState({packs})
for (let packName of packs) {
MapboxGL.offlineManager.subscribe(packName, onMapDownloadProgress, errorListener)
}
}
componentWillUnmount () {
let uniq = a => [...new Set(a)]
const packs = uniq([
...this.state.packs, // Provided by #getPacks (previously stored on phone)
...this.props.packs, // Provided by newly triggered download
])
for (let packName of packs) {
MapboxGL.offlineManager.unsubscribe(packName)
}
}
// ...
}
at first I thought this code was working great, but then later on, with no code changes, I noticed that my app was crashing a lot, especially when I switched to a tab that rendered the MapView (but also just sitting there with it open) and when it crashes I see this in the logs:
Nov 15 08:57:34 ss-MacBook-Pro appyapp[60082] <Notice>: —— log end ——
Nov 15 08:57:35 ss-MacBook-Pro SpringBoard[59272] <Error>: [KeyboardArbiter] HW kbd: Failed to set (null) as keyboard focus
Nov 15 08:57:35 ss-MacBook-Pro com.apple.CoreSimulator.SimDevice.5050E7B8-BD8C-4ED0-BCB2-A52F60D04200.launchd_sim[59255] (UIKitApplication:com.xyz.appyapp[0x221e][59277][60082]) <Notice>: Service exited due to Segmentation fault: 11
Nov 15 08:57:35 ss-MacBook-Pro backboardd[59273] <Error>: [Common] Unable to get short BSD proc info for 60082: No such process
Nov 15 08:57:35 ss-MacBook-Pro backboardd[59273] <Error>: [Common] Unable to get proc info for 60082: Undefined error: 0
Nov 15 08:57:36 ss-MacBook-Pro assertiond[59277] <Warning>: Deleted job with label: UIKitApplication:com.xyz.appyapp[0x221e][59277]
Nov 15 08:57:40 ss-MacBook-Pro locationd[59275] <Notice>: Location icon should now be in state 'Inactive'
a couple times before I also saw malloc: * error for object xxx: pointer being freed was not allocated * set a breakpoint in malloc_error_break to debug but when I tried to set the breakpoint it didn't seem to have any effect.
if I comment out the componentWillMount/componentWillUnmount code, the segfault disappears. I wonder if the double-subscribing is causing the issue, and if so, how I might adapt my code to prevent this. The fact that my code worked well for a time period, before bugging out irretrievably makes this smell like a memory leak.
Let me know if there's anything else I can do to help debug - sorry for being new at this but I'm not even sure how to generate a more verbose crash log. I can definitely code up a quick minimal repro later today, if that's helpful.
@smoll I'll start looking into this, I think what you provided is good enough for me to get going. I do have one question is this only happening to you on iOS?
@nitaliano I haven't been able to spin up Android due to an unrelated issue blocking me from concurrent development.
I did want to share one piece of info though:
I discovered that when I unsubscribe inside my reducer, once the download is fully complete, the crashes stopped. To be more explicit, I did this:
// redux/modules/downloads.js
function onMapDownloadProgress(offlinePack, status, dispatch) {
// ...
if (state === MapboxGL.OfflinePackDownloadState.Complete) {
dispatch({type: DOWNLOAD_COMPLETE, value: {destinationId: id, unsub: true}}) // <- here
db.run(`UPDATE destinations SET map_pack = '${name}' where id = '${id}';`)
return
}
// ...
}
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// ...
case DOWNLOAD_COMPLETE: {
const {destinationId, unsub} = action.value
const packName = `pack_${destinationId}`
if (unsub) MapboxGL.offlineManager.unsubscribe(packName) // <- unsubscribes the 2nd listener
return {
...state,
statuses: {
...state.statuses,
...{
[destinationId]: {
status: COMPLETE,
},
}
}
}
}
}
}
so it could be my original error was just that I was never correctly unsubscribing the listener created by MapboxGL.offlineManager.createPack
Before I made this code change, I instrumented my app with Sentry, and it was able to capture the following error, which I think is related to my originally reported issue: https://sentry.io/share/issue/f3e771618b4d465ea72298e815840e73/
Sorry for the noise on my end, but I could definitely see other users making a similar error, especially when using Mapbox with Redux, so this thread is good implicit documentation 😅
I'm going to close this out, I plan on making some redux and mobx examples so this will be good to put into the examples
So I finally figured out the root cause of all these seg faults. Basically, if you make a call to any of MapboxGL.offlineManager's methods BEFORE the onDidFinishLoadingMap event fires and the map's style is loaded, you will get seg faults.
In my use case, a user might attempt to download a map even before the screen where the MapView is rendered is ever tapped! Therefore, I had to add the following to the first screen of my app:
<MapArea
style={{height: 0, width: 0, flex: 0,/* HACK for https://github.com/mapbox/react-native-mapbox-gl/issues/766#issuecomment-346449740 */}}
onDidFinishLoadingMap={this.props.mapStylesLoaded}
/>
where mapStylesLoaded is just a redux action that sets loadedStyles: false in my store to true. Then, in my downloadStuff action, I can explicitly check that loadedStyles is true before proceeding. Not the most elegant hack in the world (I'm open to suggestions!) but I'm really glad I was able to figure out this race condition that kept causing my app to crash.
Thanks @nitaliano for the CreateOfflineRegion.js example that helped me figure out where to look.
Most helpful comment
So I finally figured out the root cause of all these seg faults. Basically, if you make a call to any of
MapboxGL.offlineManager's methods BEFORE theonDidFinishLoadingMapevent fires and the map's style is loaded, you will get seg faults.In my use case, a user might attempt to download a map even before the screen where the MapView is rendered is ever tapped! Therefore, I had to add the following to the first screen of my app:
where
mapStylesLoadedis just a redux action that setsloadedStyles: falsein my store to true. Then, in mydownloadStuffaction, I can explicitly check thatloadedStylesistruebefore proceeding. Not the most elegant hack in the world (I'm open to suggestions!) but I'm really glad I was able to figure out this race condition that kept causing my app to crash.Thanks @nitaliano for the
CreateOfflineRegion.jsexample that helped me figure out where to look.