Talking about the Mixed Development of Android App

Keywords: Android SDK Java JSON

From: Click Open Link

Hybrid App (Hybrid App) is a lightweight browser embedded in an App. Some native functions are developed by Html 5. This part of the functions can not only be updated dynamically without updating App, but also run simultaneously on Android or iOS App, making the user experience better and saving development resources.

Now let's talk about the technical issues in the development of Hybrid App. I don't know much about iOS. I'm going to talk about Android development. There may be a lot of mistakes in it. Please spray it lightly.

What are the key issues in Hybrid development

To display the function of a Html 5 page in an App, it is very simple, just a WebView. You can click on the link to jump the page. Can a function like this be called Hybrid development? Obviously not.

I think the function that a Hybrid develops in App must have is how the Html 5 page interacts with Native App. For example, if I click a button or link on the Html 5 page, can I jump to a page of Native App, if I click the share button on the Html 5 page, can I call the share function of Native App, if I can get the user information of Native App when Html loads, etc.

Look at the picture below. When you enter this Html 5 page in NetEasy Cloud Music, you click on the author: Void Editor, you will enter his home page. This home page is Native page. When you click on the play button above, the cloud concert starts Native's play interface to play music. When you click on the comment, you will enter Native's comment page.

Interaction between Html 5 and Native

WebView already supports js and Java calling each other. You just need to turn on the JavaScript script execution of WebView and then use the code. MWebView. add JavascriptInterface (new JsBridge (), "bxbxbai"); inject a Java object into the Html 5 page, and then you can call Native's function in the Html 5 page.

How did Wechat do it?

Wechat should be one of the best apps developed by Hybrid. How does it interact?

The answer is Wechat JS-SDK. As you can see from the developer's documentation, Wechat JS-SDK encapsulates various functions of Wechat, such as sharing friends circle, image interface, audio interface, payment interface geographic location interface and so on. Developers only need to call the functions in Weichat JS-SDK, and then call the functions in Weichat by JS-SDK. The advantage is that I have written an application or web page of Html 5, which can run normally in Android and iOS micro-letters.

I'll talk about it in detail below.

How does Netease Cloud Music Do

So how does Netease Cloud Music do? I used black technology to know that the interface activity of cloud music above is CommonSubject Activity. EmbedBrowser Activity, I find the corresponding function implementation code in the decompiled cloud music code, but I can't find it. But I got the address of the Html 5 page: http://music.163.com/m/topic/194001

When I opened it with Chrome, I found it was different from what was shown in App. Then I intercepted the request to enter Html 5 with Charles and found that the address of cloud music loading was http://music.163.com/m/topic/194001?type=android That's the type of mobile phone system added.

Then load this Html 5 page in my own App and you can see the following picture: @Xiaobi says that such a text can be clicked to jump to a person, clicking the Play button can play music.

From the Html source code, you can see the following information:

That is, when I click on a username, I request to jump to orpheus://user/30868859, because WebView can intercept jumping urls, so App is intercepting every url if host is orpheus launches the user's home page

After decompiling the code, you find this.mWebView.setWebViewClient(new cf(this)) in the code of cloud music; in this sentence, enter cf class, found the following code:

public boolean shouldOverrideUrlLoading(WebView webView, String url) {
	if (url.startsWith("orpheus://")) {
		RedirectActivity.a(this.activity, url);
		return true;
	}
	if ((url.toLowerCase().startsWith("http://")) || (url.toLowerCase().startsWith("https://"))) {
		return false;
	}
	try {
		this.activity.startActivity(new Intent("android.intent.action.VIEW", Uri.parse(url)));
		return true;
	} catch (ActivityNotFoundException localActivityNotFoundException) {
		localActivityNotFoundException.printStackTrace();
	}
	return true;
}

Sure enough, go back to Redirect Activity, an Activeness without any interface, dedicated to handling page Jump information, which calls a method NeteaseMusicUtils. redirect (this, getIntent (). getData (). toString (). false) handles url s. The name of the redirect method was written by me, and part of the code is as follows:

You can see that the user id in orpheus://user/30868859 is passed into Profile Acvitiy, so the user's home page is started to display user information.

Then I wrote my own code to intercept the jump of Html 5 and printed the following logs:

You can see that the Html 5 page can jump to various pages, such as the user's home page, playing music, MV interface, comment page, radio programs and so on.

summary

Generally speaking, the two main ways that I now know are

  1. js calls the code in Native
  2. Schema: WebView intercepts page jumps

The second way is easy to implement, but a fatal problem is that the interaction mode is one-way, and Html 5 cannot implement callbacks. Like the click-and-jump function in Cloud Music App, Schema can be easily implemented and is very suitable. If the requirements become complex and if Html 5 needs to get user information in Native App, it's better to use js calls.

js and Native interact

As mentioned above, WebViewbe itself supports js to call Native code, but this feature of WebView has a high-risk vulnerability under Android 4.2 (API 17). The principle of this vulnerability is that the Android system passes through. The WebView.addJavascriptInterface(Object o, String interface) method registers Java objects that can be invoked by js, but the system does not restrict the invocation of registered Java object methods. This allows an attacker to invoke any other unregistered Java object using reflection, and the attacker can do anything according to the client's capabilities. This article This vulnerability is described in detail.

For security reasons, the system after Android 4.2 stipulates that Java methods allowed to be invoked by js must be annotated with @JavascriptInterface

Cordova's Solution

Cordova is a widely used Hybrid development framework that provides a set of js and Native interaction specifications

You can see it in Cordova's SystemWebViewEngine class

private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
	if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
		Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
		// Bug being that Java Strings do not get converted to JS strings automatically.
		// This isn't hard to work-around on the JS side, but it's easier to just
		// use the prompt bridge instead.
		return;
	}
	webView.addJavascriptInterface(new SystemExposedJsApi(bridge), "_cordovaNative");
}

So when the Android system is higher than 4.2, Cordova still uses the addJavascriptInterface method, because this method is safe and simple in the high version, when it is lower than 4.2, what method?

The answer is the WebChromeClient.onJsPrompt method

WebView can set up a WebChromeClient object, which can handle three methods of js

  • onJsAlert
  • onJsConfirm
  • onJsPrompt

These three methods correspond to alert, confirm, prompt methods of js, because only prompt receives the return value, so JS can wait for Native to return a parameter after calling a Native method. Here's a piece of code from cordova.js:

/**
* Implements the API of ExposedJsApi.java, but uses prompt() to communicate.
* This is used pre-JellyBean, where addJavascriptInterface() is disabled.
*/
module.exports = {
	exec: function(bridgeSecret, service, action, callbackId, argsJson) {
		return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
	},
	setNativeToJsBridgeMode: function(bridgeSecret, value) {
		prompt(value, 'gap_bridge_mode:' + bridgeSecret);
	},
	retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) {
		return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret);
	}
};

Then just use the CordovaBridge in the onJsPrompt method to handle the prompt call of js

/**
 * Tell the client to display a prompt dialog to the user. If the client returns true, WebView will assume that the client will handle the prompt dialog and call the appropriate JsPromptResult method.
 * <p/>
 * Since we are hacking prompts for our own purposes, we should not be using them for this purpose, perhaps we should hack console.log to do this instead!
 */
@Override
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
	// Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.
	String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
	if (handledRet != null) {
		result.confirm(handledRet);
	} else {
		dialogsHelper.showPrompt(message, defaultValue, new CordovaDialogsHelper.Result() {
			@Override
			public void gotResult(boolean success, String value) {
				if (success) {
					result.confirm(value);
				} else {
					result.cancel();
				}
			}
		});
	}
	return true;
}

An Open Source Solution

Cordova is an open source solution for Apache, but it requires xml to configure Cordova Plugin information, which is cumbersome to use, and the framework is heavy. Please search for Cordova using tutorials by yourself.

