Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Die Reise nach iOS
3 Sehen und anfassen
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Multimedia
10 Jahrmarkt der Nützlichkeiten
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
Apps programmieren für iPhone und iPad von Klaus M. Rodewig, Clemens Wagner
Das umfassende Handbuch
Buch: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Rheinwerk Computing
1172 S., geb., mit DVD
49,90 Euro, ISBN 978-3-8362-2734-6
Pfeil 9 Multimedia
Pfeil 9.1 Schönschrift
Pfeil 9.1.1 Texthervorhebungen über Attributed Strings
Pfeil 9.1.2 Weitere Anzeigemöglichkeiten
Pfeil 9.1.3 Text mit Hervorhebungen über Dokumente erzeugen
Pfeil 9.1.4 Zeichenketten in Farben umwandeln
Pfeil 9.2 Einbindung von HTML-Dokumenten
Pfeil 9.2.1 Anzeige von HTML-Dokumenten
Pfeil 9.2.2 Javascript-Dateien einbinden
Pfeil 9.2.3 Das Delegate des Webviews
Pfeil 9.2.4 Webviews und Scrollviews
Pfeil 9.2.5 Der Viewport
Pfeil 9.2.6 Dynamische HTML-Seiten
Pfeil 9.2.7 HTML-Sonderzeichen maskieren
Pfeil 9.2.8 Javascript ausführen
Pfeil 9.2.9 Ereignisübergabe an die Applikation
Pfeil 9.3 Antwortcaching und Offlinemodus
Pfeil 9.3.1 Bilder nachladen
Pfeil 9.3.2 Cache Me If You Can
Pfeil 9.3.3 Let’s go offline
Pfeil 9.3.4 Protokolle
Pfeil 9.3.5 Ein datenbankbasierter Offlinecache
Pfeil 9.4 Videos
Pfeil 9.4.1 YouTube-Videos einbetten
Pfeil 9.4.2 Wiedergabe über das Media-Player-Framework
Pfeil 9.4.3 Vorschaubilder erzeugen
Pfeil 9.4.4 Videos über Layer anzeigen

Rheinwerk Computing - Zum Seitenanfang

9.3Antwortcaching und OfflinemodusZur nächsten Überschrift

Viele mobile Anwendungen hängen von einer funktionierenden Internetverbindung ab. Während viele Apps ohne Verbindung unbenutzbar sind, unterstützen hingegen andere Apps einen Offlinemodus. In dieser Betriebsart verwenden sie anstelle aktueller Serverdaten ältere Daten, die sie in früheren Programmläufen geladen haben. Ein Beispiel hierfür haben Sie bereits kennengelernt: Die Applikation SiteSchedule aus Kapitel 8, »Datenserialisierung und Internetzugriff«, zieht ihre Daten nur auf Anforderung vom Server und lädt sie ansonsten über Core Data. In vielen Fällen ist allerdings nicht so viel Aufwand notwendig, und es reicht aus, die Rohdaten zu speichern.


Rheinwerk Computing - Zum Seitenanfang

9.3.1Bilder nachladenZur nächsten ÜberschriftZur vorigen Überschrift

Das Beispielprojekt YouTubePlayer dieses Abschnitts ist eine Erweiterung und Anpassung des Beispielprojekts UniversalYouTube aus dem fünften Kapitel, die in der Übersicht auch Vorschaubilder zu den Videos anzeigt. Die Erweiterung des Viewcontrollers zum Nachladen der Bilder lässt sich mit relativ wenigen [Anm.: Jedoch zugegebenermaßen sehr langen] Anweisungen bewerkstelligen. In Listing 9.54 finden Sie den relevanten Code für die Methode collectionView:cellForItemAtIndexPath:.

Projektinformation

Den Quellcode des Beispielprojekts Markup finden Sie auf der DVD unter Code/Apps/iOS6/YouTubePlayer oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS6/YouTubePlayer.

NSArray *theThumbnails = [theItem 
valueForKeyPath:@"media$group.media$thumbnail"];

theCell.image = nil;
if([theThumbnails count] > 0) {
NSOperationQueue *theQueue = [NSOperationQueue mainQueue];
NSString *theURLString =
[theThumbnails[0] objectForKey:@"url"];

NSURL *theURL = [NSURL URLWithString:theURLString];
NSURLRequest *theRequest =
[NSURLRequest requestWithURL:theURL];

[theCell startLoadAnimation];

[NSURLConnection sendAsynchronousRequest:theRequest
queue:theQueue completionHandler:^(
NSURLResponse *inResponse, NSData *inData,
NSError *inError) {
if(inData == nil) {
NSLog(@"%@: %@", theURL, inError);
}
else {
theCell.image =
[UIImage imageWithData:inData];
}
[theCell stopLoadAnimation];
}
}];
}

Listing 9.54 Bilddaten in Collectionview-Zellen nachladen

Das JSON-Dictionary theItem mit den Daten eines Videos enthält ein Array mit URLs zu den Vorschaubildern, auf das Sie über Key-Value-Coding mit dem Pfad media$group.media$thumbnail zugreifen können. Die App verwendet immer die erste URL aus diesem Array, da das dahinterliegende Bild eine höhere Auflösung als die anderen Bilder besitzt. Sie lädt die Bilddaten mit Hilfe der Klassenmethode sendAsynchronousRequest:queue:completionHandler:, die Sie bereits in Kapitel 8, »Datenserialisierung und Internetzugriff«, kennengelernt haben. Aus den geladenen Daten erzeugt der Abschlussblock des Aufrufs die Bilddaten und übergibt sie an die Zelle.

Wenn Sie das Beispielprojekt ausführen, sehen Sie, wie die App die Bilder nachlädt (siehe Abbildung 9.14). Dabei visualisieren die Zellen den Ladeprozess jeweils durch einen UIActivityIndicatorView, dessen Animation die Methoden startLoadAnimation und stopLoadAnimation steuern.

Wenn Sie während des Ladens den Collectionview schnell hin und her scrollen, können Sie beobachten, dass manche Zellen ein Bild anstatt des schwarzen Hintergrunds und der Ladeanimation anzeigen, das die App jedoch nach einem kurzen Augenblick durch ein anderes Bild ersetzt. Anscheinend zeigt die App also hier Bilder in falschen Zellen an. Wie kommt dieser Effekt zustande?

Abbildung

Abbildung 9.14 Laden der Vorschaubilder in der Beispielapplikation

Die Ursache hierfür liegt an dem Wiederverwendungsmechanismus des Collectionviews für Zellen. Abbildung 9.15 stellt den Ablauf dieses Vorgangs grafisch dar. Dabei startet die Methode collectionView:cellForItemAtIndexPath: den Ladeprozess für eine Zelle, die Sie durch das Scrollen direkt wieder aus dem sichtbaren Bereich schieben. Dadurch fügt wiederum der Collectionview diese Zelle in die Menge der wiederverwendbaren Zellen ein. Aufgrund des Scrollens fordert die Datenquelle jedoch diese Zelle direkt wieder an, und startet dafür einen weiteren Ladeprozess. Während dieses Prozesses liefert nun der zuerst gestartete Ladeprozess seine Daten an die Zelle zurück, wodurch diese zunächst das falsche Bild anzeigt.

... und es kann noch schlimmer kommen

Wenn die Auslieferungszeit für die einzelnen Bilder sehr stark variiert, kann es sogar vorkommen, dass Bild 2 vor Bild 1 bei der Zelle ankommt. In diesem Fall zeigt die Zelle ein falsches Bild für die Zelle an.

Abbildung

Abbildung 9.15 Falsche Bildanzeige in Zellen

Glücklicherweise lässt sich dieser Darstellungsfehler jedoch relativ einfach beheben. Listing 9.54 basiert auf der fehlerhaften Annahme, dass zu jeder Zelle nur genau ein Eintrag gehört. Diese Annahme ist wegen des Wiederverwendungsmechanismus jedoch falsch. Allerdings hat jeder Eintrag einen eindeutigen Indexpfad, worauf die Lösung des Problems basiert. Dazu erfragt der Block jeweils die Zelle über die Methode cellForItemAtIndexPath: des Collectionviews, bevor er das Bild zuweist. Diese Methode liefert entweder die Zelle zum angegebenen Indexpfad, sofern sich die Zelle im sichtbaren Bereich des Collectionviews befindet, oder die Methode liefert nil. Dieses Verhalten ist für die Lösung des Problems sehr praktisch, da der Block die geladenen Bilder ja nur an sichtbare Zellen zuweisen soll. Listing 9.55 stellt die Änderung für die Lösung hervorgehoben dar:

[NSURLConnection sendAsynchronousRequest:theRequest 
queue:theQueue completionHandler:^(
NSURLResponse *inResponse, NSData *inData,
NSError *inError) {
YouTubeCell *theCell = (YouTubeCell *)
[inCollectionView cellForItemAtIndexPath:
inIndexPath];

if(inData == nil) {
NSLog(@"%@: %@", theURL, inError);
}
else {
theCell.image =
[UIImage imageWithData:inData];
}
[theCell stopLoadAnimation];
}
}];

Listing 9.55 Vermeidung von fehlerhaften Bildzuweisungen


Rheinwerk Computing - Zum Seitenanfang

9.3.2Cache Me If You CanZur nächsten ÜberschriftZur vorigen Überschrift

Als Caching bezeichnet man die Speicherung einmal geladener oder erzeugter Daten für die erneute Verwendung. Cocoa Touch verfügt bereits über fertige Komponenten zur Speicherung von Anfragedaten und deren Wiederverwendung. Sie können diesen Effekt an der Beispielapplikation verfolgen, indem Sie nach ihrem Start ein bisschen in der Übersicht hin und her scrollen. Die App lädt so die Vorschaubilder der Filme von YouTube. Schalten Sie danach die Internetverbindung Ihres Rechners beziehungsweise iOS-Gerätes aus, so dass die App keine Daten mehr von YouTube laden kann. Wenn Sie nun erneut in der Übersicht scrollen, zeigt die App trotzdem (fast) alle Vorschaubilder an, obwohl sie die Daten nach wie vor über eine URL-Anfrage lädt. In Listing 9.56 finden Sie den entsprechenden Codeausschnitt aus der Methode collectionView:cellForItemAtIndexPath: der Klasse YouTubeCollectionViewController.

