Thinking about cross-activity KVO issues

Most display apps have a Summary->Detail structure, and Detail often includes modifications to the Model. This structure has several problems with the Model:

  • How to use KVO to synchronize the state of the Model and the background View
  • How to easily get global data in Utils

Android architecture

Google's official solution is ViewModel, LiveData

  • ViewModel ViewModel is a collection of data and logic of the entire page, more like the binding part of C+M in MVC. Data synchronization relies on two points: 1. The Model is reloaded every time the resume (or other suitable timing), and the reuse of the Model is guaranteed by the Repo. 2. All fields of the Model are KVO bound to the View, and all fields bound by the View are final . Memory leakage is guaranteed by LifecycleAware. That is, all KVO fields and observers are bound to the life cycle, and data transfer between pages is guaranteed by active refresh (onResume) . The Models between pages are different objects , and the POJOs in the Models may be the same (depending on the Repo implementation).
  • LiveData LiveData is bound to the life cycle and maintains the validity of memory.
    This solution is very simple and violent. The implied meaning is that even if the POJO data is leaked, there is no problem (because the Repo does not know when to release the reference, so once it is placed in the Repo, it can only be actively deleted to release)
    because it is not afraid of POJO leakage , getting data in Utils is extremely simple.

Pass Ref, Model is bound with the life cycle

The basic idea is that Repo does ARC to Model and does simple GC manually.

The goal is to reduce the leakage of POJO data. The specifics are similar to the official ones, except that Repo counts POJOs and clears the data in memory during Activity#onResume.
The reason why it is cleaned up in onResume is because in most cases, onResume is the last Activate executed, and the data acquisition has been triggered at this time, so even the resources released by the previous Activity startActivity have been re-held by the subsequent pages. There is.
At this point, different Activities, including different Observers, all hold the same object , which ensures perfect synchronization of data modifications.
It is not easy to obtain the Model without life cycle Utils (such as logs) (logic that is completely unrelated to the life cycle, such as background polling and positioning, will not get the data), but it is simpler than deleting it after passing it with a single instance. .
This method is more in line with the definition of Repo than the latter method, and it will be easier to maintain.
demo code:
ModelHolder:

public class ModelHolder<T> {
  public final T mModel;
  public final String mKey;
  public final Set<String> mOwners = new HashSet<>();

  public ModelHolder(T model) {
    mModel = model;
    mKey = String.valueOf(model.hashCode());
  }
}
public class Repo {
  private static Repo sIntance;

  private Repo(Application application) {
    application.registerActivityLifecycleCallbacks(mCallback);
  }

  public static Repo getInstance(Application application) {
    if (sIntance == null) {
      synchronized (Repo.class) {
        if (sIntance == null) {
          sIntance = new Repo(application);
        }
      }
    }
    return sIntance;
  }

  private final Map<String, ModelHolder> mModels = new HashMap<>();
  private final Set<String> mUnreferedKeys = new HashSet<>();
  private final Map<String, Set<ModelHolder>> mOwnerModelMapping = new HashMap<>();

  private Application.ActivityLifecycleCallbacks mCallback =
      new Application.ActivityLifecycleCallbacks() {
        @Override
        public void onActivityCreated(Activity activity, Bundle bundle) {

      }

        @Override
        public void onActivityStarted(Activity activity) {

      }

        @Override
        public void onActivityResumed(Activity activity) {
          for (String key : mUnreferedKeys) {
            ModelHolder holder = mModels.get(key);
            if (holder.mOwners.isEmpty()) {
              mModels.remove(key);
            }
          }
        }

        @Override
        public void onActivityPaused(Activity activity) {

      }

        @Override
        public void onActivityStopped(Activity activity) {

      }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

      }

        @Override
        public void onActivityDestroyed(Activity activity) {
          release(activity);
        }
      };

  public <T> T get(String key, Activity owner) {
    ModelHolder<T> holder = mModels.get(key);
    if (holder == null) {
      return null;
    }
    String ownerKey = String.valueOf(owner.hashCode());
    holder.mOwners.add(ownerKey);
    Set<ModelHolder> holders = mOwnerModelMapping.get(ownerKey);
    if (holders == null) {
      holders = new HashSet<>();
      mOwnerModelMapping.put(ownerKey, holders);
    }
    holders.add(holder);
    return holder.mModel;
  }

  public <T> String put(T model) {
    ModelHolder<T> holder = new ModelHolder<>(model);
    mModels.put(holder.mKey, holder);
    return holder.mKey;
  }

  private void release(Activity owner) {
    String ownerKey = String.valueOf(owner.hashCode());
    Set<ModelHolder> holders = mOwnerModelMapping.remove(ownerKey);
    for (ModelHolder holder : holders) {
      holder.mOwners.remove(ownerKey);
      if (holder.mOwners.isEmpty()) {
        mUnreferedKeys.add(holder.mKey);
      }
    }
    holders.clear();
  }
}

Model layer for synchronization

Now the company's App has a lot of data monitoring happening on the background page. If the refactoring rashly removes these monitoring, it will definitely introduce very troublesome bugs. In fact, there is a clever way to solve this problem: do synchronization in the Model layer.
Model layer synchronization means using a common method (such as EventBus) to ensure that the modifications of different Model objects monitored by the Observer are consistent.
Basically, it can be understood with the CPU cache update strategy. A modifies, writes back to main memory and sends invalidate information on the bus. All caches with the same address that receive invalidate information read the latest data from main memory (when used).
That is to say, the synchronization here relies on update messages .
Of course, if EventBus is used, in addition to the initial value, the global POJO can also be guaranteed to be the same. More extreme initialization can also use the same POJO, but it seems strange, and the proper POJO leaks.
Because of the distributed storage, Utils seems to be unable to get the Model.
This method is minimally intrusive to use, and transfers do not need to be modified. And if the Model itself is Observable, the implementation cost is the lowest.
There is also a magical problem in the company's app. Google's LiveData is completely unsolved. There are actually two types of Model IDs:

  • repoId, corresponding to the memory address, this id uniquely identifies an object and has no business meaning
  • bizId, corresponding to the id of the business attribute, such as userId, refers to the same in business. This Id can be interchanged with repoId most of the time

However, in the company App, repoId and bizId are not common, because the models corresponding to the same bizId under different pages are different (mostly related to behavior traces). In this way, if you directly use the LiveData set, you will kneel. You can only package a data id on the bizId, which is very troublesome. And any other method also needs to maintain the data synchronization of the objects with the same bizid. At this time, this method is more convenient.

Distributed Model

The synchronization method of the Model layer above can be made a more stable solution with a slight change, or it can be regarded as the most complicated but most effective final solution.
Model has a life cycle and is registered with EventBus. There is a pair of events between Repo and Model, Repo will issue getInstance event, the only Model object in the world responds to this event, and returns its own instance to Repo. If and only when the Model declares that it needs to be temporarily held by the Repo (in order to solve the situation that A start B and finish itself causes the Model to be recycled), the Model is held by the Repo, and after responding to getInstance, it removes itself.
The life cycle of the Model has only two nodes, and the EventBus listener is registered during construction. When all bound lifecycle objects are inactive and not persistent, the EventBus listener is unregistered. Of course, when a life cycle object is inactive, the corresponding Observer should be cleaned up.
In this way, since there is no place to centrally hold the Model, the possibility of POJO memory leaks is minimized. It also ensures that Utils can get the required data at any time.
demo code:
ModelHolder

public class ModelHolder<T extends Observable> {
  private final Map<Activity, List<Observer>> mObservers = new HashMap<>();
  private Application.ActivityLifecycleCallbacks mCallback = new Application.ActivityLifecycleCallbacks() {
    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {

    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
      List<Observer> observers = mObservers.remove(activity);
      if (observers == null) {
        return;
      }
      for (Observer observer : observers) {
        mModel.deleteObserver(observer);
      }
      if (mPersistOnDestroy) {
        Repo.getInstance().persist(ModelHolder.this);
      } else {
        if (mObservers.isEmpty()) {
          EventBus.getDefault().unregister(ModelHolder.this);
          activity.getApplication().unregisterActivityLifecycleCallbacks(mCallback);
        }
      }
    }
  };

  public final T mModel;
  public boolean mPersistOnDestroy;
  public String mKey;

  public ModelHolder(Application application, T value) {
    mModel = value;
    mKey = String.valueOf(mModel.hashCode());
    EventBus.getDefault().register(this);
    application.registerActivityLifecycleCallbacks(mCallback);
  }

  @Subscribe
  public void instanceRequested(GetInstance getInstance) {
    if (getInstance.mKey.equals(mKey)) {
      EventBus.getDefault().post(new InstanceGot(this));
      Repo.getInstance().remove(this);
      mPersistOnDestroy = false;
    }
  }

  public void bind(Activity activity, Observer... observers) {
    mObservers.put(activity, Arrays.asList(observers));
    for (Observer observer : observers) {
      mModel.addObserver(observer);
    }
  }

  public static class GetInstance {
    public String mKey;

    public GetInstance(String key) {
      mKey = key;
    }
  }

  public static class InstanceGot {
    public ModelHolder mHolder;

    public InstanceGot(ModelHolder holder) {
      mHolder = holder;
    }
  }
}

Repo

public class Repo {
  private static Repo sIntance;

  public static Repo getInstance() {
    if (sIntance == null) {
      synchronized (Repo.class) {
        if (sIntance == null) {
          sIntance = new Repo();
        }
      }
    }
    return sIntance;
  }

  private final Set<ModelHolder> mPersists = new HashSet<>();
  private final ThreadLocal<Object> mHolder = new ThreadLocal<>();

  private Repo() {
    EventBus.getDefault().register(this);
  }

  public void persist(ModelHolder object) {
    mPersists.add(object);
  }

  public void remove(ModelHolder object) {
    mPersists.remove(object);
  }

  @Subscribe
  public void onInstance(ModelHolder.InstanceGot instance) {
    mHolder.set(instance.mHolder);
  }

  public <T> T get(String key) {
    EventBus.getDefault().post(new ModelHolder.GetInstance(key));
    return (T) mHolder.get();
  }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325837154&siteId=291194637