The last couple of weeks have been very busy for me. Nonetheless we (Christian Seiler, Alexander Nitch and me) managed to have a, well, sort of developer chat for the Classic Forum. Since the development process has been stale for about four years, we now have the desire to restart development. But the development process should change a little bit. While in the past I made nearly all decisions more or less alone, we now choose a more democratic way of project management and open the project much more. Decisions are made in public with a fast non-bureaucratic poll if necessary. There also is a Google group free for everyone to join the development process. The source code has moved to Github so everyone can view what happens (and what doesn't ;-) and join the development process (just fork the project, make some changes and send a pull request or open a ticket).
And since the code has grown in many ways, the first decision we made for the 4.0 release is to completely rewrite the project. You can review the (short) protocol at Github.
I always hear, one should not hate foreigners, be open-minded, don't prejudice them, etc, pp. Well, in fact, xenophobia can be found in foreigners, too. We (my wife and I) going to emigrate to Switzerland, and in preparation for that I read a lot about Switzerland and the Swiss. I also read, that the Swiss hate the Germans and they don't want us to live in Switzerland. This may be or not, but when I read this something changed in my mind. I got scared of becoming a victim of xenophobia and I didn't like to emigrate any longer.
Today I got a mail as a response of an application for a flat, saying that they'll check other competitors, too, before they can eventually give us the flat. My first thought was „This surely is because I'm german, he (the author) is xenophobe!” My second thought was „WTF? You don't know him, why do you think that? It's simply fair to check all competitors!”
As the foreigner in this case I can say, yes, xenophobia can be found on both sides and one should truly free his mind from shit like that!
I'm writing this post because I got several requests because of my training activities the last couple of months. „Why do you do this?” they ask. Well, the answer is more ore less complex:
When I was younger I used to be keen on sports. I learned Karate and Judo for about 10 years and did sports at least 3 times a week. I used to drive to school (8km one way) by bicycle, every day. Then I left school and I started the „real life” and a day to day job. Spare time began to get rare. This is the moment where I completely stopped sports. The consequences are well known: I began to get fat (slowly, but definitely visible) and I got unathletic (short-breathed, fast-sweatening, etc, pp). It got harder for me to concentrate on things, „getting into flow” got harder and harder. This really annoyed me and one and a half month ago enough was enough.
I completely changed my lifestyle. I stopped drinking alcohol, I stopped eating any fast food, I try to avoid hydrates and I began to only eat fruit, vegetables and small portions of lean meat. I began training (jogging, mostly, but also biking). The first week wars very hard. I always had the feeling that something is missing and I was always kind of hungry. Not really hungry, but, you know, kind of missing something. My stomach was just not satisfied.
But it started to get easier every day. After the first week my genera well-being felt increased. I could concentrate easier and it was easier to find the the flow-feeling. I reduced weight and my endurance strengthened. I'm no longer tired so often. I simply began to feel well. It's a really good feeling :-)
And this, ladies and gentlemen, is the cause why I wanna get fit and start sports again :-)
I know, it was very quiet for such a time now. This was because I promised myself to stop blogging until I finished my new weblog software. Well, it is finished now :-) More later.
I will write my blog postings in english now. This has two reasons: first of all I want to practise my english again (such as Jeena). I didn't use english for a long time, so I forgot so fucking much… This annoys me! So feel free to correct me, any help is appreciated.
The second reason is because I am working only on english projects the last months. To reach the people I met there it is simply necessary to switch the language to english.
New Weblog Software
The new weblog software I'm using is called „Burp”. I wrote it in Ruby, but w/o Rails :-) I used a self-written framework with ActiveRecord. It's released under the MIT License, an open source license, so feel free to copy it.
It's still not very convenient (well, much more convenient than my old Block software), but I will enhance it in the next weeks whenever there is spare time.
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?
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.
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.
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 loadView bzw. 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 UITableViewStylePlain als UITableViewStyleGrouped
UITableViewStyleGrouped mag 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, UIImageView oder UITextField!
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 Atebitsbeschrieben. 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.
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:
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.