Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Реанимация приложения

Реанимация приложения

Никита Клещин, Delivery Club – MOSDROID #16 Sulfur

Опыт починки, фичерения, рефактора, обновления приложений с многолетней историей в условиях современного бизнеса и ограниченных ресурсов. Простыми словами — хотим поделиться теми вещами, которые помогают нам не сойти с ума, делать продукт и внедрять фичи типа Slices и In-App Update.

Avatar for MOSDROID

MOSDROID

March 30, 2019
Tweet

More Decks by MOSDROID

Other Decks in Programming

Transcript

  1. НАЧАЛЬНЫЕ УСЛОВИЯ Прощай FoodPanda! Новые глобальные цели Android команда выросла

    до 4 человек +2 приложения Новое приложение работает только c FoodPanda API Текущее приложение полгода стояло на холде Разрабатывалось на аутсорсе до 2014 года, не было рефакторинга Команда QA не выросла У команды API тоже много новых задач и новые проекты
  2. ПРОДУКТОВАЯ РАЗРАБОТКА • Рефактор в счет продуктовых задач • “Предсказание”

    изменений • Технологии для решения проблемы • Contract First
  3. ЧТО ДЕЛАТЬ С КОДОМ? • Прятать внешние библиотеки за свои

    интерфейсы • Бить логику “на уровни” • Приводить код к одному виду • Trade-off
  4. ПОЛЕЗНОСТИ ДЛЯ РАЗРАБОТКИ + Репозиторий, задачки, документация + Swagger +

    Beta / HockeyApp + Jenkins / Bamboo + Artifactory + Аналитика + Firebase Performance Monitoring + Fiddler / Charles + Lynx + Hugo + Custom Crash Tracker + Debug Settings Activity
  5. private void openLynxActivity() { LynxConfig lynxConfig = new LynxConfig(); lynxConfig.setMaxNumberOfTracesToShow(4000)

    .setFilter("WTF"); Intent lynxActivityIntent = LynxActivity .getIntent(this, lynxConfig); startActivity(lynxActivityIntent); } ... public class YourApplication extends Application { @Override public void onCreate() { super.onCreate(); new LynxShakeDetector(this).init(); } } * Lynx: https://github.com/pedrovgs/Lynx
  6. @DebugLog public String getName(String first, String last) { SystemClock.sleep(15); //

    Don't ever really do this! return first + " " + last; } Log V/Example: ⇢ getName(first="Jake", last="Wharton") V/Example: ⇠ getName [16ms] = "Jake Wharton" * Hugo: https://github.com/JakeWharton/hugo
  7. public class Application extends AbstractApplication { @Override public void onCreate()

    { super.onCreate(); registerActivityLifecycleCallbacks(new LifecycleHandler()); } ... } public class LifecycleHandler implements Application.ActivityLifecycleCallbacks { @Override public void onActivityStarted(Activity activity) { tracker().onStart(activity) Logger.i(TAG, "onStart: " + activity.getClass().getSimpleName()); } ... } * https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks
  8. Stacktrace Fatal Exception: java.lang.RuntimeException: createContext failed: 12291 at com.google.maps.api.android.lib6.gmm6.vector.ba.a(...) at

    com.google.maps.api.android.lib6.gmm6.vector.bb.run(...) Log # Crashlytics - Custom logs # Platform: android # Issue: 5c5f6122f8b88c296345f87f # Session: 5C9105C2015300011623986FFABB2A46_DNE_0_v2 # Date: Tue Mar 19 2019 18:08:00 GMT+0300 (Москва, стандартное время) 0 | 6:07:49 PM | D/Event ... is registered 1 | 6:07:53 PM | D/Event SplashActivity is registered 2 | 6:07:53 PM | I/LifecycleHandler onStart: SplashActivity 3 | 6:07:53 PM | D/Event ... is registered 4 | 6:07:53 PM | D/counter amountSession: 1 5 | 6:07:54 PM | D/Event SplashActivity is unregistered 6 | 6:07:54 PM | D/Event AddressActivity is registered
  9. Manifest. BuildType: Debug <manifest ...> <application ...> <activity ... android:name="com.github.pedrovgs.lynx.LynxActivity"

    /> <activity ... android:name=".businesslayer.activities.DebugActivity" /> <activity-alias ... android:name="DeveloperOptions" android:targetActivity=".businesslayer.activities.DebugActivity" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias> </application> </manifest>
  10. public interface ITracker ... public class AbstractTracker implements ITracker ...

    public class Tracker1 extends AbstractTracker ... ... public class TrackerN extends AbstractTracker ... public class LifecycleHandler implements Application.ActivityLifecycleCallbacks { @Override public void onActivityStarted(Activity activity) { tracker().onStart(activity) Logger.i(TAG, "onStart: " + activity.getClass().getSimpleName()); } ... } public class Tracker implements ...
  11. public class Tracker implements InvocationHandler { public static final String

    TAG = "Tracker"; protected static final ClassLoader LOADER = ITracker.class.getClassLoader(); protected static final Class<?>[] INTERFACES = {ITracker.class}; public static ITracker newInstance(Collection<ITracker> trackers, Executor executor) { return (ITracker) Proxy.newProxyInstance(LOADER, INTERFACES, new Tracker(trackers, executor)); } ... @Override public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { mExecutor.post(() -> { for (ITracker tracker : mTrackers) { try { method.invoke(tracker, args); } catch (Throwable e) { Logger.e(TAG, e.getMessage(), e); } } }); return null; } } * InvocationHandler https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/InvocationHandler.html
  12. final Intent intent = activity.getIntent(); if (intent.getData() == null) {

    branch.initSession(listener, activity); } else { branch.initSession(listener, intent.getData(), activity); } ... protected class BranchListener implements Branch.BranchUniversalReferralInitListener { @Override public void onInitFinished(BranchUniversalObject o, LinkProperties link, BranchError e) { DeepLink deeplink = null; if (e == null && o!= null) { if (o.getMetadata().containsKey(ANDROID_DEEPLINK_PATH)) { String link = object.getMetadata().get(ANDROID_DEEPLINK_PATH); if (!TextUtils.isEmpty(link)) { handle(DeepLinkUtil.deeplink(link)); } } } } } * Branch.io: https://docs.branch.io/apps/android/
  13. GOOGLE ГОВОРИТ, ЧТО ЕСТЬ * Top 7 Takeaways for Android

    App Bundles: https://www.youtube.com/watch?v=st9VZuJNIbw
  14. ЧТО ДЛЯ ЭТОГО ЕСТЬ? + ProGuard (R8/D8) + VectorDrawable /

    Compat + VectorAnimation / Compat + AppBundle? + Modularization?
  15. ОБНОВЛЕНИЕ ПРИЛОЖЕНИЯ Предусматривали два вида: • Мягкое обновление • Критическое

    обновление Собственный механизм • Отправлять пользователя в маркет • Другая механика во время Staged Rollouts
  16. УДАЛЕННОЕ УПРАВЛЕНИЕ + Загружаемые настройки + Firebase Remote Config *

    Martin Fowler: Feature Toggles https://martinfowler.com/articles/feature-toggles.html
  17. @RunWith(SpannerRunner.class) public class UnitTestBenchmarks { @BenchmarkConfiguration public SpannerConfig configuration =

    new SpannerConfig.Builder() .saveResults(resultsDir, fileName) .useBaseline(baseLineFile) .medianFailureLimit(Float.MAX_VALUE) .addInstrument(RuntimeInstrumentConfig.defaultConfig()) .build(); @Benchmark public boolean instanceOf(int reps) { boolean result = false; for (int i = 0; i < reps; i++) {result = (testClass instanceof Object);} return result; } @Benchmark public boolean directComparison(int reps) { boolean result = false; for (int i = 0; i < reps; i++) {result = testClass == Object.class;} return result; } } * dk.ilios:spanner - https://github.com/cmelchior/spanner
  18. ДЕЛАЙТЕ ПРОДУКТ, А НЕ ЗАДАЧИ - В таком режиме мы

    сделали свыше 20 релизов, не считая хотфиксов и псевдо хотфиксов - Подтянули ANR-free и Crash-free почти к 100% - На один релиз приходилось, в среднем, по 23 задачи, из которых большая часть - про продукт - При этом наша аудитория растет