原文:C# 反射的委托创建器

.Net 的反射是个很好很强大的东西,不过它的效率却实在是不给力。已经有很多人针对这个问题讨论过了,包括各种各样的 DynamicMethod 和各种各样的效率测试,不过总的来说解决方案就是利用 Expression Tree、Delegate.CreateDelegate 或者 Emit 构造出反射操作对应的委托,从而实现加速反射的目的。

虽然本篇文章同样是讨论利用委托来加速反射调用函数,不过重点并不在于如何提升调用速度,而是如何更加智能的构造出反射的委托,并最终完成一个方便易用的委托创建器 DelegateBuilder。

它的设计目标是:

  1. 能够对方法调用、构造函数调用,获取或设置属性和获取或设置字段提供支持。
  2. 能够构造出特定的委托类型,而不仅限于 Func<object, object[], object> 或者其它的 Func 和 Action,因为我个人很喜欢强类型的委托,同时类似 void MyDeleagte(params int[] args) 这样的委托有时候也是很有必要的,如果需要支持 ref 和 out 参数,就必须使用自定义的委托类型了。
  3. 能够支持泛型方法,因为利用反射选择泛型方法是件很纠结的事(除非没有同名方法),而且还需要再 MakeGenericMethod。
  4. 能够支持类型的显式转换,在对某些 private 类的实例方法构造委托时,实例本身就必须使用 object 传入才可以。

其中的 3、4 点,在前几篇随笔《C# 判断类型间能否隐式或强制类型转换》和《C# 泛型方法的类型推断》中已经被解决了,并且整合到了 PowerBinder 中,这里只要解决 1、2 点就可以了,这篇随笔就是来讨论如何根据反射来构造出相应的委托。

就目前完成的效果,DelegateBuilder 可以使用起来还是非常方便的,下面给出一些示例:

class Program {
public delegate void MyDelegate(params int[] args);
public static void TestMethod(int value) { }
public void TestMethod(uint value) { }
public static void TestMethod<T>(params T[] arg) { }
static void Main(string[] args) {
Type type = typeof(Program);
Action<int> m1 = type.CreateDelegate<Action<int>>("TestMethod");
m1(10);
Program p = new Program();
Action<Program, uint> m2 = type.CreateDelegate<Action<Program, uint>>("TestMethod");
m2(p, 10);
Action<object, uint> m3 = type.CreateDelegate<Action<object, uint>>("TestMethod");
m3(p, 10);
Action<uint> m4 = type.CreateDelegate<Action<uint>>("TestMethod", p);
m4(10);
MyDelegate m5 = type.CreateDelegate<MyDelegate>("TestMethod");
m5(0, 1, 2);
}
}

可以说效果还是不错的,这里的 CreateDelegate 的用法与 Delegate.CreateDelegate 完全相同,功能却大大丰富,几乎可以只依靠 delegate type、type 和 memberName 构造出任何需要的委托,省去了自己反射获取类型成员的过程。

这里特别要强调一点:这个类用起来很简单,但是简单的背后是实现的复杂,所以各种没有发现的 bug 和推断错误是很正常的。

我再补充一点:虽然在这里我并不打算讨论效率问题,但的确有不少朋友对效率问题有点纠结,我就来详细解释下这个问题。

第一个问题:为什么要用委托来代替反射。如果手头有 Reflector 之类的反编译软件,可以看看 System.Reflection.RuntimeMethodInfo.Invoke 方法的实现,它首先需要检查参数(检查默认参数、类型转换之类的),然后检查各种 Flags,然后再调用 UnsafeInvokeInternal 完成真正的调用过程,显然比直接调用方法要慢上不少。而如果利用 Expression Tree 之类的方法构造出了委托,它就相当于只多了一层方法调用,性能不会损失多少(据说如果 Emit 用得好还能更快),因此才需要利用委托来代替反射。

第二个问题:什么时候适合用委托来代替反射。现在假设有一家公园,它的门票是 1 元,它还有一种终身票,票价是 20 元。如果我只是想进去看看,很可能以后就不再去了,那么我直接花 1 元进去是最合适的。但如果我想天天去溜达溜达,那么花 20 元买个终身票一定更加合适。