NSURLRequest *theRequest = 
[NSURLRequest requestWithURL:theURL];

theCell.image = nil;
[theCell startLoadAnimation];
[NSURLConnection sendAsynchronousRequest:theRequest
queue:theQueue completionHandler:
^(NSURLResponse *inResponse, NSData *inData,
NSError *inError) {
if(inData == nil) {
NSLog(@"%@: %@", theURL, inError);
}
else {
theCell.image = [UIImage imageWithData:inData];
}
[theCell stopLoadAnimation];
}];

Listing 9.56 Vorschaubilder über eine URL-Anfrage laden

Da der Code das Anfrageobjekt ohne explizite Angabe einer Cache-Richtlinie erzeugt, verwendet die Anfrage die Standardrichtlinie NSURLRequestUseProtocolCachePolicy. Diese Richtlinie überprüft anhand verschiedener Kriterien, ob die Anfrage Daten aus dem Cache zurückliefert oder neu vom Server lädt. Diese Semantik stellt Abbildung 9.16 dar.

Abbildung

Abbildung 9.16 Cache-Semantik für das HTTP-Protokoll

Zunächst überprüft das URL-Ladesystem, ob im Cache zu der Anfrage überhaupt Antwortdaten vorliegen. Falls es Antwortdaten gibt, überprüft das Ladesystem als Nächstes, ob es die Daten erneut validieren muss, indem es nach dem Parameter must-revalidate im Header Cache-Control der Antwort aus dem Cache sucht. Bei der Revalidierung fragt das Ladesystem über eine spezielle Anfrage den Server, ob er neuere Daten als der Cache hat. Der Server antwortet mit dem Antwortcode 304, »Not Modified«, wenn die alten Daten ihre Gültigkeit noch nicht verloren haben. Falls jedoch neuere Daten vorliegen, liefert er stattdessen den Code 200 mit den aktuellen Daten zurück.

Falls die gecachte Antwort keine Revalidierung vorsieht, sucht das Ladesystem nach dem Verfallsdatum der Daten in den Headern Cache-Control und Expires. Wenn der Cache-Control-Header den Parameter max-age enthält, berechnet sich das Verfallsdatum aus dem Zeitpunkt, an dem die App die Daten geladen hat, plus dem Wert von max-age in Sekunden. Falls der Header keinen max-age-Parameter enthält, verwendet das Ladesystem den Zeitstempel des Expires-Headers. Bei abgelaufenem Verfallsdatum lädt es die Daten vom Server erneut.

Tabelle 9.8 Wichtige HTTP-Anwortheader zur Cache-Steuerung

Header Beschreibung

Last-Modified

Enthält den Zeitpunkt der letzten Änderung der Daten auf dem Server.

Expires

Verfallsdatum der Daten

Cache-Control

Enthält verschiedene Parameter zur Cache-Steuerung, wie beispielsweise max-age oder must-revalidate.

In Listing 9.57 sehen Sie eine HTTP-Antwort mit Cache-Control- und Expires-Header. Da der Cache-Control-Header den max-age-Parameter enthält, berechnet sich das Verfallsdatum aus diesem Wert (21.600 Sekunden = 6 Stunden). Die App hat die Daten am 6. Mai 2013 um 19:43:07 Uhr [Anm.: Die Endung GMT im Zeitstempel zeigt an, dass sich der Zeitpunkt auf die Greenwich Mean Time bezieht. Sie müssen also zwei Stunden addieren, um den Zeitpunkt in der Mitteleuropäischen Sommerzeit zu erhalten.] angefragt, und somit verfallen sie am 7. Mai um 1:43:07 Uhr.

HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 30997
Last-Modified: Mon, 06 May 2013 17:43:07 GMT
Date: Mon, 06 May 2013 17:43:07 GMT
Expires: Mon, 06 May 2013 23:43:07 GMT
Cache-Control: public, max-age=21600


Listing 9.57 HTTP-Antwort mit Headern zur Cache-Steuerung

Die Kategorie NSHTTPURLResponse(TimestampHeaders) erweitert die Klasse NSHTTPURLResponse um Methoden zum Auslesen von Headern mit Zeitstempeln und stellt ein NSDateFormatter-Objekt bereit, mit dem sich die Zeitstempel für HTTP-Header formatieren und parsen lassen:

+ (NSDateFormatter *)timestampFormatter {
static NSDateFormatter *sFormatter = nil;

if(sFormatter == nil) {
NSDateFormatter *theFormatter =
[NSDateFormatter new];
NSLocale *theLocale = [[NSLocale alloc]
initWithLocaleIdentifier:@"en_US_POSIX"];

[theFormatter setDateFormat:
@"EEE, dd MMM yyyy HH:mm:ss zzz"];
[theFormatter setLocale:theLocale];
sFormatter = theFormatter;
}
return sFormatter;
}

- (NSDate *)timestampHeaderForKey:(NSString *)inKey {
NSString *theValue = self.allHeaderFields[inKey];
NSDateFormatter *theFormatter =
[[self class] timestampFormatter];

return theValue == nil ? [NSDate date] :
[theFormatter dateFromString:theValue];
}

Listing 9.58 Zeitstempel in HTTP-Antwort-Headern parsen

Sie können also über die Methode timestampHeaderForKey: alle HTTP-Header, die einen Zeitstempel enthalten, als NSDate-Objekt auslesen. Sie können mit dieser Methode also beispielsweise die Header Date, Expires oder Last-Modified auslesen.

Die Methode dateOfExpiry berechnet das Verfallsdatum nach dem oben beschriebenen Verfahren über den Cache-Control- und den Expires-Header. Dabei versucht die Methode zunächst, den max-age-Wert aus dem Cache-Control-Header zu extrahieren. Wenn dieser Wert existiert, addiert sie ihn zum Zeitstempel aus dem Date-Header. Andernfalls liefert die Methode das Datum aus dem Expires-Header als Ergebnis.

- (NSDate *)dateOfExpiry {
NSString *theValue =
self.allHeaderFields[@"Cache-Control"];
NSRange theRange = theValue == nil ?
NSMakeRange(NSNotFound, 0) :
[theValue rangeOfString:@"max-age="];

if(theRange.location == NSNotFound) {
return [self timestampHeaderForKey:@"Expires"];
}
else {
NSInteger theStartIndex =
theRange.location + theRange.length;
NSString *theOffsetValue =
[theValue substringFromIndex:theStartIndex];
NSInteger theOffset = [theOffsetValue integerValue];

NSDate *theDate =
[self timestampHeaderForKey:@"Date"];

return [theDate dateByAddingTimeInterval:theOffset];
}
}

Listing 9.59 Berechnung des Verfallsdatums aus einer HTTP-Antwort

Wie Sie zu Beginn dieses Abschnitts feststellen konnten, scheint ja das Caching schon recht gut zu funktionieren. Wenn Sie jedoch das Experiment von oben wiederholen, die App stoppen und dann die Internetverbindung ausschalten, sehen Sie nach dem Neustart der App nur einen schwarzen Bildschirm. Das liegt daran, dass die App die JSON-Daten nicht aus dem URL-Cache lädt.


Rheinwerk Computing - Zum Seitenanfang

9.3.3Let’s go offlineZur nächsten ÜberschriftZur vorigen Überschrift

Die Klasse NSURLCache implementiert die Cache-Komponente des Ladesystems. Sie kann die Daten sowohl im Hauptspeicher als auch im Dateisystem speichern, und Sie können die maximalen Bytegrößen über memoryCapacity beziehungsweise diskCapacity abfragen. Entsprechend können Sie die Werte über setMemoryCapacity: beziehungsweise setDiskCapacity: neu festlegen. Das Cache-Objekt des Ladesystems erhalten Sie über die Klassenmethode sharedURLCache. Es liegt nun nahe, das Ladesystem und den Cache für spezielle Applikationsanforderungen, beispielsweise einen Offlinemodus, anzupassen.

Sie können die Antwort der JSON-Anfrage explizit im Cache ablegen. Dazu müssen Sie den Block für die asynchrone Anfrage wie in Listing 9.60 anpassen. Diese Änderung lässt sich im Beispielprojekt von der DVD aktivieren, indem Sie in der Datei YouTubeCollectionViewController.m dem Makro USE_CACHING den Wert 1 zuweisen.

[NSURLConnection sendAsynchronousRequest:theRequest 
queue:theQueue completionHandler:^(NSURLResponse
*inResponse, NSData *inData, NSError *inError) {
NSURLCache *theCache = [NSURLCache sharedURLCache];
NSCachedURLResponse *theResponse;

if(inData == nil) {
NSLog(@"error = %@", inError);
theResponse =
[theCache cachedResponseForRequest:theRequest];
}
else {
theResponse = [[NSCachedURLResponse alloc]
initWithResponse:inResponse data:inData];

[theCache storeCachedResponse:theResponse
forRequest:theRequest];
}
if(theResponse != nil) {
[self updateItemsWithData:theResponse.data];
}
theApplication.networkActivityIndicatorVisible = NO;
}];

Listing 9.60 Explizites Caching

