Unrealized WebState

Status: launched.

On iOS, each tab is implemented by a WebState and some TabHelpers. As users can have many tabs open at the same time, but only few of them visible, an optimisation to reduce the memory pressure is to allow WebStates to exist in an incomplete state upon session restoration.

This incomplete state is called “unrealized”. When in the unrealized state, a WebState will not have a corresponding WKWebView, nor any of the objects that implement navigation (such as NavigationManager, ...).

WebState can transition from “unrealized” to “realized” either lazily when the client code request a functionality that cannot be provided in that state (such as accessing the NavigationManager, displaying, ...) or it can be forced by calling the WebState::ForceRealized() method. This is a one way transition, it is not possible for a “realized” WebState to get back into the “unrealized” state (at least in the initial implementation).

To avoid unnecessary transition to the “realized” state , the WebState::IsRealized() method can be called. This can be used by TabHelpers to delay their initialisation until the WebState become “realized”. To be informed of the transition, they can listen for the WebStateObserver::WebStateRealized() event which will be invoked upon transition of the WebState from “unrealized” to “realized”.

Features available on “unrealized” WebState

An “unrealized” WebState supports the following features:

  • registering and removing Observers
  • registering and removing WebStatePolicyDecider
  • registering ScriptCommandCallback
  • const property getters (*)
  • retrieving saved state (**)
  • attaching tab helpers

(*) : all of the const property getters can be called on an “unrealized” WebState but they may return a default value (false, nil, nullptr, empty string, ... if the information cannot be retrieved from the serialised state).

(**): retrieving the saved state is supported to save the session (as the code to save the session currently needs the state of all WebStates).

When are WebState created in “unrealized” state

The WebState are usually created in the “unrealized” state when a session is restored. The reason is that restoring a session may create many WebState when only few of them will be immediately used, while other ways to create a WebState (opening a new tab, preloading, ...) lead to immediate use.

As seen previously, WebState can only transition from the “unrealized” to the “realized” state. This means that WebState in the “unrealized” state would have been created directly in that state.

The WebState are not necessarily created in the “unrealized” state upon session restoration. This is controlled by the enable_unrealized_web_states gn variable (compilation) and the #lazily-create-web-state-on-restoration flag (runtime).

The transition to “realized” state does not require any action from the client of the WebState. Only internal state of the WebState will be affected. The observers registered will still be valid, as are the policy decider, the script callbacks, ... The client code may want to listen to the transition to activate itself if its behaviour depends on internal objects of the WebState (such as the NavigationManager).

Example of FindTabHelper

FindTabHelper is a TabHelper that implements the “find in page” feature. It wants to create a FindInPageController which needs a “realized” WebState. To support “unrealized” WebState, the creation of the FindInPageController is delayed until the WebState transitions to “realized” state.

This is done in the following way:

FindTabHelper::FindTabHelper(web::WebState* web_state) {
  DCHECK(web_state);
  web_state_observation_.Observe(web_state);
  if (web_state->IsRealized()) {
    CreateFindInPageController(web_state);
  }
}

void FindTabHelper::SetResponseDelegate(
    id<FindInPageResponseDelegate> response_delegate) {
  if (!_controller) {
    response_delegate_ = response_delegate;
  } else {
    controller_.responseDelegate = response_delegate;
  }
}

bool FindTabHelper::CurrentPageSupportsFindInPage() const {
  // As sending a message to `nil` returns the default value for a type
  // (`false` for `bool`), it is not needed to check `controller_` first.
  return [controller_ canFindInPage];
}

// Other FindTabHelper methods that implement the "find in page" feature.

void FindTabHelper::CreateFindInPageController(web::WebState* web_state) {
  DCHECK(!controller_);
  DCHECK(web_state->IsRealized());
  controller_ = [[FindInPageController alloc] initWithWebState:web_state];
  if (response_delegate_) {
    controller_.responseDelegate = response_delegate_;
    response_delegate_ = nil;
  }
}

void FindTabHelper::WebStateRealized(web::WebState* web_state) {
  CreateFindInPageController(web_state);
}