This analysis aims to provide a thorough guide to developing browser apps based on stock browser.
Stock Browser for Android
Controller
是WebView
、UI
、Activity
的桥梁
启动界面BrowserActivity
中最重要的方法是createController()
,Controller被创建时会调用
mController.setUi(new PhoneUi(this, controller))
PhoneUi
包含NavigationBarPhone
TitleBar
包含NavigationBarPhone
BaseUi
里关联一个custom_screen.xml
布局,并会创建一个TitleBar
custom_screen.xml
里有一个id
为main_content
的FrameLayout
,叫做mContentView
,
Tab
切换都会从mContentView
去添加和移除视图
<FrameLayout
android:id="@+id/fixed_titlebar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
其中fixed_title_container
是放置TitleBar的,参考BaseUi.addFixedTitleBar
NavScreen
是点击Tab Switch按钮后显示的Tab导航界面
Controller.loadUrl
-> Tab.loadUrl
Tab.loadUrl
里会1. 设置正在加载URL状态标志;2. 调用WebViewController.onPageStarted
;最后调用WebView.loadUrl
WebViewController.onPageStarted
十分关键,会调用BaseUi.onTabDataChanged
,然后会调用TitleBar.onTabDataChanged
和NavigationBarBase.onTabDataChanged
对菜单项的更新不在TitleBar.onTabDataChanged
和NavigationBarBase.onTabDataChanged
中;
有关Controller.onPageFinished
是在Tab.mWebViewClient.onPageFinished
中被回调;
这些菜单项是埋藏在“更多”菜单项里,每次点击该按钮,会创建、更新一个PopupMenu
,所以每次加载网页都不会主动更新菜单状态,而是在点击“更多”菜单项时,才更新状态(Lazy Initialization)。
在Tab加载网页时,会在TitleBar上显示一个Stop按钮,并在加载完后隐藏。Stop按钮作为控件状态更新的典型,厘清该控制流程非常重要。
- 首先发现该按钮位于
NavigationBarPhone
内; - 发现在更新Stop按钮的状态位于
onProgressStarted
、onProgressStopped
、onStateChanged
中;
UrlInputView
使用了Observer设计模式,其为一个主题(Subject)对象,而NavigationBarPhone
为观察者,彼此通过UrlInputView.StateListener.StateListener
接口来进行通信,其中NavigationBarPhone
实现UrlInputView.StateListener
接口。
NavigationBarPhone
的onProgressStarted
和onProgressStopped
被回调的地方是TitleBar.setProgress
,而TitleBar.setProgress
是被BaseUi.onProgressChanged
调用的;
BrowserActivity将onKeyDown和onKeyUp方法的处理委托给Controller的onKeyDown和onKeyUp。当按下返回键,Controller.onBackKey会被回调:
protected void onBackKey() {
if (!mUi.onBackKey()) {
WebView subwindow = mTabControl.getCurrentSubWindow();
if (subwindow != null) {
if (subwindow.canGoBack()) {
subwindow.goBack();
} else {
dismissSubWindow(mTabControl.getCurrentTab());
}
} else {
goBackOnePageOrQuit();
}
}
}
可见,Controller先将处理委托给UI.onBackKey
,仅当返回false
时,Controller会让当前的SubWindow来消化该事件,如果没有SubWindow,则由Controller.goBackOnePageOrQuit
处理该事件。
当当前Tab不为null,并且无法goBack时,如果有parent则切换到parent后再关闭tab,如果没有则关闭当前Tab,即调用Controller.closeCurrentTab
。
NavScreen
是多标签切换界面,它的布局是nav_screen.xml
。
布局中重要的类是NavTabScroller
,它主要负责Tab页滚动。
ViewConfiguration.get(Context).hasPermanentMenuKey()
判断是否有固态Menu键
NavTabScoller
通过设置BaseAdapter
来绑定数据
BrowserActivity.onLowMemory
会委托Controller.onLowMemory
释放内存。
@Override
public void onLowMemory() {
super.onLowMemory();
mController.onLowMemory();
}
Controller.onLowMemory
会调用mTabControl.freeMemory
来释放内存:
/**
* Free the memory in this order, 1) free the background tabs; 2) free the
* WebView cache;
*/
void freeMemory() {
if (getTabCount() == 0) return;
// free the least frequently used background tabs
Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
if (tabs.size() > 0) {
Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
for (Tab t : tabs) {
// store the WebView's state.
t.saveState();
// destroy the tab
t.destroy();
}
return;
}
// free the WebView's unused memory (this includes the cache)
Log.w(LOGTAG, "Free WebView's unused memory and cache");
WebView view = getCurrentWebView();
if (view != null) {
view.freeMemory();
}
}
问题是,该方法并不能实质释放内存,至少在小米4c上调试时观察Android Monitor的Memory曲线,并无内存释放。
释放内存的步骤为,先释放后台Tabs,再释放当前WebView的内存缓存。
当发生网址重定向时,WebView会多次回调WebViewClient.onPageStarted方法。比如在地址栏输入qq.com
后,回调方式如下:
V/Tab: ⇢ onPageStarted(view=BrowserWebView, url="https://qq.com/", favicon=Bitmap@23b4987f)
V/Tab: ⇢ onPageStarted(view=BrowserWebView, url="https://www.qq.com/", favicon=Bitmap@23b4987f)
V/Tab: ⇢ onPageStarted(view=BrowserWebView, url="https://xw.qq.com/index.htm", favicon=Bitmap@23b4987f)
V/Tab: ⇢ onReceivedIcon(view=BrowserWebView, icon=Bitmap@315bd90a)
V/Tab: ⇢ onPageFinished(view=BrowserWebView, url="https://xw.qq.com/index.htm")
可见每重定向一次,就会回调onPageStarted
方法,倘若需要多次重定向,则每次都会回调onPageStarted
方法。只有最终重定向的网址会被记录到历史栈中。
对一个标签页,获取Favicon时机有:
- Tab$mWebViewClient.onPageStarted
- Tab$mWebViewClient.onReceivedIcon
值得注意的是,这两个回调返回的Bitmap对象并不是同一个。那么这两个方法返回的favicon各是什么呢?
onPageStarted()返回上一个网页的favicon;而onReceivedIcon返回当前网页的favicon。
假设先访问A(qq.com),onPageStarted返回Favicon A1,onReceivedIcon返回Favicon A2,接着访问B,B是没有Favicon的站点(比如mail.126.com),onPageStarted返回Favicon A2。
WebChromeClient.onReceivedTitle(WebView view, String title)
提供了从WebView
获取标题的方法。
一般加载一个网页时,onReceivedTitle
会在onPageStarted
和onPageFinished
之间被回调,但是调用goBack
回到上一个网页,onReceivedTitle
却不会被回调。
TitleBar里带有刷/暂停按钮,这个按钮的状态变化将是接下来分析的重点