Kompromisse und andere Widerlichkeiten
Im Rahmen eines größeren CRM-Projekts (etwa 5 Mio Kunden-Datensätze) arbeite ich aktuell mit einer Tabelle „Kunden.” Um Projekt-spezifisch und ohne Sourcecode-Änderungen zusätzliche Informationen (etwa projektspezifische Informationen) speichern zu können, gibt es, verknüpft über „Kunden_ID” (1:n) eine Tabelle „Kunden_Zusatzinfos.” Die Tabelle ist eine einfache, Kunden_ID-Name-Wert-Tabelle, so dass generisch Informationen gespeichert werden können. Das ist natürlich allein schon ein Kompromiss, allerdings nicht sinnvoll anders zu lösen. Soweit, so schlecht.
Ein Problem tritt nun auf, wenn man eine Menge von Kontakten abruft, zu der ein bestimmtes Feld hinzu „gejoint” werden soll, also etwa so:
SELECT a.*,b.value AS specific_field FROM Kunden AS a LEFT JOIN Kunden_Zusatzinfos AS b ON a.Kunden_ID = b.Kunden_ID AND b.field = 'specific_field' WHERE suchkriterium
Je nach Anzahl der Kontakte kann diese Query extrem lange dauern, da aus einer zweiten Tabelle gejoint werden muss, die etwa 10x so viele Datensätze enthält. Zwar gibt es genau für sowas einen Index, aber auch der kann nur begrenzt helfen.
Abhilfe musste ich mir hier schaffen, indem ich in der Kunden-Tabelle eine weitere Spalte hinzugefügt habe, in der die Zusatz-Daten zu dem Kontakt als Array in JSON-Syntax gespeichert werden. Also z. B. ein Kontakt hat die Felder „ciao,” „newsletter,” und „lead,” so sähe das JSON-Feld so aus: {"ciao": 0, "newsletter": 1, "lead": 0}.
So kann ich ohne zusätzlichen Join alle Zusatzfelder mit auslesen. Allerdings zieht diese Methode den Nachteil der doppelten Datenhaltung mit sich: ich habe die Zusatz-Tabelle, in der die Daten stehen und die ich weiterhin brauche, um Kontakte nach spezifischen Zusatzfeldern suchen zu können und ich habe, sozusagen als Cache, das Zusatz-Feld mit der JSON-Syntax. Aber andererseits ist das halt einfach um etwa den Faktor 15 schneller als der zusätzliche Join.
Aber vielleicht fällt jemandem ja noch etwas anderes ein?
Hochzeit mit Flitterwochen
Heute ist unsere (Danielas und meine) Hochzeit. Danach gehts in die Flitterwoche. Nur, damit sich keiner fragt, wo wir sind. :)
CFPropertyList: PHP-Klassen zur Manipulation von Apples property lists
Bei meiner Arbeit für das iPhone-Projekt habe ich die Kommunikation zwischen Webservice und Applikation vor einiger Zeit auf XML-PLists umgestellt.
Allerdings hat das XML-Format (genau wie die anderen XML-basierten Protokolle, die ich vorher benutzt habe) das Problem, dass es sehr „geschwätzig” ist: so hatte ich in einem Extremfall über 80KB an Daten, die übertragen werden mussten. Prinzipiell ja nicht so wild, 80KB hat man mit heutigen DSL-Anschlüssen schnell übertragen. Aber problematisch wird das, wenn man bedenkt, dass die Daten eventuell auch über GPRS oder Edge übertragen werden müssen. In dem Fall wird es nämlich erstens schnell teuer und zweitens steigen die Ladezeiten ins Unermessliche.
Um diesem Problem entgegen zu treten, habe ich das Format umgestellt auf binäre PLists, Apples Standard-Format für PLists. Das Format ist ziemlich gut, finde ich, unterstützt es doch so Sachen wie „uniquing” von Objekten (jedes Objekt taucht nur einmal auf in einer Datei und wird ansonsten nur noch referenziert). Damit war es mir möglich, das Datenaufkommen um 60% zu reduzieren. Nachdem ich noch GZip darüber gejagt habe, war ich bei satten 1,8kb statt vorher 80kb. Ein Ergebnis, mit dem ich mehr als leben kann.
Zum Erstellen habe ich die PList-Klassen vom Kellerkind erweitert um binäre PLists. Das Ergebnis möchte ich euch nicht vorenthalten, wir (das Kellerkind und ich) haben ein Google-Code-Projekt draus gemacht.
AdHoc-Distribution via IPA-Format
Unter OS X und iPhone OS in Ordnern abgespeichert werden, die nach dem Schema Applikationsname.app benannt sind. Dass das Ordner sind, wird vor dem Nutzer allerdings versteckt, sie werden im Finder und den Datei-Dialogen nur als Applikation dargestellt.
Prinzipiell keine schlechte Sache, wenn man allerdings z. B. eine iPhone-Applikation via AdHoc an Windows-Nutzer weitergeben möchte zum Testen, dann müssen die einerseits über dieses Konzept bescheid wissen („dieser Ordner ist eine App”) und andererseits müssen sie wissen, dass sie den kompletten Ordner als Blackbox behandeln müssen (z. B.: sie müssen den ganzen Ordner in iTunes ziehen).
Komfortabler ist da das IPA-Format. Im Wesentlichen besteht das IPA-Format aus einer ZIP-Datei mit einer definierten Ordner-Struktur. Der Vorteil jedoch ist, dass sie nicht entpackt werden muss, sondern dass sie dem Nutzer als Programm-Datei erscheint. Ein Doppelklick auf so eine Datei führt z. B. zu einem Import nach iTunes. Außerdem kann man so iTunes-Artwork beilegen, was bei einer Distribution via .app-Verzeichnis nicht möglich ist.
Um das Handling zu vereinfachen, habe ich mir ein kleines Script erstellt, dass mir – mehr oder weniger – automatisch eine solche Datei erstellt, so dass ich sie nur noch versenden muss. Für Interessierte:
#!/bin/bash
if test -z "$1" -o -z "$2"; then
echo "usage:"
echo "$0 ProjectDirectory AppName"
exit
fi
mkdir /tmp/Payload
cp $1/iTunesArtwork /tmp/
cp -R $1/build/AdHoc*/$2.app /tmp/Payload/$2.app
pushd /tmp/
zip -r $2.zip Payload/ iTunesArtwork
popd
mv /tmp/$2.zip $2.ipa
rm -rf /tmp/Payload
rm /tmp/iTunesArtwork
# eof
Aufgerufen wird das Script mit zwei Parametern: dem Projekt-Verzeichnis und dem Namen der App, also z. B. ./create_ipa.sh Documents/termitool/ Termitool. Es wird dann eine Termitool.ipa im aktuellen Verzeichnis erstellt. Die Build-Konfiguration muss mit „AdHoc” beginnen. Sehr praktisch, das ganze.
iPhone-Tabellen-Performance
Die Performance von Tabellen auf den iPhone ist bei den meisten Programmen gelinde gesagt mies. Um jedoch die Programmierer in Schutz zu nehmen: das liegt nicht (nur) an ihnen. Apples iPhone-Tabellenframework funktioniert so, dass jedesmal, wenn eine Tabellenzelle in den sichtbaren Bereich scrollt, tableView:cellForRowAtIndexPath: aufgerufen wird. Diese Methode muss jedesmal neu das Zellenobjekt zurückgeben. Das gleiche gilt für etwaige Sektionen-Header und den Header und Footer. Das hat zwar den Vorteil, dass man maximale Flexiblität hat (man kann jederzeit die Zelle ändern), aber gleichzeitig auch den Nachteil, dass viel Performance verloren gehen kann. Deshalb sollte man sich an folgende Richtlinien halten:
- So wenig wie möglich
- Wenn eine Tabellenzelle erstellt wird, sollte in
tableView:cellForRowAtIndexPath:so wenig wie möglich passieren. Informationen cachen und nicht jedesmal berechnen, wenig allocs, usw. - Die Zellenview vorbereiten
- Eines der schlechtesten Sachen in Hinblick auf die Performance, die man machen kann, ist, die Zellenview jedesmal neu zu erstellen. Das kostet enorm viel Performance. Am besten wäre hier nach dem Model-View-Controller-Prinzip, dass man sich via View-Controller im
loadViewbzw.initWithNibName:bundle:eine View vorbereitet und die nur zurück gibt, mit ggfls. Anpassungen der UI-Objekte der View. - Keine (aufwendige) Arithmetik
- Anfangs habe ich jedesmal, wenn ich eine Zelle zurückgeben sollte, ein Datum formatiert. Das ist eine ganz schlechte Idee, aufwendige Arithmetik ist der Performance-Fresser schlechthin. Solche Berechnungen sollten auf jedenfall gecached werden!
- Lieber Speicher als CPU
- Allgemein gilt: lieber etwas mehr Speicher nutzen und dafür die CPU weniger beanspruchen als umgekehrt. Die CPU ist sehr stark beansprucht beim Scrollen durch Tabellen.
- Lieber
UITableViewStylePlainalsUITableViewStyleGrouped UITableViewStyleGroupedmag zwar nett aussehen, hat jedoch den entscheidenden Nachteil, dass es langsamer ist. Durch die abgerundeten Ecken und die möglicherweise transparenten Headerviews gibt es viele transparente Bereiche, und Transparenz kostet Rechenzeit.- Lieber weißer Hintergrund als Transparenz
- Transparenz kostet Rechenzeit, und das nicht wenig. Deshalb lieber einen weißen Hintergrund benutzen als Transparenz. Das gilt für quasi alle Objekte, auch
UILabel,UIImageViewoderUITextField! - Lieber nebeneinander als Übereinander
- Meistens wird eine Tabelle über die GPU gezeichnet. Die GPU jedoch kommt nur schlecht klar mit Blending. Deshalb kostet es ziemlich viel Performance, wenn man Objekte übereinander bzw. überlappend anzeigen lässt. Das ist z. B. der Fall bei einer View mit transparenten
UILabel.
Selbstverständlich gilt das selbe für tableView:viewForHeaderInSection:. Ihr seht, im wesentlichen läuft es darauf hinaus, dass man einfach sehr sparsam umgehen muss mit den (sehr begrenzten, da Embedded-Device) Ressourcen, die man hat.
Bei aufwendigen Zellenviews kann es passieren, dass trotz der Einhaltung der obigen „Richtlinien” noch Performance-Probleme auftreten. In diesem Fall kann es sinnvoll sein, die Zelle „selber” zu zeichnen. Wie genau das geht und warum das oft schneller ist, haben die Leute von Atebits beschrieben. Im Prinzip läuft es darauf hinaus, dass man durch das überschreiben von drawRect: die Zeichenaufgabe von der GPU auf die CPU verlagert und die GPU dann nur noch mit einer einzelnen, statischen View arbeiten muss, was deutlich performanter implementiert ist.
NSDictionary aus NSData
Im Rahmen meiner Umstellarbeiten an unserer iPhone-Software habe ich von einem eigenen XML-Format umgestellt auf Property Lists. Alle meine Datenstrukturen sind im Wurzelknoten ein NSDictionary, damit einige Meta-Informationen (Ergebnis-Status, ggfls. Fehlernummer und -beschreibung, …) übermittelt werden können. iPhoneOS stellt für die Deserialisierung eines NSDictionary die beiden Routinen initWithContentsOfFile: und initWithContentsOfURL: bereit.
Da ich jedoch mit asynchronen Requests arbeiten muss, damit die Benutzeroberfläche nicht einfriert, liegt mir die Plist in einem NSData vor. Leider gibt es keine Methode, um ein NSDictionary aus NSData zu deserialisieren. Das allerdings ist schnell nachgerüstet und dank Kategorien sogar elegant:
@interface NSDictionary (MWBDictionary)
+ (id)dictionaryWithData:(NSData *)data;
- (id)initWithData:(NSData *)data;
@end
@implementation NSDictionary (MWBDictionary)
+ (id)dictionaryWithData:(NSData *)data {
return [[[NSDictionary alloc] initWithData:data] autorelease];
}
- (id)initWithData:(NSData *)data {
NSString *tmp = nil;
self = (NSDictionary *)[NSPropertyListSerialization
propertyListFromData:data
mutabilityOption:NSPropertyListImmutable
format:NULL
errorDescription:&tmp];
NSAssert1(tmp == nil,@"Fehler in plist: %@",tmp);
return [self retain];
}
@end
Objective-C und das NeXTStep-Framework gefallen mir irgendwie immer besser. Es scheint tatsächlich so, als haben die Leute nachgedacht und Wert auf elegante Lösungen gelegt.
lastDayOfMonth:inYear:
Zur Dokumentation für mich selbst und für Leute, die es vielleicht mal brauchen könnten:
+ (NSInteger)lastDayOfMonth:(NSInteger)month inYear:(NSInteger)year {
switch(month) {
case 2:
if(((year % 4) == 0 && (year % 100) != 0) || ((year % 400) == 0)) return 29;
else return 28;
case 4:
case 6:
case 9:
case 11:
return 30;
default:
return 31;
}
}
Diese Funktion gibt einem den letzten Tag im Monat month eines Jahres year zurück.
Benutzerfreundliche Verwendung des Orientation-Features des iPhone
Ich sitze gerade in der Realschule Datteln und richte für meinen Vater einen Linux-Server ein. Da es dabei immer wieder zu Wartezeiten kommt (aktuell warte ich darauf, dass 500GB mit 10MB/s kopiert werden), habe ich mir aus Langeweile ein paar „Apps“ angesehen. Dabei ist mir aufgefallen, dass die meisten Programme eine Drehung des iPhone vollständig ignorieren - obwohl eine Unterstützung des Features kaum Mehrarbeit, aber deutlich mehr Benutzerfreundlichkeit bedeutet.
Warum dreht man ein Handheld-Gerät? Um diese Frage zu beantworten, muss man zunächst betrachten, wie genau gedreht wird. Der Standard-Fall dürfte sein, dass das Gerät auf die Seite gedreht wird. Der Beweggrund ist ebenso einfach wie offensichtlich: die Daten (hier im Sinne von beliebigen Inhalten), sind für den Nutzer schwer zu lesen. Er dreht das Gerät also, weil eine breitere Ansicht seiner Meinung nach einfacherer zu lesen ist.
Der weitaus seltenere Fall dürfte sein, dass der Nutzer das Handheld auf den Kopf dreht. Warum? Nun, der einzige Fall, wo das Sinn ergeben kann, ist bei einer graphischen Darstellung: der Nutzer kann in der „normalen“ Porttrait-Ansicht nicht alles erkennen oder er versucht, einen anderen Eindruck von dem Bild zu bekommen. Bei einer reinen Textdarstellung kann ich mir keinen wirklichen Grund vorstellen, warum der Nutzer das Gerät drehen sollte - ich bin aber für Ideen offen!
Nun, wie auch immer: es muss im Sinne eines jeden Programmierers sein, dass seine Applikation möglichst benutzerfreundlich ist, unabhängig vom Zielpublikum: ich kenne niemanden, der sich das Leben und die Arbeit nicht gern vereinfachen lässt. Und da ein Programmierer möchte, dass seine Software von vielen Leuten eingesetzt wird (entweder aus wirtschaftlichen Gründen oder aus idealistischen; das Oder ist nicht exklusiv), muss er wohl oder übel darauf Rücksicht nehmen - und sollte es im Sinne von guter Software auch.
Daraus ergibt sich für mich folgende wünschenswerte Verhaltensweise:
- Dreht der Nutzer das Telefon auf die Seite, sollte die Ansicht sich verbreitern und mitdrehen, damit man einen besseren Überblick bekommt. Das Paradebeispiel hier wäre wohl die Darstellung von Bildern im Querfomat.
- Dreht man das Gerät auf den Kopf, ist abhängig von den Daten, was passieren sollte: bei Bildern sollte die Ansicht sich nicht mitdrehen, sondern erhalten bleiben, so dass der Nutzer sich einen anderen Eindruck verschaffen kann. Bei Textdaten sollte die Ansicht sich mitdrehen.
Ausnahmen können natürlich sinnvoll sein; so verwendet die Anwendung „Bloomberg“ die Queransicht, um einen Graphen über den Verlauf der Aktie darzustellen, die man sich gerade ansieht. Aber im allgemeinen sollte Software an diese Verhaltensweise angepasst werden. Dabei spielt die Meinung des Programmierers auch nur eine untergeordnete Rolle: er programmiert die Software normalerweise nicht für sich selber, sondern für die Nutzer.
Plugin-System in Obj-C
Heute musste ich mir überlegen, wie ich unsere Software modularisiere. Also, konzeptionell, das war mir schon länger klar, sondern technisch. Eigentlich wollte ich einen statischen C-Array von Klassenobjekten machen, aber das scheint nicht möglich zu sein. Alternativ habe ich mir jetzt folgende Methode überlegt:
Ich speichere die Klassennamen als C-Strings in einem statischen Array. Bei applicationDidFinishLaunching: durchlaufe ich diesen Array und hole mir via objc_getClass() das Klassenobjekt. Das kann ich nun ganz normal instanzieren. Praktisch sieht das so aus:
static const char *pluginList[] = {
"ModuleA",
"ModuleB",
"ModuleC",
NULL
};
//…
- (void)applicationDidFinishLaunching:(UIApplication *)application {
id plugin,*pluginInstance;
int i;
for(i=0;pluginList[i];++i) {
plugin = objc_getClass(pluginList[i]);
pluginInstance = [[pugin alloc] initWithDelegate:self];
[pluginInstance release];
}
}
Das Plugin kann sich dann beim Delegate mit entsprechenden Routinen registrieren. Ich muss sagen, Obj-C gefällt mir inzwischen wirklich.
Info.plist
Jede Applikation unter Mac OS X und iPhone OS wird mit einer Datei namens Info.plist ausgestellt. In dieser Datei stehen Basis-Informationen wie Programm-Version, ein eindeutiger Identifier für die App (z.B. de.termitel.Termitool), Icon, usw, pp. Man kann dort auch eigene Werte speichern, etwa Runtine-Konfigurationsvariablen, die man nicht in den Sourcecode brennen möchte. Ich habe es vor kurzem dazu benutzt, um die XMLRPC-URI für unsere iPhone-App zu speichern. Auslesen kann man dann das ganze recht einfach:
NSString *bundleVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"TermitoolXMLRPCURI"];
Sehr praktisch. Muss ich schon sagen.