The following open source project is a more reasonable solution and lightweight solution for me personally. The following figure is a Demo.

https://github.com/pedant/safe-java-js-webview-bridge

The principle of this project is to use WebChromeClient.onJsPrompt method to interact. Essentially, js calls prompt function to transfer some parameters. The onJsPrompt method intercepts the prompt action, parses the data, and finally calls the corresponding Native method.

The HostJsScope class defines all methods that can be invoked by js. These methods must be static methods, and the first parameter of all methods must be WebView.

/**
* HostJsScope Functions that need to be invoked by JS must be defined as public static and must contain the parameter WebView
*/
public class HostJsScope {
	/**
	* Short bubble warning
	* @param webView Browser
	* @param message Tips
	* */
	public static void toast(WebView webView, String message) {
		Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
	}
	/**
	* System pop-up prompt box
	* @param webView Browser
	* @param message Tips
	* */
	public static void alert(WebView webView, String message) {
		// Build a Builder to display alert dialog boxes in web pages
		AlertDialog.Builder builder = new AlertDialog.Builder(webView.getContext());
		builder.setPositiveButton(android.R.string.ok, new AlertDialog.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which) {
				dialog.dismiss();
			}
		});
		builder.setTitle("Hello world")
			.setMessage(message)
			.setCancelable(false)
			.create()
			.show();
	}
	// Other code
}

The above code lists the most basic functions of clicking the Html 5 button to pop up the dialog box.

One of the most critical parts of this library is called JsCallJava, which implements the function of js to call Java methods. This class is only used for Injected WebChromeClient classes.

public class InjectedChromeClient extends WebChromeClient {
	private JsCallJava mJsCallJava;
	private boolean mIsInjectedJS;
	public InjectedChromeClient(String injectedName, Class injectedCls) {
		this(new JsCallJava(injectedName, injectedCls));
	}
	public InjectedChromeClient(JsCallJava jsCallJava) {
		mJsCallJava = jsCallJava;
	}
	// HandleAlertEvent
	@Override
	public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
		result.confirm();
		return true;
	}
	@Override
	public void onProgressChanged(WebView view, int newProgress) {
		//Why inject it here?JS
		//1 OnPageStartedIt is possible that global injection is unsuccessful, resulting in all interfaces on page scripts being unavailable at all times.
		//2 OnPageFinishedIn the end, although the global injection will succeed, the completion time may be too late, and the page will wait too long when the interface function is initially invoked.
		//3 When the progress changes, injection can just get a compromise between the above two problems.
		//Why is progress greater than25%Injection is only done, because from the test point of view only when the progress is greater than this number of pages can the framework refresh load really be guaranteed.100%Successful injection
		if (newProgress <= 25) {
			mIsInjectedJS = false;
		} else if (!mIsInjectedJS) {
			view.loadUrl(mJsCallJava.getPreloadInterfaceJS());
			mIsInjectedJS = true;
			StopWatch.log(" inject js interface completely on progress " + newProgress);
		}
		super.onProgressChanged(view, newProgress);
	}
	@Override
	public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
		result.confirm(mJsCallJava.call(view, message));
		StopWatch.log("onJsPrompt: " + view.toString() +", " + url +", " + message +", " + defaultValue + ", " + result) ;
		return true;
	}
}   

This Injected Web Chrome Client is set up for WebView. Here is a very important detail that needs to be noticed. In the onProgressChange method, a js code is injected into WebView, which is as follows:

javascript: (function(b) {
	console.log("HostApp initialization begin");
	var a = {
		queue: [],
		callback: function() {
			var d = Array.prototype.slice.call(arguments, 0);
			var c = d.shift();
			var e = d.shift();
			this.queue[c].apply(this, d);
			if (!e) {
				delete this.queue[c]
			}
		}
	};
	a.alert = a.alert = a.alert = a.delayJsCallBack = a.getIMSI = a.getOsSdk = a.goBack = a.overloadMethod = a.overloadMethod 
		= a.passJson2Java = a.passLongType = a.retBackPassJson = a.retJavaObject = a.testLossTime = a.toast = a.toast = function() {
		var f = Array.prototype.slice.call(arguments, 0);
		if (f.length < 1) {
			throw "HostApp call error, message:miss method name"
		}
		var e = [];
		for (var h = 1; h < f.length; h++) {
			var c = f[h];
			var j = typeof c;
			e[e.length] = j;
			if (j == "function") {
				var d = a.queue.length;
				a.queue[d] = c;
				f[h] = d
			}
		}
		var g = JSON.parse(prompt(JSON.stringify({
			method: f.shift(),
			types: e,
			args: f
		})));
		if (g.code != 200) {
			throw "HostApp call error, code:" + g.code + ", message:" + g.result
		}
		return g.result
	};
	//Sometimes, we want to insert some other behavior before the method is executed to check the current state or monitor it.
	//Code behavior, which requires the use of Interception or Injection Technology
	/**
	 * Object.getOwnPropertyName Returns an array of all properties of the specified object
	 *
	 * Then traverse the array and do the following processing separately:
	 * 1. Back up the original properties;
	 * 2. Check whether the attribute is a function (that is, a method);
	 * 3. If you redefine the method, do what you need to do, and then apply the original method.
	 */
	Object.getOwnPropertyNames(a).forEach(function(d) {
		var c = a[d];
		if (typeof c === "function" && d !== "callback") {
			a[d] = function() {
				return c.apply(a, [d].concat(Array.prototype.slice.call(arguments, 0)))
			}
		}
	});
	b.HostApp = a;
	console.log("HostApp initialization end")
})(window);

So how is this js code generated? The answer is in the constructor method of the JsCall Java class. What this constructor does is parse the methods in the HostJsScope class and keep the signature of each method to In private Map < String, Method > mMethodsMap, look at the js code above

a.alert = a.alert = a.alert = a.delayJsCallBack = a.getIMSI = a.getOsSdk = a.goBack = a.overloadMethod = a.overloadMethod= a.passJson2Java = a.passLongType = a.retBackPassJson = a.retJavaObject = a.testLossTime = a.toast = a.toast = function()

These are the method names defined in the HostJsScope class

So the whole execution process of this library is as follows:

  1. The JsCallJava class parses all the static methods in the HostJsScope class, puts them into a Map, and generates a piece of js code
  2. Setting Injected Chrome Client to WebView and injecting that js code into the Html5 page in the onProgressChanged method is a popular process. Native tells the Html 5 page what features I've opened up for you, so you can call me.
  3. In this way, js can invoke these methods provided by Native. That js code also converts the method executed by js into a json string and passes the prompt method of js to the onJsPrompt method. JsCallJava calls (WebView view, String msg) parse json strings, including those to be executed Method name, parameter type, and method parameter, which also verifies the method parameter type and the method parameter type in json Whether the parameter types of the same name method in HostJsScope are identical, and so on.
  4. Finally, if the method is executed correctly, the call method returns a json string code=200 or passes code=500, which passes through The return value of the prompt method is passed to js so that the Html 5 code knows if it has been correctly executed

This is the whole principle of this open source library. I personally think it is very suitable for Hybrid development. In this solution, js can receive the return value of Native, and there is no security problem on low-version mobile phones without using the addJavascriptInterface method. This method is simpler than Cordova's implementation and configuration.

So when I click on a button on the Html 5 page, such as a pop-up dialog, what's the overall process like?

Wechat's solution?

What? You asked me how Wechat solved it? I also decompiled the code of Wechat and wanted to study how they solved it. In fact, I was very curious about the way that Wechat's js calls Native and returns it.

First of all, I went to Weixin's js SDK official website to see the functions provided by js sdk, providing various powerful functions, you can see for yourself. So the question is, how can Wechat make it possible for js to call Native and return it successfully?

