android 基于uni小程序sdk的网络日志抓取实现
由于测试需求,需要在android端实现对小程序业务的网络日志捕获,来高效定位前端问题,需求如下:
1.打开小程序,能自动弹出一个debug的小浮窗。
2.点击小浮窗能进入并且看到当次小程序运行过程中的所有网络请求。
--以下uni小程序sdk统称为umpsdk
网络日志捕获
通过翻看umpsdk源码,发现umpsdk是基于weex实现,那么猜测网络层应该在weex里面,所以我们主要分析weex层应该能找到思路。对应源码包应该在uniapp-v8-release.aar。
首先我们从入口处出发,能看到WXSDKInstance这个类,用来做sdk初始化。
public IWXHttpAdapter getWXHttpAdapter() {
return WXSDKManager.getInstance().getIWXHttpAdapter();
}
public IWXStatisticsListener getWXStatisticsListener() {
return this.mStatisticsListener;
}
@Nullable
public IWebSocketAdapter getWXWebSocketAdapter() {
return WXSDKManager.getInstance().getIWXWebSocketAdapter();
}
通过读这块代码发现基础组件都是由WXSDKManager这个类来管理。我们再进入WXSDKManager分析。首先来看构造器
public static WXSDKManager getInstance() {
if (sManager == null) {
Class var0 = WXSDKManager.class;
synchronized(WXSDKManager.class) {
if (sManager == null) {
sManager = new WXSDKManager();
}
}
}
return sManager;
}
这是一个单例模式。
从语义理解getIWXHttpAdapter这个应该是一个负责网络请求的适配器组件。我们找到对应WXSDKManager中getIWXHttpAdapter的实现。
@NonNull
public IWXHttpAdapter getIWXHttpAdapter() {
if (this.mIWXHttpAdapter == null) {
this.mIWXHttpAdapter = new DefaultWXHttpAdapter();
}
return this.mIWXHttpAdapter;
}
同样也是一个判空赋值处理,我们再来看DefaultWXHttpAdapter。路径com.taobao.weex.adapter.DefaultWXHttpAdapter
public class DefaultWXHttpAdapter implements IWXHttpAdapter {
private static final DefaultWXHttpAdapter.IEventReporterDelegate DEFAULT_DELEGATE = new DefaultWXHttpAdapter.NOPEventReportDelegate();
private ExecutorService mExecutorService;
public DefaultWXHttpAdapter() {
}
private void execute(Runnable runnable) {
if (this.mExecutorService == null) {
this.mExecutorService = Executors.newFixedThreadPool(3);
}
this.mExecutorService.execute(runnable);
}
public void sendRequest(final WXRequest request, final OnHttpListener listener) {
if (listener != null) {
listener.onHttpStart();
}
this.execute(new Runnable() {
public void run() {
WXSDKInstance instance = (WXSDKInstance)WXSDKManager.getInstance().getAllInstanceMap().get(request.instanceId);
if (null != instance && !instance.isDestroy()) {
instance.getApmForInstance().actionNetRequest();
}
boolean isNetRequestSucceed = true;
WXResponse response = new WXResponse();
DefaultWXHttpAdapter.IEventReporterDelegate reporter = DefaultWXHttpAdapter.this.getEventReporterDelegate();
try {
HttpURLConnection connection = DefaultWXHttpAdapter.this.openConnection(request, listener);
reporter.preConnect(connection, request.body);
Map<String, List<String>> headers = connection.getHeaderFields();
int responseCode = connection.getResponseCode();
if (listener != null) {
listener.onHeadersReceived(responseCode, headers);
}
reporter.postConnect();
response.statusCode = String.valueOf(responseCode);
if (responseCode >= 200 && responseCode <= 299) {
InputStream rawStream = connection.getInputStream();
rawStream = reporter.interpretResponseStream(rawStream);
response.originalData = DefaultWXHttpAdapter.this.readInputStreamAsBytes(rawStream, listener);
} else {
response.errorMsg = DefaultWXHttpAdapter.this.readInputStream(connection.getErrorStream(), listener);
isNetRequestSucceed = false;
}
if (listener != null) {
listener.onHttpFinish(response);
}
} catch (IllegalArgumentException | IOException var10) {
Exception e = var10;
isNetRequestSucceed = false;
var10.printStackTrace();
response.statusCode = "-1";
response.errorCode = "-1";
response.errorMsg = var10.getMessage();
if (listener != null) {
listener.onHttpFinish(response);
}
if (var10 instanceof IOException) {
try {
reporter.httpExchangeFailed((IOException)e);
} catch (Throwable var9) {
var9.printStackTrace();
}
}
}
if (null != instance && !instance.isDestroy()) {
instance.getApmForInstance().actionNetResult(isNetRequestSucceed, (String)null);
}
}
});
}
private HttpURLConnection openConnection(WXRequest request, OnHttpListener listener) throws IOException {
URL url = new URL(request.url);
HttpURLConnection connection = this.createConnection(url);
connection.setConnectTimeout(request.timeoutMs);
connection.setReadTimeout(request.timeoutMs);
connection.setUseCaches(false);
connection.setDoInput(true);
if (request.paramMap != null) {
Set<String> keySets = request.paramMap.keySet();
Iterator var6 = keySets.iterator();
while(var6.hasNext()) {
String key = (String)var6.next();
connection.addRequestProperty(key, (String)request.paramMap.get(key));
}
}
if (!"POST".equals(request.method) && !"PUT".equals(request.method) && !"PATCH".equals(request.method)) {
if (!TextUtils.isEmpty(request.method)) {
connection.setRequestMethod(request.method);
} else {
connection.setRequestMethod("GET");
}
} else {
connection.setRequestMethod(request.method);
if (request.body != null) {
if (listener != null) {
listener.onHttpUploadProgress(0);
}
connection.setDoOutput(true);
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(request.body.getBytes());
out.close();
if (listener != null) {
listener.onHttpUploadProgress(100);
}
}
}
return connection;
}
private byte[] readInputStreamAsBytes(InputStream inputStream, OnHttpListener listener) throws IOException {
if (inputStream == null) {
return null;
} else {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int readCount = 0;
byte[] data = new byte[2048];
int nRead;
while((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
readCount += nRead;
if (listener != null) {
listener.onHttpResponseProgress(readCount);
}
}
buffer.flush();
return buffer.toByteArray();
}
}
private String readInputStream(InputStream inputStream, OnHttpListener listener) throws IOException {
if (inputStream == null) {
return null;
} else {
StringBuilder builder = new StringBuilder();
BufferedReader localBufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] data = new char[2048];
int len;
while((len = localBufferedReader.read(data)) != -1) {
builder.append(data, 0, len);
if (listener != null) {
listener.onHttpResponseProgress(builder.length());
}
}
localBufferedReader.close();
return builder.toString();
}
}
protected HttpURLConnection createConnection(URL url) throws IOException {
return (HttpURLConnection)url.openConnection();
}
@NonNull
public DefaultWXHttpAdapter.IEventReporterDelegate getEventReporterDelegate() {
return DEFAULT_DELEGATE;
}
private static class NOPEventReportDelegate implements DefaultWXHttpAdapter.IEventReporterDelegate {
private NOPEventReportDelegate() {
}
public void preConnect(HttpURLConnection connection, @Nullable String body) {
}
public void postConnect() {
}
public InputStream interpretResponseStream(@Nullable InputStream inputStream) {
return inputStream;
}
public void httpExchangeFailed(IOException e) {
}
}
public interface IEventReporterDelegate {
void preConnect(HttpURLConnection var1, @Nullable String var2);
void postConnect();
InputStream interpretResponseStream(@Nullable InputStream var1);
void httpExchangeFailed(IOException var1);
}
}
看到这里我们发现核心是HttpURLConnection来做网络请求的。这里我们发现sendRequest就是请求处理部分。
看到这里现在我们思路已经清晰了,首先做一下几点考虑:
1.从哪里hook
2.怎样hook侵入性最小
最后我们选择自定义适配器的方式,通过hook替换网络适配器来达成需求的后台处理部分。
class XXXWXHttpAdapter(private val function: (UniResponse) -> Unit) : IWXHttpAdapter {
private var mExecutorService: ExecutorService? = null
private fun execute(runnable: Runnable) {
if (mExecutorService == null) {
mExecutorService = Executors.newFixedThreadPool(3)
}
mExecutorService!!.execute(runnable)
}
override fun sendRequest(request: WXRequest, listener: IWXHttpAdapter.OnHttpListener) {
if (listener != null) {
listener.onHttpStart()
}
execute {
val instance: WXSDKInstance = WXSDKManager.getInstance().allInstanceMap
.get(request.instanceId) as WXSDKInstance
if (null != instance && !instance.isDestroy) {
instance.apmForInstance.actionNetRequest()
}
var isNetRequestSucceed = true
val response = WXResponse()
val reporter: IEventReporterDelegate = eventReporterDelegate
var uniResponse: UniResponse? = null
try {
val connection = openConnection(request, listener)
reporter.preConnect(connection, request.body)
val headers = connection.headerFields
val responseCode = connection.responseCode
if (listener != null) {
listener.onHeadersReceived(responseCode, headers)
}
reporter.postConnect()
response.statusCode = responseCode.toString()
if (responseCode in 200..299) {
var rawStream = connection.inputStream
rawStream = reporter.interpretResponseStream(rawStream)
response.originalData =
readInputStreamAsBytes(rawStream, listener)
} else {
response.errorMsg =
readInputStream(connection.errorStream, listener)
isNetRequestSucceed = false
}
uniResponse = convertUni(request, response)
if (listener != null) {
listener.onHttpFinish(response)
}
} catch (var10: IllegalArgumentException) {
val e: Exception = var10
isNetRequestSucceed = false
var10.printStackTrace()
response.statusCode = "-1"
response.errorCode = "-1"
response.errorMsg = var10.message
uniResponse = convertUni(request, response)
if (listener != null) {
listener.onHttpFinish(response)
}
if (var10 is IOException) {
try {
reporter.httpExchangeFailed(e as IOException)
} catch (var9: Throwable) {
var9.printStackTrace()
}
}
} catch (var10: IOException) {
val e: Exception = var10
isNetRequestSucceed = false
var10.printStackTrace()
response.statusCode = "-1"
response.errorCode = "-1"
response.errorMsg = var10.message
uniResponse = convertUni(request, response)
if (listener != null) {
listener.onHttpFinish(response)
}
if (var10 is IOException) {
try {
reporter.httpExchangeFailed(e as IOException)
} catch (var9: Throwable) {
var9.printStackTrace()
}
}
}
if (uniResponse != null) {
uniLog(uniResponse, function)
}
if (null != instance && !instance.isDestroy) {
instance.apmForInstance
.actionNetResult(isNetRequestSucceed, null as String?)
}
}
}
@Throws(IOException::class)
private fun openConnection(
request: WXRequest,
listener: IWXHttpAdapter.OnHttpListener?
): HttpURLConnection {
val url = URL(request.url)
val connection = createConnection(url)
connection.connectTimeout = request.timeoutMs
connection.readTimeout = request.timeoutMs
connection.useCaches = false
connection.doInput = true
if (request.paramMap != null) {
val keySets: Set<String> = request.paramMap.keys
val var6: Iterator<*> = keySets.iterator()
while (var6.hasNext()) {
val key = var6.next() as String
connection.addRequestProperty(key, request.paramMap.get(key) as String)
}
}
if ("POST" != request.method && "PUT" != request.method && "PATCH" != request.method) {
if (!TextUtils.isEmpty(request.method)) {
connection.requestMethod = request.method
} else {
connection.requestMethod = "GET"
}
} else {
connection.requestMethod = request.method
if (request.body != null) {
listener?.onHttpUploadProgress(0)
connection.doOutput = true
val out = DataOutputStream(connection.outputStream)
out.write(request.body.toByteArray())
out.close()
listener?.onHttpUploadProgress(100)
}
}
return connection
}
@Throws(IOException::class)
private fun readInputStreamAsBytes(
inputStream: InputStream?,
listener: IWXHttpAdapter.OnHttpListener?
): ByteArray? {
return if (inputStream == null) {
null
} else {
val buffer = ByteArrayOutputStream()
var readCount = 0
val data = ByteArray(2048)
var nRead: Int
while (inputStream.read(data, 0, data.size).also { nRead = it } != -1) {
buffer.write(data, 0, nRead)
readCount += nRead
listener?.onHttpResponseProgress(readCount)
}
buffer.flush()
buffer.toByteArray()
}
}
@Throws(IOException::class)
private fun readInputStream(
inputStream: InputStream?,
listener: IWXHttpAdapter.OnHttpListener?
): String? {
return if (inputStream == null) {
null
} else {
val builder = StringBuilder()
val localBufferedReader = BufferedReader(InputStreamReader(inputStream))
val data = CharArray(2048)
var len: Int
while (localBufferedReader.read(data).also { len = it } != -1) {
builder.append(data, 0, len)
listener?.onHttpResponseProgress(builder.length)
}
localBufferedReader.close()
builder.toString()
}
}
@Throws(IOException::class)
protected fun createConnection(url: URL): HttpURLConnection {
return url.openConnection() as HttpURLConnection
}
private class NOPEventReportDelegate : IEventReporterDelegate {
override fun preConnect(connection: HttpURLConnection?, body: String?) {}
override fun postConnect() {}
override fun interpretResponseStream(inputStream: InputStream?): InputStream? {
return inputStream
}
override fun httpExchangeFailed(e: IOException?) {}
}
interface IEventReporterDelegate {
fun preConnect(var1: HttpURLConnection?, var2: String?)
fun postConnect()
fun interpretResponseStream(var1: InputStream?): InputStream?
fun httpExchangeFailed(var1: IOException?)
}
companion object {
val eventReporterDelegate: IEventReporterDelegate = NOPEventReportDelegate()
}
private fun convertUni(wxRequest: WXRequest, wxResponse: WXResponse): UniResponse? =
if (wxRequest.url.contains("xxx") && wxRequest.paramMap.containsValue("application/json")) {
when {
wxRequest.paramMap.containsValue("application/json") -> {
UniResponse(
UniRequest(
wxRequest.paramMap,
wxRequest.url,
wxRequest.method,
JSONObject.parse(wxRequest.body)
),
wxResponse.statusCode,
wxResponse.data,
System.currentTimeMillis(),
wxResponse.originalData,
wxResponse.errorCode,
wxResponse.errorMsg,
wxResponse.toastMsg,
wxResponse.extendParams
)
}
wxRequest.paramMap.containsValue("application/x-www-form-urlencoded") -> {
UniResponse(
UniRequest(
wxRequest.paramMap,
wxRequest.url,
wxRequest.method,
wxRequest.body
),
wxResponse.statusCode,
wxResponse.data,
System.currentTimeMillis(),
wxResponse.originalData,
wxResponse.errorCode,
wxResponse.errorMsg,
wxResponse.toastMsg,
wxResponse.extendParams
)
}
else -> {
null
}
}
} else {
null
}
private fun uniLog(resp: UniResponse, function: (UniResponse) -> Unit) {
function(resp)
Log.i("uni-web-req", resp.toJSON())
Log.i("uni-web-req-curl", resp.convertorCURL())
}
}
fun UniResponse.toJSON(): String {
val temp = this
this.originalData?.apply {
temp.data = JSONObject.parse(this)
}
return JSONObject.toJSONString(temp)
}
fun UniResponse.convertorCURL(): String {
var curlCmd: String = "curl "
if (this.request.body != null) {
curlCmd += if (this.request.paramMap?.containsValue("application/json") == true) {
"-d '${JSONObject.toJSON(this.request.body)}' "
} else {
"-d '${this.request.body as String}'"
}
}
//parse Headers
this.request.paramMap?.forEach { item ->
curlCmd += "-H '${item.key}: ${item.value}' "
}
curlCmd += "${this.request.method} ${this.request.url}"
return curlCmd
}
如下hook替换适配器
val cls = Class.forName("com.taobao.weex.WXSDKManager")
val method = cls.getDeclaredMethod("getInstance")
val manager = method.invoke(null)
val adapterField = cls.getDeclaredField("mIWXHttpAdapter")
adapterField.isAccessible = true
adapterField.set(
manager,
XXXWXHttpAdapter {
JLog.i("onEvent ", " it >>> $it")
responseList.add(it)
}
)
自此完成日志捕获,新版本小程序sdk是基于多进程的所以我们把hook移入service,对应三个service0,1,2。分别对应进程unimp0,nuimp1,nuimp2。之后我们需要做的就是在启动小程序时,获取当前运行的进程名再做相对应的service启动即可。
浮窗控制
我们需要考虑一下几点:
1.代码尽可能少的侵入
2.尽可能少的暴露过多到业务层(因为我们需要区分debug以及release)
通过调研发现weex中有关WXModule的部分,其中框架层有对activity 生命周期的hook。经过试验发现,这里有这样一个规律:onCreate不生效,onResume仅在二次之后打开生效,onDestory每次生效。
通过阅读weex源码发现(这里不做深入探讨),WXModule在打开小程序的过程会进行一次初始化(WXModuleManager)。
至此思路已经清晰
1.我们在wxmodule构造方法中去标记小程序在前台的标志
2.在onDestory中标记小程序退出
3.采用aidl封装进程间通讯
4.通过浮窗点击的跳转自定义activity需要采用startActivityFromUniTask压入小程序进程
0 个评论
要回复文章请先登录或注册