Showing posts with label Java. Show all posts
Showing posts with label Java. Show all posts

Sunday, April 15, 2012

"Recycle" Ein Versuch einer Erkärung... Teil1

Die meisten Notesforen sind voll von Fragen, warum Javaagenten die in einer Testumgebung so gut funktioniert haben auf dem Echtserver plötzlich die Performance in den Keller ziehen, oder sogar den Agentmanager oder gleich den ganzen Server crashen. Praktisch in jedem dieser Fälle lässt sich das Problem auf ein Problem mit der Noteseigenheit des recycelns von Notesobjekten zurückführen. Also was ist diese geheimnisvolle recycle() Methode die es für jedes Objekt des Notes Java API gibt und warum ist es so wichtig Sie zu verwenden.

Um recycle zu verstehen, muss man wissen, wie das Notes Java API funktioniert.  In diesem Post gehe ich ausschließlich auf den sogenannten lokalen Zugriff ein. Die andere Art DIIOP (funktioniert meiner Meinung nach nicht stabil und vor allem nicht performant) vergesse ich mal. Lokaler Zugriff heißt dabei nicht, dass man nicht auf den Server zugreifen kann. Es heißt nur, dass auf der Maschine entweder ein Notes Client oder ein Server lokal installiert sein muss, damit man diese Methode verwenden kann.

Die Notes Java API besteht einerseits aus der "Notes.jar" die nur kleine Methodenhüllen enthält, die nichts anderes tun als die entsprechenden Funktion in der nativen C++ Bibliothek nlsxbe.dll aufzurufen. Wie der Name lsx schon andeutet, ist das der selbe Code der auch bei der Verwendung des Notes API in Lotus script verwendet wird. Nun ist es so, dass wenn man ein NotesObjekt instantiert wird im Hintergrund auch immer ein natives C++ Objekt erzeugt. Während das Javaobjekt nur minimale Systemressourcen wie Hauptspeicher und vor allem Zugriffshandels belegt, benötigt das native C++ Objekt wesentlich mehr Ressourcen des Betriebssysstems.

Ein kleines Beispiel:

NotesThread.sinitThread(); //Thread für Noteszugriff initialisieren.
try {
   Session ses = NotesFactory.createSession();
} catch (NotesException e) {
   e.printStackTrace();
}
NotesThread.stermThread(); //Thread für Noteszugriff beenden.



Und schon hat man ein klassisches Speicherloch produziert.


In der Zeile

Session ses=NotesFactory.createSession();

wird sowohl ein leichtgewichtiges Java Objekt als auch ein ziemlich schweres C++ Objekt im Hintergrund erzeugt. Jetzt könnte man einwenden sobald die Ausführung den try catch Block verlässt, gibt es keine Referenz mehr auf "ses" und der Garbage Collector wird sich um die Speicherbereinigung kümmern, wie man es von Standardjava Objekten gewohnt ist. Das macht der GC auch brav, aber er kann  nur das leichtgewichtige Javaobjekt freigeben. Das schwergewichtige C++ Objekt bleibt im Hintergrund bestehen und mangels einer Referenz auf dieses Objekt kann dieses auch nie mehr bereinigt werden. Jetzt könnte man einwenden, dass die IBM die Bereinigung des C++ Objekts in die finalize() Methode jeder Klasse implementieren könnte die sich um die Bereinigung des C++ Objekts kümmert. Ich habe das auch lange nicht verstanden, bis ich mich selber ein wenig mit den Internas der GC von Java beschäfigt habe. Ohne zu tief ins Detail zu gehen sollte man sich auf keinen Fall auf finalize verlassen. Laut JVM Spezifikation kann die JVM den Aufruf von finalize auf Später verschieben, bzw. ist es nicht mal Pflicht die finalizer bei Beendigung der JVM aufzurufen. Deshalb hat die IBM diese finalize Methode berechtigterweise nicht verwendet und  bei jeder Klasse des API die recycle Methode implementiert. Beim Aufruf dieser Methode bleibt das leichtgewichtige Javaobjekt erhalten und das native C++ Objekt wird bereinigt.( Das heißt der Speicher wird freigegeben und Zugriffshandels werden geschlossen.) Wichtig ist, dass man ein Notesjavaobjekt nach dem recycle nicht mehr verwenden darf.

