对于WebView的重度使用,如游戏音视频比较耗内存的需求,把WebView放到一个新的进程可以申请到更大的内存,修改其Activity的进程,通过AIDL与主线程通信,与主进程隔离,避免WebView的不稳定性导致所在进程异常影响主进程的正常运行造成不必要的crash,在此记录一下封装过程,欢迎一起学习和讨论~
封装WebView
自定义WebView
初始化WebView默认设置,设置javascript调用接口,回调js的dispatchEvent方法做事件分发,web进程初始化主进程调用接口,加载url时重置touch状态,监听用户的触摸操作,通过是否为用户点击(touch状态)判断当前跳转是否为重定向
1 | open class BaseWebView constructor( |
封装WebViewClient
回调接口逻辑,url跳转判断,重定向处理,对特殊链接统一处理,刷新处理,拦截指定url处理,判断是否加载完成,封装请求头,ssl错误处理
1 | class BaseWebViewClient( |
封装WebChromeClient
处理文件选择和相册选择回调,js提示回调,进度刷新回调
1 | class BaseWebChromeClient(private val progressHandler: Handler) : WebChromeClient() { |
封装WebView回调接口
1 | interface WebViewCallBack { |
封装进度条
大部分app展示web页面顶部有个web页的加载进度条提升交互体验
定义进度条操作接口
1 | interface BaseProgressSpec { |
封装进度条控制逻辑
1 | class IndicatorHandler{ |
自定义进度条
自定义FrameLayout布局,实现进度条操作接口,进度小于95时渲染匀速动画,大于等于95时渲染透明加减速动画
1 | class WebProgressBar constructor( |
封装带进度条的WebView
设置自定义进度条,添加到WebView,传入主线程handler调用刷新进度条逻辑
1 | class ProgressWebView constructor( |
封装带WebView的Fragment
fragment基类,统一设置标题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27open class BaseFragment: Fragment() {
protected var mContext: Context? = null
fun setTitle(titleId: Int) {
activity?.setTitle(titleId)
}
fun setTitle(title: CharSequence?) {
activity?.title = title
}
override fun onAttach(context: Context) {
super.onAttach(context)
this.mContext = context
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (mContext == null){
mContext = context
}
}
override fun getContext(): Context? {
return if (super.getContext() == null) mContext else super.getContext()
}
}封装带Webview的fragment基类,处理带header请求,定义页面开始加载,结束加载,指定url拦截,加载错误回调,是否重定向拦截,网页返回按键处理,相册文件选择回调处理,页面销毁时释放WebView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198abstract class BaseWebviewFragment : BaseFragment(), WebViewCallBack {
companion object {
const val INFO_HEADERS = "info_headers"
const val REDIRECT_INTERCEPT = "redirect_intercept"
const val REQUEST_CODE = 1
}
var webView: BaseWebView? = null
protected var headers: HashMap<String, String> = HashMap()
var webUrl: String? = null
var pageStarted: ((String) -> Unit)? = null
var pageFinished: ((String) -> Unit)? = null
var overrideUrlLoading: ((String) -> Boolean)? = null
var onError: ((Int, String, String) -> Unit)? = null
var onShowFileChooser: ((Intent, ValueCallback<Array<Uri>>) -> Unit)? = null
private var redirectIntercept = false
protected abstract fun getLayoutRes(): Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bundle = arguments
if (bundle != null) {
webUrl = bundle.getString(WebConstants.INTENT_TAG_URL)
if (bundle.containsKey(INFO_HEADERS)) {
headers =
bundle.getSerializable(INFO_HEADERS) as HashMap<String, String>
redirectIntercept = bundle.getBoolean(REDIRECT_INTERCEPT)
}
}
// 注册吐司命令
CommandsManager.getInstance().registerCommand(ToastCommand())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(getLayoutRes(), container, false)
webView = view.findViewById(R.id.web_view)
webView?.setHeaders(headers)
webView?.setRedirectIntercept(redirectIntercept)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webView?.registerWebViewCallBack(this)
loadUrl()
}
protected open fun loadUrl() {
webUrl?.let {
webView?.loadUrl(it)
}
}
override fun onResume() {
super.onResume()
Log.v("BaseWebviewFragment","onResume")
webView?.dispatchEvent("pageResume")
webView?.onResume()
}
override fun onPause() {
super.onPause()
Log.v("BaseWebviewFragment","onPause")
webView?.dispatchEvent("pagePause")
webView?.onPause()
}
override fun onStop() {
super.onStop()
webView?.dispatchEvent("pageStop")
}
override fun onDestroyView() {
super.onDestroyView()
webView?.dispatchEvent("pageDestroy")
clearWebView(webView)
}
open fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
onBackHandle()
} else false
}
override fun pageStarted(url: String) {
this.pageStarted?.invoke(url)
}
override fun pageFinished(url: String) {
this.pageFinished?.invoke(url)
}
override fun overrideUrlLoading(url: String): Boolean {
this.overrideUrlLoading?.let {
return it.invoke(url)
}
return false
}
override fun onError(errorCode: Int, description: String, failingUrl: String) {
onError?.invoke(errorCode, description, failingUrl)
}
/**
* 处理返回
* @return Boolean
*/
protected open fun onBackHandle(): Boolean {
return if (webView != null) {
if (webView!!.canGoBack()) {
webView?.goBack()
true
} else {
false
}
} else false
}
private fun clearWebView(m: WebView?) {
val m: WebView? = m ?: return
// 非主线程退出
if (Looper.myLooper() != Looper.getMainLooper()) return
// 停止加载处理
m?.stopLoading()
if (m?.handler != null) {
m.handler.removeCallbacksAndMessages(null)
}
// 移除webview的所有view
m?.removeAllViews()
// 获取父布局移除webview
val mViewGroup: ViewGroup? = m?.parent as? ViewGroup
mViewGroup?.removeView(m)
// 回调置空
m?.webChromeClient = null
m?.webViewClient = null
m?.tag = null
// 清理历史并销毁
m?.clearHistory()
m?.destroy()
}
override fun onShowFileChooser(
cameraIntent: Intent,
filePathCallback: ValueCallback<Array<Uri>>
) {
if (onShowFileChooser != null) {
onShowFileChooser!!.invoke(cameraIntent, filePathCallback)
} else {
mFilePathCallback = filePathCallback
//------------------------------------
//弹出选择框有:相机、相册(Android9.0,Android8.0)
//如果是小米Android6.0系统上,依然是:相机、相册、文件管理
//如果安装了其他的相机(百度魔拍)、文件管理程序(ES文件管理器),也有可能会弹出
val selectionIntent = Intent(Intent.ACTION_PICK, null)
selectionIntent.type = "image/*"
//------------------------------------
val intentArray: Array<Intent?> = arrayOf(cameraIntent)
val chooserIntent = Intent(Intent.ACTION_CHOOSER)
chooserIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.file_chooser))
chooserIntent.putExtra(Intent.EXTRA_INTENT, selectionIntent)
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
startActivityForResult(chooserIntent, REQUEST_CODE)
}
}
private var mFilePathCallback: ValueCallback<Array<Uri>>? = null
private val mCameraPhotoPath: String? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_CODE -> {
var results: Array<Uri>? = null
if (resultCode == Activity.RESULT_OK) {
if (data == null) {
if (mCameraPhotoPath != null) {
Log.d("AppChooserFragment", mCameraPhotoPath)
results = arrayOf(Uri.parse(mCameraPhotoPath))
}
} else {
val dataString = data.dataString
if (dataString != null) {
results = arrayOf(Uri.parse(dataString))
}
}
}
mFilePathCallback?.onReceiveValue(results)
mFilePathCallback = null
}
}
}
}WebViewFragment实现类,header传参,是否拦截重定向,是否同步header到cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42class WebviewFragment : BaseWebviewFragment() {
override fun getLayoutRes(): Int {
return R.layout.fragment_common_webview
}
companion object{
fun newInstance(keyUrl: String, headers: HashMap<String, String> = HashMap(), redirectIntercept: Boolean,isSyncToCookie: Boolean): WebviewFragment? {
val fragment = WebviewFragment()
fragment.arguments = getBundle(keyUrl, headers, redirectIntercept)
if (isSyncToCookie) {
syncCookie(keyUrl, headers)
}
return fragment
}
private fun getBundle(url: String, headers: HashMap<String, String>, redirectIntercept:Boolean): Bundle? {
val bundle = Bundle()
bundle.putString(WebConstants.INTENT_TAG_URL, url)
bundle.putSerializable(INFO_HEADERS, headers)
bundle.putBoolean(REDIRECT_INTERCEPT, redirectIntercept)
return bundle
}
/**
* cookie同步到WebView
*
* @param url WebView要加载的url
* @return true 同步cookie成功,false同步cookie失败
* @Author JPH
*/
private fun syncCookie(url: String?, map: Map<String, String?>): Boolean {
val cookieManager = CookieManager.getInstance()
for (key in map.keys) {
cookieManager.setCookie(url, key + "=" + map[key])
}
val newCookie = cookieManager.getCookie(url)
return !TextUtils.isEmpty(newCookie)
}
}
}封装带WebView的Activity
支持自定义标题布局,设置带WebView的Fragment并添加到Activity,按键处理,注册使用网页标题事件,网页Activity关闭事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108open class WebActivity : AppCompatActivity() {
private var title: String? = null // 标题设置
protected var webviewFragment: BaseWebviewFragment? = null
// 标题布局可自定义
protected fun getLayoutTitle(): Int = R.layout.title_normal
// 添加标题布局
val titleView by lazy { LayoutInflater.from(this).inflate(getLayoutTitle(), fl_title) }
companion object{
// 启动入口
fun startCommonWeb(context: Context, title: String?, url: String?,header: HashMap<String, String> = HashMap()) {
val intent = Intent(context, WebActivity::class.java)
intent.putExtra(WebConstants.INTENT_TAG_TITLE, title)
intent.putExtra(WebConstants.INTENT_TAG_URL, url)
intent.putExtra(WebConstants.INTENT_TAG_HEADERS, header)
if (context is Service) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CommandsManager.getInstance().registerCommand(titleUpdateCommand)
setContentView(R.layout.activity_common_web)
// 注册标题刷新回调
// 设置标题和url
title = intent.getStringExtra(WebConstants.INTENT_TAG_TITLE)
val url = intent.getStringExtra(WebConstants.INTENT_TAG_URL)?:""
// 如果是默认布局设置标题
if (getLayoutTitle() == R.layout.title_normal){
val textView = titleView.findViewById(R.id.tv_title) as TextView
textView.text = title
}
// 填充webviewFragment
val fm = supportFragmentManager
val transaction = fm.beginTransaction()
webviewFragment = null
val params = intent.extras?.getSerializable(WebConstants.INTENT_TAG_HEADERS) as HashMap<String, String>
webviewFragment = WebviewFragment.newInstance(
url,
params,
redirectIntercept = false,
isSyncToCookie = true
)
transaction.replace(R.id.web_view_fragment, webviewFragment as Fragment).commit()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// 调用webviewFragment的按键回调
if (webviewFragment != null && webviewFragment is BaseWebviewFragment && event != null) {
val flag: Boolean = webviewFragment!!.onKeyDown(keyCode, event)
if (flag) {
return flag
}
}
return super.onKeyDown(keyCode, event)
}
/**
* 标题刷新命令
*/
private val titleUpdateCommand: Command = object : Command {
override fun name(): String {
return WebConstants.COMMAND_UPDATE_TITLE
}
override fun exec(context: Context, params: ArrayMap<String, String>, resultBack: ResultBack) {
if (params.containsKey(WebConstants.COMMAND_UPDATE_TITLE_PARAMS)) {
if (getLayoutTitle() == R.layout.title_normal){
val textView = titleView.findViewById(R.id.tv_title) as TextView
textView.text = params[WebConstants.COMMAND_UPDATE_TITLE_PARAMS]
}
}
}
}
/**
* 默认title布局返回 回调
* @param view View
*/
fun back(view: View){
finish()
}
override fun finish() {
val map = ArrayMap<String, String>().apply {
this[WebFinishCommand.FINISH_COMMAND_PARAMS] = "true"
}
CommandDispatcher.getInstance().execByOther(WebFinishCommand.FINISH_COMMAND, Gson().toJson(map))
super.finish()
}
/**
* 点击右侧button 回调
* @param view View
*/
fun clickRight(view: View){
}
}分发消息
命令模式封装消息管理器,对当前分发的消息采用当前进程自行处理,或跨进程处理,根据传参判断是否需要回调给h5
1 | /** |
1 | /** |
1 | class CommandDispatcher { |
- 进程连接管理类,绑定另一个服务进程,连接断开时解绑重连,返回跨进程通信接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67class ProcessConnector private constructor(context: Context) {
private var mContext: Context = context.applicationContext
// 跨进程通信接口
private var iHandleAction: IHandleAction? = null
private var mConnectBinderPoolCountDownLatch: CountDownLatch = CountDownLatch(1) // 个数为1的同步变量
// 单例对象
companion object {
private var instance: ProcessConnector? = null
fun getInstance(context: Context) = instance ?: synchronized(this) {
instance ?: ProcessConnector(context).also { instance = it }
}
}
init {
connectToOtherProcessService()
}
// 绑定另一个服务进程
private fun connectToOtherProcessService() {
val targetClass = if(!isMainProcess(mContext)) RemoteBindMainService::class.java else MainBindRemoteService::class.java
// 绑定主进程服务
mContext.bindService(Intent(mContext,targetClass), object : ServiceConnection {
// 服务断开
override fun onServiceDisconnected(name: ComponentName) {
Log.v("ServiceConnect","跨进程连接断开")
}
// 服务连接
override fun onServiceConnected(name: ComponentName, service: IBinder) {
Log.v("ServiceConnect","跨进程连接成功")
// 绑定断开回调
iHandleAction = IHandleAction.Stub.asInterface(service)
try {
iHandleAction?.asBinder()?.linkToDeath(object : IBinder.DeathRecipient {
// 解绑重连
override fun binderDied() {
iHandleAction?.asBinder()?.unlinkToDeath(this, 0)
iHandleAction = null
connectToOtherProcessService()
}
}, 0)
} catch (e: RemoteException) {
e.printStackTrace()
}
mConnectBinderPoolCountDownLatch.countDown()
}
}, Context.BIND_AUTO_CREATE)
// 线程同步阻塞
try {
mConnectBinderPoolCountDownLatch.await()
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
// 返回跨进程调用接口
fun getAidlInterface(): IBinder? {
return iHandleAction?.asBinder()
}
}跨进程通信
跨进程接口定义
定义AIDL跨进程调用接口定义AIDL跨进程调用回调接口1
2
3interface IHandleAction {
void handleAction(String actionName, String jsonParams, in ICallback callback);
}1
2
3interface ICallback {
void onResult(int responseCode, String actionName, String response);
}跨进程接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class AidlInterface (val context: Context): IHandleAction.Stub() {
private val gson: Gson by lazy { Gson() }
override fun handleAction(
actionName: String?,
jsonParams: String?,
callback: ICallback?
) {
Log.v("AidlInterface","${Process.myPid()}进程正在跨进程执行命令$actionName")
if (actionName != null){
// 跨进程处理命令
CommandsManager.getInstance().execCommand(context, actionName,
gson.fromJson(jsonParams, ArrayMap::class.java) as? ArrayMap<String, String>, object : ResultBack {
override fun onResult(status: Int, action: String, result: Any?) {
try {
// 原进程处理回调
callback?.onResult(status, actionName, Gson().toJson(result))
} catch (e: Exception) {
e.printStackTrace()
}
}
})
}
}
}声明主进程服务和Web进程服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 主进程绑定web进程
*/
class MainBindRemoteService : Service() {
override fun onBind(intent: Intent?): IBinder? {
val pid = Process.myPid()
Log.d(
"MainBindRemoteService", String.format(
"web进程: %s",
"当前进程ID为:$pid---主进程连接web进程成功"
)
)
// web进程操作对象
return AidlInterface(this)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* web绑定主进程服务
*/
class RemoteBindMainService: Service() {
override fun onBind(intent: Intent?): IBinder? {
val pid = Process.myPid()
Log.d(
"RemoteBindMainService", String.format(
"主进程: %s",
"当前进程ID为:$pid----web连接主进程成功"
)
)
// 主进程操作对象
return AidlInterface(this)
}
}最后利用ContentProvider初始化封装的SDK,兼容>=Android 9.0 不同进程中使用Webview需要配置不同的缓存目录1
2
3<service android:name="com.example.weblib.service.MainBindRemoteService"
android:process=":remoteweb"/>
<service android:name="com.example.weblib.service.RemoteBindMainService" />1
2
3
4
5
6
7
8
9
10
11
12class WebInitializer : ContentProvider() {
override fun onCreate(): Boolean {
Log.v("SdkInitializer","WebLib初始化进程:${getProcessName(context!!)}")
if (context == null) return true
// >=Android 9.0 在不同进程中使用Webview需要配置缓存目录,配置WebLib进程使用的Webview缓存目录
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
WebView.setDataDirectorySuffix("remote")
}
return true
}
...
}1
2
3
4
5
6<provider
android:authorities="${applicationId}.library-installer"
android:name="com.example.weblib.WebInitializer"
android:multiprocess="true"
android:process=":remoteweb"
android:exported="false"/>