With doubt, I decompiled the Android client of Wechat and saw the wxjs.js file in assers/jsapi. I think this is the source code of Wechat js sdk.

Let me start by saying that I don't know much about the js code. I can only read Weixin's js code with a guess. If there's a js God interested in this, I hope we can discuss it together (jian) Fei (zao).

Seeing the code in wxjs.js, I guess Wechat used this _Weixin JSBridge data structure to communicate with Native at that time, right?

var __WeixinJSBridge = {
	// public
	invoke:_call,
	call:_call,
	on:_onfor3rd,
	env:_env,
	log:_log,
	// private
	// _test_start:_test_start,
	_fetchQueue: _fetchQueue,
	_handleMessageFromWeixin: _handleMessageFromWeixin,
	_hasInit: false,
	_continueSetResult: _continueSetResult
};

Then I saw the following code, I think it should provide the function of sharing content to the circle of friends.

// share timeline
_on('menu:share:timeline',function(argv){
  _log('share timeline');
  var data;
  if (typeof argv.title === 'string') {
	data = argv;
	_call('shareTimeline',data);
  }else{
	data = {
		// "img_url": "",
		// "img_width": "",
		// "img_height": "",
		"link": document.documentURI || _session_data.init_url,
		"desc": document.documentURI || _session_data.init_url,
		"title": document.title
	};
	var shareFunc = function(_img){		  
	  if (_img) {
		  data['img_url'] = _img.src;
		  data['img_width'] = _img.width;
		  data['img_height'] = _img.height;						
	  }
	  _call('shareTimeline',data);
	};
	getSharePreviewImage(shareFunc);
  }
});

Note the last sentence: call ('share Timeline', data); look at Weixin JSBridge The call attribute, and then I found the _call method.

function _call(func,params,callback) {
	var curFuncIdentifier = __WeixinJSBridge.call;
	if (curFuncIdentifier !== _callIdentifier) {
		return;
	}
	if (!func || typeof func !== 'string') {
		return;
	};
	if (typeof params !== 'object') {
		params = {};
	};
	var callbackID = (_callback_count++).toString();
	if (typeof callback === 'function') {
	  _callback_map[callbackID] = callback;
	};
	var msgObj = {'func':func,'params':params};
	msgObj[_MESSAGE_TYPE] = 'call';		
	msgObj[_CALLBACK_ID] = callbackID;
	_sendMessage(JSON.stringify(msgObj));
}

The general idea is to convert this _call ('share Timeline', data) into a json string, and you can see from this that Wechat's approach is very similar to that of the open source library above, simple and secure. _ The call method finally calls the _sendMessage method to send the message

//Add messages to the sending queue, and iframe's ready queue is weixin://dispatch_message/
function _sendMessage(message) {
	_sendMessageQueue.push(message);
	_readyMessageIframe.src = _CUSTOM_PROTOCOL_SCHEME + '://' + _QUEUE_HAS_MESSAGE;
	// var ifm = _WXJS('iframe#__WeixinJSBridgeIframe')[0];
	// if (!ifm) {
	//   ifm = _createQueueReadyIframe(document);
	// }
	// ifm.src = _CUSTOM_PROTOCOL_SCHEME + '://' + _QUEUE_HAS_MESSAGE;
};

As can be seen from the above code, Wechat's js sdk also replaces js method calls with a url like weixin://dispatch_message/which is json-encapsulated data. So I guess Wechat's approach is similar to the blocking URL of Netease Cloud Music? If that's the case, it's very insecure. Any Html 5 page can forge a similar one: weixin://dispatch_message/ url to call the function of Weixin, but the good thing is that Weixin must bring appid to every js call.

After decompiling the Wechat code, I saw the following code:

I think this is the open interface of Wechat Html 5, right? But by comparing the official websites of Wechat js sdk, I can see that many functions provided by App are not found in js sdk. It doesn't matter much. I think Wechat can use other functions as long as it upgrades js sdk, because Native has been opened.~