Wir müssen also ein ses.recylce() in unseren Code einfügen. Die Frage ist nur Wo?

Falsch wäre es das recycle einfach im Try catch einzubauen. wie z.B.

NotesThread.sinitThread(); //Thread für Noteszugriff initialisieren.
try {

     Session ses = NotesFactory.createSession();
     //Irgendwas mit der Session machen.
     ses.recycle(); //Dies ist ein schwerer Fehler

} catch (NotesException e) {
      e.printStackTrace();

}
NotesThread.stermThread(); //Thread für Noteszugriff beenden.

Das ses.recycle() ist problematisch, da wenn im Codeteil "irgendwas" ein Fehler auftritt in das Catch verzweigt wird und dann das recycle nie ausgeführt wird. Für die verlässliche Ressourcenfreigabe gibt es beim try catch die die finally Klausel.

NotesThread.sinitThread(); //Thread für Noteszugriff initialisieren.
Session ses=null;

try {
     ses = NotesFactory.createSession();
     //Irgendwas mit der Session machen.

     ses.recycle(); //Dies ist ein schwerer Fehler

} catch (NotesException e) {
      e.printStackTrace();

}
} finally {
     if (ses != null) {
         try {
              ses.recycle();
         } catch (NotesException e) {
               e.printStackTrace();
         }
     }

}
NotesThread.stermThread(); //Thread für Noteszugriff beenden.

Nun wird das recycle auf jeden Fall ausgeführt. Egal was im "Irgendwas" Codeteil passiert oder Welche Fehler auftritt das finally wird auf jeden Fall ausgeführt und der Code wird ausgeführt. Ich würde sogar das NotesThread.stermThread() in den finally Bereich verschieben, damit falls eine andere Exception als NotesException innerhalb des try catches Blocks auftritt auch das Beenden der Notesumgebung zuverlässig durchgeführt wird. Dieses minimale Codepattern mit zuverlässigen recycle der Session beim Verlassen des Thread/Programms sollte jedes Standaloneprogramm oder RCP Plugin oder Servlet beinhalten. Alles andere führt über über kurz oder lang zu Instabilitäten. Die einzige Ausnahme zum recyclen von Session gilt für Agents. In Agents wird eine Session vom Agentmanager zur Verfügung gestellt und diese darf man nicht recyclen, da sich um das Recyclen der Agentmanager selber kümmert.

Hier ein Beispiel für einen Agent:

public class JavaAgent extends AgentBase {

    public void NotesMain() {
      Database otherDatabase=null;
      try {
          Session session = getSession();
          AgentContext agentContext = session.getAgentContext();
          Database currentDatbase=session.getCurrentDatabase();          //Diese Datenbank sollte man nicht recyclen.
          otherDatabase=session.getDatabase("", "test.nsf"); //Diese Datenbank sollte man recyclen.
         
       } catch(Exception e) {
          e.printStackTrace();
       }
      finally{
          if(otherDatabase!=null){
            try {
                otherDatabase.recycle();
            } catch (NotesException e) {
                e.printStackTrace();
            }
          }
      }
   }
}


Wir haben in diesem Post einmal gesehen, wie man zuverlässig die Session beim verlassen seines Codes bereinigt und auch ein wenig geschaut wie das ganze bei Agents funktioniert. Wenn man diese Ratschläge beherzigt ist man schon vor einigen Problemen gefeit, da das recyclen der Session sämtliche anderen erzeugte C++ Objekte die von der Session abgeleitet sind auch recycelt Im nächsten Teil meines Erklärungsversuch von recycle kümmern wir uns dann um Objekte die nicht so lange warten können bis die Session recycelt werden.


Performanceproblem bei getCount des ViewNavigators

In meinem letzten Post habe ich über die großen Performancegewinne bei Verwendung eines ViewNavigators bei iterieren über eine View geschrieben. (Link) Gerade bei RCP Anwendungen gibt es da jedoch einen kleinen Fallstrick.

Für die Anzeige des Fortschritts des Jobs benötigt man am Anfang die Gesamtanzahl der Dokumente die  man verarbeiten möchte. Da würde einen als erstes die Methode getCount() von ViewNavigator ins Auge springen. Diese ist aber sehr langsam. Wenn man wirklich alle Dokumente der View verarbeitet sollte man lieber die getEntryCount() Methode der View verwenden. Falls man diese nicht verwenden kann, sollte man eventuell darüber nachdenken lieber einen unendlich Fortschrittsbalken zu verwenden.

