未验证 提交 49a3af6f 编写于 作者: W Wizz.Xu 提交者: GitHub

Merge pull request #3 from didi/master

update
---
name: "【DoKit生态场景】-描述出现的问题"
about: DoKit生态场景比如Android、iOS、小程序、Flutter,后面跟你需要描述的内容
title: "【DoKit生态场景】-描述出现的问题"
labels: ''
assignees: jtsky
---
It is recommended to refer to the following process before submitting issues:https://github.com/didi/DoraemonKit/issues/745
If you still cannot solve your problem, you can submit your issue according to the following template
Please complete the following informations.
> Android、iOS? OS version? Brand?
> Expected behavior and actual behavior.
> Steps to reproduce the problem.
> More informations such as error messages and stack traces are welcomed.
>
建议提issues之前可以参考一下DoKit社区答疑流程:https://github.com/didi/DoraemonKit/issues/745
假如还是无法解决你的问题,你可以按照以下模板来提交你的issue
请补充如下信息。
> Android 还是 iOS?系统版本是多少?手机品牌是什么?(如有)
> 期望的表现和实际的表现。(如有)
> 问题重现的步骤。(如有)
> 其他的错误信息和堆栈信息如果有也可以一并提供出来。(如有)
# Release Notes
### 3.0.7
1、DoKit iOS github issues bug fixed
### 3.0.4
1、DoKit iOS端文件同步助手正式上线
### 3.0.2
1、支持每一个内置Kit,进行位置重排,添加删除某一个Kit
......@@ -242,4 +245,4 @@
3、性能工具:帧率、CPU、内存、流量、自定义
4、视觉工具:颜色吸管、组件检查、对齐标尺
\ No newline at end of file
4、视觉工具:颜色吸管、组件检查、对齐标尺
......@@ -7,7 +7,7 @@
Pod::Spec.new do |s|
s.name = 'DoraemonKit'
s.version = '3.0.6'
s.version = '3.0.7'
s.summary = 'iOS各式各样的工具集合'
s.description = <<-DESC
iOS各式各样的工具集合 Desc
......
## [0.2.12] - 修改页面层级,现在DoKitApp将以Stack存放用户传入的Widget,防止各种InheritedWidget影响到用户Widget
## [0.2.11] - 修复一些UI异常问题;日志模块增加错误信息过滤,防止被错误日志冲刷掉;
## [0.2.10] - 修复DoKitApp再某些case下获取高度为0的异常.
## [0.2.9] - 修改DoKitApp类型,自定义Overlay容器防止各种InheritedWidget使用异常.
## [0.2.8] - 修复网络请求返回结果乱码问题;增加method—channel耗时统计;日志/方法通道/网络请求增加清空按钮.
## [0.2.7] - 网络请求增加RequestHeaders信息展示,增加非文本类型的返回结果size展示.
## [0.2.6] - 日志功能优化与bug修复.
## [0.2.5] - 修改demo包名为example.
## [0.2.4] - 发布pub包去除demo.
......
......@@ -5,9 +5,6 @@
## 支持flutter版本
1.17.5<=version<=1.22.4,其余版本未做过兼容性测试
## 最新版本
**0.2.6**
## Pub地址
[DoKit For Flutter](https://pub.dev/packages/dokit)
......@@ -16,7 +13,7 @@
```
dependencies:
dokit: ^0.2.6
dokit: ^0.2.12
```
在main函数入口初始化。 DoKit使用runZone的方式进行日志捕获,方法通道的捕获,如果你的app需要使用同样的方式会有冲突。
......
......@@ -102,7 +102,7 @@ class DoKit {
}
static void dispose({@required BuildContext context}) {
Overlay.of(context).widget.initialEntries.forEach((element) {
doKitOverlayKey.currentState.widget.initialEntries.forEach((element) {
// element.remove();
});
}
......
......@@ -63,8 +63,12 @@ class DoKitBinaryMessenger extends BinaryMessenger {
if (info != null && result != null) {
if (info.methodCodec != null) {
info.results = info.methodCodec.decodeEnvelope(result);
info.endTimestamp = new DateTime.now().millisecondsSinceEpoch;
} else if (info.messageCodec != null) {
info.results = info.messageCodec.decodeMessage(result);
info.endTimestamp = new DateTime.now().millisecondsSinceEpoch;
}else{
info.endTimestamp = new DateTime.now().millisecondsSinceEpoch;
}
}
} catch (e) {}
......
......@@ -311,7 +311,7 @@ class DoKitHttpClientRequest implements HttpClientRequest {
Future<HttpClientResponse> monitor(Future<HttpClientResponse> future) async {
HttpClientResponse response = await future;
return DoKitHttpClientResponse(response, recordResponse, encoding);
return DoKitHttpClientResponse(response, recordResponse);
}
void recordResponse(int code, String result, String header, int size) {
......@@ -337,9 +337,8 @@ extension HttpClientRequestExt on HttpClientRequest {
class DoKitHttpClientResponse implements HttpClientResponse {
final HttpClientResponse origin;
final Function(int, String, String, int) recordResponse;
final Encoding encoding;
DoKitHttpClientResponse(this.origin, this.recordResponse, this.encoding);
DoKitHttpClientResponse(this.origin, this.recordResponse);
@override
Future<bool> any(bool Function(List<int> element) test) {
......@@ -483,6 +482,18 @@ class DoKitHttpClientResponse implements HttpClientResponse {
headers['content-type'].toString().contains('xml'));
}
Encoding getEncoding() {
var charset;
if (headers != null &&
headers.contentType != null &&
headers.contentType.charset != null) {
charset = headers.contentType.charset;
} else {
charset = "utf-8";
}
return Encoding.getByName(charset);
}
@override
StreamSubscription<List<int>> listen(void Function(List<int> event) onData,
{Function onError, void Function() onDone, bool cancelOnError}) {
......@@ -490,17 +501,20 @@ class DoKitHttpClientResponse implements HttpClientResponse {
onData(result);
try {
if (isTextResponse()) {
if (encoding != null) {
recordResponse(statusCode, encoding.decode(result),
headers?.toString(), result.length);
if (getEncoding() != null) {
recordResponse(statusCode, getEncoding().decode(result),
headers?.toString(), contentLength);
} else {
recordResponse(statusCode, "返回结果解析失败", headers?.toString(), 0);
recordResponse(
statusCode, "返回结果解析失败", headers?.toString(), contentLength);
}
} else {
recordResponse(statusCode, "返回结果不支持解析", headers?.toString(), 0);
recordResponse(
statusCode, "返回结果不支持解析", headers?.toString(), contentLength);
}
} catch (e) {
recordResponse(statusCode, "返回结果解析失败", headers?.toString(), 0);
recordResponse(
statusCode, "返回结果解析失败", headers?.toString(), contentLength);
}
}
......@@ -595,21 +609,22 @@ class DoKitHttpClientResponse implements HttpClientResponse {
if (isTextResponse()) {
if (event is Uint8List) {
Uint8List result = event;
if (encoding != null) {
recordResponse(statusCode, encoding.decode(result.toList()),
if (getEncoding() != null) {
recordResponse(statusCode, getEncoding().decode(result.toList()),
headers?.toString(), event.length);
} else {
recordResponse(statusCode, "返回结果解析失败", headers?.toString(), 0);
recordResponse(
statusCode, "返回结果解析失败", headers?.toString(), contentLength);
}
} else if (event is String) {
recordResponse(
statusCode, event, headers?.toString(), event.codeUnits.length);
recordResponse(statusCode, event, headers?.toString(), contentLength);
} else {
recordResponse(statusCode, 'unknown type:${event.runtimeType}',
headers?.toString(), 0);
headers?.toString(), contentLength);
}
} else {
recordResponse(statusCode, "返回结果不支持解析", headers?.toString(), 0);
recordResponse(
statusCode, "返回结果不支持解析", headers?.toString(), contentLength);
}
});
return s;
......
......@@ -344,7 +344,20 @@ class _HttpItemWidgetState extends State<HttpItemWidget> {
height: 1.5,
color: Color(0xff666666))),
TextSpan(
text: '\nHeader: ',
text: '\nRequestHeader: ',
style: TextStyle(
height: 1.5,
fontSize: 10,
color: Color(0xff333333),
fontWeight: FontWeight.bold)),
TextSpan(
text: '${widget.item.request.header}',
style: TextStyle(
fontSize: 10,
height: 1.5,
color: Color(0xff666666))),
TextSpan(
text: '\nResponseHeader: ',
style: TextStyle(
height: 1.5,
fontSize: 10,
......
......@@ -7,11 +7,16 @@ import 'package:dokit/util/util.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../dokit.dart';
import 'apm.dart';
class LogKit extends ApmKit {
FlutterExceptionHandler originOnError;
CommonStorage _error = new CommonStorage();
CommonStorage get error => _error;
@override
Widget createDisplayPage() {
return LogPage();
......@@ -32,6 +37,14 @@ class LogKit extends ApmKit {
return 'images/dk_log_info.png';
}
@override
bool save(IInfo info) {
if ((info as LogBean).type == LogBean.TYPE_ERROR) {
_error.save(info);
}
return super.save(info);
}
@override
void start() {
resetOnErrorInstance();
......@@ -86,17 +99,26 @@ class LogManager {
?.getAll();
}
List<IInfo> getErrors() {
return ApmKitManager.instance
.getKit<LogKit>(ApmKitName.KIT_LOG)
?.error
?.getAll();
}
void addLog(int type, String msg) {
if (ApmKitManager.instance.getKit(ApmKitName.KIT_LOG) != null) {
LogBean log = new LogBean(type, msg);
LogKit kit = ApmKitManager.instance.getKit(ApmKitName.KIT_LOG);
kit.save(log);
listener?.call(log);
if (type != LogBean.TYPE_ERROR || LogPageState._showError) {
listener?.call(log);
}
}
}
void addException(String exception) {
addLog(LogBean.TYPE_EXCEPTION, exception);
addLog(LogBean.TYPE_ERROR, exception);
}
}
......@@ -106,7 +128,6 @@ class LogBean implements IInfo {
static const int TYPE_INFO = 3;
static const int TYPE_WARN = 4;
static const int TYPE_ERROR = 5;
static const int TYPE_EXCEPTION = 6;
final int type;
final String msg;
......@@ -134,6 +155,8 @@ class LogPage extends StatefulWidget {
class LogPageState extends State<LogPage> {
ScrollController _offsetController =
ScrollController(); //定义ListView的controller
static bool _showError = false;
Future<void> _listener(LogBean logBean) async {
if (!mounted) return;
// if there's a current frame,
......@@ -164,12 +187,46 @@ class LogPageState extends State<LogPage> {
@override
Widget build(BuildContext context) {
List<IInfo> items = LogManager.instance.getLogs().reversed.toList();
List<IInfo> items = LogPageState._showError
? LogManager.instance.getErrors().reversed.toList()
: LogManager.instance.getLogs().reversed.toList();
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
this.setState(() {
_showError = !_showError;
});
},
child: Container(
height: 44,
width: 44,
padding: EdgeInsets.only(left: 16),
child: Image.asset(
_showError
? 'images/dk_channel_check_h.png'
: 'images/dk_channel_check_n.png',
package: DoKit.PACKAGE_NAME,
height: 13,
width: 13),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
this.setState(() {
_showError = !_showError;
});
},
child: Text('只显示异常',
style: TextStyle(
color:
_showError ? Color(0xff337cc4) : Color(0xff333333),
fontSize: 12))),
Container(
decoration: new BoxDecoration(
border: new Border.all(color: Color(0xff337cc4), width: 1),
......@@ -186,6 +243,10 @@ class LogPageState extends State<LogPage> {
.getKit<LogKit>(ApmKitName.KIT_LOG)
.getStorage()
.clear();
ApmKitManager.instance
.getKit<LogKit>(ApmKitName.KIT_LOG)
.error
.clear();
});
},
child: Text('清除本页数据',
......
......@@ -23,7 +23,9 @@ class ChannelInfo implements IInfo {
final String method;
final dynamic arguments;
final int timestamp;
final int startTimestamp;
int endTimestamp = 0;
final int type;
dynamic results;
bool expand = false;
......@@ -31,7 +33,7 @@ class ChannelInfo implements IInfo {
MessageCodec messageCodec;
ChannelInfo(this.channelName, this.method, this.arguments, this.type)
: this.timestamp = new DateTime.now().millisecondsSinceEpoch;
: this.startTimestamp = new DateTime.now().millisecondsSinceEpoch;
@override
String getValue() {
......@@ -69,7 +71,7 @@ class MethodChannelKit extends ApmKit {
@override
IStorage createStorage() {
return CommonStorage(maxCount: 120);
return CommonStorage(maxCount: 240);
}
@override
......@@ -82,6 +84,7 @@ class MethodChannelKit extends ApmKit {
if (!ChannelPageState.showSystemChannel &&
((info as ChannelInfo).type == ChannelInfo.TYPE_SYSTEM_RECEIVE ||
(info as ChannelInfo).type == ChannelInfo.TYPE_SYSTEM_SEND)) {
super.save(info);
return false;
}
bool result = super.save(info);
......@@ -331,7 +334,7 @@ class _ChannelItemWidgetState extends State<ChannelItemWidget> {
text: TextSpan(children: [
TextSpan(
text:
'[${TimeUtils.toTimeString(widget.item.timestamp)}]',
'[${TimeUtils.toTimeString(widget.item.startTimestamp)}]',
style: TextStyle(
fontSize: 9,
color: Color(0xff333333),
......@@ -352,6 +355,14 @@ class _ChannelItemWidgetState extends State<ChannelItemWidget> {
color: (widget.item.type % 2 != 0
? Color(0xffd0607e)
: Color(0xff337cc4))))),
TextSpan(
text:
' Cost:${widget.item.endTimestamp > 0 ? ((widget.item.endTimestamp - widget.item.startTimestamp).toString() + 'ms') : '-'} ',
style: TextStyle(
fontSize: 9,
color: Color(0xff666666),
height: 1.5,
)),
TextSpan(
text: '\nChannelName: ',
style: TextStyle(
......
......@@ -160,7 +160,7 @@ class RouteInfoPageState extends State<RouteInfoPage> {
]))));
}
route = route.parent;
if (route.parent != null && route.parent.parent != null) {
if (route != null && route.parent != null) {
widgets.add(Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
alignment: Alignment.center,
......@@ -169,7 +169,7 @@ class RouteInfoPageState extends State<RouteInfoPage> {
));
}
// 过滤掉dokit自带的navigator
} while (route.parent != null && route.parent.parent != null);
} while (route != null);
return widgets;
}
......
......@@ -90,7 +90,7 @@ class InfoItem extends StatelessWidget {
padding: EdgeInsets.only(top: 14, bottom: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
label,
......@@ -99,17 +99,18 @@ class InfoItem extends StatelessWidget {
color: Color(0xff333333),
),
),
Container(
width: MediaQuery.of(context).size.width - 130,
margin: EdgeInsets.only(left: 10),
child: Text(
text ?? '-',
textAlign: TextAlign.end,
style: TextStyle(
fontSize: 16,
color: Color(0xff666666),
),
))
Expanded(
child: Container(
margin: EdgeInsets.only(left: 10),
child: Text(
text ?? '-',
textAlign: TextAlign.end,
style: TextStyle(
fontSize: 16,
color: Color(0xff666666),
),
)),
)
],
));
}
......
......@@ -51,9 +51,9 @@ class ViewCheckerKit extends VisualKit {
return;
}
hasShow = true;
Overlay.of(context).insert(focusEntry, below: entrance);
Overlay.of(context).insert(infoEntry, below: focusEntry);
Overlay.of(context).insert(rectEntry, below: infoEntry);
doKitOverlayKey.currentState.insert(focusEntry, below: entrance);
doKitOverlayKey.currentState.insert(infoEntry, below: focusEntry);
doKitOverlayKey.currentState.insert(rectEntry, below: infoEntry);
}
static bool hide(BuildContext context) {
......
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
final GlobalKey<OverlayState> doKitOverlayKey = GlobalKey<OverlayState>();
// 谷歌提供的DevTool会判断入口widget是否在主工程内申明(runApp(new MyApp()),MyApp必须在主工程内定义,估计是根据source file来判断的),
// 如果在package内去申明这个入口widget,则在Flutter Inspector上的左边树会被折叠,影响开发使用。故这里要求在main文件内使用DoKitApp(MyApp())的形式来初始化入口
class DoKitApp extends MaterialApp {
class DoKitApp extends StatefulWidget {
// 放置dokit悬浮窗的容器
static GlobalKey rootKey = new GlobalKey();
......@@ -12,8 +15,14 @@ class DoKitApp extends MaterialApp {
Widget get origin => _origin;
Widget _origin;
DoKitApp(this._origin)
: super(key: DoKitApp.rootKey, home: _DoKitWrapper(_origin));
DoKitApp(Widget widget)
: _origin = _DoKitWrapper(widget),
super(key: rootKey);
@override
State<StatefulWidget> createState() {
return _DoKitAppState();
}
}
class _DoKitWrapper extends StatelessWidget {
......@@ -26,3 +35,113 @@ class _DoKitWrapper extends StatelessWidget {
return _origin;
}
}
class _DoKitAppState extends State<DoKitApp> {
final List<OverlayEntry> entries = <OverlayEntry>[];
final supportedLocales = const <Locale>[Locale('en', 'US')];
@override
void initState() {
super.initState();
entries.clear();
entries.add(new OverlayEntry(builder: (context) {
return widget.origin;
}));
}
Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* {
yield DefaultMaterialLocalizations.delegate;
yield DefaultCupertinoLocalizations.delegate;
yield DefaultWidgetsLocalizations.delegate;
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
widget.origin,
_MediaQueryFromWindow(
child: Localizations(
locale: supportedLocales.first,
delegates: _localizationsDelegates.toList(),
child: Overlay(
key: doKitOverlayKey,
)))
],
),
);
}
}
class _MediaQueryFromWindow extends StatefulWidget {
const _MediaQueryFromWindow({Key key, this.child}) : super(key: key);
final Widget child;
@override
_MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState();
}
class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
// ACCESSIBILITY
@override
void didChangeAccessibilityFeatures() {
setState(() {
// The properties of window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
// METRICS
@override
void didChangeMetrics() {
setState(() {
// The properties of window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
@override
void didChangeTextScaleFactor() {
setState(() {
// The textScaleFactor property of window has changed. We reference
// window in our build function, so we need to call setState(), but
// we don't need to cache anything locally.
});
}
// RENDERING
@override
void didChangePlatformBrightness() {
setState(() {
// The platformBrightness property of window has changed. We reference
// window in our build function, so we need to call setState(), but
// we don't need to cache anything locally.
});
}
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: widget.child,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
......@@ -21,7 +21,7 @@ class DoKitBtn extends StatefulWidget {
overlayEntry = new OverlayEntry(builder: (context) {
return this;
});
OverlayState rootOverlay = Overlay.of(DoKitApp.appKey.currentContext);
OverlayState rootOverlay = doKitOverlayKey.currentState;
assert(rootOverlay != null);
rootOverlay.insert(overlayEntry);
ApmKitManager.instance.startUp();
......@@ -100,7 +100,7 @@ class DoKitBtnState extends State<DoKitBtn> {
if (showDebugPage) {
closeDebugPage();
} else {
Overlay.of(context).insert(debugPage, below: owner);
doKitOverlayKey.currentState.insert(debugPage, below: owner);
showDebugPage = true;
}
}
......
......@@ -59,7 +59,11 @@ class ResidentPageState extends State<ResidentPage> {
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
int topMargin = MediaQuery.of(context).orientation==Orientation.portrait ? 100 : 0;
int topMargin =
MediaQuery.of(context).orientation == Orientation.portrait ? 100 : 0;
if (height == 0) {
return Container();
}
return Positioned(
child: Container(
width: width,
......
name: dokit
description: 开发工具集
version: 0.2.6
version: 0.2.12
homepage: https://www.dokit.cn/#/index/home
......
......@@ -27,8 +27,8 @@
<img src="https://javer.oss-cn-shanghai.aliyuncs.com/doraemon/github/DoraemonKit_github.png" width = "150" height = "150" alt="DoraemonKit" align=left />
<img src="https://img.shields.io/github/license/didi/DoraemonKit.svg" align=left />
<img src="https://img.shields.io/badge/Android-3.3.5-blue.svg" align=left />
<img src="https://img.shields.io/badge/iOS-3.0.4-yellow.svg" align=left />
<img src="https://img.shields.io/badge/Flutter-0.2.5-blue.svg" align=left />
<img src="https://img.shields.io/badge/iOS-3.0.7-yellow.svg" align=left />
<img src="https://img.shields.io/badge/Flutter-0.2.12-blue.svg" align=left />
<img src="https://img.shields.io/badge/miniapp-0.0.1-red.svg" align=left />
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" align=left />
</div>
......@@ -47,6 +47,7 @@
## 使用手册
访问[DoKit官网](https://www.dokit.cn/),点击"[使用中心](http://xingyun.xiaojukeji.com/docs/dokit/#/intro)"。
**温馨提示:当前DoKit的所有功能都只针对Debug环境,Release环境未经过实际验证,所以请大家严格按照官方文档来集成,也不建议大家在Release环境上使用DoKit的任何功能。如果大家一定要在Release环境上使用,请自行进行充分的测试和验证,DoKit官方将不承担任何责任和损失。**
## 更新日志
- [iOS-ReleaseNotes](Doc/iOS-ReleaseNotes.md)
......@@ -228,7 +229,7 @@ tips : 如果使用我们滴滴优秀的开源跨端方案 [chameleon](https:/
## 项目成员
**发起者**
**创始人**
[yixiangboy(易翔)](https://github.com/yixiangboy)
**负责人**
[jtsky(金台)](https://github.com/jtsky)
......
......@@ -20,12 +20,10 @@
NSString *imageName = nil;
CGFloat scale = [UIScreen mainScreen].scale;
if (ABS(scale-3) <= 0.001){
if (ABS(scale-3) <= 0.001) {
imageName = [NSString stringWithFormat:@"%@@3x",name];
}else if(ABS(scale-2) <= 0.001){
} else {
imageName = [NSString stringWithFormat:@"%@@2x",name];
}else{
imageName = name;
}
UIImage *image = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:imageName ofType:@"png"]];
if (!image) {
......
......@@ -98,8 +98,10 @@ static NSString * const kDoraemonProtocolKey = @"doraemon_protocol_key";
});
}else if(DoraemonWeakNetwork_WeakSpeed == [[DoraemonNetworkInterceptor shareInstance].weakDelegate weakNetSelecte]){
DoKitLog(@"yd WeakUpFlow Net");
[[DoraemonNetworkInterceptor shareInstance].weakDelegate handleWeak:[DoraemonUrlUtil getHttpBodyFromRequest:self.request] isDown:NO];
[self.task resume];
[[DoraemonNetFlowManager shareInstance] httpBodyFromRequest:self.request bodyCallBack:^(NSData *body) {
[[DoraemonNetworkInterceptor shareInstance].weakDelegate handleWeak:body isDown:NO];
[self.task resume];
}];
}else{
[self.task resume];
}
......@@ -201,15 +203,15 @@ static NSString * const kDoraemonProtocolKey = @"doraemon_protocol_key";
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
assert([NSThread currentThread] == self.clientThread);
//判断服务器返回的证书类型, 是否是服务器信任
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
//强制信任
NSURLCredential *card = [[NSURLCredential alloc]initWithTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, card);
}
}
//- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
// assert([NSThread currentThread] == self.clientThread);
// //判断服务器返回的证书类型, 是否是服务器信任
// if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// //强制信任
// NSURLCredential *card = [[NSURLCredential alloc]initWithTrust:challenge.protectionSpace.serverTrust];
// completionHandler(NSURLSessionAuthChallengeUseCredential, card);
// }
//}
// 去掉一些我们不关心的链接, 与UIWebView的兼容还是要好好考略一下
+ (BOOL)ignoreRequest:(NSURLRequest *)request{
......
......@@ -28,6 +28,6 @@
@property (nonatomic, copy) NSString *topVc;//流量触发时候的顶层vc
+ (DoraemonNetFlowHttpModel *)dealWithResponseData:(NSData *)responseData response:(NSURLResponse*)response request:(NSURLRequest *)request;
+ (void)dealWithResponseData:(NSData *)responseData response:(NSURLResponse*)response request:(NSURLRequest *)request complete:(void (^)(DoraemonNetFlowHttpModel *model))complete;
@end
......@@ -6,24 +6,19 @@
//
#import "DoraemonNetFlowHttpModel.h"
#import "DoraemonNetFlowManager.h"
#import "NSURLRequest+Doraemon.h"
#import "DoraemonUrlUtil.h"
@implementation DoraemonNetFlowHttpModel
+ (DoraemonNetFlowHttpModel *)dealWithResponseData:(NSData *)responseData response:(NSURLResponse*)response request:(NSURLRequest *)request{
+ (void)dealWithResponseData:(NSData *)responseData response:(NSURLResponse*)response request:(NSURLRequest *)request complete:(void (^)(DoraemonNetFlowHttpModel *model))complete {
DoraemonNetFlowHttpModel *httpModel = [[DoraemonNetFlowHttpModel alloc] init];
//request
httpModel.request = request;
httpModel.requestId = request.requestId;
httpModel.url = [request.URL absoluteString];
httpModel.method = request.HTTPMethod;
NSData *httpBody = [DoraemonUrlUtil getHttpBodyFromRequest:request];
httpModel.requestBody = [DoraemonUrlUtil convertJsonFromData:httpBody];
httpModel.uploadFlow = [NSString stringWithFormat:@"%zi",[DoraemonUrlUtil getRequestLength:request]];
//response
httpModel.mineType = response.MIMEType;
httpModel.response = response;
......@@ -33,12 +28,12 @@
httpModel.responseBody = [DoraemonUrlUtil convertJsonFromData:responseData];
httpModel.totalDuration = [NSString stringWithFormat:@"%fs",[[NSDate date] timeIntervalSince1970] - request.startTime.doubleValue];
httpModel.downFlow = [NSString stringWithFormat:@"%lli",[DoraemonUrlUtil getResponseLength:(NSHTTPURLResponse *)response data:responseData]];
return httpModel;
[[DoraemonNetFlowManager shareInstance] httpBodyFromRequest:request bodyCallBack:^(NSData *body) {
httpModel.requestBody = [DoraemonUrlUtil convertJsonFromData:body];
NSUInteger length = [DoraemonUrlUtil getHeadersLengthWithRequest:request] + [body length];
httpModel.uploadFlow = [NSString stringWithFormat:@"%zi", length];
complete(httpModel);
}];
}
@end
......@@ -7,6 +7,8 @@
#import <Foundation/Foundation.h>
typedef void(^HttpBodyCallBack)(NSData *body);
@interface DoraemonNetFlowManager : NSObject
+ (DoraemonNetFlowManager *)shareInstance;
......@@ -16,4 +18,6 @@
- (void)canInterceptNetFlow:(BOOL)enable;
- (void)httpBodyFromRequest:(NSURLRequest *)request bodyCallBack:(HttpBodyCallBack)complete;
@end
......@@ -13,7 +13,10 @@
#import "UIViewController+Doraemon.h"
#import "DoraemonHealthManager.h"
@interface DoraemonNetFlowManager() <DoraemonNetworkInterceptorDelegate>
@interface DoraemonNetFlowManager() <DoraemonNetworkInterceptorDelegate, NSStreamDelegate>
@property (nonatomic, copy) HttpBodyCallBack bodyCallBack;
@property (nonatomic, strong) NSMutableData *bodyData;
@end
......@@ -40,21 +43,79 @@
}
}
- (void)httpBodyFromRequest:(NSURLRequest *)request bodyCallBack:(HttpBodyCallBack)complete {
NSData *httpBody = nil;
if (request.HTTPBody) {
httpBody = request.HTTPBody;
complete(httpBody);
return;
}
if ([request.HTTPMethod isEqualToString:@"POST"]) {
NSInputStream *stream = request.HTTPBodyStream;
[stream setDelegate:self];
self.bodyCallBack = complete;
[stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[stream open];
} else {
complete(httpBody);
}
}
#pragma mark -- NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventHasBytesAvailable:
{
if (!self.bodyData) {
self.bodyData = [NSMutableData data];
}
uint8_t buf[1024];
NSInteger len = 0;
len = [(NSInputStream *)aStream read:buf maxLength:1024];
if (len) {
[self.bodyData appendBytes:(const void *)buf length:len];
}
}
break;
case NSStreamEventErrorOccurred:
{
NSError * error = [aStream streamError];
NSString * errorInfo = [NSString stringWithFormat:@"Failed while reading stream; error '%@' (code %ld)", error.localizedDescription, error.code];
NSLog(@"%@",errorInfo);
}
break;
case NSStreamEventEndEncountered:
{
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
aStream = nil;
if (self.bodyCallBack) {
self.bodyCallBack([self.bodyData copy]);
}
self.bodyData = nil;
}
break;
default:
break;
}
}
#pragma mark -- DoraemonNetworkInterceptorDelegate
- (void)doraemonNetworkInterceptorDidReceiveData:(NSData *)data response:(NSURLResponse *)response request:(NSURLRequest *)request error:(NSError *)error startTime:(NSTimeInterval)startTime {
DoraemonNetFlowHttpModel *httpModel = [DoraemonNetFlowHttpModel dealWithResponseData:data response:response request:request];
if (!response) {
httpModel.statusCode = error.localizedDescription;
}
httpModel.startTime = startTime;
httpModel.endTime = [[NSDate date] timeIntervalSince1970];
httpModel.totalDuration = [NSString stringWithFormat:@"%f",[[NSDate date] timeIntervalSince1970] - startTime];
httpModel.topVc = NSStringFromClass([[UIViewController topViewControllerForKeyWindow] class]);
[[DoraemonNetFlowDataSource shareInstance] addHttpModel:httpModel];
[[DoraemonHealthManager sharedInstance] addHttpModel:httpModel];
[DoraemonNetFlowHttpModel dealWithResponseData:data response:response request:request complete:^(DoraemonNetFlowHttpModel *httpModel) {
if (!response) {
httpModel.statusCode = error.localizedDescription;
}
httpModel.startTime = startTime;
httpModel.endTime = [[NSDate date] timeIntervalSince1970];
httpModel.totalDuration = [NSString stringWithFormat:@"%f",[[NSDate date] timeIntervalSince1970] - startTime];
httpModel.topVc = NSStringFromClass([[UIViewController topViewControllerForKeyWindow] class]);
[[DoraemonNetFlowDataSource shareInstance] addHttpModel:httpModel];
[[DoraemonHealthManager sharedInstance] addHttpModel:httpModel];
}];
}
- (BOOL)shouldIntercept {
......
......@@ -13,7 +13,9 @@
+ (NSDictionary *)convertDicFromData:(NSData *)data;
+ (NSUInteger)getRequestLength:(NSURLRequest *)request;
+ (NSUInteger)getHeadersLengthWithRequest:(NSURLRequest *)request;
+ (void)requestLength:(NSURLRequest *)request callBack:(void (^)(NSUInteger))callBack;
+ (NSUInteger)getHeadersLength:(NSDictionary *)headers ;
......@@ -21,6 +23,4 @@
+ (int64_t)getResponseLength:(NSHTTPURLResponse *)response data:(NSData *)responseData;
+ (NSData *)getHttpBodyFromRequest:(NSURLRequest *)request;
@end
......@@ -6,6 +6,7 @@
//
#import "DoraemonUrlUtil.h"
#import "DoraemonNetFlowManager.h"
@implementation DoraemonUrlUtil
......@@ -39,7 +40,15 @@
return jsonObj;
}
+ (NSUInteger)getRequestLength:(NSURLRequest *)request{
+ (void)requestLength:(NSURLRequest *)request callBack:(void (^)(NSUInteger))callBack {
NSUInteger headersLength = [self getHeadersLengthWithRequest:request];
[[DoraemonNetFlowManager shareInstance] httpBodyFromRequest:request bodyCallBack:^(NSData *body) {
NSUInteger bodyLength = [body length];
callBack(headersLength + bodyLength);
}];
}
+ (NSUInteger)getHeadersLengthWithRequest:(NSURLRequest *)request {
NSDictionary<NSString *, NSString *> *headerFields = request.allHTTPHeaderFields;
NSDictionary<NSString *, NSString *> *cookiesHeader = [self getCookies:request];
if (cookiesHeader.count) {
......@@ -47,11 +56,7 @@
[headerFieldsWithCookies addEntriesFromDictionary:cookiesHeader];
headerFields = [headerFieldsWithCookies copy];
}
NSUInteger headersLength = [self getHeadersLength:headerFields];
NSData *httpBody = [[self class] getHttpBodyFromRequest:request];
NSUInteger bodyLength = [httpBody length];
return headersLength + bodyLength;
return [self getHeadersLength:headerFields];
}
+ (NSUInteger)getHeadersLength:(NSDictionary *)headers {
......@@ -95,28 +100,4 @@
return responseLength;
}
+ (NSData *)getHttpBodyFromRequest:(NSURLRequest *)request{
NSData *httpBody;
if (request.HTTPBody) {
httpBody = request.HTTPBody;
}else{
if ([request.HTTPMethod isEqualToString:@"POST"]) {
if (!request.HTTPBody) {
uint8_t d[1024] = {0};
NSInputStream *stream = request.HTTPBodyStream;
NSMutableData *data = [[NSMutableData alloc] init];
[stream open];
while ([stream hasBytesAvailable]) {
NSInteger len = [stream read:d maxLength:1024];
if (len > 0 && stream.streamError == nil) {
[data appendBytes:(void *)d length:len];
}
}
httpBody = [data copy];
[stream close];
}
}
}
return httpBody;
}
@end
......@@ -115,7 +115,9 @@
#pragma mark -- DoraemonNetworkInterceptorDelegate
- (void)doraemonNetworkInterceptorDidReceiveData:(NSData *)data response:(NSURLResponse *)response request:(NSURLRequest *)request error:(NSError *)error startTime:(NSTimeInterval)startTime {
[[DoraemonWeakNetworkWindow shareInstance] updateFlowValue:[NSString stringWithFormat:@"%zi",[DoraemonUrlUtil getRequestLength:request]] downFlow:[NSString stringWithFormat:@"%lli",[DoraemonUrlUtil getResponseLength:(NSHTTPURLResponse *)response data:data]] fromWeak:NO];
[DoraemonUrlUtil requestLength:request callBack:^(NSUInteger length) {
[[DoraemonWeakNetworkWindow shareInstance] updateFlowValue:[NSString stringWithFormat:@"%zi",length] downFlow:[NSString stringWithFormat:@"%lli",[DoraemonUrlUtil getResponseLength:(NSHTTPURLResponse *)response data:data]] fromWeak:NO];
}];
}
- (BOOL)shouldIntercept {
......
......@@ -181,7 +181,8 @@
- (DoraemonMockBaseModel *)getSelectedData:(NSURLRequest *)request dataArray:(NSArray *)dataArray{
NSString *path = request.URL.path;
NSString *query = request.URL.query;
NSData *httpBody = [DoraemonUrlUtil getHttpBodyFromRequest:request];
// 这里暂时使用不严谨body match
NSData *httpBody = request.HTTPBody;
NSDictionary *requestBody = [DoraemonUrlUtil convertDicFromData:httpBody];
DoraemonMockBaseModel *selectedApi;
for (DoraemonMockBaseModel *api in dataArray) {
......
......@@ -8,6 +8,7 @@
#import "DoraemonDemoURLProtocol1.h"
#import "DoraemonUrlUtil.h"
#import "DoraemonNetFlowManager.h"
static NSString * const kDoraemonDemoUrlProtocolKey = @"doraemon_demo_url_protocol_1_key";
......@@ -48,10 +49,11 @@ static NSString * const kDoraemonDemoUrlProtocolKey = @"doraemon_demo_url_protoc
- (void)stopLoading{
NSLog(@"11111 == stopLoading");
NSData *httpBody = [DoraemonUrlUtil getHttpBodyFromRequest:self.request];
NSString* requestBody = [DoraemonUrlUtil convertJsonFromData:httpBody];
NSLog(@"11111 == requestBody = %@",requestBody);
[self.connection cancel];
[[DoraemonNetFlowManager shareInstance] httpBodyFromRequest:self.request bodyCallBack:^(NSData *httpBody) {
NSString* requestBody = [DoraemonUrlUtil convertJsonFromData:httpBody];
NSLog(@"11111 == requestBody = %@",requestBody);
[self.connection cancel];
}];
}
......
......@@ -8,6 +8,7 @@
#import "DoraemonDemoURLProtocol2.h"
#import "DoraemonUrlUtil.h"
#import "DoraemonNetFlowManager.h"
static NSString * const kDoraemonDemoUrlProtocol2Key = @"doraemon_demo_url_protocol_2_key";
......@@ -48,10 +49,11 @@ static NSString * const kDoraemonDemoUrlProtocol2Key = @"doraemon_demo_url_proto
- (void)stopLoading{
NSLog(@"22222 == stopLoading");
NSData *httpBody = [DoraemonUrlUtil getHttpBodyFromRequest:self.request];
NSString* requestBody = [DoraemonUrlUtil convertJsonFromData:httpBody];
NSLog(@"22222 == requestBody = %@",requestBody);
[self.connection cancel];
[[DoraemonNetFlowManager shareInstance] httpBodyFromRequest:self.request bodyCallBack:^(NSData *httpBody) {
NSString* requestBody = [DoraemonUrlUtil convertJsonFromData:httpBody];
NSLog(@"22222 == requestBody = %@",requestBody);
[self.connection cancel];
}];
}
......
......@@ -222,10 +222,11 @@
session.requestSerializer = [AFHTTPRequestSerializer serializer];// 请求
session.responseSerializer = [AFHTTPResponseSerializer serializer];// 响应
session.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
[session GET:@"https://www.taobao.com/" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
[session GET:@"https://www.taobao.com/" parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(@"success %@",string);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"failure");
}];
......@@ -238,7 +239,7 @@
}
- (void)netForAFNetworking2{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];// 请求
manager.responseSerializer = [AFHTTPResponseSerializer serializer];// 响应
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
......@@ -249,10 +250,10 @@
// NSLog(@"请求失败");
// }];
[manager POST:@"https://www.taobao.com/" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[manager POST:@"https://www.taobao.com/" parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(@"success %@",string);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"failure");
}];
}
......
......@@ -130,14 +130,15 @@
// NSLog(@"请求失败");
// }];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];// 请求
manager.responseSerializer = [AFHTTPResponseSerializer serializer];// 响应
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
[manager GET:@"https://www.taobao.com/" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[manager GET:@"https://www.taobao.com/" parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(@"request success %@",string);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"request failure");
}];
}
......
......@@ -20,7 +20,7 @@ target :'DoraemonKitDemo' do
#pod 'DoraemonKit', :subspecs => ['Core','WithLogger','WithGPS','WithLoad','WithWeex', 'WithDatabase', 'WithMLeaksFinder'], :path => '../../'
#pod 'DoraemonKit', :subspecs => ['Core'], :path => '../../'
pod 'DoraemonKit', :subspecs => ['Core','WithLogger','WithGPS','WithLoad','WithDatabase', 'WithMLeaksFinder','WithWeex'], :path => '../../'
pod 'AFNetworking','2.6.3'
pod 'AFNetworking'
pod 'SDWebImage'
pod 'SocketRocket', '~> 0.5'
pod 'SDWebImageWebPCoder'
......
PODS:
- AFNetworking (2.6.3):
- AFNetworking/NSURLConnection (= 2.6.3)
- AFNetworking/NSURLSession (= 2.6.3)
- AFNetworking/Reachability (= 2.6.3)
- AFNetworking/Security (= 2.6.3)
- AFNetworking/Serialization (= 2.6.3)
- AFNetworking/UIKit (= 2.6.3)
- AFNetworking/NSURLConnection (2.6.3):
- AFNetworking (4.0.1):
- AFNetworking/NSURLSession (= 4.0.1)
- AFNetworking/Reachability (= 4.0.1)
- AFNetworking/Security (= 4.0.1)
- AFNetworking/Serialization (= 4.0.1)
- AFNetworking/UIKit (= 4.0.1)
- AFNetworking/NSURLSession (4.0.1):
- AFNetworking/Reachability
- AFNetworking/Security
- AFNetworking/Serialization
- AFNetworking/NSURLSession (2.6.3):
- AFNetworking/Reachability
- AFNetworking/Security
- AFNetworking/Serialization
- AFNetworking/Reachability (2.6.3)
- AFNetworking/Security (2.6.3)
- AFNetworking/Serialization (2.6.3)
- AFNetworking/UIKit (2.6.3):
- AFNetworking/NSURLConnection
- AFNetworking/Reachability (4.0.1)
- AFNetworking/Security (4.0.1)
- AFNetworking/Serialization (4.0.1)
- AFNetworking/UIKit (4.0.1):
- AFNetworking/NSURLSession
- CocoaLumberjack (3.6.2):
- CocoaLumberjack/Core (= 3.6.2)
- CocoaLumberjack/Core (3.6.2)
- CocoaLumberjack (3.7.0):
- CocoaLumberjack/Core (= 3.7.0)
- CocoaLumberjack/Core (3.7.0)
- DoraemonKit/Core (3.0.6):
- FMDB
- GCDWebServer
......@@ -65,12 +59,12 @@ PODS:
- libwebp/mux (1.1.0):
- libwebp/demux
- libwebp/webp (1.1.0)
- SDWebImage (5.8.4):
- SDWebImage/Core (= 5.8.4)
- SDWebImage/Core (5.8.4)
- SDWebImageWebPCoder (0.6.1):
- SDWebImage (5.10.4):
- SDWebImage/Core (= 5.10.4)
- SDWebImage/Core (5.10.4)
- SDWebImageWebPCoder (0.8.2):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.7)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.5.1)
- WeexSDK (0.28.0)
- WXDevtool (0.24.0):
......@@ -81,7 +75,7 @@ PODS:
- GCDWebServer
DEPENDENCIES:
- AFNetworking (= 2.6.3)
- AFNetworking
- DoraemonKit/Core (from `../../`)
- DoraemonKit/WithDatabase (from `../../`)
- DoraemonKit/WithGPS (from `../../`)
......@@ -118,27 +112,27 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
FBRetainCycleDetector:
:commit: 1ff2adee84a6ee94a1ae82526104a188774eb90a
:commit: 32c4afc1fc17553f9b69e4edd82cfa3c73dbb331
:git: https://github.com/facebook/FBRetainCycleDetector.git
YYDebugDatabase:
:commit: 87f4214ab9656b75dd74dfcc042cc3b5067ebab6
:git: https://github.com/y500/YYDebugDatabase.git
SPEC CHECKSUMS:
AFNetworking: cb8d14a848e831097108418f5d49217339d4eb60
CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94
AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
CocoaLumberjack: e8955b9d337ac307103b0a34fd141c32f27e53c5
DoraemonKit: 919709427c30af94e265532a879ac41797d296d1
FBRetainCycleDetector: 46daef95c2dfa9be34b53087edf6a8f34e4c749c
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3
SDWebImage: cf6922231e95550934da2ada0f20f2becf2ceba9
SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21
SDWebImage: c666b97e1fa9c64b4909816a903322018f0a9c84
SDWebImageWebPCoder: f56ab499e3ea57dfeb6c3187dce183b10e160db0
SocketRocket: d57c7159b83c3c6655745cd15302aa24b6bae531
WeexSDK: 78861d2f8b78f67e30580c15a54f5b420456db39
WXDevtool: 95b70c73c06fc3299d65bd53ba4b3e0b0087f3cb
YYDebugDatabase: e684a7f79fca2e3673a23347cefb822f911f3124
PODFILE CHECKSUM: 6c5cced982a83d1a6dbfb6e8501724985d70d130
PODFILE CHECKSUM: a955cdaf3e59b0dc21224f229ea928cf97cc0173
COCOAPODS: 1.10.1
COCOAPODS: 1.8.4
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册