在Android中。JSBridge已经不是什么新奇的事物了,各家的实现方式也略有差异。

大多数人都知道WebView存在一个漏洞。见WebView中接口隐患与手机挂马利用,尽管该漏洞已经在Android 4.2上修复了,即使用@JavascriptInterface取代addJavascriptInterface,可是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现。所以我们仅仅能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本号的方案。

首先我们来了解一下为什么要使用JSBridge,在开发中。为了追求开发的效率以及移植的便利性,一些展示性强的页面我们会偏向于使用h5来完毕。功能性强的页面我们会偏向于使用native来完毕。而一旦使用了h5,为了在h5中尽可能的得到native的体验,我们native层须要暴露一些方法给js调用,比方,弹Toast提醒。弹Dialog,分享等等,有时候甚至把h5的网络请求放着native去完毕,而JSBridge做得好的一个典型就是微信。微信给开发人员提供了JSSDK,该SDK中暴露了非常多微信native层的方法,比方支付,定位等。

那么。怎么去实现一个兼容Android各版本号又具有一定安全性的JSBridge呢?我们知道。在WebView中,假设java要调用js的方法。是非常easy做到的,使用WebView.loadUrl(“javascript:function()”)就可以,这样。就做到了JSBridge的native层调用h5层的单向通信,可是h5层怎样调native层呢,我们须要寻找这么一个通道。细致回顾一下,WebView有一个方法,叫setWebChromeClient,能够设置WebChromeClient对象,而这个对象中有三个方法。各自是onJsAlert,onJsConfirm,onJsPrompt。当js调用window对象的相应的方法,即window.alertwindow.confirmwindow.prompt,WebChromeClient对象中的三个方法相应的就会被触发,我们是不是能够利用这个机制,自己做一些处理呢?答案是肯定的。

至于js这三个方法的差别,能够详见w3c JavaScript 消息框 。一般来说,我们是不会使用onJsAlert的,为什么呢?由于js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响。而confirm和prompt的使用频率相对alert来说,则更低一点。那么究竟是选择confirm还是prompt呢,事实上confirm的使用频率也是不低的,比方你点一个链接下载一个文件,这时候假设须要弹出一个提示进行确认。点击确认就会下载。点取消便不会下载,相似这种场景还是非常多的,因此不能占用confirm。而prompt则不一样,在Android中。差点儿不会使用到这种方法,就是用。也会进行自己定义。所以我们全然能够使用这种方法。该方法就是弹出一个输入框。然后让你输入,输入完毕后返回输入框中的内容。因此。占用prompt是再完美只是了。

到这一步,我们已经找到了JSBridge双向通信的一个通道了。接下来就是怎样实现的问题了。本文中实现的仅仅是一个简单的demo,假设要在生产环境下使用。还须要自己做一层封装。

要进行正常的通信,通信协议的制定是不可缺少的。

我们回忆一下熟悉的http请求url的组成部分。

形如http://host:port/path?param=value。我们參考http,制定JSBridge的组成部分,我们的JSBridge须要传递给native什么信息,native层才干完毕相应的功能,然后将结果返回呢?显而易见我们native层要完毕某个功能就须要调用某个类的某个方法,我们须要将这个类名和方法名传递过去。此外,还须要方法调用所需的參数,为了通信方便。native方法所需的參数我们规定为json对象。我们在js中传递这个json对象过去。native层拿到这个对象再进行解析就可以。为了差别于http协议,我们的jsbridge使用jsbridge协议,为了简单起见,问号后面不适用键值对。我们直接跟上我们的json字符串,于是就有了形如以下的这个uri

jsbridge://className:port/methodName?

jsonObj

有人会问,这个port用来干嘛,事实上js层调用native层方法后,native须要将运行结果返回给js层。只是你会认为通过WebChromeClient对象的onJsPrompt方法将返回值返回给js不就好了吗,事实上不然,假设这么做,那么这个过程就是同步的。假设native运行异步操作的话,返回值怎么返回呢?这时候port就发挥了它应有的作用。我们在js中调用native方法的时候,在js中注冊一个callback,然后将该callback在指定的位置上缓存起来,然后native层运行完毕相应方法后通过WebView.loadUrl调用js中的方法,回调相应的callback。那么js怎么知道调用哪个callback呢?于是我们须要将callback的一个存储位置传递过去,那么就须要native层调用js中的方法的时候将存储位置回传给js,js再调用相应存储位置上的callback,进行回调。

于是,完整的协议定义例如以下:

jsbridge://className:callbackAddress/methodName?

jsonObj

假设我们须要调用native层的Logger类的log方法。当然这个类以及方法肯定是遵循某种规范的,不是全部的java类都能够调用。不然就跟文章开头的WebView漏洞一样了,參数是msg。运行完毕后js层要有一个回调。那么地址就例如以下

jsbridge://Logger:callbackAddress/log?

{"msg":"native log"}

至于这个callback对象的地址。能够存储到js中的window对象中去。至于怎么存储,后文会慢慢倒来。

上面是js向native的通信协议,那么另一方面,native向js的通信协议也须要制定,一个不可缺少的元素就是返回值,这个返回值和js的參数做法一样。通过json对象进行传递。该json对象中有状态码code提示信息msg,以及返回结果result。假设code为非0,则运行过程中发生了错误,错误信息在msg中,返回结果result为null。假设运行成功,返回的json对象在result中。以下是两个样例。一个成功调用,一个调用失败。

{
"code":500,
"msg":"method is not exist",
"result":null
}
{
"code":0,
"msg":"ok",
"result":{
"key1":"returnValue1",
"key2":"returnValue2",
"key3":{
"nestedKey":"nestedValue"
"nestedArray":["value1","value2"]
}
}
}

那么这个结果怎样返回呢。native调用js暴露的方法就可以。然后将js层传给native层的port一并带上,进行调用就可以,调用的方式就是通过WebView.loadUrl方式来完毕,例如以下。

mWebView.loadUrl("javascript:JSBridge.onFinish(port,jsonObj);");

关于JsBridge.onFinish方法的实现。后面再叙述。前面我们提到了native层的方法必须遵循某种规范。不然就非常不安全了。在native中,我们须要一个JSBridge统一管理这些暴露给js的类和方法,而且能实时增加,这时候就须要这么一个方法

JSBridge.register("jsName",javaClass.class)

这个javaClass就是满足某种规范的类,该类中有满足规范的方法,我们规定这个类须要实现一个空接口,为什么呢?主要作用就混淆的时候不会错误发生,另一个作用就是约束JSBridge.register方法第二个參数必须是该接口的实现类。那么我们定义这个接口

public interface IBridge{
}

类规定好了。类中的方法我们还须要规定,为了调用方便,我们规定类中的方法必须是static的,这样直接依据类而不必新建对象进行调用了(还要是public的)。然后该方法不具有返回值,由于返回值我们在回调中返回,既然有回调,參数列表就肯定有一个callback。除了callback,当然还有前文提到的js传来的方法调用所需的參数,是一个json对象,在java层中我们定义成JSONObject对象;方法的运行结果须要通过callback传递回去。而java运行js方法须要一个WebView对象。于是,满足某种规范的方法原型就出来了。

public static void methodName(WebView web view,JSONObject jsonObj,Callback callback){

}

js层除了上文说到的JSBridge.onFinish(port,jsonObj);方法用于回调。应该另一个方法提供调用native方法的功能,该函数的原型例如以下

JSBridge.call(className,methodName,params,callback)

在call方法中再将參数组合成形如以下这个格式的uri

jsbridge://className:callbackAddress/methodName?jsonObj

然后调用window.prompt方法将uri传递过去,这时候java层就会收到这个uri,再进一步解析就可以。

万事具备了,仅仅欠怎样编码了,别急,以下我们一步一步的来实现,先完毕js的两个方法。新建一个文件,命名为JSBridge.js

(function (win) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
var JSBridge = win.JSBridge || (win.JSBridge = {});
var JSBRIDGE_PROTOCOL = 'JSBridge';
var Inner = {
callbacks: {},
call: function (obj, method, params, callback) {
console.log(obj+" "+method+" "+params+" "+callback);
var port = Util.getPort();
console.log(port);
this.callbacks[port] = callback;
var uri=Util.getUri(obj,method,params,port);
console.log(uri);
window.prompt(uri, "");
},
onFinish: function (port, jsonObj){
var callback = this.callbacks[port];
callback && callback(jsonObj);
delete this.callbacks[port];
},
};
var Util = {
getPort: function () {
return Math.floor(Math.random() * (1 << 30));
},
getUri:function(obj, method, params, port){
params = this.getParam(params);
var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
return uri;
},
getParam:function(obj){
if (obj && typeof obj === 'object') {
return JSON.stringify(obj);
} else {
return obj || '';
}
}
};
for (var key in Inner) {
if (!hasOwnProperty.call(JSBridge, key)) {
JSBridge[key] = Inner[key];
}
}
})(window);

能够看到。我们里面有一个Util类,里面有三个方法。getPort()用于随机生成port,getParam()用于生成json字符串。getUri()用于生成native须要的协议uri,里面主要做字符串拼接的工作,然后有一个Inner类,里面有我们的call和onFinish方法,在call方法中,我们调用Util.getPort()获得了port值,然后将callback对象存储在了callbacks中的port位置,接着调用Util.getUri()将參数传递过去。将返回结果赋值给uri。调用window.prompt(uri, “”)将uri传递到native层。而onFinish()方法接受native回传的port值和运行结果,依据port值从callbacks中得到原始的callback函数,运行callback函数,之后从callbacks中删除。最后将Inner类中的函数暴露给外部的JSBrige对象。通过一个for循环一一赋值就可以。

当然这个实现是最最简单的实现了。实际情况要考虑的因素太多,由于本人不是非常精通js,所以仅仅能以java的思想去写js,没有考虑到的因素姑且忽略吧。比方内存的回收等等机制。

这样,js层的编码就完毕了,接下来实现java层的编码。

上文说到java层有一个空接口来进行约束暴露给js的类和方法,同一时候也便于混淆

public interface IBridge {
}

首先我们要将js传来的uri获取到,编写一个WebChromeClient子类。

public class JSBridgeWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(JSBridge.callJava(view, message));
return true;
}
}

之后不要忘记了将该对象设置给WebView

WebView mWebView = (WebView) findViewById(R.id.webview);
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
mWebView.setWebChromeClient(new JSBridgeWebChromeClient());
mWebView.loadUrl("file:///android_asset/index.html");

核心的内容来了。就是JSBridgeWebChromeClient中调用的JSBridge类的实现。

前文提到该类中有这么一个方法提供注冊暴露给js的类和方法

JSBridge.register("jsName",javaClass.class)

该方法的实现事实上非常easy,从一个Map中查找key是不是存在,不存在则反射拿到相应的Class中的全部方法。将方法是public static void 类型的。而且參数是三个參数,各自是Webview,JSONObject。Callback类型的,假设满足条件。则将全部满足条件的方法put进去,整个实现例如以下

public class JSBridge {
private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>(); public static void register(String exposedName, Class<? extends IBridge> clazz) {
if (!exposedMethods.containsKey(exposedName)) {
try {
exposedMethods.put(exposedName, getAllMethod(clazz));
} catch (Exception e) {
e.printStackTrace();
}
}
} private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
HashMap<String, Method> mMethodsMap = new HashMap<>();
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method : methods) {
String name;
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
continue;
}
Class[] parameters = method.getParameterTypes();
if (null != parameters && parameters.length == 3) {
if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == Callback.class) {
mMethodsMap.put(name, method);
}
}
}
return mMethodsMap;
}
}

而至于JSBridge类中的callJava方法,就是将js传来的uri进行解析,然后依据调用的类名别名从刚刚的map中查找是不是存在。存在的话拿到该类全部方法的methodMap。然后依据方法名从methodMap拿到方法,反射调用。并将參数传进去。參数就是前文说的满足条件的三个參数,即WebView,JSONObject。Callback。

 methodHashMap = exposedMethods.get(className);

            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if (method != null) {
try {
method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return null;
}" data-snippet-id="ext.7245ba2e634a08805a3436f575725685" data-snippet-saved="false" data-codota-status="done">public static String callJava(WebView webView, String uriString) {
String methodName = "";
String className = "";
String param = "{}";
String port = "";
if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
Uri uri = Uri.parse(uriString);
className = uri.getHost();
param = uri.getQuery();
port = uri.getPort() + "";
String path = uri.getPath();
if (!TextUtils.isEmpty(path)) {
methodName = path.replace("/", "");
}
} if (exposedMethods.containsKey(className)) {
HashMap<String, Method> methodHashMap = exposedMethods.get(className); if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if (method != null) {
try {
method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return null;
}

看到该方法中使用了 new Callback(webView, port)进行新建对象。该对象就是用来回调js中回调方法的java相应的类。这个类你须要将js传来的port传进来之外,还须要将WebView的引用传进来,由于要使用到WebView的loadUrl方法,为了防止内存泄露,这里使用弱引用。假设你须要回调js的callback,在相应的方法里调用一下callback.apply()方法将返回数据传入就可以,

 mWebViewRef;

    public Callback(WebView view, String port) {
mWebViewRef = new WeakReference(view);
mPort = port;
} public void apply(JSONObject jsonObject) {
final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
if (mWebViewRef != null && mWebViewRef.get() != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mWebViewRef.get().loadUrl(execJs);
}
}); } }
}
" data-snippet-id="ext.5fc9ec3243b9e8d49c59bbae9af4abb3" data-snippet-saved="false" data-codota-status="done">public class Callback {
private static Handler mHandler = new Handler(Looper.getMainLooper());
private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
private String mPort;
private WeakReference<WebView> mWebViewRef; public Callback(WebView view, String port) {
mWebViewRef = new WeakReference<>(view);
mPort = port;
} public void apply(JSONObject jsonObject) {
final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
if (mWebViewRef != null && mWebViewRef.get() != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mWebViewRef.get().loadUrl(execJs);
}
}); } }
}

唯一须要注意的是apply方法我把它扔在主线程运行了,为什么呢,由于暴露给js的方法可能会在子线程中调用这个callback,这种话就会报错,所以我在方法内部将其切回主线程。

编码完毕的差点儿相同了,那么就剩实现IBridge就可以了,我们来个简单的。就来显示Toast为例好了,显示完给js回调。尽管这个回调没有什么意义。

public class BridgeImpl implements IBridge {
public static void showToast(WebView webView, JSONObject param, final Callback callback) {
String message = param.optString("msg");
Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
if (null != callback) {
try {
JSONObject object = new JSONObject();
object.put("key", "value");
object.put("key1", "value1");
callback.apply(getJSONObject(0, "ok", object));
} catch (Exception e) {
e.printStackTrace();
}
}
} private static JSONObject getJSONObject(int code, String msg, JSONObject result) {
JSONObject object = new JSONObject();
try {
object.put("code", code);
object.put("msg", msg);
object.putOpt("result", result);
return object;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}

你能够往该类中扔你须要的方法。可是必须是public static void且參数列表满足条件,这样才干找到该方法。

不要忘记将该类注冊进去

JSBridge.register("bridge", BridgeImpl.class);

进行一下简单的測试,将之前实现好的JSBridge.js文件扔到assets文件夹下,然后新建index.html。输入


    JSBridge

JSBridge 測试

  • 測试showToast
" data-snippet-id="ext.72aeb753849e78157e5829319fd466ef" data-snippet-saved="false" data-codota-status="done"><!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>JSBridge</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>
<script src="file:///android_asset/JSBridge.js" type="text/javascript"></script>
<script type="text/javascript"> </script>
<style> </style>
</head> <body>
<div>
<h3>JSBridge 測试</h3>
</div>
<ul class="list">
<li>
<div>
<button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">
測试showToast
</button>
</div>
</li>
<br/>
</ul>
</body>
</html>

非常easy,就是按钮点击时调用JSBridge.call()方法,回调函数是alert出返回的结果。

接着就是使用WebView将该index.html文件load进来測试了

mWebView.loadUrl("file:///android_asset/index.html");

效果例如以下图所看到的

能够看到整个过程都走通了,然后我们測试下子线程回调,在BridgeImpl中增加測试方法

public static void testThread(WebView webView, JSONObject param, final Callback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
JSONObject object = new JSONObject();
object.put("key", "value");
callback.apply(getJSONObject(0, "ok", object));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}).start();
}

