Асинхронне завантаження зображень у списки - підходи та найкращі практики (Частина 2) SIC! програмне забезпечення
У своєму останньому дописі я вже представляв загальні труднощі у зв'язку з асинхронними процесами завантаження у списках. За допомогою простого сценарію я розробив складність цього варіанту використання, не вдаючись до надто детальних деталей.

З іншого боку, я хотів би спрямувати цю статтю в дещо більш технічний напрямок - і таким чином виконати своє обіцяння розкрити деякі найкращі практики та хитрощі, які зарекомендували себе в наших проектах. Я хотів би показати, як плавно прокручувати списки можна порівняно легко впровадити, та представити деякі корисні інструменти, які можуть вам у цьому допомогти. Між рядками я зупинюсь на декількох різнобічних підходах, які зазвичай рекомендуються, але несуть не зовсім незначні проблеми.
Неефективний список виявляється у різкій поведінці прокрутки або навіть у тривалому блокуванні всієї взаємодії користувача. Причинами цього є блокування викликів до основного потоку, який іноді відповідає за малювання елементів інтерфейсу користувача або відправлення подій. Ці дзвінки не завжди повинні здійснюватися явно, але вони також можуть бути результатом необережного управління пам'яттю, як це видно на малюнку 1. Звичайно, це стало набагато кращим із введенням одночасного збирача сміття в Android 2.3 - але все-таки варто створювати об’єкти лише тоді, коли це дійсно необхідно, оскільки це, як відомо, дорога операція.
Для того, щоб відстежувати непотрібно виділені об'єкти, зарекомендувало себе використання відстежувача виділення Android, який є частиною Android SDK - або, точніше, інструменту Dalvik Debug Monitor Server (DDMS). Це дозволяє записувати всі покоління об’єктів у вільно вибирається часовий проміжок часу.
На малюнку 2 показано запис, зроблений під час прокрутки списку. Виділений запис говорить, наприклад, що 92-байтний (Розмір розміщення) BitmapFactory.Options об’єкт (Виділений клас) був створений із робочого потоку (Ідентифікатор потоку) під час завантаження зображення з кешу (Виділено в). Шляхом відповідного сортування окремих стовпців можна порівняно легко виявити непотрібні множинні екземпляри.
Оптимізація завжди має сенс, якщо блок коду викликається часто або більші об’єкти, такі як буфери, створюються кілька разів. У нашому випадку особлива увага приділяється методу getView () адаптера списку. Оскільки це так чи інакше викликається лише з основного потоку, об’єкти можуть утримуватися як змінні-члени - і, таким чином, повторно використовуватися. Шаблон ViewHolder вже дотримується подібного принципу. У цьому контексті слід також перевірити, чи використовуються більш ефективні структури даних can: Примітивні типи даних переважно віддають перевагу відповідним класам обгортки. Особливу обережність слід проявляти також до неявного та дорогого автобоксу. Однак розробник також повинен бути знайомий із нещодавно доданими структурами даних, такими як розріджений масив або кеш LRU (також доступний як клас сумісності).
Ще декілька рекомендацій щодо управління пам'яттю:
Для того, щоб список взагалі плавно прокручувався, потрібно відтворити щонайменше 25 кадрів на секунду. Це відповідає часовому вікну максимум 40 мілісекунд на кадр. Якщо сказати дещо простіше, то на виклик методу getView () адаптера списку, включаючи всі операції макетування та рендерингу ієрархії подання, може знадобитися максимум 40 мілісекунд, щоб сприйняти його як рідину. Будь-яке перевищення виявлялося б у незначному або навіть початковому заїканні.
На перший погляд, це здається зручним завданням для сучасних мобільних процесорів. Однак при детальному огляді швидко зрозумієш, що цей часовий проміжок часу більш ніж вузький. Доступ до власних функцій, таких як проста операція з існуванням файлів (), може зайняти 25% (= 10 мс) цього часу. Отже, те саме стосується і доступу до бази даних. Доступ до постійного сховища для читання та особливо для запису є повільним. Крім того, час відгуку на дзвінки іноді надзвичайно коливається. Той самий доступ до запису до файлової системи може зайняти 20 мс і 2 секунди. З цієї причини найкращою практикою є передача таких операцій робочим потокам. Суворий режим, який надає Android SDK, приділяє дуже пильну увагу дотриманню цього правила (за бажанням).
Щоб відстежувати дорогі дзвінки в основному потоці, використання Traceview добре зарекомендувало себе. Це також частина інструментів Android SDK.
На малюнку 3 наведено приклад фрагмента трасування, який було записано під час прокрутки списку. Тут ви можете бачити, що основний потік вже звільнений від деяких робіт іншими потоками. Наприклад, Thread-15 в даний час зчитує дані з потоку, тоді як основний потік все ще може відправляти події і, таким чином, залишається чуйним. Виклики можна додатково розподілити за необхідності, так що ви отримаєте дуже точне уявлення про те, який потік виконує яку операцію в який час або наскільки дорогий кожен з них.
Загалом, слід уважніше розглянути терміни основної нитки. Усі операції, які блокують це непотрібно довго, повинні передаватися в робочі потоки. З одного боку, для цієї мети доступні AsyncTasks, які вже звільняють вас від управління потоком потоків та синхронізації з основним потоком. Але також численні реалізації ExecutorService, які можна створити за допомогою класу Executors Factory, є дуже хорошою та адаптивною альтернативою. З іншого боку, вам слід утриматися від створення та запуску потоків вручну, оскільки це створює занадто високі накладні витрати і в гіршому випадку Падіння може призвести навіть до аварії (швидке переглядання 10000 записів у списку).
Під час прокрутки велика частина обчислювального часу витрачається на макет та операції візуалізації подання списку та його дочірніх подань. Оскільки це має бути зроблено з основного потоку через однопоточну модель, тут також є значний потенціал для оптимізації. Щоб розкрити це, нам подобається використовувати Hirarchy Viewer, який також є частиною інструментів Android SDK.
На рисунку 4 показаний приклад деревоподібної структури ListView, елементи якого містять зображення та текст. У цьому поданні за допомогою кольорових індикаторів можна визначити непотрібні контейнери для розмітки, а також дорогі операції з компонування та малювання. Слід зазначити, однак, що кольори слід розуміти як відносні, а не абсолютні значення: Макет кадру тут на малюнку має для вимірювання його розміру (міри), розташування та вирівнювання (макета) та малювання себе та себе Подання дочірньої дитини (малювання) мають червоний індикатор, оскільки для цього було потрібно 100% обчислювального часу в батьківському поданні - і не через абсолютний час.
Для того, щоб оптимізувати ефективність вашого власного макета, слід дотримуватись таких основних правил:
- Деревоподібна структура ієрархії подань повинна бути якомога рівнішою. З кожним вкладанням обчислювальний час для міри + розмітки значно збільшується.
- RelativeLayout є потужнішим та ефективнішим, ніж, наприклад, вкладені лінійні макети, і їм завжди слід віддавати перевагу.
- Якщо розмір переглядів вже відомий під час компіляції, слід віддавати перевагу специфікаціям фіксованого розміру в незалежних від щільності пікселях (dip) перед wrap_content. Це дещо пришвидшує процес вимірювання.
- Чим менше переглядів використовується, тим краще. Часто достатньо встановити один або кілька складених малюнків для TextView замість того, щоб використовувати глибоко вкладений макет!
Щоб завершити загальний вигляд плавно прокручуваного списку, ми рекомендуємо затухати та відключати анімовані асинхронні активовані ImageViews або ProgressViews. Вся взаємодія з користувачем виглядає більш гладкою завдяки більш м’яким переходам. Фрагмент коду нижче показує, як це можна зробити дуже просто:
Структура передбачає notifyDataSetChanged адаптера, який буде викликаний при внесенні змін до даних (зокрема, коли завантажується зображення, наприклад). Це сигнал для них, щоб у майбутньому переробити весь список на невизначений час. Хоча такий підхід в принципі не викликає заперечень, ми зазвичай йдемо іншим шляхом: зворотний виклик передається в процес асинхронного завантаження в методі getView. Це реалізація внутрішнього класу і, отже, містить посилання на відповідний ConvertedView або його ViewHolder. Таким чином ми гарантуємо, що система повинна лише перекроювати фактичні зміни. Крім того, такий підхід є більш сумісним із відображенням ProgressView, як ви швидко переконаєтесь.
У цій публікації я продемонстрував найпоширеніші причини виникнення “невдалих списків”. Оскільки досвід показав, що їх не завжди можна локалізувати безпосередньо в коді, я представив два інструменти, які можуть допомогти вам у пошуку: The Allocation Tracker для відстеження непотрібних об'єктів і Traceview для пошуку дорогих блокуючих дзвінків. Щоб запобігти виникненню цих проблем у майбутньому, я сформулював кілька загальних порад, які зарекомендували себе при розробці наших проектів.
В основному, багато чого можна досягти за допомогою продуманого управління пам’яттю, послідовного використання робочих потоків, високопродуктивних макетів та відповідних знань про стосунки. Навіть якби я міг лише багато чого торкнутися в цій статті, я все одно сподіваюся, що я виправдав ваші сподівання щодо обіцянки, яку я детально згадав, і що зміг дати вам кілька нових вражень!