相对应的,1 元的门票就是反射,20 元的终身票就是委托——如果某个方法我只是偶尔调用一下,那么直接用反射就好了,反正损失也不是很大;如果我需要经常调用,花点时间构造个委托出来则是更好的选择,虽然构造委托这个过程比较慢,但它受用终身的。

第三个问题:怎么测试委托和反射的效率。测试效率的前提就是假设某个方法是需要被经常调用的,否则压根没必要使用委托。那么,基本的结构如下所示:

Stopwatch sw = new Stopwatch();
Type type = typeof(Program);
sw.Start();
Action<int> action = type.CreateDelegate<Action<int>>("TestMethod");
for (int i = 0; i < 10000; i++)
{
action(i);
}
sw.Stop();
Console.WriteLine("DelegateBuilder:{0} ms", sw.ElapsedMilliseconds);
sw.Start();
MethodInfo method = type.GetMethod("TestMethod");
for (int i = 0; i < 10000; i++)
{
method.Invoke(null, new object[] { i });
}
sw.Stop();
Console.WriteLine("Reflection:{0} ms", sw.ElapsedMilliseconds);

这里将构造委托的过程和反射得到 MethodInfo 的过程都放在了循环的外面,是因为它们只需要获取一次,就可以一直使用的(也就是所谓的“预处理”)。至于时候将它们放在 StopWatch 的 Start 和 Stop 之间,就看是否想将预处理所需的时间也计算在内了。

目前我能想到的问题就这三个了,如果还有什么其它相关问题,可以联系我。

言归正传,下面就来分析如何为反射构造出相应的委托。为了简便起见,我将使用 Expression Tree 来构造委托,这样更加易读,而且效率也并不会比 Emit 低多少。对于 Expression 不熟悉的朋友可以参考 Expression 类

一、从 MethodInfo 创建方法的委托

首先从创建方法的委托说开来,因为方法的委托显然是最常用、最基本的了。Delegate 类为我们提供了一个很好的参考,它的 CreateDelegate 方法有十个重载,这些重载之间的关系可以用下面的图表示出来,他们的详细解释可见 MSDN

图1 Delegate.CreateDelegate

这些方法的确很给力,用起来也比较方便,尽管在我看来还不够强大:)。为了易于上手,自己的方法委托创建方法的行为也应该类似于 Delegate.CreateDelegate 方法,因此接下来会先分析 CreateDelegate 方法的用法,然后再解释如何自己创建委托。

1.1 创建开放的方法委托

CreateDelegate(Type, MethodInfo) 和 CreateDelegate(Type, MethodInfo, Boolean) 的功能是相同的,都是可以创建静态方法的委托,或者是显式提供实例方法的第一个隐藏参数(称开放的实例方法,从 .Net Framework 2.0 以后支持)的委托。以下面的类为例:

class TestClass {
public static void TestStaticMethod(string value) {}
public void TestMethod(string value) {}
}

要创建 TestStaticMethod 方法的委托,需要使用 Action<string> 委托类型,代码为

Delegate.CreateDelegate(typeof(Action<string>), type.GetMethod("TestStaticMethod"))

得到的委托的效果与 TestStaticMethod(arg1) 相同。

要创建 TestMethod 方法的委托,则需要使用 Action<TestClass, string> 委托类型才可以,第一个参数表示要在其上调用方法的 TestClass 的实例:

Delegate.CreateDelegate(typeof(Action<TestClass, string>), type.GetMethod("TestMethod"))

得到的委托的效果与 arg1.TestMethod(arg2) 相同。

这个方法的用法很明确,自己实现起来也非常简单:

首先对开放的泛型方法构造相应的封闭的泛型方法,做法与上一篇《C# 使用 Binder 类自定义反射》中的 2.2.2 处理泛型方法 一节使用的算法相同,这里就不再赘述了。

接下就可以直接利用 Expression.Call 创建一个方法调用的委托,并对每个参数添加一个强制类型转换(Expression.Convert)即可。需要注意的是如果 MethodInfo 是实例方法,那么第一个参数要作为实例使用。最后用 Expression 构造出来的方法应该类似于:

// method 对应于静态方法。
returnType MethodDelegate(PT0 p0, PT1 p1, ... , PTn pn) {
return method((T0)p0, (T1)p1, ... , (Tn)pn);
}
// method 对应于实例方法。
returnType MethodDelegate(PT0 p0, PT1 p1, ... , PTn pn) {
return ((T0)p0).method((T1)p1, ... , (Tn)pn);
}

