Tips: Logga Obj-C meddelanden

Det är ganska enkelt att koppla på loggning av obj-C-meddelanden. Värre är det att inte drunkna i mängden…

Ola Bäckström 10 juli 2008

Om du kör ett program från terminalen så kan du koppla på den inbyggda meddelandeloggningen.

Om du kör bash, vilket är troligt, slå:

export NSObjCMessageLoggingEnabled=YES

och sedan startar du ditt Obj-C program från samma terminalfönster. Använd ett annat terminalfönster och slå ps för att få reda på process-id (PID) till programmet. Nu finner du en logfil under /tmp/ som kallas msgSends-XXX, där XXX ska motsvara ditt program.

För att tolka de tre fälten, spana här. Notera att det bara är namnet på metoden (selectorn) som visas, inte ev. argument.

Jag har använt ovanstående för att gräva i odokumenterade AppKit-klasser. Tyvärr är datamängden stor (=enorm), använder en editor med sökning för att hoppa mellan relevanta rader.

,

Kommentera

---

_NSBindingAdaptor: Under ytan ... del 2

[editerad 081023, hade missat vad ba var satt till]
Fortsättning på del 1.

Resan fortsätter in i en av AppKits hemliga klasser. Hittar vi något?

Ola Bäckström 9 juli 2008

(för stämningens skull, lägg till musik från Doktor Drøvels hemlighet)

Vad kan vi finna ut om _NSBindingAdapter?

Jo, vår fina Nu är en lispinterpreter som ligger alldeles ovanpå Obj-Cns runtime . Vi kan lätt lista vilken klass som adaptern ärver från:

%(set ba (y _bindingAdaptor))
<_NSBindingAdaptor:34c590>
%
% (ba superclass)
NSObject
% 

ba sätts alltså till y:s _NSBindingAdapter. ba är alltså ett ganska enkelt objekt… som speciellt inte ärver från NSBinder som vi tidigare funderat runt.

Nu introducerar egna klasser (t ex NuCell som används för att bygga listor på klassiskt lispmanér) men Nu-språket lägger också till en del finnesser på existerande AppKit klasser. Speciellt på NSObject finns en del nya grejer. De två vi bryr oss om är:

+ (NSArray *) instanceMethodNames
+ (NSArray *) instanceVariableNames

Helt fantastiska funktioner för introspektion… Vi kan be en klass att redogöra vilka instansmetoder och instansvariabler som objekt av den klassen kan agera på.

(För att det verkligen ska gå in så repeterar jag) … vi kan alltså få reda på vilka instansvariabler vilken klass som helst lägger till – även Apples hemliga AppKitklasser.

Adapterns metoder

Det vi vill undersöka är _NSBindingAdapter, så vi slår


