Parcelable vs Serializable: Istina je negde tamo

Autor ovog članka je profesionalni softverski inženjer i nudi konsultantske usluge android razvoja i obuke. Više informacija ovde.

Do skora sam isključivo koristio java serijalizaciju kao mehanizam prenosa objekata između aktivnosti. Bio sam svestan Parcelable interfejsa i navodnih prednosti koje isti nudi ali nisam imao potrebu za njim jer nikad nisam iskusio realni pad performansi usled korišćenja java serijalizacije. Dok je sa jedne strane dovoljno samo obeležiti klasu Serializable interfejsom Parcelable ipak zahteva dosta “boilerplate” koda. Kada sam se nedavno uključio na projekat koji ne koristi java serijalizaciju već Parcelable mehanizam nije mi trebalo dugo da se naviknem jer: a) ovaj pristup je navodno za red veličine brži i b) postoji super biblioteka, Parceler, koja lišava programera potrebe da piše dodatni kod i sama genereše potrebne metode.

Upravo sam implementirao prvu veću funkcionalnost kada je počela da se javlja greška za koju ranije nisam čuo – aplikacija je pucala bacajući TransactionTooLargeException (ili samo uz log E: !!! FAILED BINDER TRANSACTION !!! na pre API 15 nivou). Brza google pretraga me je uvukla u svet RPCa i transakcionih bafera, svet koga sam bio blaženo nesvestan do tad. Saznao sam da postoji nešto što se zove “binder transaction buffer”, i još da ga dele sve transakcije koje se dešavaju u istom trenutku. Istina moj kod transferuje prilično veliki graf objekata kroz intent za startovanje druge aktivnosti, što možda nije najbolja ideja, ali nemoguće da udaram u limit od 1MB?

Šta se ovde dešava?

Prva stvar koju sam pokušao bila je da se vratim onome što znam, java serijalizaciji, i problemi su nestali kao rukom odnešeni. Onda sam se zapitao kolika je granica ovog mehanizma ali nisam uspeo da je dosegnem čak ni sa 100x većim grafom objekata. Još ranije sam primetio da kada se graf objekata pošalje putem Parcelable/Parceler kombinacije na drugoj strani nećete dobiti isti objekat sa više referenci ka njemu već kopije za svaku referencu. To možda nije iznenađujuće kad se malo razmisli ali je svako drugačije nego kada se koristi java serijalizacija.

Da bih mogao da utvrdim šta se dešava u malo kontrolisanijim uslovima kreirao sam malu test aplikaciju. Apk možete naći  ovde a kod ovde. tu ćete naći EmptyObject klasu, koja je, gle iznenađenja, prazna. Hteo sam da vidim koliko ovih beskorisnih stvarčica mogu da natrpam u ArrayList-u i pošaljem između dve aktivnosti kada korstim Parcelable/Parcelar a koliko kada koristim java serijalizaciju. Takodje sam kreirao klasu Package koja sadrži samo referencu ka ArrayList<EmptyObject> i ništa više. Staviću samo jednu instancu Package klase u intent i pokušati da startujem drugu aktivnost njome.

Rezultati eksperimenta

Parcelable/Parceler mehanizam može da prebaci paket od 4 045 EmptyObject instanci. Kada se paket ovim mehanizmom pretvori u byte[] rezultujući niz je velik 517 764 bajtova. Ako se doda još samo jedna instanca aplikacija puca bacajući TransactionTooLargeException, tako da ispada da je 517 892 previše. U isto vreme paket od 4 045 EO instanci ima samo 24 482 bajtova kada se serijalizuje u byte[] tako da se može preneti između dve aktivnosti bez problema. Granica java serijalizacije je na 86 276 EO instanci u paketu, to jest 517 928 bajtova. Jasno je da bez obzira na korišćenu tehniku ograničavajući faktor je količina bajtova koja može da se prenese. Na mom Moto X (2nd gen) telefonu sa Androidom verzije 5.0 ta granica je nešto manje od 512KB. Ova granica zavisi od uređaja: Na drugim uređajima sam video raspon od nešto boljih rezultata na Galaxy S5 and S4, koji mogu da prenesu par KB više, do skoro punog MB na HTC One M8. Izgleda da “runtime” okruženje ne pravi razliku (dalvik/ART). Jasno je da java serijalizacija kreira mnogo manje nizove bajtova nego kada se primeni Parcelable/Parceler kombinacija na isti graf objekata.