构造开放的方法委托的核心方法如下所示:

private static Delegate CreateOpenDelegate(Type type,
MethodInfo invoke, ParameterInfo[] invokeParams,
MethodInfo method, ParameterInfo[] methodParams)
{
// 要求参数数量匹配,其中实例方法的第一个参数用作传递实例对象。
int skipIdx = method.IsStatic ? 0 : 1;
if (invokeParams.Length == methodParams.Length + skipIdx)
{
if (method.IsGenericMethodDefinition)
{
// 构造泛型方法的封闭方法,对于实例方法要跳过第一个参数。
Type[] paramTypes = GetParameterTypes(invokeParams, skipIdx, 0, 0);
method = method.MakeGenericMethodFromParams(methodParams, paramTypes);
if (method == null) { return null; }
methodParams = method.GetParameters();
}
// 方法的参数列表。
ParameterExpression[] paramList = GetParameters(invokeParams);
// 构造调用参数列表。
Expression[] paramExps = GetParameterExpressions(paramList, skipIdx, methodParams, 0);
if (paramExps != null)
{
// 调用方法的实例对象。
Expression instance = null;
if (skipIdx == 1)
{
instance = ConvertType(paramList[0], method.DeclaringType);
if (instance == null)
{
return null;
}
}
Expression methodCall = Expression.Call(instance, method, paramExps);
methodCall = GetReturn(methodCall, invoke.ReturnType);
if (methodCall != null)
{
return Expression.Lambda(type, methodCall, paramList).Compile();
}
}
}
return null;
}

1.2 创建第一个参数封闭的方法委托

CreateDelegate(Type, Object, MethodInfo) 和 CreateDelegate(Type, Object, MethodInfo, Boolean) 是最灵活的创建委托的方法,可以创建静态或实例方法的委托,可以提供或不提供第一个参数。先来给出所有用法的示例:

class TestClass {
public static void TestStaticMethod(string value) {}
public void TestMethod(string value) {}
}

对于 TestStaticMethod (静态方法)来说:

  1. 若 firstArgument 不为 null,则在每次调用委托时将其传递给方法的第一个参数,此时称为通过第一个参数封闭,要求委托的签名包括方法除第一个参数之外的所有参数,使用方法为

    Delegate.CreateDelegate(typeof(Action), "str", type.GetMethod("TestStaticMethod"))
    

    得到的委托的效果与 TestStaticMethod(firstArgument) 相同。

  2. 若 firstArgument 为 null,且委托和方法的签名匹配(即所有参数类型都兼容),则此时称为开放的静态方法委托,使用方法为
    Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestStaticMethod"))
    

    得到的委托的效果与 TestStaticMethod(arg1) 相同。

  3. 若 firstArgument 为 null,且委托的签名以方法的第二个参数开头,其余参数类型都兼容,则此时称为通过空引用封闭的委托,使用方法为
    Delegate.CreateDelegate(typeof(Action), null, type.GetMethod("TestStaticMethod"))
    

    得到的委托的效果与 TestStaticMethod(null) 相同。

对于 TestMethod (实例方法)来说:

  1. 若 firstArgument 不为 null,则 firstArgument 被传递给隐藏的实例参数(就是 this),这时成为封闭的实例方法,要求委托的签名必须和方法的签名匹配,使用方法为

    Delegate.CreateDelegate(typeof(Action<string>), new TestClass(), type.GetMethod("TestMethod"))
    

    得到的委托效果与 firstArgument.TestMethod(arg1) 相同。

  2. 若 firstArgument 为 null,且委托显示包含方法的第一个隐藏参数(就是 this),则此时称为开放的实例方法委托,使用方法为
    Delegate.CreateDelegate(typeof(Action<TestClass, string>), null, type.GetMethod("TestMethod"))
    

    得到的委托效果与 arg1.TestMethod(arg2) 相同。

  3. 若 firstArgument 为 null,且委托的签名与方法的签名匹配,则此时称为通过空引用封闭的委托,使用方法为
    Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestMethod"))
    

    这种用法比较奇怪,这种用法类似于对空实例调用实例方法(null.TestMethod(obj)),在方法体内得到的 this 就是 null,在实际当中不是很有用。