Peformance Trick beim Durchlesen von Views


Eines vorweg für alle die mit Java auf Kriegsfuß stehen. Der Beispielcode ist zwar in Java. Das selbe sollte jedoch auch in anderen Sprachen z.B. Lotusscript funktionieren.  Ich zeige dann am Ende des Posts den Code auch in Lotusscript versprochen

In vielen meiner Programme oder Agenten kommt Code vor, der alle Dokumente einer bestimmten View lesen und verarbeiten muss.  wie z.B.


...
Document doc = view.getFirstDocument();
while (doc != null) {
     Document tempdoc = doc;
    
//Mach irgendwas mit diesem doc

     doc = view.getNextDocument(doc);
     tempdoc.recycle();
}

...

Ausführungsdauer  dieses Code bei unserer CRM Datenbank ca. 43 Sekunden.

Dabei war ich bis jetzt immer sehr enttäuscht über die Performance die Notes hier bietet. Das lesen von größeren Datenmengen dauert einfach ewig. Was hauptsächlich daran liegt, dass jeder Lesevorgang in der View eine Transaktion auslöst. Viel effizienter wäre es wenn Notes gleich ganze Blöcke lesen würde. Bisher gab es dazu keine Möglichkeit im Domino API.  Bei der view gibt es das auch nach wie vor nicht. Aber im Rahmen der Performanceverbesserungen bei xPages wurden neue Features beim ViewNavigator eingeführt und die kann man sich hier zunutze machen.

Hier der selbe Code wie oben nur umgebaut auf die Verwendung eines ViewNavigators:
...
ViewNavigator navigator = view.createViewNav();
ViewEntry entry = navigator.getFirstDocument();
while (entry != null) {
   Document doc = entry.getDocument();
 
//Mach irgendwas mit diesem doc
   ViewEntry tempEntry = entry;
   entry = navigator.getNextDocument();
   tempEntry.recycle();
}

...
Das bringt natürlich noch keine Verbesserung des Laufzeitverhalten, sondern bringt sogar eine Verschlechterung auf 53 Sekunden.

Aber der ViewNavigator besitzt neue Methoden die ein geblocktes Lesen erlauben. Die Voraussetzung dafür ist einmal, dass man die View aus dem man den Navigator erstellt das Autoupdate abgewöhnt mit view.setAutoUpdate(false).

Dann kann man den ViewNavigator mit navigator.setBufferMaxEntries(x) mitteilen, dass man gerne x Sätze auf einmal lesen will. Dies verbessert die Performance enorm. (Update laut Domino Wiki darf der Wert x zwischen (2 und 400) sein. Ich habe festgestellt, dass höhere Werte als 400 eine leicht bessere performance bringen. Siehe auch Kommentag von Ulrich Krause)

Folgender Code von oben ergänzt mit Blocking läuft auf meiner Maschine in unter 10 Sekunden statt der 43 Sekunden im usprünglichen Code ohne Blocking. 

...
view.setAutoUpdate(false);
ViewNavigator navigator = view.createViewNav();
navigator.setBufferMaxEntries(1024);
ViewEntry entry = navigator.getFirstDocument();
while (entry != null) {
   Document doc = entry.getDocument();
 
//Mach irgendwas mit diesem doc
   ViewEntry tempEntry = entry;
   entry = navigator.getNextDocument();
   tempEntry.recycle();
}

...
Das ist ja schon mal nicht schlecht.

Es gibt aber noch 2 zusätzliche Optionen mit denen man unter bestimmten Umständen noch zusätzliche Performance erzielen kann.

Wenn man wie in den obigen Beispiel die viewEntrys Spalten nicht benötigt, dann kann man das laden der Spaltenwerte unterdrücken. Je nach dem wie viele Spalten die View enthält, habe ich in meinem Tests nocheinmal eine Laufzeitreduktion um 10% erreichen können. Diese Option kann man mit navigator.setEntryOptions(ViewNavigator.VN_ENTRYOPT_NOCOLUMNVALUES) setzen.