From the above Weixin JSBridge, you can see that there is a familiar handleMessageFromWeixin, which is the callback interface of js to handle Native. I use this string to search in the Weixin code. The results are as follows:

So, I guess roughly that the js call Native function in Weixin is in the way of intercepting url, and the Native call back is in the way of evaluateJavascript.

I also found the corresponding function in js sdk:

function _handleMessageFromWeixin(message) {
    var curFuncIdentifier = __WeixinJSBridge._handleMessageFromWeixin;
    if (curFuncIdentifier !== _handleMessageIdentifier) {
        return '{}';
    }

    var ret;
    var msgWrap
    if (_isUseMd5 === 'yes') {
      var realMessage = message[_JSON_MESSAGE];
      var shaStr = message[_SHA_KEY];
      var arr = new Array;
      arr[0] = JSON.stringify(realMessage);
      arr[1] = _xxyy;
      var str = arr.join("");
      var msgSha = '';
        var shaObj = CryptoJS.SHA1(str);
        msgSha = shaObj.toString();
        if (msgSha !== shaStr) {
            _log('_handleMessageFromWeixin , shaStr : ' + shaStr + ' , str : ' + str + ' , msgSha : ' + msgSha);
            return '{}';

        }
        msgWrap = realMessage;
    }
    //A lot of code is omitted

Wechat's approach should be very basic. It uses native functions, but it is safe. Because the client of Wechat has an appid for every js call, it also adds some security.

All of the above is based on my correct analysis.

Some personal thoughts

Now all kinds of new technologies are also trying to solve the efficiency problem of Native development. They want to use technology to solve a set of code running on Android and iOS clients. I believe these problems will be solved with the development of technology. I'm also looking forward to the upcoming Facebook launch. React Native Android

What functions does Hybrid development apply to

This article talks about the development of HBrid, which is the embedding of Html App in the Native client. Wechat should do the best in this respect. Because of the efficiency and power consumption of Html 5, I personally feel that users can not satisfy the experience of Web App, and Hybrid App is only suitable for some scenarios. Some basic functions, such as calling the camera of the mobile phone, acquiring geographical location, login and registration functions, etc., make the function of Native, make Html 5 call better, and this experience is better.

If you make a login and registration function Html 5, in a weak network environment, the experience should be very poor, maybe you have not loaded the page for half a day. You might say that I can pre-load Html 5 code and load it directly when I open App, so I say you're making trouble for yourself. If so, Native development may be faster.

So what situation is suitable for Html 5 development? Like some active pages, such as secondkill, group buying and so on, it is suitable to do Html 5, because these pages may involve very dazzling and complex, Html 5 development may be simpler, the key is that these pages are short in time and update faster, because an activity may be changed for a week, next week, if so, you can not do Native.

summary

There is such a sentence Ancient mottos :

If you have a hammer in your hand, everything looks like a nail.

Don't think Hybrid development can boast the platform, just use Hybrid to develop any functions. In fact, that's what Facebook thought in the early days, and then because of the low rendering efficiency of WebView, it changed the whole application to Native development. Here

Quote a passage from Facebook:

Today, we're releasing a new version of Facebook for Android that's been rebuilt in native code to improve speed and performance. To support the unique complexity of Facebook stories across devices, we're moving from a hybrid native/webview to pure native code, allowing us to optimize the Facebook experience for faster loading, new user interfaces, disk cache, and so on.

This paper mainly talks about the technical realization principle of interaction between js and Native in Hybrid development. There are also many errors in the estimation, which I hope Daniel pointed out.

Finally, I think that open source library is a very good solution. The solution is ingenious, simple and safe. At that time, I debug for half a day to understand the principle, I patted the thigh, this method is really good!! Netease cloud music solution is applicable to its scenario, without callback, Native only needs to process the corresponding information, and then to achieve page jump, play music, play MV and other functions, this method is also simple and easy to use.

Posted by friedice on Sun, 23 Jun 2019 16:57:47 -0700