Nećete verovati šta se sledeće dogodilo

Možda se pitate zašto mi je trebala Package klasa, tj. zašto nisam pokušao direktno da zapakujem listu EO instanci. Baš to sam hteo da uradim da bih pojednostavio stvari ali ta izmena nije prošla lagano kao što sam očekivao. Odjednom nisam mogao više da pošaljem više od 86k objekata java serijalizacijom, nisam čak mogao da pošaljem ni bednih 4k koje su granica za Parcelable/Parceler. Najčudnije od svega, ako bih poslao listu od 10 referenci na isti objekat sa druge strane bi izronilo 10 različitih objekata! Nisam imao pojma šta se dešava.

Pošto ArrayList implementira Serializable interfejs, a u njoj su isto “serijalizabilni” EmptyObject objekti mislio sam da koristim Intent.putExtra(String name, Serializable value) varijantu putExtra metoda. I jesam, ali ona nije radila ono što sam ja očekivao. Ako pogledate kod ove metode videćete da ona sam prosleđuje poziv putSerializable(String key, Serializable value) metodi Bundle klase (BaseBundle počevši od API nivoa 21) a sve što ova metoda radi je da ubaci Serializable objekat u mapu objekata. Ova mapa će biti pretvorena u binarni zapis kasnije metodom writeArrayMapInternal(ArrayMap<String, Object> val) iz klase Parcel. Tu stvari postaju zanimljive. Ova metoda će proći mapom i nad svakim objektom pozvati writeValue(Object v) metodu, parče koda koje je ništa drugo do glorifikovan switch blok sa brdo instanceof provera i writeThis ili writeThat poziva. Naša lista nažalost neće biti serijalizovana writeSerializable(Serializable s) metodom već writeList(List val) metodom koja će serijalizovati svaki objekt u listi ponaosob. Eto, pozvali smo metodu očekujući da serijalizuje čitavu listu odjednom ali u realnosti će svaki objekat biti serijalizovan ponaosob što dovodi do neefikasnosti i potencijalnog gubljenja referenci…

Ok, ali Parcelable je i dalje 10x brži?

Ne znam, zato što nemam nameru da počnem da pišem čiste Parcelable klase. Ipak test aplikacija meri vreme od kreiranja intenta do raspakivanja sa druge strane, mada pomalo naivno. Moram priznati da nisam testirao neki veliki uzorak da bih mogao nešto da tvrdim ali izgleda da pobojlšanje od 10x ili 17x u odnosu na java serijalizaciju za ovaj scenario nije realno. Parcelable/Parceler mehanizam jeste brži, ali bliže 2x redu veličine. A ako zanemarimo prazne objekte i vratimo se na realni graf objekata sa početka ove priče stvar postaje dosta čudna. Ne samo da tu moram koristiti java serijalizaciju jer alternativa jednostavno puca, u stvari je i brže koristiti java serijalizaciju i to do 3x brže. Ovo sam potvrdio testirajući na 10+ uređaja, ne znam zašto i u ovom trenutku plašim se i da pitam.

Zaključak

Neću vam reći da zaboravite na druge i vratite se java serijalizaciji. Možda ja negde grešim, ili su jednostavno noviji android telefoni toliko brži  i bolji od starih da neki zaključci više ne važe. Moj savet vam je da ne verujete slepo u sve što pročitate na internetu i da testirate na svom konkretnom primeru. To će vam omogućiti da izvedete validne zaključke.

Podeli ovaj post preko...

Leave a Reply

Your email address will not be published. Required fields are marked *