% ((ba instanceMethodNames) list)
("_discardEditingForAllBinders" 
"_editor:didChangeEditingState:bindingAdaptor:" 
"_handleValidationError:description:inEditor:errorUserInterfaceHandled:bindingAdaptor:" 
"_objectDidTriggerAction:bindingAdaptor:" 
"_validateAndCommitValueInEditor:editingIsEnding:errorUserInterfaceHandled:bindingAdaptor:" 
"addBinder:" 
"binders" 
"boundOutlineView:isItemExpandable:" 
"browser:createRowsForColumn:inMatrix:"
"browser:selectRow:inColumn:"
"browser:willDisplayCell:atRow:column:"
"collectionView:didChangeToSelectionIndexes:" 
"contentBinder"
"controller:didChangeToFilterPredicate:"
"controller:didChangeToSelectionIndexPaths:"
"controller:didChangeToSelectionIndexes:"
"controller:didChangeToSortDescriptors:" 
"dealloc" 
"defaultSortDescriptorPrototypeForTableColumn:" 
"drawer:didChangeToState:" 
"editableBinder" 
"editorDidBeginEditing:" 
"editorDidEndEditing:" 
"enabledStateForMenuItem:" 
"finalize" 
"handleValidationError:description:inEditor:errorUserInterfaceHandled:" 
"init" 
"numberOfRowsInTableView:" 
"objectDidTriggerAction:" 
"objectDidTriggerDoubleClickAction:" 
"outlineColumn:willDisplayCell:row:" 
"outlineColumn:willDisplayOutlineCell:row:" 
"outlineView:child:ofItem:" 
"outlineView:didExpandItem:" 
"outlineView:numberOfChildrenOfItem:" 
"outlineView:willCollapseItem:" 
"outlineView:willDisplayCell:forTableColumn:row:"
"outlineView:willDisplayOutlineCell:forTableColumn:row:" 
"referenceBinder" 
"removeBinder:" 
"searchFieldCellOrControlDidClearRecents:" 
"searchMenuTemplate" 
"selectionMechanismWasDismissed:" 
"tabView:didSelectTabViewItem:"
"tableColumn:didChangeToWidth:"
"tableColumn:willDisplayCell:row:"
"tableView:didChangeToSelectedRowIndexes:"
"tableView:didChangeToSortDescriptors:"
"tableView:shouldEditTableColumn:row:" 
"tableView:updateVisibleRowInformation:"
"tableView:willDisplayCell:forTableColumn:row:"
"updateInvalidatedFont:forObject:"
"updateInvalidatedObjectValue:forObject:"
"updateInvalidatedTextColor:forObject:"
"validateAndCommitValueInEditor:editingIsEnding:errorUserInterfaceHandled:"
"window:didChangeToVisibleState:" 
"windowDidResize:")
%

Ojojoj. (För att nush ska skriva ut resultatet fint har vi förvandlat returvärdet från typen NSArray till en lista.)
Det som, för mig, speciellt sticker ut är

tableColumn:willDisplayCell:row:
tableView:didChangeToSelectedRowIndexes: tableView:didChangeToSortDescriptors: 
tableView:shouldEditTableColumn:row: 
tableView:updateVisibleRowInformation:
tableView:willDisplayCell:forTableColumn:row:

Som verkar indikera på att _NSBindingAdaptor kan agera som NSTableViews delegat, och på så sätt förse “bindningsmekaniken” med information om vad själva tabellkontrollen (vyn i MVC) håller på med. Vidare…

"editorDidBeginEditing:" 
"editorDidEndEditing:" 

verkar indikera på att _NSBindingAdaptor kan ta en roll i koordination av vyer som editerar samma objekt, (se NSEditorRegistration). Notera den lilla skillnaden att metoderna ovan börjar med editorDid... medan de i det publicerade NSEditorRegistration-protokollet börjar på objectDid....

Adapterns instansvariabler

Nu slår vi

% ((ba instanceVariableNames) list)
("_binders" "_contentBinder" "_editableBinder" "_referenceBinder")
% 

Aha… till varje bundet objekt (i vårt fall y) krokas en bindningsadapter på som i sin tur håller ordning på 4 saker. Getters finns, men setters är inte helt lätt att finna ur den långa listan ovan:

  1. _binders hör antagligen ihop med getter binders och metoden addBinder:.
  2. _contentBinder hör nog ihop med gettern contentBinder.
  3. _editableBinder hör nog ihop med gettern editableBinder
  4. _referenceBinder hör nog ihop med gettern referenceBinder

Cirkeln sluts

I vårt fall, med objektet y bundit mot x, så är alla de tre sista tomma (nil) men den första pekar på ett nytt odokumenterat objekt, på adressen 0x3655f0:

% ((ba binders) list)
(<NSObjectParameterBinder: 0x3655f0>{object: <TTCrap: 0x3435f0>, bindings: attr=val})
%
% (set binderOne ((ba binders) objectAtIndex:0))
<NSObjectParameterBinder:3655f0>
% (binderOne description)
"<NSObjectParameterBinder: 0x3655f0>{object: <TTCrap: 0x3435f0>, bindings: attr=val}"
% (binderOne superclass)
NSBinder
%

En binder av typen NSObjectParameterBinder håller reda på att vi har knutit ihop attributen attr och val. Detta är en av de 13 vi tidigare sett via F-scripts klassbrowser

Arbetshypotes

Jag tror att varje objekt som binds mot ett annat får en bindningsadaptor tillkopplad. Sedan, för varje bindning som görs läggs ett binder-objekt (subklassad från NSBinder) till i listan ovan. För objekt som NSTableView som har många bindingsmöjligheter (content, isEditable etc) kan det alltså kopplas till många NSBinders. Just nu ser min skiss ut så här:

Det är osäkerheter i bilden ovan.
Nu stoppar vi här. Andas. Nu ska jag dyka in i vad dessa NSBinders gör! Kanske en senare artikel.

,

---

_NSBindingAdaptor: Under ytan i binderträsket, del 1

[editerad 081023, förtydligat exemplet som visar att dataflöde baklänges inte fungerar]
Jag har inte kunnat lämna den odokumenterade NSBinderklassen och klassklustret runt om denna, utan sökt vidare. I denna artikel spanar vi speciellt på _NSBindingAdaptor. I analysen används fantastiska Nu . Lisp & Cocoa i ett…

Ola Bäckström 9 juli 2008

F-script är ju väldigt polerat och lättillgängligt – bara att hämta binären och köra. Nu däremot kräver att man hämtar hem och bygger själv. Tröskeln känns högre… men när man väl lyckats med det och startar nush (den fristående Nu-tolken) skingras mörkret. En välbryggd Cocoa-Lispinterpreter står beredd att hjälpa oss i ned under ytan, bortanför Apple-dokumenterat område.

Ånej ännu ett språk?

Har man den minsta lispbakgrund och god kännedom om Obj-C klarar man snabbt av den nya syntaxen. Här är ett exempel:

(class TTWrap is NSObject
	(ivar (id) val)
	(ivar-accessors))

(class TTCrap is NSObject
	(ivar (id) attr)
	(ivar-accessors))

Koden ovan skapar två nya klasser TTWrap och TTCrap med varsin attribut (instansvariabel), kallad val och attr. Eftersom vi ber snällt att få accessorer fixade är vi klara. Enklare än Obj-C!

Sedan behöver vi två instanser, en av varje klass:

(set x (TTWrap new))
(set y (TTCrap new))

Varje rad kan jämföras med x=[TTWrap new]; i Obj-C, där x är en global variabel av typen id.

Sedan sätter vi attributen via de automatgenererade setterna:

(x setVal:"apa")
(y setAttr:"traktor")

detta är liknar ju vanlig Obj-C något oerhört. För att dubbelkolla att vi gör rätt printar vi ut värdet av y:

% y
<TTCrap:3435f0>
%

(procenttecknet är nushprompten)
Öhh? jaja… vi fick ut att y ligger på addressen 0x3435f0 och är av klassen TTCrap. Bra, men värdet på attributen?

% (y attr)
apa
%

Finfint! Nu kan vi gå vidare.

Hur kommer _NSBinderAdapter in i bildern?

Nu när vi har två objekt kan vi binda ihop dem:

(y bind:"attr" toObject:x withKeyPath:"val" options:nil) 

Vanlig binding alltså. Vi knyter ihop x.val med y.attr. Vi kollar att bindningen fungerar

% (y attr)
apa
% (x setVal:"gorilla")
% (y attr)
gorilla
% 

y.attr har plötsligt fått samma värde som x.val, och vidare uppdateras y.attr av nya värden på x.val. Precis som det ska.

Dataflöde baklänges fungerar inte:

% (y setAttr:"nytt")
% (y attr)
nytt
% (x val)
"gorilla"
% 

Denna enkelrättade bindning gäller för oss när vi bundit två attribut med AppKit standard-bindningar.

Man nu börjar äventyret. Slå

% (y _bindingAdaptor)
<_NSBindingAdaptor:34c590>
%

Va? har y en bindnings-vadå? Har x det också?

% (x _bindingAdaptor)
()
% 