在index.html中增加

  • 測试子线程回调
  • " data-snippet-id="ext.b1809e021627d8303574ede18b360951" data-snippet-saved="false" data-codota-status="done"><ul class="list">
    <li>
    <div>
    <button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">
    測试子线程回调
    </button>
    </div>
    </li>
    <br/>
    </ul>

    理想的效果应该是3秒钟之后回调弹出alert显示

    非常完美,代码也不多,就实现了功能。假设你须要使用到生成环境中去,上面的代码你一定要再自己封装一下,由于我仅仅是简单的实现了功能。其它因素并没有考虑太多。

    当然你也能够參考一个开源的实现

    Safe Java-JS WebView Bridge

    最后还是惯例,贴上代码

    http://download.csdn.net/detail/sbsujjbcy/9446915

    Android JSBridge的原理与实现的更多相关文章

    1. WebView JS交互 JSBridge 案例 原理 MD

      Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

    2. Hybrid APP基础篇(四)->JSBridge的原理

      说明 JSBridge实现原理 目录 前言 参考来源 前置技术要求 楔子 原理概述 简介 url scheme介绍 实现流程 实现思路 第一步:设计出一个Native与JS交互的全局桥对象 第二步:J ...

    3. Android Touch事件原理加实例分析

      Android中有各种各样的事件,以响应用户的操作.这些事件可以分为按键事件和触屏事件.而Touch事件是触屏事件的基础事件,在进行Android开发时经常会用到,所以非常有必要深入理解它的原理机制. ...

    4. android handler工作原理

      android handler工作原理 作用 便于在子线程中更新主UI线程中的控件 这里涉及到了UI主线程和子线程 UI主线程 它很特别.通常我们会认为UI主线程将页面绘制完成,就结束了.但是它没有. ...

    5. android MultiDex multidex原理原理下遇见的N个深坑(二)

      android MultiDex 原理下遇见的N个深坑(二) 这是在一个论坛看到的问题,其实你不知道MultiDex到底有多坑. 不了解的可以先看上篇文章:android MultiDex multi ...

    6. android MultiDex multiDex原理(一)

      android MultiDex 原理(一) Android分包MultiDex原理详解 转载请注明:http://blog.csdn.net/djy1992/article/details/5116 ...

    7. 解析 Android Things 技术原理

      2012 年 6 月,由 IoT-GSI(Global Standards Initiative on Internet of Things)发布的白皮书“ITU-T Y.4000/Y.2060”[1 ...

    8. Atitit.android  jsbridge v1新特性

      Atitit.android  jsbridge v1新特性 1. Java代码调用js并传参其实是通过WebView的loadUrl方法去调用的.只是参数url的写法不一样而已1 2. 三.JAVA ...

    9. Android JSBridge原理与实现

      在Android中,JSBridge已经不是什么新鲜的事物了,各家的实现方式也略有差异.大多数人都知道WebView存在一个漏洞,详细信息见你不知道的 Android WebView 使用漏洞,虽然该 ...

    随机推荐

    1. python3 UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f9e0' in position 230: illegal multibyte sequence

      最近在保存微博数据到(csv文件)时报错: UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f9e0' in positio ...

    2. 敏捷方法之极限编程(XP)和 Scrum区别

      敏捷(Agile)作为一种开发流程, 目前为各大公司所采用, 敏捷流程的具体实践有XP 和Scrum, 似乎很少有文章介绍这两者的区别, 发现一篇外文, 见解非常深刻, 特将其翻译一把. 原文(DIF ...

    3. cognos report上钻下钻报表处理方法(1)

      cognos  report开发中追溯行为,也可以称为上钻下钻行为大致遇到了两种情况 第一种:根据当前报表样式在维度范围内上钻下钻. 第二种:给追溯行为指定报表,传递参数. 可能还有其他情况,这里就不 ...

    4. Mac 苹果OS X小技巧:如何更改文件的默认打开方式

      OS X小技巧:如何更改文件的默认打开方式 1.command + i 打开简介 2.选择合适的软件打开方式 3.选择全部更改 如图: 转自:http://digi.tech.qq.com/a/201 ...

    5. XTU1236 Fraction

      Fraction Accepted : 124 Submit : 806 Time Limit : 1000 MS Memory Limit : 65536 KB Fraction Problem D ...

    6. Jmeter测试报告可视化(Excel, html以及jenkins集成)

      做性能测试通常在none GUI的命令行模式下运行Jmeter. 例如: jmeter -n -t /opt/las/JMeter/TestPlan/test.jmx -l /opt/las/JMet ...

    7. Android Bundle存储数据类型

      曾经被问到这样一个问题:Bundle能存哪些数据类型,不能存哪些数据类型? 当时那个汗啊,因为,平常使用Bundle,要么使用基本数据类型,要么序列化自定义的Class,那到底能存哪些类型,不能存哪些 ...

    8. 触发器五(建立INSTEAD OF触发器)(学习笔记)

      INSTEAD OF触发器 对于简单视图,可以直接执行INSERT,UPDATE和DELETE操作但是对于复杂视图,不允许直接执行INSERT,UPDATE和DELETE操作.为了在具有以上情况的复杂 ...

    9. MySQL 读写分离 使用驱动com.mysql.jdbc.ReplicationDriver

      说明文档:http://dev.mysql.com/doc/refman/5.1/en/connector-j-reference-replication-connection.html 代码例子: ...

    10. Bootstrap系列 -- 15. 下拉选择框select【转发】

      <form role="form"> <div class="form-group"> <select class="f ...