Асинхронно зареждане на изображения в списъци - подходи и най-добри практики (част 2) SIC! софтуер

В последния си пост вече представих общи трудности във връзка с асинхронни процеси на зареждане в списъци. Използвайки прост сценарий, разработих сложността на този случай на употреба, без да навлизам в твърде много подробности.

изображения

От друга страна, бих искал да насоча тази статия в малко по-техническа посока - и по този начин да изпълня обещанието си да разкрия някои най-добри практики и трикове, доказали се в нашите проекти. Бих искал да покажа колко гладко превъртане на списъците може да се приложи относително лесно и да въведа някои полезни инструменти, които могат да ви помогнат. Между редовете ще разгледам няколко различни подхода, които обикновено се препоръчват, но са изпълнени с не съвсем незначителни проблеми.

Слабо изпълняващият се списък се проявява в поведение на скролиране или дори в продължително блокиране на цялото взаимодействие с потребителя. Причините за това са блокиране на извиквания към основната нишка, която понякога е отговорна за изчертаване на елементите на потребителския интерфейс или изпращане на събития. Тези обаждания не винаги трябва да се извършват изрично, но могат да бъдат и резултат от небрежно управление на паметта, както може да се види на Фигура 1. Със сигурност това стана много по-добро с въвеждането на Concurrent Garbage Collector в Android 2.3 - но все пак си струва да се създават обекти само когато това е наистина необходимо, тъй като това е известно като скъпа операция.

За да се проследят ненужно разпределените обекти, използването на Android Allocation Tracker се доказа, което е част от Android SDK - или по-точно инструмента Dalvik Debug Monitor Server (DDMS). Това позволява записването на всички поколения обекти в свободно избираем времеви прозорец.

Фигура 2 показва запис, направен по време на превъртане през списък. Маркираният запис казва, например, че 92-байтов (Размер на разпределение) BitmapFactory.Options обект (Разпределен клас) е създаден от работна нишка (Thread Id), докато изображението се зарежда от кеша (Разпределено в). Чрез съответното сортиране на отделните колони относително лесно могат да бъдат открити ненужни множество екземпляри.

Оптимизацията винаги има смисъл, ако кодов блок се извиква често или по-големи обекти като Буферът може да бъде създаден няколко пъти. В нашия случай трябва да се обърне специално внимание на метода getView () на адаптера за списък. Тъй като това така или иначе се извиква само от основната нишка, обектите могат да се държат като променливи-членове - и по този начин да се използват повторно. Шаблонът ViewHolder вече следва подобен принцип. В този контекст също трябва да се провери дали се използват по-ефективни структури от данни може: Примитивните типове данни обикновено се предпочитат пред съответните класове на обвивката. Специално внимание трябва да се обърне и при неявното и скъпо автоматично боксиране. Разработчикът обаче трябва да е запознат и с новодобавените структури от данни като разредения масив или кеша LRU (предлага се и като клас на съвместимост).

Още някои препоръки относно управлението на паметта:

За да може списъкът да се превърта плавно изобщо, трябва да се изобрази поне 25 кадъра в секунда. Това съответства на времеви прозорец от максимум 40 милисекунди на кадър. Казано по-просто, извикването на метода getView () на адаптера на списъка, включително всички операции по оформление и изобразяване на йерархията на изгледа, може да отнеме максимум 40 милисекунди, за да се възприеме като флуид. Всяко превишаване би се проявило в леко или дори започване на заекване.

На пръв поглед това изглежда като управляема задача за съвременните мобилни процесори. При по-внимателно разглеждане обаче човек бързо осъзнава, че този времеви прозорец е повече от тесен. Достъпът до родните функции, като например проста съществуваща () файлова операция, може да отнеме 25% (= 10ms) от това време. Следователно същото се отнася и за достъпа до база данни. Достъпът за четене и особено за запис до постоянното хранилище е бавен. В допълнение, времето за реакция на повикванията понякога варира значително. Същият достъп за запис във файловата система може да отнеме 20ms и 2 секунди. Поради тази причина е най-добрата практика да се възлагат такива операции на работни нишки. Строгият режим, който се осигурява от Android SDK, обръща много голямо внимание на спазването на това правило (при желание).

За проследяване на скъпи разговори в основната нишка, използването на Traceview се е доказало. Той също е част от Android SDK Tools.

Фигура 3 показва пример за откъс от проследяване, който е бил записан при превъртане през списък. Тук можете да видите, че основната нишка вече е освободена от част от работата от някои други нишки. Например, Thread-15 в момента чете данни от поток, докато основната нишка все още може да изпраща събития и по този начин остава отзивчива. Обажданията могат да бъдат допълнително подразделени според нуждите, така че да получите много точна картина коя нишка извършва коя операция по кое време или колко скъпи са в детайли.

