I'm using NSUbiquitousKeyValueStore to store some app settings. My logic is: when I save data locally, I save it to NSUbiquitousKeyValueStore also as a backup. When I need settings, I read locally and I only use iCloud key-value store if no data is found locally (after app is reinstalled, for example). If user has several devices sharing one icloud id, he can write settings on one device and download them to another (I warn him about rewrite).
I have a strange issue. Steps:
Installed an app and save its data to NSUbiquitousKeyValueStore. Made sure data is there.
Removed the app (assuming data is still persists in iCloud).
Waited several minutes just in case, then installed and launched the app from inside Xcode.
Tried to read a settings key using [[NSUbiquitousKeyValueStore defaultStore] dataForKey: #"mykeyname"] - sometimes it's ok, but sometimes key is not found!
Waited for 15 seconds, tried again. Success. Confused.
So it seems like ios needs some time to make remote key-value storage for my app available locally for dataForKey: call.
If I'd wrote such a system (actually I did - some time ago, in another life) there obviously must be a delay before asking and receiving a key-value data. So I'd like to have some notification saying: "we finished downloading/syncing key-value storage on first start" or something similar.
As far as I understand I can work with NSUbiquitousKeyValueStore in main thread synchronously (which is convenient for me). But [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] returns a valid url, and then I get "key isn't found". So I can't rely on it. Is there a way to be sure NSUbiquitousKeyValueStore works an is downloaded? It's important especially with slow internet.
UPDATE
Adding [[NSUbiquitousKeyValueStore defaultStore] synchronize] (as written in apple docs) to init and load was helped a little. Still there are many questions to iCloud.
Yesterday I've successfully saved data to the key-value store on phone 1 and restored on phone 2.
Today I've deleted app on phone 2 and tried to restore the data. But even [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] returned valid URL and I called [[NSUbiquitousKeyValueStore defaultStore] synchronize] I get nil when call dataForKey: MY_DATA_KEY.
When I tried to restore data from icloud on phone 1 (app is still installed) it succeeds, but when I reinstalled on this phone the app restore doesn't succeed any more.
Temporary solution is: "turn off iCloud->Documents&Data - turn off and on network - turn on Documents&Data", but also you should wait several minutes, and then it should work.
So, questions:
do you have such problems with iCloud?
Is there any way to find out is data not available or just not downloaded yet?
Is there any known "latency" of iCloud? I've heard about 7 seconds, but it's obviously not true.
It seems that when app isn't unistalled updates of iCloud data are pretty fast (seconds), but when you reinstall the app icloud needs several minutes to actualize key-value store. Is there any way to force this process?
P.S.
Below is my CloudHelper for your reference - pretty simple c++ class to write/read binary data to/from iCloud key-value store. It is not compilable, I've adapted it for SO somewhat to make more clear - removed my engine related code. Still if you remove MySystem::... calls it works pretty well. Except that I mentioned before.
class CloudHelper
{
public:
static bool init();
static void deInit();
//save our data to iCloud with
static int saveData(unsigned char* data, int from, int count);
//get our data from iCloud
static unsigned char * loadData(int *retsize, int * retint);
//does iCloud work for us
static bool isEnabled();
//do we have our key in iCloud
static int isAvailable();
static const int RESULT_OK = 0;
static const int RESULT_NO_CONNECTION = 1;
static const int RESULT_NOT_FOUND = 2;
static const int RESULT_SYNC_ERROR = 3;
private:
static bool enabled;
static NSURL *ubiq;
};
bool CloudHelper::enabled = false;
NSURL *CloudHelper::ubiq = NULL;
#define MY_DATA_KEY #"my_data_key"
int CloudHelper::saveData(unsigned char* data, int from, int count)
{
if ([NSUbiquitousKeyValueStore defaultStore])
{
NSData *d = [[[NSData alloc] initWithBytes:(data + from) length:count] autorelease];
[[NSUbiquitousKeyValueStore defaultStore] setData:d forKey: MY_DATA_KEY)];
if ([[NSUbiquitousKeyValueStore defaultStore] synchronize] != TRUE)
return RESULT_SYNC_ERROR;
return RESULT_OK;
}
return RESULT_NO_CONNECTION;
}
unsigned char * CloudHelper::loadData(int *retsize, int * retint)
{
if ([NSUbiquitousKeyValueStore defaultStore])
{
[[NSUbiquitousKeyValueStore defaultStore] synchronize];
NSData *d = [[NSUbiquitousKeyValueStore defaultStore] dataForKey: MY_DATA_KEY];
if (d != NULL)
{
if (retsize != NULL)
*retsize = d.length;
if (retint != NULL)
*retint = RESULT_OK;
return d.bytes;
}
else
{
if (retsize != NULL)
*retsize = -1;
if (retint != NULL)
*retint = RESULT_NOT_FOUND;
}
}
else
{
if (retsize != NULL)
*retsize = -1;
if (retint != NULL)
*retint = RESULT_NO_CONNECTION;
}
return NULL;
}
int CloudHelper::isAvailable()
{
int result = RESULT_NO_CONNECTION;
if ([NSUbiquitousKeyValueStore defaultStore])
{
[[NSUbiquitousKeyValueStore defaultStore] synchronize];
NSData *d = [[NSUbiquitousKeyValueStore defaultStore] dataForKey: MY_DATA_KEY];
if (d != NULL)
result = RESULT_OK;
else
result = RESULT_NOT_FOUND;
}
else
result = RESULT_NO_CONNECTION;
return result;
}
void CloudHelper::deInit()
{
enabled = false;
[ubiq release];
}
bool CloudHelper::init()
{
enabled = false;
NSURL *ubiq_ = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
[[NSUbiquitousKeyValueStore defaultStore] synchronize];
if (ubiq)
{
enabled = true;
ubiq = [ubiq_ retain]; //save for further use
}
else
{
//is implemented elsewhere: this writes a local file with a counter, and if it is < REMINDER_COUNT allows us to show a warning to users
bool allow = MySystem::isAllowToShowDialog();
if (allow)
{
//determines network state with Apple's Reachability
if (!MySystem::isNetworkAvailable())
MySystem::showMessageBox(#"Network error"); //No network
else
MySystem::showMessageBox(#"You should log into your iCloud account to be able to backup your settings."); //No login
}
}
return enabled;
}
UPDATE 2
It's 2016. Android has become ios's evil twin, the humanity has discovered gravitational waves, Higgs have received his nobel, Microsoft has bought and killed Nokia. But iCloud is still as stupid as it was.
Finally I've made my own stack of network services on several VPS. I refused to use third-party services, because most of them are unstable and unpredictable. And yet I need iCloud. Because another die-born child of apple does not work. SecKeyChain. Its service dies when my game starts. So I decided to store random UUID in cloud to distinguish users (there is no device id anymore) even after reinstall. But what could go wrong? Everything! I've spend two days to make this stupid s*it to deploy without errors, and now it loses my data from time to time!
Thank you Apple, thank, thank, thank! La-la-la! Hip-hip hooray! (sounds of circus music, fading into weeping)
Conclusion
Temporary solution is:
- call synchronize before get data from key-value store
- to be sure it would work "turn off iCloud->Documents&Data - turn off and again on network - turn on Documents&Data", but also you should wait several minutes before iCloud downloads all needed data
Note: when app is installed and already worked (saved/loaded) with key-value store updates of iCloud data are pretty fast (7-15 sec), but when you reinstall the app it seems that icloud needs several minutes to actualize key-value store.
I'd be glad to hear your thoughts, because icloud looks like almost unusable feature. But I don't want to set up my own server to merely get the same functionality.
I am setting one dummy key to NSUbiquitousKeyValueStore and calling synchronize on app launch. The result is not 100% solution but somewhat better. You can try this.
Because obviously your app shouldn't hang while waiting for a slow netork. It's all in the iCloud Design Guide.
Register for NSUbiquitousKeyValueStoreDidChangeExternallyNotification, call -synchronize, and hopefully a notification should eventually arrive.
If the data is already up-to-date, I don't think you get a notification, and I don't think there's an wasy way to know how old the data is.
Related
While retrieving metadata from media files, I've run into a memory issue I cannot figure out.
I want to retrieve metadata for media files either stored in the local app storage or in the iTunes area. For this I use AVAsset. While looping through these files I can see the memory consumption rising constantly. And not just a little. It is significant and end up stalling the app when I enumerate my iTunes library on the phone.
The problem seems to be accessing the metadata property on the AVAsset class. I've narrowed in down to one line of code: 'let meta = ass.metadata'. Having that line of code (without any references) makes the app consume memory. I've included an example of my code structure.
func processFiles(_ files:Array)
{
var lastalbum : String = ""
var i : Int = 0
for file in files
{
i += 1
view.setProgressPosition(CGFloat(i)/CGFloat(files.count))
lastalbum = updateFile(file.url,lastalbum,
{ (album,title,artist,composer) in
view.setProgressNote(album,title,artist+" / "+composer)
})
}
}
func updateFile(_ url:URL,_ lastalbum:String,iPod:Bool=false,
_ progress:(String,String,String,String) -> Void) -> String
{
let ass = AVAsset(url:url)
let meta = ass.metadata
for item in meta
{
// Examine metadata
}
// Use metadata
// Callback with status
}
It seems that memory allocated in the updateFile method, is kept, even when the function is ended. However, once the processFile function completes and the app returns to a normal state, all memory is released again.
So in conclusion, this is not a real leak, but still a significant problem. Any good ideas as to what goes wrong? Is there any way I can force the memory management to run a cleanup?
As suggested in the comment on the post, the solution for this is to wrap the specific code in a 'autoreleasepool' block. I've tested this both with a small set of local media files and also with my rather large iTunes media library (70GB). After implementing the 'autoreleasepool' the memory buildup is eliminated.
func updateFile(_ url:URL,_ lastalbum:String,iPod:Bool=false,
_ progress:(String,String,String,String) -> Void) -> String
{
autoreleasepool
{
let ass = AVAsset(url:url)
let meta = ass.metadata
for item in meta
{
// Examine metadata
}
// Use metadata
// Callback with status
}
}
I am receiving memory warnings in didReceiveMemoryWarning. I know memory warnings have different levels like level-1,level-2. Is there any way determine the warning level? Example:
if(warning level == 1)
<blah>
Hope this helps!!!
There are 4 levels of warnings (0 to 3). These are set from the kernel memory watcher, and can be obtained by the not-so-public function OSMemoryNotificationCurrentLevel().
typedef enum {
OSMemoryNotificationLevelAny = -1,
OSMemoryNotificationLevelNormal = 0,
OSMemoryNotificationLevelWarning = 1,
OSMemoryNotificationLevelUrgent = 2,
OSMemoryNotificationLevelCritical = 3
} OSMemoryNotificationLevel;
How the levels are triggered is not documented. SpringBoard is configured to do the following in each memory level:
Warning (not-normal) — Relaunch, or delay auto relaunch of nonessential background apps e.g. Mail.
Urgent — Quit all background apps, e.g. Safari and iPod.
Critical and beyond — The kernel will take over, probably killing SpringBoard or even reboot.
I know there is no way to (except the private/undocumented API) know the memory level warning. So you should not use that.
Check out this question to see undocumented API to get memory warning level.
My first advice would be to research the memory warning notification in the docs (e.g., what are the contents of its userInfo dictionary, if present). I don't know if it provides any details or not.
But ultimately, you shouldn't speculate on the level of the memory warning, just assume the worst and release as much unused data as you can.
There is no (public, working) way to get the current memory pressure level from the system on a customer device. There is however a way to get notified of memory pressure changes using the Dispatch Source API.
Memory pressure dispatch sources can be used to notify an application of changes to memory pressure. This can be more fine-grained than the notifications provided by UIKit and includes the capability to be notified when memory pressure returns to normal.
For example:
Objective-C:
dispatch_source_t memorySource = NULL;
memorySource = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0L, (DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL), [self privateQueue]);
if (memorySource != NULL) {
dispatch_block_t eventHandler = dispatch_block_create(DISPATCH_BLOCK_ASSIGN_CURRENT, ^{
if (dispatch_source_testcancel(memorySource) == 0 ){
dispatch_source_memorypressure_flags_t memoryPressure = dispatch_source_get_data(memorySource);
[self didReceiveMemoryPressure:memoryPressure];
}
});
dispatch_source_set_event_handler(memorySource, eventHandler);
dispatch_source_set_registration_handler(memorySource, eventHandler);
[self setSource:memorySource];
dispatch_activate([self source]);
}
Swift 4:
if let source:DispatchSourceMemoryPressure = DispatchSource.makeMemoryPressureSource(eventMask: .all, queue:self.privateQueue) as? DispatchSource {
let eventHandler: DispatchSourceProtocol.DispatchSourceHandler = {
let event:DispatchSource.MemoryPressureEvent = source.data
if source.isCancelled == false {
self.didReceive(memoryPressureEvent: event)
}
}
source.setEventHandler(handler:eventHandler)
source.setRegistrationHandler(handler:eventHandler)
self.source = source
self.source?.activate()
}
Note that the event handler is also being used as the "registration handler". This will cause the event handler to fire when the dispatch source is activated, effectively telling the application of what the "current" value is when the source is activated.
I know that there are a few postings on this, but just want to make sure there is something that I am not missing / current.
Using sqlcipher, with an unencrypted database, want to encrypt it. Encrypting a new database is working fine.
Am trying the sqlcipher rekey with an existing database seems NOT to be working (Database remains unencrypted).
[fmdb open];
NSString *sel = #"SELECT count(*) FROM sqlite_master";
FMResultSet *fmr = [self executeQuery : fmdb : sel];
if ( [fmr next] ) // unencrypted
{
NSLog(#"Encrypting");
fmdb.key = #"";
[fmdb rekey : #"somekey"];
}
Otherwise will have to use one of the other PRAGMA methods, etc.
Does rekey only work with databases that are already encrypted?
This is using the FMDatabase Framework, but under the hood in the framework it is doing ...
- (BOOL)rekey:(NSString*)key {
#ifdef SQLITE_HAS_CODEC
if (!key) {
return NO;
}
int rc = sqlite3_rekey(db, [key UTF8String], (int)strlen([key UTF8String]));
if (rc != SQLITE_OK) {
NSLog(#"error on rekey: %d", rc);
NSLog(#"%#", [self lastErrorMessage]);
}
return (rc == SQLITE_OK);
#else
return NO;
#endif
}
It does run though the sqlite3_rekey, no errors, but database does not get encrypted.
All of the previous comments on this question are incorrect. You cannot use rekey to encrypt a plaintext database. Rekey is only to be used to change the encryption key on an encrypted database.
The correct way to encrypt a plaintext database is attach and export - see examples here http://sqlcipher.net/sqlcipher-api/#sqlcipher_export
The trick was that when the database is used to check for encryption (next time opening app) when it is already encrypted, but do not use a key to do a select, this will fail, but then the database will HAVE to be closed and reopened again with the key.
With the release of iOS 5 we are getting more and more errors when setting the serialized option for the sqlite database (so its save to be used for multithreading). We are getting SQLITE_MISUSE error code on sqlite3_config. Has someone noticed this odd behavior? And does someone know how I can fix this? It works perfectly fine on previous iOS versions.
here is the code:
- (sqlite3 *)getNewDBConnection {
NSLog(#"sqlite3 lib version: %s", sqlite3_libversion());
//sqlite3_config() has to be called before any sqlite3_open calls.
if (sqlite3_threadsafe() > 0) {
int retCode = sqlite3_config(SQLITE_CONFIG_SERIALIZED);
if (retCode == SQLITE_OK) {
NSLog(#"Can now use sqlite on multiple threads, using the same connection");
} else {
NSLog(#"setting sqlite thread safe mode to serialized failed!!! return code: %d", retCode);
}
} else {
NSLog(#"Your SQLite database is not compiled to be threadsafe.");
}
sqlite3 *newDBconnection;
// Open the database
if (sqlite3_open([[self getDatabaseFilePath] UTF8String], &newDBconnection) == SQLITE_OK) {
NSLog(#"Database Successfully Opened :)");
} else {
sqlite3_close(newDBconnection);
NSLog(#"Error in opening database :(");
}
return newDBconnection;
}
and this is the output:
sqlite3 lib version: 3.7.7
setting sqlite thread safe mode to serialized failed!!! return code: 21
Database Successfully Opened :)
I struggled long and hard with this as well and finally got the solution.
As #enobufs said, sqlite3_config() needs to be called before sqlite3_initialize(). However, the OS might initialize SQLite for us so I also do a sqlite3_shutdown() before the sqlite3_config().
sqlite3_shutdown()
sqlite3_config()
sqlite3_initialize().
Then its also necessary to use the same connection for every query as it is the access to the database connection that gets serialized. As described here http://www.sqlite.org/capi3ref.html#sqliteconfigserialized
So I create a connection as soon as the app starts up and the pass that connection to every class that needs it.
Is the sqlite3_config() called before sqlite3_initialize()? The function returns SQLITE_MISUSE if called after sqlite3_initialize() and before sqlite3_shutdown(). See http://www.sqlite.org/c3ref/config.html for more details.
TO start off with - this app doesn't need to get into the App Store.
I'm thinking something along the lines of the following should work:
mach_port_t *p;
void *uikit = dlopen(UIKITPATH, RTLD_LAZY);
int (*SBSSpringBoardServerPort)() =
dlsym(uikit, "SBSSpringBoardServerPort");
p = (mach_port_t *)SBSSpringBoardServerPort();
dlclose(uikit);
void *sbserv = dlopen(SBSERVPATH, RTLD_LAZY);
int (*setAPMode)(mach_port_t* port, const char* appID, BOOL suspended, void* unknown, void* unknown2) =
dlsym(sbserv, "SBSLaunchApplicationWithIdentifier");
setAPMode(p, "com.apple.weather", NO, nil, nil);
dlclose(sbserv);
However I'm getting exc_bad_access, which is likely due to the fact that it needs an auth token - I could be wrong though.
Alternatively I'm trying using the following:
Class $SBApplicationController=objc_getClass("SBApplicationController");
NSLog(#"[$SBApplicationController sharedInstance], %#", [$SBApplicationController sharedInstance]);
Sadly the output is null - so I guess this can't be done within the application.
Any ideas? This is driving me crazy - thanks!
The iOS sandbox will block or kill any process which isn't started by iOS.