Nej (en tom lista är lisps/Nus sätt att säga nil).

Vad som skett är att y fått en bindningsadapter. En spännande grej, vi ska spana mer på den, men först ska vi bara kolla upp KVO-kunnskapen. (Du minns väl att KVB bygger ovanpå KVO?)

Finns KVO-proxyobjekt på riktigt?

Ett proxyobjekt borde ha poppat in och ersatt det observerade objektet, x alltså:

% x
<NSKVONotifying_TTWrap:34c870>
% y
<TTCrap:3435f0>
% 

Riktigt! Via isa-swappning har x plötsligt blivit av klassen NSKVONotifying_TTWrap. Spännande, och intressant är att ber man detta objekt om vilken klass den är av:

% (x class)
TTWrap
% 

Illusionen är total: proxyklassen skickar vidare nästan allt till den vanliga TTWrap klassen.

,

---

Mysteriösa NSBinder

I mitt grävande i Cocotron hittade jag referenser till några intressanta klasser, där alla ärver från _NSBinder . Det visar sig att i vanliga Apple-AppKit finns en klass kallad NSBinder som är odokumenterad. Vad är detta?

Ola Bäckström 25 juni 2008

Bakgrund

Den här korta artikeln skrapar lite på ytan runt NSBinder. Det kan vara intressant när man vill göra egna GUI-kontroller som är “bindings-enablade”. Saken är den att typiska operationer man skulle vilja göra för att hålla sitt UI-objekt synkat både upp- och nedströms längs knytningen är implementerade i NSBinder (och dess olika subtyper).

“Fyndet”

Friskrivning: naturligtvis är inte jag den förste som funnit NSBinder. Men det verkar vara få (=0?) direkta referenser med information på nätet.

Själva upptakten till fyndet var som sagt grävandet i Cocotron. Hittade dessa 3 klasser

_NSBinder
_NSKVOBinder
_NSMultipleValueBinder

Dessa skapas och används i kontroller för att sköta datatransporten som är involverad i en “binding”. Både i den riktning man automatiskt får när man anropar “bind:toObject:withKeyPath:options:” (nedströms) men också i motsatt riktning (uppströms).
Det är uppenbart att gänget runt Cocotron redan funnit ut vad Apples egen NSBinder gör, men Cocotron-implementationen är dock inte en exakt spegel av Apples varianter. Däremot är funktionen för det informella protokollet BindingSupport den samma. Det är ju ett publicerat API, som då följdaktligen måste implementeras till punkt och pricka om man ska göra en klon av Cocoa.

Jag drog sonika upp F-scripts Klassbrowser , och där letade jag efter klasser med binder i sig. Mycket riktigt:

I denna klassbrowser ser man att NSBinder ärver direkt från NSObject och vidare att ett helt gäng klasser i sin tur ärver från NSBinder.

Browern visar vilka klassmetoder (meta) methods som finns till varje klass, men den listar inte de vanliga metoderna som agerar på objekt (av den klassen), tyvärr. Jag kan dock inte F-script tillräckligt för att bedöma om jag enkelt kan lista alla metoder.

Används dessa hemliga klasser?

Ja, om man googlar med sökorden “NSBinder” och “cocoa” finner man en massa stackdumpar. Men är det någon (utanför Apple) som direkt, med egen vilja använder dem i sina egna kontrollklasser… Om jag bara visste!

,

Kommentera

---

Analys av Cocotrons NSTableView implementation

En fortsättning på min tidigare förvirrade Samlingsvyartikel . Målet är att begripa hur en hemgjord samlingsvy (alltså en vy som presenterar och tillåter editering av många likartade objekt) kan göras. Typexemplet är NSTableView.

Har funnit god inspiration hos Cocotron . Där i källkoden hittar man alla (?) AppKit-klasser implementerade av Christopher Lloyds mfl.

Så här följer djupanalys av vad NSTableView gör i Cocotrons tappning. Notera att detta kräver en god Cocoa-kunskap för att följa.

Ola Bäckström 25 juni 2008