Durch das explizite Cachen der Antwort überleben die JSON-Daten auch den Neustart der App. Nach einem Neustart mit ausgeschalteter Internetverbindung zeigt der Collectionview die gleichen Zellen wie beim letzten Lauf mit Internetverbindung an. Allerdings glänzt dieses Vorgehen nicht durch Stabilität. Nach einer längeren Pause zwischen Stopp und Neustart der App zeigt der Collectionview keine Zellen an, weil die Daten im Cache ihre Gültigkeit verloren haben. Leider beachtet der Cache anscheinend auch nicht immer die Werte aus dem Header und lädt Daten erneut, auch wenn sie nach dem Cache-Control- beziehungsweise Expires-Header ihre Gültigkeit noch nicht verloren haben.

Die Klasse NSURLCache lässt sich somit für einen zuverlässigen Offlinemodus einer App nicht verwenden. In diesem Zustand sollte die App auf die meisten geladenen Daten zugreifen können, die sie beim letzten Lauf mit Internetverbindung geladen hat, und dabei sollte auch die Länge der Zeitspanne zum letzten Lauf mit Internetverbindung keine Rolle spielen. Die Klasse NSURLCache reicht hierfür nicht aus, da dieser Cache die Daten nicht beliebig lange vorhält. Dieser Mangel lässt sich durch die Verwendung eines eigenen Caches vermeiden. Für die Implementierung sind zwei wesentliche Schritte notwendig:

  1. Implementierung einer Klasse für das Offline-Caching
  2. Einbindung dieser Cache-Klasse in das URL-Ladesystem

Da die Implementierung des Caches mehr Aufwand als seine Einbindung erfordert, wenden wir uns zunächst seiner Einbindung in das URL-Ladesystem zu. Dazu benötigen wir allerdings die Cache-Klasse aus dem ersten Schritt, wobei uns jedoch vorläufig ihre Schnittstelle ausreicht. Der Offlinecache soll die folgende Schnittstelle besitzen, die sich sehr stark an der Schnittstelle der Klasse NSURLCache orientiert:

@protocol OfflineCacheDelegate;

@interface OfflineCache : NSObject

@property (nonatomic, weak)
id<OfflineCacheDelegate> delegate;
@property (nonatomic) NSUInteger capacity;
@property (nonatomic, readonly) NSUInteger size;

+ (OfflineCache *)sharedOfflineCache;
+ (void)setSharedOfflineCache:(OfflineCache *)inCache;

- (id)initWithCapacity:(NSUInteger)inCapacity
path:(NSString *)inBaseDirectory;
- (NSCachedURLResponse *)cachedResponseForRequest:
(NSURLRequest *)inRequest;
- (void)storeCachedResponse:
(NSCachedURLResponse *)inResponse
forRequest:(NSURLRequest *)inRequest;
- (NSDate *)expirationDateOfResponseForRequest:
(NSURLRequest *)inRequest;
- (void)removeAllCachedResponses;
- (void)removeCachedResponseForRequest:
(NSURLRequest *)inRequest;

@end

@protocol OfflineCacheDelegate<NSObject>

@optional
- (BOOL)offlineCache:(OfflineCache *)inOfflineCache
shouldReturnCachedResponseForRequest:
(NSURLRequest *)inRequest;

@end

Listing 9.61 Schnittstelle des Caches für den Offlinemodus

Die Einbindung des Caches erfolgt dabei über eine Implementierung einer Unterklasse der Klasse NSURLProtocol.


Rheinwerk Computing - Zum Seitenanfang

9.3.4ProtokolleZur nächsten ÜberschriftZur vorigen Überschrift

Die erste Komponente in einer URL ist der Name des Protokolls, also beispielsweise HTTP, HTTPS oder FTP. Es gibt allerdings keine fest vorgegebene Menge von möglichen Namen oder Protokollen, und viele Anwendungen definieren eigene Protokollnamen oder sogar eigene Protokolle. Das Beispielprogramm WebView nutzte in Abschnitt 9.2.9, »Ereignisübergabe an die Applikation«, einen eigenen Protokollnamen, um Anweisungen aus einer Webseite an das umgebende Programm zu übermitteln. Dabei hat es dafür nicht nur den Namen des Protokolls, sondern auch den Aufbau der restlichen URL festgelegt.

Protokolle wie HTTP oder FTP legen jedoch nicht nur den Aufbau ihrer URLs fest, sondern auch die Regeln, wie der Client-Rechner die Daten zu einer URL ermitteln kann. Wenn das Ladesystem von iOS also die Daten zu einer URL abrufen will, muss es wissen, was dafür zu tun ist. Das beschreiben Unterklassen von NSURLProtocol. Das Beispielprojekt implementiert eine eigene Protokollklasse, um den Offlinecache einzubinden. Dieses Protokoll ist allerdings kein Ersatz für die HTTP-Protokollimplementierung von Cocoa Touch, sondern hängt sich nur in die Verarbeitung der Anfragen ein, um an den entscheidenden Stellen das Gewünschte zu tun.

Die Klasse für ein Protokoll muss mindestens vier Methoden implementieren. Das Ladesystem benutzt die Klassenmethode canInitWithRequest:, um zu entscheiden, ob das Protokoll die Anfrage verarbeiten kann. Falls die Methode YES zurückliefert, erzeugt es ein Objekt des Protokolls und startet die Anfrage über startLoading. Die Methode stopLoading erlaubt den Abbruch einer laufenden Anfrage. Die vierte Methode ist schließlich die Klassenmethode canonicalRequestForRequest:, die dem Protokoll eine Vereinheitlichung von Anfragen für den Cache erlaubt.

Das Protokoll des Beispielprojekts hat die Klasse OfflineHTTPProtocol und soll zwar HTTP(S)-Anfragen verarbeiten, jedoch nicht das HTTP-Protokoll neu implementieren. Dazu markiert es alle Anfragen, die es bereits angenommen hat, und lehnt alle so markierten Anfragen in canInitWithRequest: ab. Die Markung erfolgt dabei im Initialisierer, der außerdem die Verwendung des Standardcaches über die Cache-Policy NSURLRequestReloadIgnoringCacheData ausschaltet.

- (id)initWithRequest:(NSURLRequest *)inRequest 
cachedResponse:(NSCachedURLResponse *)inResponse
client:(id<NSURLProtocolClient>)inClient {
NSMutableURLRequest *theRequest =
[inRequest mutableCopy];

[NSURLProtocol setProperty:@YES forKey:kUseHTTPProtocol
inRequest:theRequest];
[theRequest setCachePolicy:
NSURLRequestReloadIgnoringCacheData];
return [super initWithRequest:theRequest
cachedResponse:inResponse client:inClient];
}

Listing 9.62 Der Initialisierer des neuen Protokolls

Für die Markierung verwendet das Protokoll die Methode setProperty:forKey:inRequest: der Klasse NSURLProtocol. Damit kann das Protokoll beliebige Informationen in einer Anfrage der Klasse NSMutableURLRequest ablegen, die sich über die Methode propertyForKey:inRequest: abfragen lassen. Die Methode canInitWithRequest: stellt damit fest, ob ein Objekt der Klasse OfflineHTTPProtocol die Anfrage bearbeitet. Das Protokoll akzeptiert also nur HTTP(S)-Anfragen, die es noch nicht markiert hat. Als Markierung verwendet es dabei einfach eine konstante Zeichenkette kUseHTTPProtocol, deren Definition Sie in der Implementierungsdatei finden.

+ (BOOL)canInitWithRequest:(NSURLRequest *)inRequest {
return [inRequest.URL.scheme hasPrefix:@"http"] &&
[NSURLProtocol propertyForKey:kUseHTTPProtocol
inRequest:inRequest] == nil;
}

Listing 9.63 Der Anfragefilter des Offlineprotokolls

Jetzt fehlt nur noch die Implementierung der eigentlichen Anfrage, wofür sich die Klasse NSURLConnection nutzen lässt, wenn sie hierfür die Implementierung des Standardprotokolls nutzt. Genau für diesen Zweck hat der Initialisierer ja die Markierung gesetzt. Der Ablauf einer Anfrage sieht also folgendermaßen aus:

  1. Die Applikation erzeugt eine Anfrage und startet sie über eine der bekannten Methoden (z. B. connectionWithRequest:delegate: oder dataWithContentsOfURL:).
  2. Das URL-Ladesystem sucht das passende Protokoll, findet dabei die Klasse OfflineHTTPProtocol als erste Möglichkeit und erzeugt von ihr ein Objekt mit der Anfrage.
  3. Das Protokoll erstellt eine modifizierte Version der Anfrage mit der beschriebenen Markierung.
  4. Das URL-Ladesystem startet die Anfrage, indem es die Nachricht startLoading an das Protokollobjekt sendet.
  5. Die Methode startet über connectionWithRequest:delegate: eine neue Anfrage mit dem markierten Anfrageobjekt.
  6. Für diese neue Anfrage sucht das URL-Ladesystem ebenfalls ein passendes Protokoll. Da die neue Anfrage jedoch die entsprechende Markierung besitzt, findet es diesmal die Klasse der Standardimplementierung und nicht OfflineHTTPProtocol.
  7. Schließlich erzeugt das Ladesystem ein Objekt der Standardprotokollimplementierung und führt damit die Anfrage durch.

Die Implementierung der Methode startLoading startet also einfach eine URL-Anfrage über den Convenience-Konstruktor connectionWithRequest:delegate: mit dem Protokollobjekt als Delegate. Außerdem speichert sie das dabei erzeugte Connection-Objekt in einer privaten Property, damit die Methode stopLoading darüber gegebenenfalls die Anfrage abbrechen kann.

- (void)startLoading {
self.connection = [NSURLConnection
connectionWithRequest:self.request delegate:self];
}

- (void)stopLoading {
[self.connection cancel];
}

Listing 9.64 Starten und Stoppen der URL-Anfrage im Protokoll

Außerdem muss sich das Protokoll während der Durchführung der Anfrage das Antwortobjekt und die empfangenen Daten merken, um sie im Cache ablegen zu können. Die entsprechenden Property-Deklarationen befinden sich in der anonymen Kategorie der Klasse, die auch die Klasse um die Protokolle NSURLConnectionDelegate und NSURLConnectionDataDelegate erweitert.

