Programování pro iOS - 7. Správa paměti a starý restík - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Soutěž

Sponzorem soutěže je:

IDIF

 

Odkud pochází fotografka Anne Erhard?

V dnešní soutěži hrajeme o:

Seriály

Více seriálů



Software

Programování pro iOS - 7. Správa paměti a starý restík

15. září 2010, 00.00 | Základní funkcionalitu naší bouřkové aplikace máme hotovou. Ještě však pořád zbývá doplnit několik věcí. Minule jsme se naučili ukládat a načítat data. Dnes si ukážeme další standardní paměťovou techniku iOSu. A doplníme také jeden malý restík.

Operační systém iOS sice podporuje virtualizaci adres, takže každá aplikace běží ve svém vlastním adresovém prostoru, nemá ale virtualizaci paměti. Všechny aplikace se tedy musí vejít do reálné paměti, swapování momentálně nepoužívaných stránek na disk není k dispozici.

Proto je třeba pamětí šetřit, a na požádání dokonce uvolnit i tu, již aplikace sice používá, ale jejíž obsah může poměrně snadno znovu vygenerovat (připomeňme protokol NSDiscardableContent a třídu NSCache, které jsou ovšem v iOSu k dispozici až od verze 4 nahoru). Ačkoli u naší aplikace nejde o významné množství paměti, uvolňování implementujeme z cvičných důvodů.

Obvykle aplikace v iOS uvolňují paměť na základě standardní zprávy applicationDidReceiveMemoryWarning: v delegátu aplikace, případně didReceiveMemoryWarning na úrovni řídicích objektů rámců (odpovídající metody ostatně nalezneme i v projektu – aplikace Xcode je tam uložila při jeho vytváření na základě projektového vzoru). V našem případě se to ale zrovna nehodí, protože správu "paměti" máme zabalenou do samostatného zástupného objektu. Jistě, mohli bychom u něj doplnit službu purgeMemory a tu volat z řídicího objektu; tím bychom ale přišli o konsistenci rozhraní se standardní třídou NSMutableArray. Využijeme proto alternativní možnosti, standardních notifikací (co to je?).

Je to velmi jednoduché: dvě metody ve třídě OCSAutosavedMutableArray velmi mírně modifikujeme a třetí přidáme, to je vše:

// OCSAutosavedMutableArray.m
...
+(NSMutableArray*)array {
  NSMutableArray *new=
    (NSMutableArray*)[[self alloc] autorelease];
  [[NSNotificationCenter defaultCenter]
    addObserver:new
    selector:@selector(memoryWarning)
    name:UIApplicationDidReceiveMemoryWarningNotification
    object:nil];
  return new;
}
-(void)memoryWarning {
    [contents release]; contents=nil;
}
-(void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [contents release];
    [super dealloc];
}
...

Jde sem nebo se vzdaluje?

Už dávno jsme si slíbili, že paměti měření využijeme pro ohlášení toho, zda se bouřka blíží nebo vzdaluje, ale dosud jsme se k tomu nějak nedostali; doplníme proto jen v rychlosti a jednoduše tuto službu nyní.

Nejprve přidáme "outlet" pro další textové pole:

// MainViewController.h
...
@interface MainViewController:UIViewController
  <FlipsideViewControllerDelegate> {
    IBOutlet UILabel *range,*speed;
}
...

a pak uložíme odpovídající objekt do hlavního rámce v Interface Builderu a s "outletem" jej propojíme pomocí "drátu"; to už umíme dávno.

Pak implementujeme patřičný kód do metody rangeButtonTapped:

// MainViewController.m
...
-(IBAction)rangeButtonTapped {
  ...
  range.text=[NSString stringWithFormat:@"%.1 f km",dist/1000.];
  if (self.history.count>0) {
    NSDictionary *d=[self.history lastObject];
    NSTimeInterval prev=[[d objectForKey:@"timestamp"]       timeIntervalSinceReferenceDate];
    if ([NSDate timeIntervalSinceReferenceDate]-prev>2/*30*60*/)
      speed.text=@"";
    else {
      double pdist=[[d objectForKey:@"dist"] doubleValue];
      speed.text=pdist>dist?
        @"Bouře se blíží!":
        @"Bouře se vzdaluje...";
      }
    }
  }
  [self.history addObject:[NSDictionary     dictionaryWithObjectsAndKeys:
  ...
}
...

Je asi zřejmé, proč omezujeme výpočet pouze na měření v rozsahu půl hodiny: nemělo by valný smysl srovnávat měření den nebo ještě více vzdálená, jež se celkem evidentně budou týkat každé jiné bouřky.

Mimochodem, je asi zřejmé, že zde by toho bylo hodně co vylepšovat: předně, měli bychom vyhodnotit paměť měření hned po spuštění aplikace, aby se aktuální informace o rychlosti bouře neztratila při jejím ukončení a novém spuštění. Naopak bychom po uplynutí nějakého času mohli informaci – která již zřejmě nebude aktuální – zrušit. Měli bychom rozhodně také spočítat a zobrazit konkrétní rychlost, jakou se vůči pozorovateli bouřka pohybuje. Je-li tato rychlost hodně malá, měli bychom informovat, že bouře zůstává na místě, ani se nevzdaluje, ani se neblíží.

Mohli bychom také měnit grafické pozadí rámce podle toho, zda se bouře blíží nebo vzdaluje a jak rychle. Mohli bychom vyhodnotit více měření než jen to poslední a počítat průměrnou rychlost a také to, zda se pohyb celkově zrychluje nebo zpomaluje. Mohli bychom z těchto údajů a z aktuální naposledy spočtené vzdálenosti vyhodnotit, za jak dlouho bude bouřka přímo nad námi, a uživatele o tom informovat... vynalézavosti se meze nekladou, a cokoli z toho (nebo všechno najednou) si můžete implementovat za domácí úkol, chcete-li – jediné, co není zcela triviální, je změna grafického pozadí rámce. Přijdete na to sami dříve, než si v příštím dílu ukážeme, jak to nejsnáze řešit?

Nač máme ten druhý rámec?

Hned na samém začátku jsme zvolili projektový vzor "Utility Application"; v něm je vedle hlavního rámce ještě jeden – "FlipsideView" –, připravený pro nastavení a další informace o aplikaci. Rámec sám a jeho přepínání tam a zpět funguje "automaticky"; k tomu nám odpovídající kód vygeneroval hned na samém začátku projektový vzor, takže zde máme práci uspořenou; informace a nastavení ale musíme doplnit sami.

Stran informací si ovšem do rámce může napsat kdokoli cokoli – prostě tam vložíme UILabel a rovnou do něj napíšeme "Moje subgeniální bouřkoměrná aplikace, Copyright © MujMac, bla bla bla..."

S nastavením to zdaleka není tak jednoduché. Ba naopak; bohužel, znovu zde narážíme na to, že iOS nepodporuje objektové vazby "bindings", a proto je právě implementace takovýchto nastavení poměrně úmorná a ne zcela triviální.

Poněkud lepší to je u komplikovanějších aplikací, jež nastavení nemají přímo v jednom z vlastních rámců, ale úplně mimo aplikaci, v samostatném bloku předvoleb uvnitř aplikace Settings; tomu se ale budeme věnovat až mnohem později.

Pojďme si do pomocného rámce uložit dvojici předvoleb:

• výběr mezi metrickým a imperiálním zobrazením jednotek; k tomu použijeme třeba přepínač UISegmentedControl, jakési "mnohotlačítko", jež má jednu variantu vybranou a ostatní neaktivní (známe je třeba z aplikace Calendar jako přepínač mezi režimy seznamu, denním kalendářem a měsíčním přehledem);

• pro nastavení kapacity historie měření použijeme UIPickerView – to jsou ta "točítka", jejichž prostřednictvím se vkládají kupříkladu časy a data. Pokud bychom použili obyčejné textové pole, bylo by to snazší; pro volbu "pickeru" ale máme dva dobré důvody: (a) jeho použití je daleko pohodlnější pro uživatele, ale hlavně (b) se naučíme s ním pracovat. Jak uvidíme, vůbec to není triviální!

Nastavení jednotek

Nemáme-li "bindings", musíme každý ovladač pro nějakou předvolbu implementovat zhruba týmž způsobem:

• na samém začátku, když se rámec inicializuje, načteme aktuální hodnotu předvolby programově pomocí služeb třídy NSUserDefaults (se kterou jsme se důvěrně seznámili již v Mac OS X, a v iOS funguje stejně) – a pozměníme ovladač tak, aby ji zobrazoval. Vždy tedy musíme mít "outlet", jehož prostřednictvím se k ovladači dostaneme;

• kdykoli se obsah ovladače změní, musíme na to v kódu zareagovat, a předvolby opět prostřednictvím třídy NSUserDefaults změnit. Zde záleží na tom, jak který konkrétní ovladač přesně funguje; některé po změně posílají "akci" (pro nás právě takovým příkladem bude UISegmentedControl), jiné informují o změnách prostřednictvím služeb delegáta (to, jak uvidíme za chvilku, je právě UIPickerView).

Pro práci s "mnohotlačítkem" tedy potřebujeme jeden "outlet" a jednu "akci"; oba přidáme do hlavičkového souboru, který definuje rozhraní řídicího objektu sekundárního rámce:

// FlipsideViewController.h
...
@interface FlipsideViewController:UIViewController {
    IBOutlet UISegmentedControl *units;
}
-(IBAction)unitsChanged;
...

Otevřeme objektovou síť tohoto rámce – tedy soubor "FlipsideView.xib" – v Interface Builderu. Do jejího rámce ("View") uložíme pomocí myši "Segmented Control" z "Library / Inputs & Values", upravíme vhodně texty v jeho tlačítkách a případně další atributy, a propojíme jej s kódem – to už by nyní pro nás dávno měla být stará vesta. Vypadat by to mohlo kupříkladu nějak takto:

přičemž návodný text "Používat jednotky" je samozřejmě praobyčejný UILabel, který jsme do rámce přidali, aby se v grafickém uživatelském rozhraní uživatel snáze orientoval.

Zbývá napsat odpovídající kód. Jak již víme, v řídicím objektu FlipsideViewController to budou dva prvky: nastavení po inicializaci rámce a uložení do předvoleb po změně. Kdo snad neznáte API třídy NSUserDefaults, nejprve se na ně podívejte – a pak už bude kód zcela zřejmý, včetně toho, že předvolbu ukládáme jako "ImperialUnits" s možností booleovského nastavení ano/ne:

// FlipsideViewController.m
...
-(void)viewDidLoad {
  [super viewDidLoad];
  units.selectedSegmentIndex=
    [[NSUserDefaults standardUserDefaults]
      boolForKey:@"ImperialUnits"]?1:0;
}
...
-(IBAction)unitsChanged {
  [[NSUserDefaults standardUserDefaults]
    setBool:units.selectedSegmentIndex==1
    forKey:@"ImperialUnits"];
}
...

To ale není vše: tím jsme sice korektně implementovali možnost nastavení předvolby; ale ještě se podle ní také musíme řídit. Jinými slovy, kdekoli, kde zobrazujeme vzdálenost (případně rychlost, pokud jste si již doplnili výše navržené zobrazení toho, jak rychle se bouře blíží nebo vzdaluje), musíme vzít v potaz nastavení předvolby "ImperialUnits", a informaci zobrazit odpovídajícím způsobem.

Vzhledem k tomu, že se tak děje na více místech a dokonce ve dvou různých zdrojových souborech – vzdálenost zobrazuje jak hlavní rámec, tak i historie měření –, vyplatí se to sjednotit. V plnohodnotné produkční aplikaci bychom si asi pro takovéto pomocné služby vytvořili nový zdrojový soubor; zde si tu práci ušetříme, a službu poněkud nečistě přilepíme k řídicímu objektu hlavního rámce:

// MainViewController.h
...
@interface MainViewController:UIViewController {
...
@end

NSString *OCSLengthWithUnits(double metres);
...

Mimochodem: proč funkce a ne metoda? Neexistuje žádné jednoznačné kritérium, jež by v takovýchto případech preferovalo to či ono; metoda by ale byla ještě pevněji navázaná ke třídě MainViewController, s níž má nová funkčnost jen málo co dělat. Proto je zde asi funkce malinko lepší a čistší – stejně jako by byla nová třída, speciální jen pro tento účel; tu se nám ale pro takovou drobnost nevyplatí dělat.

Uvnitř implementace řídicího objektu si ukážeme zároveň obojí: jak obsah funkce, tak i její použití:

//  MainViewController.m
...
NSString *OCSLengthWithUnits(double metres) {
  if ([[NSUserDefaults standardUserDefaults]
    boolForKey:@"ImperialUnits"])
    return [NSString
      stringWithFormat:@"%.1f mi",metres/1609.344];
  return [NSString
    stringWithFormat:@"%.1f km",metres/1000.];
}

@implementation MainViewController
...
-(IBAction)rangeButtonTapped {
  ...
  range.text=OCSLengthWithUnits(dist);
  ...
}
...

Podobně triviální bude použití i na druhém místě:

// HistoryViewController.m
...
-(UITableViewCell*)tableView:(UITableView*)tableView
  cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  ...
  NSDictionary *d=[delegate.history
    objectAtIndex:indexPath.row];
  cell.textLabel.text=
    OCSLengthWithUnits([[d objectForKey:@"dist"] doubleValue]);
  ...
}
...

Kapacita historie a práce s UIPickerem

S kapacitou historie to bude horší; jak hned uvidíme, s ovládáním třídy UIPicker je spousta práce. Nejprve si do rozhraní přidáme odpovídající "outlet"; navíc připojíme odkaz na oba protokoly, jež nadále musí řídicí objekt sekundárního rámce implementovat – datový zdroj a delegát "pickeru":

// FlipsideViewController.h
...
@interface FlipsideViewController:UIViewController
  <UIPickerViewDelegate, UIPickerViewDataSource> {
    IBOutlet UISegmentedControl *units;
    IBOutlet UIPickerView *capacity;
}
...

Novou "akci" ovšem nepotřebujeme, jelikož UIPickerView informuje o změnách prostřednictvím metod delegáta.

Pak v Interface Builderu "picker" do rámce uložíme – nalezneme jej pro změnu ve skupině "Data Views" okna "Library" – a propojíme s kódem. Tentokrát musíme natáhnout trojici "drátů": outlet, datový zdroj a delegáta:

Programování nás čeká poněkud více než minule; především se totiž vůbec musíme postarat o to, aby "picker" měl dva sloupce, a v každém z nich aby se zobrazovaly číslice od 0 do 9 – to je pro zadávání kapacity historie, u níž nemají valný smysl hodnoty přesahující 100, asi ideální.

K tomu je třeba implementovat následující metody – odpovídající zprávy "picker" posílá při konfiguraci datovému zdroji a delegátovi, a na jejich základě se zobrazí jeho obsah:

// FlipsideViewController.m
...
-(NSInteger)numberOfComponentsInPickerView:
  (UIPickerView*)pickerView {
    return 2;
}
-(NSInteger)pickerView:(UIPickerView*)pickerView
  numberOfRowsInComponent:(NSInteger)component {
    return 10;
}
-(NSString*)pickerView:(UIPickerView*)pickerView
  titleForRow:(NSInteger)row
  forComponent:(NSInteger)component {
    return [NSString stringWithFormat:@"%d",row];
}
...

Teprve nyní na základě této konfigurace můžeme implementovat vlastní práci s předvolbou – nazveme ji HistoryCapacity a bude v ní číslo, určující kapacitu pole s uloženými měřeními:

//  FlipsideViewController.m
...
-(void)viewDidLoad {
  [super viewDidLoad];
  units.selectedSegmentIndex=
    [[NSUserDefaults standardUserDefaults]
      boolForKey:@"ImperialUnits"]?1:0;
  int hc=[[NSUserDefaults standardUserDefaults]
    integerForKey:@"HistoryCapacity"];
  [capacity selectRow:hc/10 inComponent:0 animated:NO];
  [capacity selectRow:hc%10 inComponent:1 animated:NO];
}
...
-(void)pickerView:(UIPickerView*)pv
  didSelectRow:(NSInteger)row
  inComponent:(NSInteger)component {
    [[NSUserDefaults standardUserDefaults]
      setInteger:[pv selectedRowInComponent:0]*10+
                 [pv selectedRowInComponent:1]
      forKey:@"HistoryCapacity"];
}
...

Zbývá opět použití předvolby v kódu hlavního rámce, kde omezujeme počet položek v paměti – to je samozřejmě triviální:

//  MainViewController.m
...
-(IBAction)rangeButtonTapped {
  ...
    int hc=[[NSUserDefaults standardUserDefaults]
      integerForKey:@"HistoryCapacity"];
    while (self.history.count>hc)
      [self.history removeObjectAtIndex:0];
  }
}
...

Ale pozor, není to vše! Uvědomme si, že po instalaci a prvém spuštění aplikace musí být počet položek v historii nenulový; někde tedy musíme určit výchozí hodnotu, která platí pokud ještě nikdy nikdo do předvoleb nezasáhl. K tomu slouží standardně tzv. registrační doména třídy NSUserDefaults; nejvhodnější metodou pro její nastavení je inicializace nějakého hodně "základního" objektu, nejspíše asi aplikačního delegáta:

//  iOS_3_ExampleAppDelegate.m
...
+(void)initialize {
  [[NSUserDefaults standardUserDefaults]
    registerDefaults:
      [NSDictionary dictionaryWithObjectsAndKeys:
        [NSNumber numberWithInt:50],@"HistoryCapacity",
    nil]];
}
@end

Tím máme aplikaci skoro hotovou; příště už ji dokončíme.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Tipy a Triky  

 » Rubriky  » Začínáme s  

 » Rubriky  » Software  

 

 

 

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: