在Android Webview的assets目录下开发Hybrid App的一些坑…

crying_android 好久没写blog了,忙起来真是没法.. 在上一篇文章《基于Android Webview的Hybrid App开发的前端优化》的最后一条“以上都不是”里提到了“其实Hybrid App的最佳实践,还是应该把所有的html css js和主要的图片资源离线存储在Android的asset文件夹下,然后由Android实现从服务器端到手机的这个www主文件夹的更新机制,这样才不用凡事从server端下载..”没想到这么快就应用到新的项目中了。 当然,还只是实现了将webapp放在Android app的assets下,通过json api与server交互这步,理想中的通过app文件操作的更新机制还没有做,但是已经是梦想照进现实的节奏。感谢Android开发的同事包容了我这边不厌其烦反反复复的修改调试,才让这个相对Cutting Edge的方法得以应用到产品中。原本使用Hybrid App的初衷是为了分担Android开发同事的一些工作,但由于实践经验不足,而且Android webview确实还存在一些bug和兼容问题,最后评估下来反而多耗了不少时间精力。因此我们对于Hybrid App方式的使用暂时告一段落,web侧工作重心回归数据和后台开发,但是这轮开发的经验和教训还是很宝贵的,也给了我们更多信心,相信再过一小段时间,HTML5在移动设备的大规模应用就会到来。 好的,梦醒了还是总结下在Android Webview的assets目录下开发Hybrid App的一些坑,和各种跳过或者踩过坑的方案吧。 【坑 #1】 部分Android 4.0设备(HTC、海尔等)LoadUrl不能识别?参数或#hashtag 项目开始的时候,web部分是单独开发的,为了以后加入更多模块的扩展考虑,这个项目采用了AngularJs MVVM前端框架和ui-router做页面路由,其实就是单页应用(SPA)。当然,web程序无论采用什么技术,传参数只有两种方式 (1) ?参数形式,如top.html?id=1 (2) #hashtag形式,如index.html#topic/1 如果把程序部署在server端,或者Android的assets目录下,使用Webview访问的方式为

1
2
3
4
mWebView =(WebView)findViewById(R.id.mb_webview);
// String url\_server = "http://www.awebird.com/demo\_project/index.html#demo/123";
String url\_assets = "file:///android\_asset/www/demo_project/index.html#demo/123";
mWebView.loadUrl(url_assets);

我们之前的项目已经大量使用了第一种url_server的方式进行开发,webview loadUrl远程链接也从没有出过问题,所以就顺理成章的开发着,但是等web端做的差不多了,部署到Android工程的assets目录下测试时,遇到一个非常棘手的问题,有一台HTC Android 4.0.3的测试机总是无法打开网页(Page not found) 1 由于Android开发侧的同事不懂Web,我这边对Android的了解也是汗毛级别,所以遇到这种问题的纠结与困惑就不赘述,直接给出“坑#1_desc” Android的issue 17535 (Issue 17535: WebView - URL mechanism is broken - passing parameters does not work) https://code.google.com/p/android/issues/detail?id=17535 打开这个页面,一阵惊叹,反正我是第一次在google groups或者stackoverflow上看到这么多老外对一个bug如此大面积的愤怒情绪爆发,这个密集程度快赶上国内门户网站中国足球新闻下的评论了Orz.. 具体bug不细描述,其实就是Android 3.0以后的webview在访问assets目录下的本地html资源时,带?参数和#hashtag的URL机制不能被识别,上面链接(#148楼)里2012年6月29号有google的开发人员回帖说这个问题已经在Jelly Bean被修复,当然这个只是Android代码的修复,所以现在市面上还是有很多手机的Android版本依然存在这个问题,就像我们不能强迫用户都改用Chrome,不能强迫国行Android用户root手机安装GMS服务包一样,我们也没办法让“找不到网页”的用户换手机或者升级系统,所以问题还是要被解决,下面提供三种方案,我们使用了最后一种 (1)从Android侧扩展webview类进行处理 相关的方案和jar包已经在下面的链接列出 Step1~6即可解决。由于要对项目java部分进行改动,所以我们这边app侧没有采纳次方案,但应该是可行的。多提一句,我这边测试过,Phonegap的开源版本Apache Cordova就已经修复了这个问题 http://bricolsoftconsulting.com/fixing-the-broken-honeycomb-and-ics-webview/ https://github.com/bricolsoftconsulting/WebViewIssue17535Fix (2)loadUrl(‘index.html’)后,再loadUrl一句js跳转语句的方法 这个方法,是得到V2EX的Archangel_SDY童鞋提醒,就是先只载入不带参数的页面(可以显示空白页或者loading效果),在页面载入后(onPageFinished),在loadUrl一句javascript用于跳转页面,下面的locationTo是在index.html写好的js函数,具体内容可以自己控制,只要接收到参数就ok Android侧 (onKeyDown和java interface是用于处理返回事件问题的)

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
mWebView = (WebView)findViewById(R.id.mb_webview);
String base\_url = file:///android\_asset/www/demo_project/index.html;
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
mWebView.loadUrl("javascript:locationTo(#demo/"+"123"+"')");
super.onPageFinished(view, url);
}
}
mWebView.loadUrl(base_url);

@Override
public boolean onKeyDown(int keyCode, KeyEvent event){
if(keyCode == KeyEvent.KEYCODE_BACK) {
//点击返回键时
if(mWebView.canGoBack()){
mWebView.goBack();// 返回前一个页面
}else{
finish();
}
return true;
}
return super.onKeyDown(keyCode, event);
}

//Javascript Interface
public class demo\_java\_interface{
/\*\*
\* 关闭当前页面
*/
public void closeCurrectActivity(){
finish();
}
}

Web侧index.html中js (sessionStorge和javascript Interface调用代码是处理手机返回键事件用的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
function locationTo(data_path){
if(!sessionStorage.getItem("once")){
sessionStorage.setItem("once", "1");
location.href=data_path;
}else{
//call demo\_java\_interface.goback()..
if(window.demo\_java\_interface){
window.demo\_java\_interface.closeCurrectActivity();
}else{
console.log('call java to closeCurrentActivity');
}
}
}
</script>

当然,由于我手上没有这个问题的测试机(Android开发在不同城市Orzzzz),其实也不能确认这个方法是否真正有效,因为插入了一个中间页面,所以带来了返回和跳转问题,我们很快放弃了这个方案,继续寻找其它方法 (3)重写onReceivedError()方法 #最终采纳并测试有效版本# 参见 http://stackoverflow.com/questions/6542702/basic-internal-links-dont-work-in-honeycomb-app/7297536#7297536 代码(只支持#hashtag)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mWebView = (WebView)findViewById(R.id.mb_webview);

String url = file:///android\_asset/www/demo\_project/index.html#demo/123;

@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
{
if (failingUrl.contains("#")) {
String\[\] temp;
temp = failingUrl.split("#");
mWebView.loadUrl(temp\[0\]);
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
mWebView.loadUrl(failingUrl);
}
}

mWebView.loadUrl(url);

**【坑 #2】所有Android Webview不能捕获#hashtag变化 Hybrid App中Android侧经常需要捕获webview中的url变化来做一些响应(比如改变header title的文字,处理终端的返回键等等) 但是无论是Server端的Url还是assets中的Url,webview都无法捕获到#hashtag的变化,也就是说index.html#step1 –> index.html#step2 –> index.html#step3 … 这些页面跳转是不能被Android监控到的。 这里提供两个解决方案 (1)在webapp里面使用javascript window.onhashchange里面调用Android的Javascript Interface方法,通知Android hash变化 http://stackoverflow.com/questions/15176519/android-webview-is-it-possible-to-detect-url-hash-change (2)自己在页面跳转逻辑里,根据需要调用Android的Javascript Interface方法实现需要的功能** 比如我这边用Angularjs,就在每个url对应的controller里面,调用Android侧的Javascript Interface方法,后来极端一点,由于不同页面功能点差别较大,直接废弃了web侧的ui-router功能,所有的跳转都交给java处理,也就是每个web页面(包括SPA中的一个状态)都交给Android的一个单独Activity,也就是做Activity的条状,Web侧的router白白浪费了T-T 其实上面的(1)和(2)是同一种方法,Android的Java和Webview里的web交互只有两种办法,一个是url捕获,一个是Javascript Interface,这里在#hashtag不能被url捕获到的情况下,就只能使用Javascript Interface了。 这样处理的一个结果就是Hybrid app里的web部分加入的java接口调用,如果脱离了App环境,要单独处理,或者通过检测Javascript Interface对象是否存在,来决定是采用web方法还是app方法跳转

1
2
3
4
5
6
7
8
9
$rootScope.Java_go2step2 = function){
if(window.demo\_java\_interface_obj){
//如果java interface对象存在,调用Java方法跳转Activity
window.demo\_java\_interface_obj.go2step2);
}else{
//否则,使用web跳转
$state.go('step2');
}
}

总之,不完美… 【坑 #3】Android 2.X Webview不能Scroll滑动 发现有HTML5的页面在Android 4.x下一切正常,在Android 2.X下不能滑动(Scroll)的问题 这个问题无疑是Android的 bug 目前有两个解决方案 (1)Html中去掉meta viewport tag (未经验证)

1
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0" />

http://stackoverflow.com/questions/10552702/cant-scroll-webview-in-android 由于viewport对于移动端的页面适配很重要,前端的同学说不能去掉,试过去掉后样式乱掉了,所以没有采用此方案 (2)Android中的layout布局,将webview布局在ScrollView中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ScrollView
android:id="@+id/scrollView1"
android:layout\_width="fill\_parent"
android:layout\_height="wrap\_content"
android:layout_above="@+id/tab"
android:layout_alignParentTop="true"
android:background="#00000000"
android:scrollbars="none" >
<LinearLayout
android:layout\_width="fill\_parent"
android:layout\_height="fill\_parent"
android:background="#00000000"
android:orientation="vertical" >
<WebView
android:id="@+id/mb_webview"
android:layout\_width="fill\_parent"
android:layout\_height="wrap\_content" />
</LinearLayout>
</ScrollView>

这是Android侧的方案,上面的布局xml只是一个简单demo,web童鞋遇到了可以跟app开发的童鞋沟通解决。我们采用的是此方案 【—–Scroll问题后续——】 后续开发过程中,Android的同事说不能使用上面的ScrollView布局,因为了整体的布局架构有冲突,所以这个问题再次无解,下面是后ScrollView时代的血泪史 (1)采用iScroll angulrjs可以使用ng-iscroll iscroll可以让webapp乍一看很有原生的感觉,但是我个人认为在webview存在不少问题,而且体验也不够顺畅,除非下拉刷新获取更多数据这种场景,应该尽量不用 使用iscroll后测试确实可以在Android 2.3的手机上Scroll,但是有个问题,是因为我们的页面是先载入,在动态$http.post获取数据,更新页面内容的,所以Scroll的范围特别是高度总是会出现可以滑动到最底部,但是又弹回上面的问题.. 后来分析是isroll的区域判断了初始页面的位置,所以在$http.post的数据获取后,$timeout延时refresh myscroll 上面的方案貌似解决了,但是在webview下又出现了页面闪烁的现象,而且scroll的体验很不好 (2)最后痛定思痛,还是删除了所有的iscroll部分,重新寻找原因,重要发现元凶

1
overflow:hidden

是他,是他,就是他! [原因] Android 2.X不支持这句css [解决方案] 去掉,加上 height:auto [参考] Scrollable DIV on mobile phones 【还在坑里没爬出来的总结…】 通过这个Hybrid App项目的实践,我们遇到了不少麻烦,HTML5在移动端的应用已经基本成熟,但是还有一些问题,让我们再耐心一点,做好准备,等Android特别是webview再成熟一点,等手机终端再快一点,等大家发了压岁钱、年终奖都换了新手机… 很多问题就自愈了(同时一大波新问题随之到来)LOL 本文地址:http://awebird.com/blog/art/190/