Като цяло трябва да разгледате по-отблизо времето на основната нишка. Всички операции, които блокират това ненужно дълго, трябва да бъдат възложени на работещи нишки. За това, от една страна, са налични AsyncTasks, които вече ви освобождават от управлението на пул от нишки, както и от синхронизирането с основната нишка. Но също така многобройните имплементации на ExecutorService, които могат да бъдат създадени чрез клас Executors Factory, са много добра и адаптивна алтернатива.От друга страна, трябва да се въздържате от ръчно създаване и стартиране на нишки, тъй като това създава твърде високи режийни разходи и в най-лошия случай Падането може дори да доведе до срив (бързо прелистване на 10 000 записа в списъка).

Докато превъртате, голяма част от изчислителното време се изразходва за оформлението и рендирането на изгледа на списъка и неговите дъщерни изгледи. Тъй като това трябва да се направи от основната нишка поради модела с една нишка, тук също има значителен потенциал за оптимизация. За да разкрием това, обичаме да използваме Hirarchy Viewer, който също е част от Android SDK Tools.

Фигура 4 показва пример за дървовидната структура на ListView, всеки от елементите на който съдържа изображение и текст. В този изглед ненужни контейнери за оформление, както и скъпи операции за оформление и чертане могат да бъдат идентифицирани с помощта на цветните индикатори. Трябва да се отбележи обаче, че цветовете трябва да се разбират като относителни, а не като абсолютни стойности: Оформлението на рамката тук на снимката има за измерване на нейния размер (мярка), подреждане и подравняване (оформление) и рисуване на себе си Изгледите на деца (рисуване) имат червен индикатор, тъй като за това се изисква 100% от изчислителното време в родителския изглед - а не поради абсолютното време.

За да оптимизирате ефективността на собственото си оформление, трябва да се спазват следните основни правила:

  • Дървесната структура на йерархията на изгледите трябва да бъде възможно най-плоска. С всяко влагане, изчислителното време за мярка + оформление се увеличава значително.
  • RelativeLayout е по-мощен и по-ефективен от, например, вложени линейни оформления и винаги трябва да бъде предпочитан пред тях.
  • Ако размерът на изгледите вече е известен по време на компилиране, трябва да се предпочитат спецификации с фиксиран размер в независими от плътността пиксели (dip) пред wrap_content. Това ускорява до известна степен процеса на измерване.
  • Колкото по-малко изгледи се използват, толкова по-добре. Често е достатъчно да зададете едно или повече съставни чертежи за TextView, вместо да използвате дълбоко вложено оформление!

За да завършите цялостния външен вид на плавно скролиращ списък, препоръчваме избледняване и анимиране на асинхронно задействани ImageViews или ProgressViews. Цялото взаимодействие с потребителя изглежда по-гладко поради по-меките преходи. Кодовият фрагмент по-долу показва как това може да се направи много лесно:

Рамката предвижда notifyDataSetChanged на адаптера да бъде извикан, когато се правят промени в данните (включително, например, когато е заредено изображение). Това им сигнализира, че ще прекроят целия списък в неопределен момент от време в бъдеще. Въпреки че този подход по принцип не е неприятен, обикновено вървим по различен начин: Обратното повикване се предава на асинхронния процес на зареждане в метода getView. Това е изпълнение на вътрешен клас и по този начин съдържа препратка към съответния ConvertedView или неговия ViewHolder. По този начин ние гарантираме, че системата трябва само да преначертае действителните промени. В допълнение, този подход е по-съвместим с показването на ProgressView, както бързо ще видите.

В тази публикация показах най-честите причини за „нестабилни списъци“. Тъй като опитът показва, че те не винаги могат да бъдат локализирани директно в кода, представих два инструмента, които могат да ви подкрепят при търсенето: Allocation Tracker, за да проследи ненужното генериране на обект, и Traceview, за да намерите скъпи, блокиращи обаждания. За да предотвратя възникването на тези проблеми в бъдеще, аз формулирах няколко общи съвета, които се доказаха в развитието на нашите проекти.

По принцип много може да се постигне чрез внимателно управление на паметта, последователно използване на работещи нишки, високоефективни оформления и съответно познаване на връзките. Дори да бях в състояние да засегна само много в тази статия, все пак се надявам, че съм оправдал очакванията ви за обещанието, което съм дал в детайли и че успях да ви дам някои нови впечатления!