将以上六点总结来看,就是根据方法是静态方法还是实例方法,以及委托与方法签名的匹配方式就可以决定如何构造委托了。下面就是判断的流程图:

图2 方法委托的流程图

对于开放的静态或实例方法,可以使用上一节完成的方法;对于封闭的静态或实例方法,做法也比较类似,只要将 firstArgument 作为静态方法的第一个参数或者是实例使用即可;在流程图中特地将通过空引用封闭的实例方法拿出来,是因为 Expression 不能实现对 null 调用实例方法,只能够使用 Delegate.CreateDelegate 来生成委托,然后在外面再套一层自己的委托以实现强制类型转换。这么做效率肯定会更低,但毕竟这种用法基本不可能见到,这里仅仅是为了保证与 CreateDelegate 的统一。

1.3 创建通用的方法委托

这里我多加了一个方法,就是创建一个通用的方法委托,这个委托的声明如下:

public delegate object MethodInvoker(object instance, params object[] parameters);

通过这个委托,就可以调用任意的方法了。要实现这个方法也很简单,只要用 Expression 构造出类似于下面的方法即可。

object MethodDelegate(object instance, params object[] parameters) {
  // 检查 parameters 的长度。
  if (parameters == null || parameters.Length != n + 1) {
    throw new TargetParameterCountException();
  }
  // 调用方法。
  return instance.method((T0)parameters[0], (T1)parameters[1], ... , (Tn)parameters[n]);
}

对于泛型方法,显然无法进行泛型参数推断,直接报错就好;对于静态方法,直接无视 instance 参数就可以。

public static MethodInvoker CreateDelegate(this MethodInfo method)
{
ExceptionHelper.CheckArgumentNull(method, "method");
if (method.IsGenericMethodDefinition)
{
// 不对开放的泛型方法执行绑定。
throw ExceptionHelper.BindTargetMethod("method");
}
// 要执行方法的实例。
ParameterExpression instanceParam = Expression.Parameter(typeof(object));
// 方法的参数。
ParameterExpression parametersParam = Expression.Parameter(typeof(object[]));
// 构造参数列表。
ParameterInfo[] methodParams = method.GetParameters();
Expression[] paramExps = new Expression[methodParams.Length];
for (int i = 0; i < methodParams.Length; i++)
{
// (Ti)parameters[i]
paramExps[i] = ConvertType(
Expression.ArrayIndex(parametersParam, Expression.Constant(i)),
methodParams[i].ParameterType);
}
// 静态方法不需要实例,实例方法需要 (TInstance)instance
Expression instanceCast = method.IsStatic ? null :
ConvertType(instanceParam, method.DeclaringType);
// 调用方法。
Expression methodCall = Expression.Call(instanceCast, method, paramExps);
// 添加参数数量检测。
methodCall = Expression.Block(GetCheckParameterExp(parametersParam, methodParams.Length), methodCall);
return Expression.Lambda<MethodInvoker>(GetReturn(methodCall, typeof(object)),
instanceParam, parametersParam).Compile();
}

二、从 ConstructorInfo 创建构造函数的委托

创建构造函数的委托的情况就很简单了,构造函数没有静态和实例的区分,不存在泛型方法,而且委托和构造函数的签名一定是匹配的,实现起来就如同 1.1 创建开放的方法委托,不过这是用到的实 Expression.New 方法而不是 Expression.Call 了。

public static Delegate CreateDelegate(Type type, ConstructorInfo ctor, bool throwOnBindFailure)
{
ExceptionHelper.CheckArgumentNull(ctor, "ctor");
CheckDelegateType(type, "type");
MethodInfo invoke = type.GetMethod("Invoke");
ParameterInfo[] invokeParams = invoke.GetParameters();
ParameterInfo[] methodParams = ctor.GetParameters();
// 要求参数数量匹配。
if (invokeParams.Length == methodParams.Length)
{
// 构造函数的参数列表。
ParameterExpression[] paramList = GetParameters(invokeParams);
// 构造调用参数列表。
Expression[] paramExps = GetParameterExpressions(paramList, 0, methodParams, 0);
if (paramExps != null)
{
Expression methodCall = Expression.New(ctor, paramExps);
methodCall = GetReturn(methodCall, invoke.ReturnType);
if (methodCall != null)
{
return Expression.Lambda(type, methodCall, paramList).Compile();
}
}
}
if (throwOnBindFailure)
{
throw ExceptionHelper.BindTargetMethod("ctor");
}
return null;
}