Bakgrund

En NSTableView ärver från NSControl. Det är alltså en NSView som kan hantera input från användaren (och naturligtvis presentera output). De flesta standard-kontroller lämnar över delar av ansvaret till objekt av typen NSActionCell. Dessa celler kan presentera information (alltså “rita” via drawWithFrame:inView:) men också lagra information (se NCCells metoder setObjectValue:, setStringValue:, setDoubleValue: etc).

Men hur snacket mellan kontrollen och actioncellen ser ut är inte särskilt tydligt beskrivet i Apples enda (?) sida om detta . Det som står där gäller inte helt riktigt för NSTableView heller, utan passar bättre på enklare klasser som NSButtonView.

Så om du vill veta mer … häng med!

Open source alternativet Cocotron

Cocotronprojektets uttalade mål är:

…is to provide an easy to use cross-platform solution for Objective-C development. In particular, source code level compatibility with … Apple’s frameworks…

Så trots att det är klasser avsedda att köras på icke-OS X-maskiner (Windows eller Linux) så är det intressant att läsa källkoden. Apples egen hemliga AppKit-källkod kan inte avvika väldigt mycket, eftersom klassernas uppförsel är tänkt vara de samma. Kan nämna att källkoden ser ut att vara i bra skick och att det måste ha varit ett hästjobb att i princip skriva om hela Cocoa från början.

NSTableView, intressanta delar i implementationen

Det jag speciellt vill veta är detta:

  1. Ansvarsfördelning NSControl mot NSActionCell när det gäller hantering av användarinput.
  2. På vilket sätt bindings kommer in i bilden. Är NSActionCell involverat över huvud taget?

Det som vi inte får fram i denna dissektion är hur IB-paletter fungerar och inte heller någonting om NSEditor. Det får bli en senare fråga.
Bindings kommer vi bara delvis in på, men jag kan svara redan nu att NSActionCell har inget direkt med bindings att göra. Däremot håller objekt av klassen NSTableColumn reda på bindingarna, medan själva NSTableView ser till att dataflödet uppströms fungerar, mha KVC.

Till saken, nu spanar vi in i Cocotrons implementation av NSTableView.m :

Snabböversikt

Den stora majoriteten av metoderna är setters/getters för alla de olika attributen (eller properties, på obj-c-språk), exempel är rowHeight och intercellSpacing . Inget konstigt där.

Sedan finns några enkla hjälpfunktioner för att lokalisera rader/kolumner/celler i vyn. Exempel är

rectOfRow:
rectOfColumn:
rowAtPoint:
frameOfCellAtColumn:
rowsInRect:

de gör det man skulle tro de gör. Bra med tydliga metodnamn!

Det finns ett antal getters/setters för datakälla (dataSource) och delegat, verkar ganska rättframt, på några ställen kollas om datakällan verkligen svarar på tilltal. Noteras är att klassen inte kräver att man bara väljer en metod att förse den med tabellinformation: det är fullt möjligt att både ange datasource och via bindings knyta den till en NSArrayController. Det verkar dock som databinding har prioritet över datakälla.

Scenario: Användare klickar på något inne i vyn.

Eftersom vi ärver in från NSResponder (indirekt via NSControl) så kommer ett musklick att generera ett anrop till mouseDown:. Implementationen här är intressant; jag listar en pseudo-kod baserat på den riktiga källkoden:

-(void)mouseDown:(NSEvent *)event {
    Finn aktuell rad och kolumn som ligger muspekaren befinner sig på.
    Finn vilken cell, låt oss kalla den clickedCell, som ligger på denna rad och kolumn. 
    OM ( clickedCell ärver från NSButtonCell )
        Se till att clickedCell har rätt värde enl. ev. datakälla.
        Tala om för clickedCell att den blev tryckt på.
        Informera dataKällan (dataSource) om nytt cell-värde.  // men binding då? går det inte knyta en knapp i Cocotron?
        Hissa NeedDisply-flaggan för vyn.
    ANNARS
        OM ( det är ett enkelklick )
			Långt tjorvigt block som reder ut om det påverkar markeringen (bla beroende på ALT- eller SHIFT-knappar).
			Långt tjorvigt block som reder ut om det är en början till drag&drop.
			Skicka target-action.
		OM ( det är ett dubbelklick )
			OM ( datakällan eller ev. binding tillåter editering)
				Påbörja celleditering genom att anropa egna metoden editColumn:row:withEvent:select: 
			ANNARS
        		Skicka target-doubleAction.
}