Bei manchen Arten von Views kann es auch etwas bringen, die Entryoption navigator.setEntryOptions(ViewNavigator.VN_ENTRYOPT_NOCOUNTDATA); zu setzen. Laut meinen Informationen verhindert es die automatische Zählung bei kategorisierten Ansichten. Ich habe es aber noch nicht praktisch verwendet.

Noch einen zusätzlichen Performancegewinn, kann man erzielen, wenn man nur bestimmte Dokumente aus der View verarbeiten will, in dem man in der View die Selektionsfelder als Spalten aufnimmt, und dann das Dokument nur dann liest, wenn im Viewentry die Spaltenwerte den Selektionskriterien entsprechen. Hier sind die Performancegewinne noch stärker ausgeprägt als bei der klassischen Methode. Außerdem kann man natürlich noch die ganzen anderen Vorteile eines ViewNavigators verwenden. z.B. einen ViewNavigator mit allen Dokumenten einer bestimmten Kategorie einer kategorisierten View zu erstellen.

Hier noch wie am Anfang versprochen das oben angeführte Beispiel in Lotus script:

...
view.Autoupdate=false
Set navigator=view.Createviewnav()
navigator.Buffermaxentries=1024
navigator.Entryoptions=1  ' keine Spaltenwerte lesen.
Set entry=navigator.Getfirst()
While Not entry Is Nothing
    Set doc=entry.Document
    'Mach was mit dem Dokument
    Set entry=navigator.Getnext(entry)
Wend

... 

Würde mich über Kommentare zu den Performanceverbesserungen die Ihr in euren Anwendungen erzielen konntet freuen.

Friday, April 13, 2012

Standardausgabe von Javaplugins im Notesstandardclient

Oft schreibt man zu Debugzwecken in eigenen Plugins Statusmeldungen an die Standardausgabe. Diese werden wenn man den Notesclient aus Eclipse aufruft auch toll in der Console von Eclipse angezeigt. Aber wo findet man diese Ausgabe wenn man den Client normal gestartet hat. Die Antwort ist ganz einfach im Menü "Hilfe" ->Support gibt es den Punkt "Trace anzeigen" wenn man diesen aufruft, bekommt man eine XML Seite in der neben vielen anderen Meldungen auch die Ausgaben die man in seien Plugins an die Standardausgabe geschickt hat angezeigt werden.


Problem mit nicht Casesensitiver Suche in SQL

Bei vielen alphanumerischen Suchen in meinen Javaprogrammen habe ich bisher folgendes Pattern verwendet:

Ein PreparedStatement mit folgenden SQL erstellt "select *from table where ucase(name)=?"

Sting caseInsentiveSuchbegriff=suchbegriff.toUpperCase();
ps.setString(caseInsentiveSuchbegriff)
ResultSet rs=ps.executeQuery();

Das hat bisher auch sehr gut funktioniert, bis mir gestern ein Benutzer gesagt hat, dass er keine Suchbegriff mit einem "ß" findet. Nach kurzer Debugsitzung war auch klar warum. Java verwendet für Strings Unicode in dem kein großes "ß" definiert ist und verwandelt daher ein "ß" in ein "SS". Unserer Datenbank db/2 auf OS/400 gibt bei ucase('ß') aber ein "ß" zurück. Daher findet das Query keinen Satz.

Die einfachste Lösung für das Problem ist die upperCase auf lowerCase umzubauen und dann funktionieren auch Suchen nach "ß" einwandfrei.

P.S.

Interessant ist in diesem Zusammenhang, dass folgender Code false ergibt.

"groß".equals("groß".toUpperCase().toLowerCase());

Was ganz logisch ist, da "groß" nach dem toUpperCase() zu "GROSS" wird und bei dem toLowerCase() dann zu "gross" konvertiert wird.
 


Bestehende Eclipse RCP auf den Mac portieren Teil2

Nach dem ich mich in Teil 1 damit beschäftigt habe, wie man eine Eclipse RCP für den Mac kompilieren kann, möchte ich mich jetzt damit beschäftigen, was man beim Zugriff auf Lotus Notes aus der RCP heraus beachten muss. damit diese auch auf dem Mac funktioniert.


Bevor wir uns um die Portierung auf den Mac kümmern ein paar prinzipielle Dinge wie der Zugriff auf Notes technisch funktioniert. 