与通用的方法委托类似的,我也使用下面的委托

public delegate object InstanceCreator(params object[] parameters);

来创建通用的构造函数的委托,与通用的方法委托的实现也很类似。

public static Delegate CreateDelegate(Type type, ConstructorInfo ctor, bool throwOnBindFailure)
{
ExceptionHelper.CheckArgumentNull(ctor, "ctor");
CheckDelegateType(type, "type");
MethodInfo invoke = type.GetMethod("Invoke");
ParameterInfo[] invokeParams = invoke.GetParameters();
ParameterInfo[] methodParams = ctor.GetParameters();
// 要求参数数量匹配。
if (invokeParams.Length == methodParams.Length)
{
// 构造函数的参数列表。
ParameterExpression[] paramList = GetParameters(invokeParams);
// 构造调用参数列表。
Expression[] paramExps = GetParameterExpressions(paramList, 0, methodParams, 0);
if (paramExps != null)
{
Expression methodCall = Expression.New(ctor, paramExps);
methodCall = GetReturn(methodCall, invoke.ReturnType);
if (methodCall != null)
{
return Expression.Lambda(type, methodCall, paramList).Compile();
}
}
}
if (throwOnBindFailure)
{
throw ExceptionHelper.BindTargetMethod("ctor");
}
return null;
}

三、从 PropertyInfo 创建属性的委托

有了创建方法的委托作为基础,创建属性的委托就非常容易了。如果委托具有返回值那么意味着是获取属性,不具有返回值(返回值为 typeof(void))意味着是设置属性。然后利用 PropertyInfo.GetGetMethod 或 PropertyInfo.GetSetMethod 来获取相应的 get 访问器或 set 访问器,最后直接调用创建方法的委托就可以了。

封闭的属性委托也同样很有用,这样可以将属性的实例与委托绑定。

对于属性并没有创建通用的委托,是因为属性的访问分为获取和设置两部分的,这两部分难以有效的结合到一块。

四、从 FieldInfo 创建字段的委托

在创建字段的委托时,就不能使用现有的方法了,而必须用 Expression.Assign 自己完成字段的赋值。字段的委托同样可以分为开放的字段委托和使用第一个参数封闭的字段委托,其判断过程如下:

图3 字段委托流程图

字段的处理很简单,就是通过 Expression.Field 访问字段,然后通过 Expression.Assign 对字段进行赋值,或者直接返回字段的值。图中单独列出来的“通过空引用封闭的实例字段”,同样是因为不能用代码访问空对象的实例字段,这显然是个毫无意义的操作,不过为了与通过空引用封闭的属性得到的结果相同,这里总是抛出 System.NullReferenceException。

五、从 Type 创建成员委托

这个方法提供了创建成员委托的最灵活的方式,它可以根据给出的成员名称、BindingFlags 和委托的签名决定是创建方法、构造函数、属性还是字段的委托。

它的做法就是,依次利用 PowerBinder.Cast 在 type 中查找与给定委托签名匹配的方法、属性和字段,并尝试为每个匹配的成员构造委托(使用前面四个部分中给出的方法)。当某个成员成功构造出委托,那么它就是最后需要的那个。

由于 PowerBinder 可以支持查找泛型方法和显式类型转换,因此构造委托的时候也自然就能够支持泛型方法和显式类型转换了。

DelegateBuilder 构造委托的方法算是到此结束了,完整的源代码可见 DelegateBuilder.cs,总共大约 2500 行,不过其中大部分都是注释和各种方法重载(目前有 54 个重载),VS 代码度量的结果只有 509 行。