Koden här är i verkligheten än mer grisig, men den ska alltså separera på ett antal olika operationer, knapptryckning, markering och drag&drop samt slutligen editering av cellinnehåll. Notera att i fallen drag&drop och editering så slutförs inte operationen här: NSTableView håller reda på att en av dessa operationer har påbörjats.

Jag misstänker att det saknas lite kod runt knapptryckning som har med databinding att göra: det verkar inte vara möjligt att knyta upp en knapp.

Anta att användaren dubbelklickat, för att editera en cell, då blir innehållet i editColumn:row:withEvent:select:
en rackarns viktig funktion. Denna anropas internt från 2 andra platser, nämligen

  1. mouseDown:, som vi tittat på ovan. Det gäller
  2. textDidEndEditing:. Aha, Eureka, här kommer Field editor in i bilden. Den här metoden anropas av fälteditorn när en celleditering är färdig. Lite beroende på konfiguration så kan en ny celleditering påbörjas. Trycker användaren på TAB så ska cellen till höger påbörjas att editeras.

Men hur kan fälteditorn anropa något i ett NSTableView-objekt? Jo, vid varje texteditering så riggas en delegat till fälteditorn -och i det här fallet sätts då själva NSTableView-objektet. Notera att det inte är den editerade cellen som är delegat!

Scenario: Editering av textfält påbörjas

Nu behöver vi grotta i editColumn:row:withEvent:select:


-(void)editColumn:(int)column row:(int)row withEvent:(NSEvent *)event select:(BOOL)select {

	Ett litet stycke som verifierar att: 
		1. kolumn tillåter editering
		2. delegat tillåter editering
		3. datakälla (dataSource) tillåter editering ELLER databinding är read-write

	// nedan börjar allvaret...
	Kopiera (via NSCopying) den aktuella cellen, kalla denna nya cell för editingCell 
	OM ( editingCell ärver från NSTextFieldCell )    
		Justera lite attribut på editingCell, för att se till att den ritar exakt över den ursprungliga cellen.
		Se till att editingCell har rätt värde enl. ev. datakälla.
		Se till att editingCell har rätt värde enl. ev. databinding.
		Begär att få tag på fönstrets fält-editor.
		Be editingCell att konfigurera fält-editorn.   // via NSCells metod setUpFieldEditorAttributes:		 
		OM (select-flaggan hissad)
		 	anropa editingCells	selectWithFrame:inView:editor:delegate:start:length: 
		ANNARS
			anropa editingCells editWithFrame:inView:editor:delegate:event:

	Hissa NeedDisply-flaggan för vyn.
}   

Här tror jag Cocotron har genat lite, de tillåter alltså bara texteditering om tabellen består av NSTextFieldCell. Men i AppKit kan man stoppa in egna celler.

aha…tag ett djupt andetag nu… som du ser anropas editWithFrame:... eller selectWithFrame:... på cellen, dessa anropar i sin tur en privat/hemlig metod _setupFieldEditorWithFrame:controlView:editor:delegate: ärvt från NSCell. Det är den metoden som gör de viktiga anropen för att “sätta i gång” fälteditorn:

[view addSubview:editor];
[[view window] makeFirstResponder:editor];
[editor setDelegate:delegate];

notera att mer än ovanstående görs; saker som har med scrollbarhet (NSClipView) att göra.

OJOJ. Jag tror jag måste göra en figur baserat på detta. Kanske ett lämpligt ämne för en kommande artikel?

,

Kommentera

---

« Äldre