Die Klassen des Java APIs von Lotus Notes befinden Sich in der Notes.jar. Diese Datei finden Sie unter Windows im Verzeichnis C:\Program Files (x86)\IBM\Lotus\Notes\jvm\lib\ext.  Diese Datei muss sich im Klassenpfad befinden, damit Sie verwendet werden kann. Ich habe diese Datei der Einfachheit in ein Plugin importiert, dass die Klassen exportiert, damit ich nicht jedesmal die Notes.jar suchen muss. Prinzipiell kennt das Java API von Notes zwei Zugriffsmöglichkeiten:

DIIOP

Dafür muss ein eigener DIIOP Task am Server laufen, der die Zugriffe auf die Dominodaten durchführt. Auf den Client ist dann nichts mehr weiter nötig als die Notes.jar. Ich verwende diese Möglichkeit nicht sehr gerne, da erstens die Performance sehr bescheiden ist und DIIOP viele Bugs hat.

Lokaler Zugriff

Dabei fungiert die Notes.jar nur als leichtgewichtiger Wrapper für die DLL nlsxbe.dll die Teil des Notes Clients ist. Die Notes.jar besteht sozusagen nur aus JNI Aufrufen. Jeder Aufruf einer Methode in der Notes.jar ruft im Hintergrund eigentlich eine Methode in der nlsxbe.dll auf. Das API der liblsxbe.dll wurde ursprünglich für Lotusscript erstellt und wird von der Notes.jar mitbenützt. Der Vorteil davon ist, dass sich das Java Api praktisch 1:1 zu dem Lotusscript API verhält. Was die Einarbeitung von Lotusscript Anwendern verkürzt. Der Nachteil für Javaentwickler ist, dass das API teilweise seltsame Eigenheiten wie z.B. die Notwendigkeit von recycle() zum aufräumen von C++ Objekten aus nlsxbe.dll notwendig macht.

Leider ist es jetzt aber so, dass die nlsxbe.dll im Hintergrund noch andere DLL's des Notes Client verwendet.

Warum muss man das ganze überhaupt wissen? Ganz einfach wenn man in sein Programm die Notes.jar einbindet und per lokalen Zugriff auf Notes Daten zugreifen will, muss die JVM die notwendigen DLL's finden. Die JVM sucht die DLL's im sogenannten Library_Path. Dieser wird mittels der JVM Option "-Djava.library.path="C:\Program Files (x86)\IBM\Lotus\Notes" gesetzt. Nun findet die JVM zumindest mal die nlsxbe.dll. Aber das die nlsxbe.dll noch andere DLL's laden will, dabei aber vom Java library path nichts mehr weiß, muß das selbe Verzeichnis wie oben auch im Windows PATH sein.

In einer RCP setzt man den Library Path am besten in der Product Beschreibung auf der Launcherseite. Diese Einstellungen können und müssen für jede Plattform extra gesetzt werden.

Hier die Einstellung für Windows

Die Doppelten "\\" sind notwendig, damit beim Erstellungsvorgang der RCP keine Zeilenschaltungen eingefügt werden.

Zusätzlich muss auf Windows unbedingt noch in der Systemsteuerung des Zielcomputers die Pfadvariable angepasst werden. Leider gibt es keine Möglichkeit dies direkt in der RCP Anwendungen zu machen. Da ich sowieso immer MSI installer baue, ist das aber kein Problem.

Hier die Einstellung für den Mac

Am Mac werden DLL's die hier die Endung .dyld haben nicht über den Pfad aufgelöst, sondern der Mac sucht diese in diversen Standardsuchpfaden in denen Notes aber nicht enthalten ist und in den Verzeichnissen die in der Umgebungsvariable "DYLD_Library_PATH" angegeben sind. Wie kann man so eine Umgebungsvariable am Mac setzen. Wie immer am Mac geht das nicht so einfach wie in anderen Umgebungen. Während es unter Windows ein UI gibt um die Umgebungsvariablen zu ändern muss man in Mac OS X wie in Unix mit Konfigurationsdateien arbeiten:

Eine globale Änderung der Umgebungsvariablen kann man für einen Benutzer machen indem man die Datei "environment.plist" im versteckten Verzeichnis ".MacOSX"anlegt. (Wie zeige ich versteckte Dateien und Ordner am Mac an?)

Die Datei muss folgenden Aufbau haben:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>DYLD_Library_PATH</key>
    <string>/Applications/Notes.app/Contents/MacOS</string>