C# 反射的委托创建器的更多相关文章

  1. java JDK8 学习笔记——第17章 反射与类加载器

    第十七章 反射与类加载器 17.1 运用反射 反射:.class文档反映了类基本信息,从Class等API取得类信息的方式称为反射. 17.1.1 Class与.class文档 1.java.lang ...

  2. StructureMap.dll 中的 GetInstance 重载 + 如何利用 反射动态创建泛型类

    public static T GetInstance<T>(ExplicitArguments args); // // Summary: // Creates a new instan ...

  3. Responsive Web CSS – 在线响应式布局创建器

    如果您已经使用了 CSS 或前端框架,创建响应式布局应该不难. 然而,如果你刚涉足这类布局,Responsive Web CSS 可以帮助你快速上手. 这是一个基于 Web 的工具,使任何人都可以通过 ...

  4. ubuntu 启动项创建器 选择不了CD镜像,IOS镜像的解决方法

    自己系统是ubuntu14.04 , 想使用 ubuntu自带的启动项创建器(usb-creator-gtk)做一个CDLinux的U盘启动项, 打开程序后发现U盘识别了, 在添加镜像的时候,发现怎么 ...

  5. 【Unity】4.5 树木创建器

    分类:Unity.C#.VS2015 创建日期:2016-04-11 一.简介 在地形编辑器一节中,已经告诉了你如何使用已经创建好的树来形成大片树林.这一节告诉你在 Unity 5.3.4中如何利用[ ...

  6. C#反射动态创建实例并调用方法

    在.Net 中,程序集(Assembly)中保存了元数据(MetaData)信息,因此就可以通过分析元数据来获取程序集中的内容,比如类,方法,属性等,这大大方便了在运行时去动态创建实例. MSDN解释 ...

  7. Spring 中如何自动创建代理(spring中的三种自动代理创建器)

    Spring 提供了自动代理机制,可以让容器自动生成代理,从而把开发人员从繁琐的配置中解脱出来 . 具体是使用 BeanPostProcessor 来实现这项功能. 这三种自动代理创建器 为:Bean ...

  8. .Net 中的反射(动态创建类型实例) - Part.4

    动态创建对象 在前面节中,我们先了解了反射,然后利用反射查看了类型信息,并学习了如何创建自定义特性,并利用反射来遍历它.可以说,前面三节,我们学习的都是反射是什么,在接下来的章节中,我们将学习反射可以 ...

  9. Python基础2:反射、装饰器、JSON,接口

    一.反射 最近接触到python的反射机制,遂记录下来已巩固.但是,笔者也是粗略的使用了__import__, getattr()函数而已.目前,笔者的理解是,反射可以使用户通过自定义输入来导入响应的 ...

随机推荐

  1. lua执行的两种方式

    一.交互模式 二.脚本式 创建一个以lua结尾的文件,例如hello.lua文件内容 println("hello world")

  2. 构建高性能高并发Java系统 .

    转:http://blog.csdn.net/nengyu/article/details/7591854 场景这里指的高性能高并发服务器是一个有状态的服务,可以理解成web或者socket服务器,每 ...

  3. C++输入cin详解

    输入原理: 程序的输入都建有一个缓冲区,即输入缓冲区.一次输入过程是这样的,当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin函数直接从输入缓冲区中取数据.正因为cin函数是直接从缓冲区取数据 ...

  4. oracle11g 导出表报EXP-00011:table不存在。

    oracle11g 导出表报EXP-00011:table不存在. oracle11g,在用exp命令备份数据库时,如果表中没有数据报EXP-00011错误,对应的表不存在.这导致对应的空表无法备份. ...

  5. 实现读取文本数据,在将数据导入mysql

    import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java ...

  6. jquery下拉框应用

    <!DOCTYPE html> <html lang="en"> <head> <script src="http://code ...

  7. java-day12

    数据结构 常用的数据存储结构:栈,队列,数组,列表,红黑树. 栈:先进后出(入口和出口在用一侧) 队列:先进先出 数组: 查询快:因为数组的地址是连续的,通过数组的首地址找到数组中的元素. 增/删慢: ...

  8. assignment of day nine

    一.简述定义函数的三种方式 1.空函数:用于占位 2.有参函数:有参数的函数 3.无参函数:没有参数的函数 二.简述函数的返回值 1.如果函数没有返回值,默认返回None 2.函数可以通过return ...

  9. 21-Ubuntu-文件和目录命令-复制文件和目录-cp

    cp 将给出的文件或目录复制到另一个文件或目录,相当于DOS下的copy命令 选项 含义 -f 已经存在的目标文件直接覆盖,不提示 -i 覆盖文件前提示 -r 若给出的源文件是目录文件,则cp将递归复 ...

  10. iOS开发系列-Charles

    概述 Charles相当于一个插在服务器和客户端之间的"过滤器".当客户端向服务器发起请求的时候,先到charles进行过滤,然后charles在把最终的数据发送给服务器: 注意: ...