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

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

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

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

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

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 задачи, из которых большая часть - про продукт - При этом наша аудитория растет