@interface OfflineHTTPProtocol() 
<NSURLConnectionDelegate, NSURLConnectionDataDelegate>

@property (nonatomic, weak) NSURLConnection *connection;
@property (nonatomic, strong) NSURLResponse *response;
@property (nonatomic, strong) NSMutableData *data;

@end

Listing 9.65 Die anonyme Kategorie des Protokolls

Schwache Verbindung

Die Kategorie in Listing 9.65 verwendet den Speicherverwaltungstyp weak für das Connection-Objekt, was völlig ausreicht, da das URL-Ladesystem bestehende Verbindungen hält und am Ende freigibt.

Das Protokoll kann nun über die Delegate-Methoden die Anfrage verarbeiten und die empfangenen Daten abspeichern. Allerdings muss es das Antwortobjekt und die Daten auch an die Anfrageverarbeitung weiterreichen, die die ursprüngliche Anfrage initiiert hat. Dazu übergibt das Ladesystem ein Client-Objekt an den Initialisierer des Protokolls (siehe Listing 9.62). Das Client-Objekt implementiert das Objective-C-Protokoll NSURLProtocolClient, das Methoden mit analogen Namen zum Connection-Delegate deklariert. Damit lassen sich die Delegate-Methoden implementieren, da die meisten die eingehenden Daten einfach nur weiterreichen:

- (void)connection:(NSURLConnection *)inConnection 
didFailWithError:(NSError *)inError {
self.response = nil;
self.data = nil;
[self.client URLProtocol:self didFailWithError:inError];
}

- (void)connection:(NSURLConnection *)inConnection
didReceiveData:(NSData *)inData {
[self.data appendData:inData];
[self.client URLProtocol:self didLoadData:inData];
}

- (void)connection:(NSURLConnection *)inConnection
didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)inChallenge {
[self.client URLProtocol:self
didReceiveAuthenticationChallenge:inChallenge];
}

- (void)connection:(NSURLConnection *)inConnection
didCancelAuthenticationChallenge:
(NSURLAuthenticationChallenge *)inChallenge {
[self.client URLProtocol:self
didCancelAuthenticationChallenge:inChallenge];
}

Listing 9.66 Methodenimplementierungen für das Protokoll

Auch die Implementierung der Methode connection:didReceiveResponse: ist kein Hexenwerk. Sie speichert das Antwortobjekt in der Property response und initialisiert die Property data mit einem geeigneten Objekt. An das Client-Objekt leitet sie allerdings nicht nur das Antwortobjekt weiter, sondern teilt ihm auch das gewünschte Cache-Verhalten mit. Da das Protokoll die Daten ja in einem eigenen Cache ablegt, braucht das Ladesystem sie nicht noch einmal im Standardcache abzulegen. Deshalb übergibt die Methode den Wert NSURLCacheStorageNotAllowed.