</dict>
</plist>

Falls man Lotus Notes in ein anderes Verzeichnis installiert hat, muss man natürlich den Pfad anpassen.

Diese Vorgehendsweise hat aber den Nachteil, dass auch andere Programme durch diese Änderung beeinflusst werden und Apple in manchen Versionen seines Betriebssystems diese Vorgehendsweise überhaupt unterbunden hat.

Deshalb die bessere Möglichkeit ist es in der von Eclipse erstellten Applikation die info.plist zu ändern. Die info.plist ist am Mac die zentrale Datei die steuert wie ein Programm aufgerufen werden soll. Man findet die Datei wenn man mit der rechten Maustaste auf eine Applikation klickt und in dem Kontextmenü den Punkt "Paketinhalt zeigen" aufruft. Dann zeigt der Finder den Inhalt der Applikation an. Im Verzeichnis Contents befindet sich dann die info.plist. Einfach die Datei mit dem Editor öffnen und innerhalb des dict Blocks folgenden Inhalt einfügen.

<key>LSEnvironment</key>
<dict>
     <key>DYLD_LIBRARY_PATH</key>
     <string>/Applications/Notes.app/Contents/MacOS</string>
</dict>

Vorsicht Änderungen an der info.plist werden in MacOSX gecached und der Mac erkennt diese Änderungen leider nicht. Deshalb muss man nach der Änderung unbedingt den Cache aktualisieren:

Dazu geht man in ein Terminal und führt folgenden Befehl aus:

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -v -f /Applications/eclipse/XXX.app

Wobei /eclipse/XXX durch den Applikationsnamen und Pfad ersetzt werden sollte.

Dann sollte auch der Zugriff auf das Java API von Notes funktionieren.

Thursday, April 12, 2012

Bestehende Eclipse RCP auf den Mac portieren Teil1

Ich habe eine Eclipse RCP Anwendung erstellt, die Daten über das Java API aus einer Notesdatenbank ausliest und verarbeitet. Diese Anwendung wurde in erster Linie für Windows erstellt aber durch die Plattformunabhängigkeit von Java sollte die Anwendung ja auch problemlos am Mac laufen. Bei meinen Versuchen der Portierung auf den Mac bin ich dabei auf ein paar Hürden gestossen, die nicht ganz einfach zum Umschiffen waren:

Teil1: Installation des Deltapacks in die Targetplattform

Das Deltapack muss die selbe Version haben wie die Targetplattform. Für 3.7 findet man das Delta Pack z.B. auf http://download.eclipse.org/eclipse/downloads/drops/R-3.7-201106131736/winPlatform.php .


Den Inhalt des Zips dann einfach in das Verzeichnis der Targetplattform entpacken.

In seiner Eclipse Entwicklungsumgebung muss man nach dem Entpacken die Targetplattform neu einlesen, damit Eclipse die Änderungen erkennt.


Danach sollte es problemlos möglich sein die bestehende Anwendung nicht nur für die aktuelle Plattform sondern auch für andere Plattformen wie den Mac zu erstellen.

Man geht dazu in die Produktdefinition der RCP Anwendung. Als erstes auf die Seite "Dependencies" und klickt dort auf den Knopf "Add required plugins". Es sollten dann automatisch die Fragmente für die verschiedenen Platfformen hinzugefügt werden.

 Jetzt kann man auf der Overview Seite auf den Link "Eclipse product export wizard" klicken. Wenn das Deltapack ordnungsgemäß installiert wurde, dann bekommt man in dem Assistenten die Möglichkeit die RCP für verschiedene Plattformen zu packen. Ganz wichtig ist, es wenn man eine Anwendung für den Mac erstellt diese unbedingt in ein ZIP zu exportieren, da es sonst zu Problemen mit den Dateinamen unter Windows kommen kann.

Auf der nächsten Seite des Assistenten kann man dann die Zielplattform auswählen dier erstellt werden soll. In meinen Fall Mac OS cocoa.


Das erstellte ZIP kann man dann einfach auf dem Mac in den Programme ordner entpacken und die Anwendung kann schon mal aufgerufen werden.

Für den Zugriff auf Notes sind jedoch noch weitere Schritte nötig, die ich im nächsten Blog Post näher erklären will.


ad