- (void)connection:(NSURLConnection *)inConnection 
didReceiveResponse:(NSURLResponse *)inResponse {
long long theCapacity =
inResponse.expectedContentLength;

self.response = inResponse;
if(theCapacity < 8192) {
theCapacity = 8192;
}
self.data = [NSMutableData
dataWithCapacity:theCapacity];
[self.client URLProtocol:self
didReceiveResponse:inResponse
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

Listing 9.67 Antwort der Anfrage entgegennehmen

Die Methode connectionDidFinishLoading: muss die empfangenen Daten im Offlinecache ablegen. Dazu erzeugt sie ein Objekt der Klasse NSCachedURLResponse und übergibt es über die Methode storeCachedResponse:forRequest: an den Offlinecache:

- (void)connectionDidFinishLoading: 
(NSURLConnection *)inConnection {
OfflineCache *theCache =
[OfflineCache sharedOfflineCache];
NSCachedURLResponse *theResponse =
[[NSCachedURLResponse alloc]
initWithResponse:self.response data:self.data];

[theCache storeCachedResponse:theResponse
forRequest:self.request];
[self.client URLProtocolDidFinishLoading:self];
}

Listing 9.68 Anfrage beenden und Daten im Cache ablegen

Das Protokoll speichert nun alle Antwortdaten im Offlinecache; sinnvollerweise sollte es gegebenenfalls vorhandene Daten beim Start einer neuen Anfrage auch von dort verwenden. Dazu müssen Sie nur die Methode startLoading aus Listing 9.64 um ein paar Anweisungen erweitern. Wenn die Methode Daten findet, startet sie keine neue Anfrage, sondern gaukelt stattdessen dem Client-Objekt den Ablauf einer Anfrage vor, indem sie die drei Nachrichten URLProtocol:didReceiveResponse:cachePolicy:, URLProtocol:didLoadData: und URLProtocolDidFinishLoading: mit den entsprechenden gecachten Daten an das Client-Objekt sendet.

- (void)startLoading {
OfflineCache *theCache =
[OfflineCache sharedOfflineCache];
NSCachedURLResponse *theResponse =
[theCache cachedResponseForRequest:self.request];

if(theResponse == nil) {

self.connection = [NSURLConnection
connectionWithRequest:self.request
delegate:self];
}
else {
[self.client URLProtocol:self
didReceiveResponse:theResponse.response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self
didLoadData:theResponse.data];
[self.client URLProtocolDidFinishLoading:self];
}
}

Listing 9.69 Starten einer Anfrage mit Verwendung des Caches

Damit steht die Einbindung des Caches in das URL-Ladesystem. Als Nächstes wenden wir uns der Implementierung des Caches zu.


Rheinwerk Computing - Zum Seitenanfang

9.3.5Ein datenbankbasierter OfflinecacheZur nächsten ÜberschriftZur vorigen Überschrift

Bevor wir genauer auf Implementierungsdetails eingehen, wollen wir die Anforderungen an den Cache festlegen. Er soll folgende Anforderungen erfüllen:

  1. Er muss die gespeicherten Antwortdaten beliebig lange vorhalten.
  2. Dabei darf er jedoch eine vorgegebene Größe für die Daten nicht überschreiten.
  3. Der Zugriff auf die Daten zu einer bestimmten Anfrage sollte möglichst wenig Zeit verbrauchen.

Eine einfache Möglichkeit für die Umsetzung ist die Verwendung eines Verzeichnisses und die Speicherung der Daten in einzelnen Dateien. Allerdings lässt sich damit die zweite Anforderung bei genauerer Betrachtung nur sehr schwer umsetzen. Die Anforderung impliziert nämlich auch, dass der Cache bei Überschreitung der vorgegebenen Größe alte Einträge löschen darf. Der Cache muss nun anhand von zwei unterschiedlichen Kriterien auf die Einträge zugreifen: Zu einer Anfrage soll er möglichst schnell die Antwortdaten liefern, und bei Überschreitung der vorgegebenen Größe soll er die Einträge anhand ihres Alters auflisten. Das ist bei einem rein dateibasierten System zwar möglich, doch lässt es sich in der Regel nur sehr schwer realisieren.

Für die Umsetzung ist eine einfache Datenbank besser geeignet, und iOS enthält standardmäßig die Programmbibliothek SQLite. SQLite ist eine C-Bibliothek, mit der Sie relationale Datenbanken verwalten können. Dabei legt es die Daten einer Datenbank in einer einzigen Datei ab. Im Gegensatz zu anderen Datenbanksystemen – wie beispielsweise PostgreSQL, Oracle Database oder MySQL – besitzt es keine Rechteverwaltung, und die Datenverwaltung erfolgt auch nicht über einen eigenen Server. Eine Client-Server-Datenbank wäre als Cache-Basis im besten Fall auch umständlich und im schlechtesten unbrauchbar.

Das Datenbankschema für den Cache besteht aus der Tabelle cache_entry und drei Indizes:

CREATE TABLE cache_entry (
id INTEGER PRIMARY KEY,
creation_time DATETIME NOT NULL
DEFAULT CURRENT_TIMESTAMP,
last_access_time DATETIME NOT NULL
DEFAULT CURRENT_TIMESTAMP,
expiration_date DATETIME NOT NULL,
search_key TEXT NOT NULL,
request BLOB,
response BLOB,
data BLOB,
user_info BLOB
);
CREATE INDEX cache_entry_creation_time_idx ON
cache_entry(creation_time);
CREATE INDEX cache_entry_last_access_time_idx ON
cache_entry(last_access_time);
CREATE INDEX cache_entry_expiration_date_idx ON
cache_entry(expiration_date);
CREATE INDEX cache_entry_search_key_idx ON
cache_entry(search_key);

Listing 9.70 Das Datenbankschema des Offlinecaches

Die Tabelle cache_entry enthält dabei sämtliche Informationen der Antworten: Das Anfrage- und Anwortobjekt in den Spalten request beziehungsweise response, die Anwortdaten in der Spalte data und das User-Info-Dictionary in der Spalte user_info. Diese Spalten haben alle den Datentyp BLOB, der die Speicherung beliebig langer Binärdaten erlaubt. Jeder Cache-Eintrag besitzt eine eindeutige Kennung in Form einer ganzen Zahl, die die Spalte id enthält, und die Spalten creation_time, last_access_time und expiration_date speichern die Erzeugungszeit, den Zeitpunkt des letzten Zugriffs auf den Cache-Eintrag sowie das Verfallsdatum. Die Spalte search_key enthält schließlich eine Zeichenkette, mit der der Cache zu einer Anfrage den passenden Eintrag findet.

Der Zugriff auf die Datenbank in SQLite erfolgt über C-Funktionen. Die freie Bibliothek FMDB (https://github.com/ccgus/fmdb) von Gus Mueller stellt einen Objective-C-Wrapper für die SQLite-Bibliothek zur Verfügung, der die Handhabung erheblich vereinfacht. Sie können ihn einbinden, in dem Sie die jeweils fünf Header- und Implementierungsdateien unter https://github.com/ccgus/fmdb in Ihr Projekt einbinden. Sie finden diese Dateien im Beispielprojekt in der Gruppe FMDB. Außerdem müssen Sie natürlich die Bibliothek libsqlite3.dylib in das Projekt hinzufügen, die die Funktionen von SQLite enthält.

Die Methode initWithCapacity:path: initialisiert die öffentlichen und privaten Propertys des Cache-Objekts und versucht, die Datenbank über die Methode open zu öffnen. Falls das Öffnen fehlschlägt, liefert der Initialisierer nil zurück.

- (id)initWithCapacity:(NSUInteger)inCapacity 
path:(NSString *)inPath {
self = [super init];
if(self) {
self.capacity = inCapacity;
self.baseDirectory = inPath;
if(![self open]) {
self = nil;
}
}
return self;
}

Listing 9.71 Der Initialisierer des Offlinecaches

Die Methode open erzeugt ein Datenbankobjekt über die Methode createDatabaseWithError: und versucht es über seine open-Methode zu öffnen.

- (BOOL)open {
NSError *theError = nil;
FMDatabase *theDatabase =
[self createDatabaseWithError:&theError];

if([theDatabase open]) {
self.database = theDatabase;
return YES;
}
else {
NSLog(@"open: %@", theError);
return NO;
}
}

Listing 9.72 Öffnen der Datenbank für den Cache

Das Erzeugen des Datenbankobjekts erfordert etwas mehr Aufwand. Die Methode muss gegebenenfalls das Verzeichnis für die Datenbankdatei anlegen, wofür sie die Methode createDirectoryAtPath:withIntermediateDirectories:attributes:error: der Klasse NSFileManager verwendet. Diese Methode liefert YES, wenn das Verzeichnis bereits existiert oder sie es angelegt hat. Falls die Datenbankdatei in diesem Verzeichnis existiert, erzeugt die Methode ein Datenbankobjekt. Andernfalls kopiert sie die Datei cache.db aus dem Ressourcenverzeichnis der App in das Cache-Verzeichnis und öffnet die Kopie.

Diese Datei enthält eine Datenbank mit dem Schema aus Listing 9.70 und wurde über den Befehl sqlite3 cache.db im Terminal erzeugt. Das Kommandozeilenprogramm sqlite3 erlaubt den interaktiven Zugriff auf SQLite-Datenbankdateien. Nach dem Start des Befehls zeigt es einen Prompt an; dort fügen Sie die SQL-Befehle zur Erzeugung des Schemas ein. Nach der Ausführung des letzten Befehls beenden Sie das Programm mit der Tastenkombination ctrl+D.

- (FMDatabase *)createDatabaseWithError:(NSError **)outError {
NSFileManager *theManager =
[NSFileManager defaultManager];
FMDatabase *theDatabase = nil;

if([theManager createDirectoryAtPath:self.baseDirectory
withIntermediateDirectories:YES attributes:nil
error:outError]) {
if([theManager fileExistsAtPath:self.databaseFile]) {
theDatabase = [FMDatabase
databaseWithPath:self.databaseFile];
}
else {
NSString *thePath = [[NSBundle mainBundle]
pathForResource:@"cache" ofType:@"db"];

if([theManager copyItemAtPath:thePath
toPath:self.databaseFile error:outError]) {
theDatabase = [FMDatabase
databaseWithPath:self.databaseFile];
}
}
}
return theDatabase;
}

Listing 9.73 Erzeugen des Datenbankobjekts

Der Cache verfügt nun über eine Datenbank, in der er seine Daten ablegen kann.

Ich packe meinen Koffer...

Der Cache soll nun in den Spalten Objekte der Klassen NSURLRequest, NSURLResponse, NSData, NSDictionary und anderen ablegen. Da diese Objekte wiederum andere Objekte enthalten können, trifft hier die Bezeichnung Objektgraph besser zu. Beispielsweise enthält ja ein NSDictionary-Objekt in der Regel Zeichenketten für die Schlüssel und andere Objekte für die Werte.

Das Foundation-Framework enthält die Klassen NSKeyedArchiver und NSKeyedUnarchiver, die eine leichte Serialisierung von Objektgraphen, also die Umwandlung des Graphen in Binärdaten, beziehungsweise Deserialisierung (Rückumwandlung der Binärdaten in einen Objektgraphen) erlauben. Dabei müssen alle Objekte im Graphen jeweils das Protokoll NSCoding implementieren, das die Implementierung der Methoden encodeWithCoder: zur Serialisierung und initWithCoder: verlangt. Die Methode initWithCoder: haben Sie bereits im Zusammenhang mit NIBs und Storyboards kennengelernt; und tatsächlich verwendet Cocoa Touch diesen Serialisierungsmechanismus auch zur Deserialisierung der View-Hierarchien aus den NIBs.

Die Serialisierung eines Objektgraphen erfolgt über eine der beiden Klassenmethoden archivedDataWithRootObject: oder archiveRootObject:toFile: von NSKeyedArchiver; also beispielsweise

NSDictionary *theDictionary = ...;
NSData *theDictionaryData = [NSKeyedArchiver
archivedDataWithRootObject:theDictionary];

Listing 9.74 Serialisierung eines Dictionarys

Mehr ist hier nicht zu tun, wenn alle Objekte im Dictionary das Protokoll NSCoding implementieren. Die Klassen NSURLRequest, NSURLResponse, NSData und NSDictionary implementieren bereits das Protokoll, und so brauchen Sie für die Cache-Implementierung nichts mehr zu unternehmen.

Wenn Sie beispielsweise die Droid-Objekte aus dem zweiten Kapitel serialisieren möchten, sieht die Implementierung der Methode encodeWithCoder: so aus:

// Droid.h
@interface Droid : NSObject<NSCoding>

@property(copy) NSString *droidID;

[...]

@end
// Droid.m
- (void)encodeWithCoder:(NSCoder *)inCoder {

[inCoder encodeObject:self.droidID forKey:@"droidID"];
}


Listing 9.75 Implementierung der Serialisierung

Sie brauchen also in encodeWithCoder: einfach nur alle Eigenschaften des Objekts, die die serialisierten Daten speichern sollen, unter einem Schlüssel in das übergebene Objekt der Klasse NSCoder zu schreiben. Unterklassen müssen zwar nicht mehr das Protokoll deklarieren, sollten jedoch encodeWithCoder: überschreiben, wenn sie zusätzliche Daten serialisieren möchten. Dabei sollte diese Implementierung auch immer die Methode der Superklasse aufrufen, damit auch deren Daten serialisiert werden. Wenn Sie die Droidenklassen um Kampfdroiden erweitern, könnte das beispielsweise so aussehen:

// BattleDroid.h
@interface BattleDroid : Droid

@property (nonatomic) NSUInteger countOfFights;

@end
// BattleDroid.m
- (void)encodeWithCoder:(NSCoder *)inCoder {
[super encodeWithCoder:inCoder];
[inCoder encodeInteger:self.countOfFights
withKey:@"countOfFights"];
}

Listing 9.76 Implementierung der Serialisierung in Unterklassen

Für die Deserialisierung können Sie die Klassenmethoden unarchiveObjectWithData: oder unarchiveObjectWithFile: der Klasse NSKeyedUnarchiver verwenden. Wie die Serialisierung erfolgt auch die Deserialisierung in nur einem Schritt:

NSData *theData = ...;
NSDictionary *theDictionary =
[NSKeyedUnarchiver unarchiveObjectWithData:theData];

Listing 9.77 Deserialisierung eines Dictionarys

Wundertüte

Die Methode unarchiveObjectWithData: entpackt die Daten und liefert das deserialisierte Objekt zurück. Dabei nimmt weder diese Methode noch die gesamte Anweisung in Listing 9.77 eine Typprüfung vor. Falls die Daten also beispielsweise nur eine einfache Zeichenkette enthalten, werden Sie diesen Fehler nicht bei der Deserialisierung, sondern in der Regel erst bei der Verwendung des deserialisierten Objekts bemerken.

Für die Deserialisierung eigener Klassen extrahieren Sie die Daten in der Methode initWithCoder: analog zur Serialisierung:

// Droid.m
- (id)initWithCoder:(NSCoder *)inCoder {
self = [super init];
if(self) {
self.droidID =
[inCoder decodeObjectForKey:@"droidID"];
}
return self;
}

Listing 9.78 Implementierung der Deserialisierung

Auch hier sollten Unterklassen von serialisierbaren Klassen immer die Methode initWithCoder: der Oberklasse aufrufen, damit auch diese ihre Daten aus dem NSCoder-Objekt auslesen kann.

// BattleDroid.m
- (id)initWithCoder:(NSCoder *)inCoder {
self = [super initWithCoder:inCoder];
if(self) {
self.countOfFights =
[inCoder decodeIntegerForKey:@"countOfFights"];
}
return self;
}

Listing 9.79 Implementierung der Deserialisierung in Unterklassen

Cache-Einträge finden

Der Cache soll nun zu einer Anfrage einen passenden Cache-Eintrag finden, sofern es einen gibt. Dazu muss er wissen, wann zwei Anfragen gleich sind. Eine einfache Möglichkeit, die Gleichheit von Anfragen zu definieren, ist, die Gleichheit der Anfragen auf die Gleichheit ihrer enthaltenen URLs zurückzuführen. Zwei Anfragen sind also genau dann für den Cache gleich, wenn ihre URLs gleich sind.

Dieses Verfahren klingt zunächst einmal ganz vernünftig; es bringt jedoch Probleme mit sich, wenn die App unterschiedliche HTTP-Methoden mit gleichen URLs durchführt. Wenn sie beispielsweise Ressourcen vom Webserver über GET-Anfragen lädt und über HEAD-Anfragen (siehe Abschnitt 8.4.3, »Auf dem Webserver nichts Neues«) die Header abfragt. In diesem Fall sollte der Cache die Gleichheit von zwei Anfragen über die Gleichheit ihrer Methoden und ihrer URLs festlegen. Dieses Vorgehen verwendet die Klasse OfflineCache.

Damit dieses spezielle Verhalten möglichst wenig Abhängigkeiten im gesamten Code des Caches erzeugt, gibt es eine Hilfsmethode, die die Gleichheit von zwei Anfragen bestimmt. Die Methode cachedRequest:isEqualToRequest: in Listing 9.80 verwendet dabei die standardisierten URLs der Anfragen. Diese Methode liefert eine gleichwertige URL mit normalisiertem Pfad; d. h., die Methode ersetzt die Pfadbestandteile ./ und ../ durch gleichwertige Ausdrücke. So wandelt sie beispielsweise die URL https://github.com/Cocoaneheads////iPhone/./tree/Auflage_2/../master/Apps in die gleichwertige URL https://github.com/Cocoaneheads////iPhone/tree/master/Apps um. Wie Sie an dem Beispiel sehen, ersetzt sie dabei leider nicht mehrere aufeinanderfolgende Schrägstriche durch einen.

- (BOOL)cachedRequest:(NSURLRequest *)inCachedRequest 
isEqualToRequest:(NSURLRequest *)inRequest {
if([inCachedRequest.HTTPMethod
isEqualToString:inRequest.HTTPMethod]) {
NSURL *theCachedURL =
[inCachedRequest.URL standardizedURL];
NSURL *theURL = [inRequest.URL standardizedURL];

return [theCachedURL isEqual:theURL];
}
else {
return NO;
}
}

Listing 9.80 Die Gleichheit von zwei Anfragen bestimmen

Diese Methode können Sie allerdings nicht in einer SQL-Suchanfrage verwenden, um einen passenden Eintrag zu finden. Deswegen besitzt die Tabelle cache_entry die Spalte search_key, mit der sich schneller nach passenden Anfragen suchen lässt. Da die Gleichheit von zwei Anfragen auf der URL und der HTTP-Methode der Anfrage basiert, sollte der Inhalt des Suchschlüssels auch diese zwei Werte berücksichtigen. Andererseits sollte sich zu einer Anfrage leicht ein eindeutiger Suchschlüssel erzeugen lassen. Diese Aufgabe übernimmt die Methode searchKeyForRequest:, die einfach eine Zeichenkette aus der HTTP-Methode und der standardisierten URL erzeugt.

- (NSString *)searchKeyForRequest:(NSURLRequest *)inRequest {
NSURL *theURL = [inRequest.URL standardizedURL];

return [NSString stringWithFormat:@"%@:%@",
inRequest.HTTPMethod, theURL];
}

Listing 9.81 Suchschlüssel zu einer Anfrage berechnen

Mit diesen Hilfsmethoden lässt sich nun die Methode cachedResponseForRequest: wie in Listing 9.82 implementieren. Dazu sucht die Methode über den Suchschlüssel nach dem Cache-Eintrag in der Tabelle cache_entry. Sie verwendet dazu die Methode executeQuery: der Klasse FMDatabase, die als ersten Parameter eine SQL-Suchanweisung (SELECT-Anweisung) erhält. Dabei ersetzt die Methode die enthaltenen Platzhalter, also die Fragezeichen, durch die weiteren Methodenparameter. Das Fragezeichen in der Bedingung search_key = ? wird also durch den Wert der Variablen theKey ersetzt.

- (NSCachedURLResponse *)cachedResponseForRequest: 
(NSURLRequest *)inRequest {
NSCachedURLResponse *theResult = nil;
NSString *theKey = [self searchKeyForRequest:inRequest];
FMResultSet *theResultSet = [self.database executeQuery:
@"SELECT response, data, user_info FROM cache_entry
WHERE search_key = ?", theKey];

if([theResultSet next]) {
NSURLResponse *theResponse =
[theResultSet decodedObjectForColumnIndex:0];
NSData *theData =
[theResultSet dataForColumnIndex:1];
NSDictionary *theUserInfo =
[theResultSet decodedObjectForColumnIndex:2];

theResult = [[NSCachedURLResponse alloc]
initWithResponse:theResponse data:theData
userInfo:theUserInfo
storagePolicy:NSURLCacheStorageAllowed];
[theResultSet close];
[self.database executeUpdate:
@"UPDATE cache_entry SET last_access_time =
CURRENT_TIMESTAMP WHERE search_key = ?",
theKey];
}
return theResult;
}

Listing 9.82 Cache-Eintrag zu einer Anfrage auslesen

Das Ergebnis des Methodenaufrufs ist ein Objekt der Klasse FMResultSet, das die Ergebnisse beschreibt. Die Klasse basiert auf dem Entwurfsmuster Iterator. Ein Iterator erlaubt den Zugriff auf die Elemente einer Sammlung, ohne deren Struktur zu kennen. Für die Klasse FMResultSet bedeutet das, dass ein Objekt immer ein Element aus der Ergebnismenge repräsentiert. Der Aufruf der Methode next führt dazu, dass das Result-Set-Objekt zum nächsten Element vorrückt. Bei erfolgreichem Vorrücken liefert die Methode das Ergebnis YES, ansonsten NO. Um direkt nach der Suche zum ersten Element zu gelangen, müssen Sie ebenfalls die Methode next aufrufen.

Die Methode in Listing 9.82 liest also nur die Spalten response, data und user_info der ersten Ergebniszeile aus, die ja alle drei Binärdaten enthalten. FMDB kann solche Spaltenwerte als Objekte der Klasse NSData zurückliefern, was für die Spalte data auch vollkommen ausreicht. Die Spalten response und user_info enthalten hingegen die Daten serialisierter NSURLResponse- beziehungsweise NSDictionary-Objekte. Hier muss die App die Datenobjekte also erst über die Methode unarchiveObjectWithData: der Klasse NSKeyedUnarchiver deserialisieren.

Für diese Deserialisierung erweitert die Kategorie FMResultSet(OfflineCache) die Klasse FMResultSet um die Methode decodedObjectForColumnIndex:, um eine einfache Deserialisierung an mehreren Stellen zu ermöglichen.

@implementation FMResultSet(OfflineCache)

- (id)decodedObjectForColumnIndex:(int)inIndex {
NSData *theData =
[self dataForColumnIndex:inIndex];

return theData == nil ? nil : [NSKeyedUnarchiver
unarchiveObjectWithData:theData];
}

@end

Listing 9.83 Cache-Eintrag zu einer Anfrage auslesen

Nachdem die Methode cachedResponseForRequest: den Cache-Eintrag ausgelesen hat, aktualisiert sie noch den Zeitstempel last_access_time; er enthält den Zeitpunkt, an dem der Cache den Eintrag zuletzt gelesen oder geschrieben hat.

Momentan hat die Cache-Implementierung allerdings noch einen Schönheitsfehler: Wenn die App eine Antwort einmal geladen hat, liefert sie der Cache immer zurück. Das Protokoll lädt sie nur nach dem Löschen aus dem Cache erneut. Dieses Ereignis sollte allerdings nur sehr selten eintreten. Die Methode cachedResponseForRequest: sollte also anhand einer zusätzlichen Bedingung prüfen, ob er eine gecachte Antwort zurückliefert oder nicht.

Bei der Formulierung dieser Bedingung, treten einige Fragen auf:

  • Welche Antworten soll der Cache bei einer bestehenden Internetverbindung liefern? Keine, alle vorhandenen oder nur die noch nicht verfallenen?
  • Wie soll sich der Cache verhalten, wenn nur eine Verbindung über das Mobilfunknetz besteht? Soll die Applikation gecachte Antworten verwenden oder sie trotz langsamer Verbindung lieber neu laden?

Die Antworten auf diese Fragen kann hier nur der konkrete Anwendungsfall liefern. Deswegen ist es sinnvoll, diese Logik über Delegation auszulagern, anstatt sie fest im Cache zu verdrahten. Das Delegateprotokoll OfflineCacheDelegate besitzt aus diesem Grund die Methode offlineCache:shouldReturnCachedResponseForRequest:. Die Einbindung dieser Delegate-Methode erfolgt über die Hilfsmethode shouldReturnCachedResponseForRequest: der Klasse OfflineCache und eine Änderung der Methode cachedResponseForRequest: aus Listing 9.82:

- (BOOL)shouldReturnCachedResponseForRequest: 
(NSURLRequest *)inRequest {
if([self.delegate respondsToSelector:@selector
(offlineCache:shouldReturnCachedResponseForRequest:)
]) {
return [self.delegate offlineCache:self
shouldReturnCachedResponseForRequest:inRequest];
}
else {
return YES;
}
}

- (NSCachedURLResponse *)cachedResponseForRequest:
(NSURLRequest *)inRequest {
NSCachedURLResponse *theResult = nil;

if([self shouldReturnCachedResponseForRequest:
inRequest]) {
NSString *theKey =
[self searchKeyForRequest:inRequest];
[...]
}
return theResult;
}

Listing 9.84 Cache-Verhalten über Delegate steuern

Eine Implementierung der Delegate-Methode für das Beispielprojekt erfolgt später. Sie benötigt noch eine Methode, mit der sie das Verfallsdatum einer Antwort im Cache abfragen kann. Für die Abfrage eines einzelnen Wertes in der Datenbank bietet die Klasse FMDatabase spezielle Methoden an, die auch die Verwaltung des Result-Sets übernehmen. Beispielsweise liefert Ihnen die Methode dateForQuery: den Wert der ersten Spalte aus der ersten Zeile des Ergebnisses. Dabei sind die Parameter der Methode wieder analog zu executeQuery:

- (NSDate *)expirationDateOfResponseForRequest: 
(NSURLRequest *)inRequest {
NSString *theSearchKey =
[self searchKeyForRequest:inRequest];

return [self.database dateForQuery:
@"SELECT expiration_date FROM cache_entry
WHERE search_key = ?", theSearchKey];
}

Listing 9.85 Abfrage des Verfallsdatums eines Cache-Eintrags

Cache-Einträge anlegen und löschen

Beim Schreiben der Cache-Einträge gibt es zwei mögliche Fälle: Entweder muss der Cache einen neuen Eintrag in der Datenbank anlegen, oder er muss einen bestehenden überschreiben. Das ist abhängig davon, ob es bereits einen Eintrag mit dem entsprechenden Suchschlüssel gibt. Mit Standard-SQL-Befehlen (z. B. SQL99) lassen sich diese beiden Fälle nur durch mindestens zwei Anweisungen umsetzen, indem der Cache zunächst den Eintrag mit dem Suchschlüssel löscht und dann den neuen Eintrag einfügt. Listing 9.86 stellt diese beiden SQL-Anweisungen hervorgehoben dar.

- (void)storeCachedResponse: 
(NSCachedURLResponse *)inResponse
forRequest:(NSURLRequest *)inRequest {
NSURLResponse *theResponse = inResponse.response;
NSDate *theExpirationDate =
[self expirationDateForResponse:theResponse];
NSData *theRequestData = [NSKeyedArchiver
archivedDataWithRootObject:inRequest];
NSData *theResponseData = [NSKeyedArchiver
archivedDataWithRootObject:theResponse];

NSData *theUserInfo = [NSKeyedArchiver
archivedDataWithRootObject:inResponse.userInfo];
NSString *theSearchKey =
[self searchKeyForRequest:inRequest];

[self.database executeUpdate:
@"DELETE FROM cache_entry WHERE search_key = ?",
theSearchKey];
if(![self.database executeUpdate:
@"INSERT INTO cache_entry (
expiration_date, search_key, request, response, data, user_info)
VALUES (?, ?, ?, ?, ?, ?)", theExpirationDate,
theSearchKey, theRequestData, theResponseData,
inResponse.data, theUserInfo]) {
NSLog(@"error: %@", [self.database lastError]);
}
[self shrinkIfNeeded];
}
}

Listing 9.86 Anlegen eines neuen Cache-Eintrags

Um diese beiden Anweisungen auszuführen, müssen Sie die Methode executeUpdate: und nicht executeQuery: verwenden. Auch hier übergeben Sie im ersten Parameter die Anweisung mit Platzhaltern und in den weiteren Parametern die Werte für die Platzhalter. Allerdings liefert executeUpdate: kein Result-Set, sondern nur einen booleschen Wert zurück, der bei erfolgreicher Ausführung den Wert YES hat.

Außerdem bietet der Cache die Möglichkeit, einen einzelnen oder alle Einträge gleichzeitig über die Methoden removeCachedResponseForRequest: und removeAllCachedResponses zu löschen.

- (void)removeCachedResponseForRequest: 
(NSURLRequest *)inRequest {
NSString *theSearchKey =
[self searchKeyForRequest:inRequest];

[self.database executeUpdate:
@"DELETE FROM cache_entry WHERE search_key = ?",
theSearchKey];
}

- (void)removeAllCachedResponses {
[self.database executeUpdate:
@"DELETE FROM cache_entry"];
}

Listing 9.87 Cache-Einträge löschen

Aufräumen

Der Cache soll die Gesamtgröße der Daten begrenzen. Dazu muss er bei einer Überschreitung der Kapazität Antworten selbständig löschen. Dabei lässt sich die Gesamtgröße als

  • die Summe der Größen alle Antwortdaten (logische Größe) oder
  • die Größe der gesamten Datenbank (physikalische Größe)

auffassen. Die logische Größe beschreibt also die Gesamtmenge der reinen Daten, die der Cache enthält. Mit diesem Wert lässt sich häufig die benötigte Kapazität des Caches besser abschätzen. Wenn Sie beispielsweise für jedes Bild im Beispielprojekt YouTubePlayer maximal 200 kB rechnen, erhalten Sie bei maximal 48 Einträgen eine Datengesamtgröße von 48 × 200 kB = 9.600 kB ≈ 10 MB. Der Cache sollte also 10 MB Daten aufnehmen können, damit die Applikation auch ohne Internetverbindung die Bilder zu allen Einträgen anzeigen kann. Die logische Größe des Caches lässt sich über eine SQL-Anfrage berechnen, die die Datengrößen aller Einträge summiert:

- (NSInteger)logicalSize {
return [self.database intForQuery:
@"SELECT SUM(LENGTH(data)) FROM cache_entry"];
}

Listing 9.88 Berechnung der logischen Größe des Cache

Wenn die Größe des Flash-Speichers sehr stark begrenzt ist, kann es sinnvoller sein, die Kapazität des Caches anhand seiner physikalischen Größe festzulegen. Als physikalische Größe des Caches verwendet die Klasse OfflineCache die Dateigröße der SQLite-Datenbank. Dieser Wert lässt sich über die Methode attributesOfItemAtPath:error: der Klasse NSFileManager ermitteln:

- (NSInteger)physicalSize {
NSFileManager *theManager =
[NSFileManager defaultManager];
NSDictionary *theAttributes =
[theManager attributesOfItemAtPath:self.databaseFile
error:NULL];

return [theAttributes fileSize];
}

Listing 9.89 Berechnung der physikalischen Größe des Caches

Welche Größe der Cache nun verwendet, lässt sich über die Delegate-Methode sizeForOfflineCache: festlegen, die gegebenenfalls die Methode size aufruft.

- (NSInteger)size {
if([self.delegate respondsToSelector:
@selector(sizeForOfflineCache:)]) {
return [self.delegate sizeForOfflineCache:self];
}
else {
return [self logicalSize];
}
}

Listing 9.90 Berechnung der Größe des Caches über das Delegate

Die Methode shrinkIfNeeded verkleinert den Cache bei Überschreitung der Kapazität, indem sie den Eintrag mit der ältesten Zugriffszeit löscht. Die Kennung id dieses Eintrags lässt sich über die SQL-Anweisung

SELECT id FROM cache_entry ORDER BY last_access_time LIMIT 1

bestimmen. Durch ORDER BY last_access_time ordnet die Anweisung die Einträge anhand deren Zugriffszeiten in aufsteigender Reihenfolge, und LIMIT 1 begrenzt das Ergebnis auf einen Eintrag; also liefert die gesamte Anweisung nur die Kennung des ältesten Eintrags. Der lässt sich nun über

DELETE FROM cache_entry WHERE id = ?

löschen. Allerdings können Sie das Löschen auch mit einer SQL-Anweisung bewerkstelligen, indem Sie die erste Anfrage als Unteranfrage in die zweite einfügen:

DELETE FROM cache_entry WHERE id = (SELECT id 
FROM cache_entry ORDER BY last_access_time LIMIT 1)

Die Methode shrinkIfNeeded verwendet nun diese Anweisung, um den Cache zu verkleinern:

- (void)shrinkIfNeeded {
if(self.size > self.capacity) {
[self.database executeUpdate:
@"DELETE FROM cache_entry WHERE id = (SELECT id
FROM cache_entry ORDER BY last_access_time
LIMIT 1)"];
}
}

Listing 9.91 Verkleinerung des Caches

Allerdings kann es nun vorkommen, dass nach der Verkleinerung der Cache immer noch zu groß ist. Dieses Problem lässt sich durch das Ersetzen der if-Anweisung durch eine while-Schleife, also while(self.size > self.capacity) { [...] }, beheben. Das verkleinert zwar den Cache-Inhalt zuverlässig auf die vorgegebene Größe, hat jedoch zugleich den Nachteil, dass sie im schlimmsten Fall die App blockiert.

Da das Löschen in den meisten Fällen auch nicht unbedingt sofort erfolgen muss, lässt es sich auch gut im Hintergrund ausführen. Dazu lässt sich eine Operationqueue verwenden. Sie hat die Klasse NSOperationQueue und erlaubt die parallele und sequentielle Abarbeitung von Operationen der Klasse NSOperation. Jede Applikation verfügt über mindestens eine Operationqueue, die Sie über die Klassemethode mainQueue erhalten. Der Cache fügt nach dem Löschen eines Eintrags eine neue Operation in die Operationqueue ein, wenn die Größe des Caches immer noch die Kapazität übersteigt. Die Operation ruft dabei wieder die Methode shrinkIfNeeded auf. Die Abfrage der Größe verhindert übrigens ein endloses Einfügen neuer Operationen, was zu einem Speicherüberlauf führen würde, da ja jeder Aufruf von storedCachedResponse:forRequest: ansonsten eine neuen Endlosschleife erzeugte.

- (NSOperationQueue *)operationQueue {
return [NSOperationQueue mainQueue];
}

- (void)scheduleShrinkIfNeeded {
if(self.size > self.capacity) {
NSOperation *theOperation =
[NSBlockOperation blockOperationWithBlock:^{
[self shrinkIfNeeded];
}];

theOperation.queuePriority =
NSOperationQueuePriorityVeryLow;
[self.operationQueue addOperation:theOperation];
}
}

Listing 9.92 Cache-Verkleinerung in Operationqueue ausführen

Die Methode shrinkIfNeeded muss jetzt die Methode scheduleShrinkIfNeeded aufrufen, um die Operationen zu starten.

- (void)shrinkIfNeeded {
if(self.size > self.capacity) {
[self.database executeUpdate:
@"DELETE FROM cache_entry WHERE id = (SELECT id
FROM cache_entry ORDER BY last_access_time
LIMIT 1)"];
[self scheduleShrinkIfNeeded];
}
}

Listing 9.93 Starten der Operationen im Hintergrund

Durch die Hintergrundausführung kann es allerdings passieren, dass mehrere Threads gleichzeitig auf die SQLite-Datenbank zugreifen wollen. Dafür ist allerdings die Klasse FMDatabase nicht ausgelegt. Der Cache muss also sicherstellen, dass immer nur ein Thread gleichzeitig auf die Datenbank zugreift. Dazu lassen sich @synchronized-Blöcke verwenden, die ein Objekt als Argument erhalten.

@synchronized(anObject) {
[...]
}

Listing 9.94 Ein »@synchronized«-Block

Dieses Konstrukt stellt nun sicher, dass sich immer nur ein Thread gleichzeitig in einem dieser Blöcke mit dem gleichen Argument-Objekt befinden kann. Alle anderen Threads, die einen solchen Block betreten möchten, müssen so lange warten, bis der Thread seinen Block verlässt. Erst dann kann einer der wartenden Threads seinen @synchronized-Block betreten. Ein Beispiel und Tabelle 9.9 sollen dieses Verhalten veranschaulichen:

Tabelle 9.9 Parallele Threads und Synchronisation

Thread A Thread B Thread C
id a = @"A";

id b = a;

id c = "C";

@synchronized(a) {
[...];
}

@synchronized(b) {
[...];
}

@synchronized(c) {
[...];
}

Während sich Thread A in der Ausführung eines @synchronized-Blocks befindet, befinden sich die beiden anderen Threads jeweils genau vor der Ausführung von solchen Blöcken. Wie verhalten sich diese beiden Threads in dieser Situation? Thread B muss warten, da sein Block das gleiche Objekt wie Thread A verwendet, auch wenn er über eine andere Variable, b anstatt a, darauf verweist. Thread B kann also seinen @synchronized-Block erst dann betreten, wenn Thread A seinen verlassen hat. Im Gegensatz zu den Threads A und B verwendet Thread C ein anderes Objekt als Argument für seinen @synchronized-Block. Aus diesem Grund darf Thread C seinen @synchronized-Block betreten und muss nicht auf Thread A warten.

Der Cache lässt sich nun threadsicher gestalten, indem Sie die Zugriffe auf die Datenbank in @synchronized-Blöcke kapseln. Da der Cache das Datenbankobjekt gegen konkurrierende Zugriffe schützen soll, verwenden Sie es am besten auch als Argument der Blöcke. Die Methode removeAllCachedResponses aus Listing 9.87 sieht nach dieser Änderung so aus:

- (void)removeAllCachedResponses {
@synchronized(self.database) {
[self.database executeUpdate:
@"DELETE FROM cache_entry"];
}
}

Listing 9.95 »@synchronized«-Block in einer Methode

Die Klasse OfflineCache sichert alle Datenbankabfragen und -aktualisierungen durch entsprechende @synchronized-Blöcke ab.

Thread-Sicherheit mit FMDB

Das FMDB-Framework bietet über die Klasse FMDatabaseQueue noch einen eigenen Mechanismus, threadsichere Datenbankzugriffe zu gewährleisten. Sie erzeugen eine Datenbank-Queue analog zu einer Datenbank:

self.databaseQueue = 
[FMDatabaseQueue databaseQueueWithPath:thePath];

Die Ausführung erfolgt nun nicht mehr über direkte Methodenaufrufe, sondern über Blöcke. Die Methode removeAllCachedResponses aus Listing 9.95 sieht damit beispielsweise so aus:

- (void)removeAllCachedResponses {
[self.databaseQueue inDatabase:
^(FMDatabase *inDatabase) {
[inDatabase executeUpdate:
@"DELETE FROM cache_entry"];
}];
}

Allerdings dürfen Sie die Methodenaufrufe für die Datenbank-Queue nicht ineinander verschachteln, da das zu einem Deadlock führt; der Thread wartet hier auf die Beendigung der Methode, in der er sich gerade befindet.

Wenn die Methode einen Wert aus der Datenbank zurückliefert, muss sie den Wert in einer Variablen zwischenspeichern. Da Blöcke auf alle lokalen Variablen der umgebenden Methode nur lesend zugreifen können, müssen Sie die Variablen für Rückgabewerte durch den Modifizierer __block markieren. Dadurch können Sie dieser Variablen einen neuen Wert zuweisen. [Anm.: Das funktioniert allerdings nicht bei asynchron ausgeführten Blöcken, da hier das Ergebnis in der Regel erst nach Beendigung der aufrufenden Methode zur Verfügung steht, und die kann dann natürlich nichts mehr mit dem Ergebnis anfangen. Verwenden Sie also den Modifizierer __block so selten wie möglich. ] Die Methode logicalSize sieht damit beispielsweise so aus:

- (NSInteger)logicalSize {
__block NSInteger theResult = 0;

[self.databaseQueue inDatabase:
^(FMDatabase *inDatabase) {
theResult = [inDatabase intForQuery:
@"SELECT SUM(LENGTH(data)) FROM cache_entry"];
}];
return theResult;
}

Wir haben hier auf eine Einbindung der Queue verzichtet, da wir hierfür sehr viele bereits besprochene Methoden hätten überarbeiten müssen. Das hätte aus unserer Sicht das Verständnis der ohnehin schon recht komplexen Cache-Klasse zusätzlich erschwert. Trotzdem finden Sie in der Datei OfflineCache.m auch eine Implementierung auf Basis von FMDatabaseQueue, die Sie über das Makro USE_DATABASE_QUEUE in der Datei aktivieren.

Offlinecache und Protokoll in die Applikation einbinden

Zwar stellt diese Implementierung von canInitWithRequest: sicher, dass die Klasse OfflineHTTPProtocol HTTP(S)-Anfragen verarbeitet, allerdings kennt das URL-Ladesystem dieses Protokoll noch nicht. Sie müssen es dafür über die Klassenmethode registerClass: von NSURLProtocol registrieren.

Wer zuletzt kommt, mahlt zuerst

Durch die Registrierung des Protokolls kennt das Ladesystem jeweils zwei Protokolle, die HTTP(S)-Anfragen verarbeiten können. Das Ladesystem sucht das passende Protokoll zu einer Anfrage immer in der umgekehrten Registrierungsreihenfolge. Da das Ladesystem die Standardprotokolle direkt beim Start der Anwendung registriert, genießt das neue Protokoll also immer Vorrang vor den Standardprotokollen.

Außerdem muss die Applikation ein Cache-Objekt erzeugen. Die Beispielapplikation verwendet für die Ablage der Datenbankdatei einen Unterordner des Standardcache-Verzeichnisses, das sie über die Funktion NSSearchPathForDirectoriesInDomains ermittelt. Das alles geschieht im App-Delegate in der Methode application:didFinishLaunchingWithOptions: über die folgenden Anweisungen:

NSArray *thePaths = NSSearchPathForDirectoriesInDomains( 
NSCachesDirectory, NSUserDomainMask, YES);
NSString *thePath = [thePaths[0]
stringByAppendingPathComponent:@"OfflineCache"];
OfflineCache *theCache = [[OfflineCache alloc]
initWithCapacity:10485760 path:thePath];

theCache.delegate = self;
[OfflineCache setSharedOfflineCache:theCache];
[NSURLProtocol registerClass:[OfflineHTTPProtocol class]];
self.reachability =
[Reachability reachabilityForInternetConnection];

Listing 9.96 Konfiguration des Caches und Registrierung des Protokolls

Außerdem implementiert das App-Delegate, das auch das Delegate des Caches ist, die Methode offlineCache:shouldReturnCachedResponseForRequest:, um das Verhalten des Caches zu beeinflussen. Das Beispielprojekt soll gecachte Antworten verwenden, wenn der Eintrag noch nicht verfallen ist oder keine WLAN-Verbindung zum Internet besteht. Die erste Bedingung können Sie über die Methode expirationDateOfResponseForRequest: der Klasse OfflineCache realisieren, und für die zweite Bedingung verwenden Sie die Klasse Reachability aus Abschnitt 8.4.10, »Überprüfung der Erreichbarkeit«.

- (BOOL)offlineCache:(OfflineCache *)inOfflineCache 
shouldReturnCachedResponseForRequest:
(NSURLRequest *)inRequest {
NSDate *theExpirationDate = [inOfflineCache
expirationDateOfResponseForRequest:inRequest];

return [theExpirationDate compare:[NSDate date]] ==
NSOrderedDescending ||
self.reachability.currentReachabilityStatus !=
kReachabilityWiFi;
}

Listing 9.97 Steuerung des Caches

Vielleicht fragen Sie sich nun, was Ihnen der ganze Aufwand gebracht hat. Schließlich lassen sich ja die paar Bilder auch wesentlich einfacher speichern. Der Unterschied besteht darin, dass der Cache alle Daten speichert, die das Beispielprojekt über das URL-Ladesystem lädt. Sie können das ganz leicht über den Button Developer in der Werkzeugleiste prüfen, der in einem Webview die Entwicklerseite von YouTube öffnet. Drücken Sie bei bestehender Internetverbindung den Button. Die Applikation öffnet ein Pop-over mit der Webseite. Öffnen Sie einige Unterseiten, und merken Sie sich die Links, die Sie angeklickt haben. Danach schließen Sie das Pop-over, unterbrechen die Internetverbindung und öffnen das Pop-over erneut. Sie können nun, trotz unterbrochener Internetverbindung, die gleichen Seiten wie zuvor betrachten. Der Webview lädt diese Seiten nun aus dem Offlinecache.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück




Copyright © Rheinwerk Verlag GmbH, Bonn 2014
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de


  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad






Apps programmieren für iPhone und iPad
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Apps programmieren für iPhone und iPad






Apps programmieren für iPhone und iPad


Zum Katalog: Einstieg in Objective-C 2.0 und Cocoa






Einstieg in Objective-C 2.0 und Cocoa


Zum Katalog: Spieleprogrammierung mit Android Studio






Spieleprogrammierung mit Android Studio


Zum Katalog: Android 5






Android 5


Zum Katalog: iPhone und iPad-Apps entwickeln






iPhone und iPad-Apps entwickeln


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo