通常我们需要监测ASP.NET MVC 或 Web API 的应用程序的性能时,通常采用的是自定义性能计数器,性能计数器会引发无休止的运维问题(损坏的计数器、权限问题等)。这篇文章向你介绍一个新的替代性能计数器的工具Metrics.NET,因为是它是内部的,所以我们能够向系统中添加更多更有意义的度量标准。

Metrics.NET(https://github.com/etishor/Metrics.NET)是一个给CLR 提供度量工具的包,它是移植自Java的metrics,支持的平台 .NET 4.5.1, .NET 4.5, .NET 4.0 和 Mono 3.8.0,在c#代码中嵌入Metrics代码,可以方便的对业务代码的各个指标进行监控, 提供5种度量的类型:Gauges, Counters, Histograms, Meters,Timers:

Gauges

Gauge是最简单的度量类型,只有一个简单的返回值,例如,你的应用中有一个由第三方类库中保持的一个度量值,你可以很容易的通过Gauge来度量他

  1. long milliseconds = this.ConvertTicksToMilliseconds(elapsedTicks);
  2. String controllerName = this.actionInfo.ControllerName;
  3. String actionName = this.actionInfo.ActionName;
  4. string counterName = string.Format("{0} {1} {2}", controllerName, actionName, COUNTER_NAME);
  5. Metric.Context(this.actionInfo.ActionType).Gauge(counterName, () => milliseconds, Unit.Custom("Milliseconds"));

那么Metrics会创建一个叫做[MVC] Account LogOn Last Call Elapsed Time.Gauge的Gauge,返回最新的一个请求的时间。

Counters

Counter是一个简单64位的计数器:

  1. String categoryName = this.actionInfo.ControllerName;
  2. String instanceName = this.actionInfo.ActionName;
  3. string counterName = string.Format("{0} {1} {2}", categoryName, instanceName, COUNTER_NAME);
  4. this.callsInProgressCounter = Metric.Context(this.actionInfo.ActionType).Counter(counterName, Unit.Custom(COUNTER_NAME));
  5.  
  6. /// <summary>
  7. /// Constant defining the name of this counter
  8. /// </summary>
  9. public const String COUNTER_NAME = "ActiveRequests";
  10.  
  11. private Counter callsInProgressCounter;
  12.  
  13. /// <summary>
  14. /// Method called by the custom action filter just prior to the action begining to execute
  15. /// </summary>
  16. /// <remarks>
  17. /// This method increments the Calls in Progress counter by 1
  18. /// </remarks>
  19. public override void OnActionStart()
  20. {
  21. this.callsInProgressCounter.Increment();
  22. }
  23.  
  24. /// <summary>
  25. /// Method called by the custom action filter after the action completes
  26. /// </summary>
  27. /// <remarks>
  28. /// This method decrements the Calls in Progress counter by 1
  29. /// </remarks>
  30. public override void OnActionComplete(long elapsedTicks, bool exceptionThrown)
  31. {
  32. this.callsInProgressCounter.Decrement();
  33. }
  34. 所有的Counter都是从0开始,上述代码描述的当前的请求数。

Histograms-直方图

Histrogram是用来度量流数据中Value的分布情况,例如,每一个POST/PUT请求中的内容大小:

  1. public PostAndPutRequestSizeMetric(ActionInfo info)
  2. : base(info)
  3. {
  4. this.histogram = Metric.Context(this.actionInfo.ActionType).Histogram(COUNTER_NAME, Unit.Bytes, SamplingType.FavourRecent);
  5. }
  6.  
  7. /// <summary>
  8. /// Constant defining the name of this counter
  9. /// </summary>
  10. public const String COUNTER_NAME = "Post & Put Request Size";
  11.  
  12. /// <summary>
  13. /// Reference to the performance counter
  14. /// </summary>
  15. private Histogram histogram;
  16.  
  17. public override void OnActionStart()
  18. {
  19. var method = this.actionInfo.HttpMethod.ToUpper();
  20. if (method == "POST" || method == "PUT")
  21. {
  22. histogram.Update(this.actionInfo.ContentLength);
  23. }
  24. }

Histrogram 的度量值不仅仅是计算最大/小值、平均值,方差,他还展现了分位数(如中位数,或者95th分位数),如75%,90%,98%,99%的数据在哪个范围内。

传统上,中位数(或者其他分位数)是在一个完整的数据集中进行计算的,通过对数据的排序,然后取出中间值(或者离结束1%的那个数字,来计算99th分位数)。这种做法是在小数据集,或者是批量计算的系统中,但是在一个高吞吐、低延时的系统中是不合适的。

一个解决方案就是从数据中进行抽样,保存一个少量、易管理的数据集,并且能够反应总体数据流的统计信息。使我们能够简单快速的计算给定分位数的近似值。这种技术称作reservoir sampling。

Metrics中提供两种类型的直方图:uniform跟biased。

Uniform Histograms

Uniform Histogram提供直方图完整的生命周期内的有效的中位数,它会返回一个中位值。例如:这个中位数是对所有值的直方图进行了更新,它使用了一种叫做Vitter’s R的算法,随机选择了一些线性递增的样本。

当你需要长期的测量,请使用Uniform Histograms。在你想要知道流数据的分布中是否最近变化的话,那么不要使用这种。

Biased Histograms

Biased Histogram提供代表最近5分钟数据的分位数,他使用了一种forward-decayingpriority sample的算法,这个算法通过对最新的数据进行指数加权,不同于Uniform算法,Biased Histogram体现的是最新的数据,可以让你快速的指导最新的数据分布发生了什么变化。Timers中使用了Biased Histogram。

Meters

Meter度量一系列事件发生的比率:


  1. public DeltaExceptionsThrownMetric(ActionInfo info)
  2. : base(info)
  3. {
  4. this.deltaExceptionsThrownCounter
  5. = Metric.Context(this.actionInfo.ActionType).Meter(COUNTER_NAME, Unit.Errors, TimeUnit.Seconds);
  6. }
  7.  
  8. /// <summary>
  9. /// Constant defining the name of this counter
  10. /// </summary>
  11. public const String COUNTER_NAME = "Errors";
  12.  
  13. /// <summary>
  14. /// Reference to the performance counter
  15. /// </summary>
  16. private Meter deltaExceptionsThrownCounter;
  17.  
  18. /// <summary>
  19. /// Method called by the custom action filter after the action completes
  20. /// </summary>
  21. /// <remarks>
  22. /// If exceptionThrown is true, then the Total Exceptions Thrown counter will be
  23. /// incremented by 1
  24. /// </remarks>
  25. public override void OnActionComplete(long elapsedTicks, bool exceptionThrown)
  26. {
  27. if (exceptionThrown)
  28. this.deltaExceptionsThrownCounter.Mark();
  29. }
  1.  

Meter需要除了Name之外的两个额外的信息,事件类型(enent type)跟比率单位(rate unit)。事件类型简单的描述Meter需要度量的事件类型,在上面的例子中,Meter是度量失败的请求数,所以他的事件类型也叫做“Errors”。比率单位是命名这个比率的单位时间,在上面的例子中,这个Meter是度量每秒钟的失败请求次数,所以他的单位就是秒。这两个参数加起来就是表述这个Meter,描述每秒钟的失败请求数。

Meter从几个角度上度量事件的比率,平均值是时间的平均比率,它描述的是整个应用完整的生命周期的情况(例如,所有的处理的请求数除以运行的秒数),它并不描述最新的数据。幸好,Meters中还有其他3个不同的指数方式表现的平均值,1分钟,5分钟,15分钟内的滑动平均值。

Hint:这个平均值跟Unix中的uptime跟top中秒数的Load的含义是一致的。

Timers

Timer是Histogram跟Meter的一个组合

  1. public TimerForEachRequestMetric(ActionInfo info)
  2. : base(info)
  3. {
  4. String controllerName = this.actionInfo.ControllerName;
  5. String actionName = this.actionInfo.ActionName;
  6. string counterName = string.Format("{0}{1}", controllerName, actionName);
  7.  
  8. this.averageTimeCounter = Metric.Context(this.actionInfo.ActionType).Timer(counterName, Unit.Requests, SamplingType.FavourRecent,
  9. TimeUnit.Seconds, TimeUnit.Milliseconds);
  10. }
  11.  
  12. #region Member Variables
  13. private Timer averageTimeCounter;
  14. #endregion
  15.  
  16. /// <summary>
  17. /// Method called by the custom action filter after the action completes
  18. /// </summary>
  19. /// <remarks>
  20. /// This method increments the Average Time per Call counter by the number of ticks
  21. /// the action took to complete and the base counter is incremented by 1 (this is
  22. /// done in the PerfCounterUtil.IncrementTimer() method).
  23. /// </remarks>
  24. /// <param name="elapsedTicks">A long of the number of ticks it took to complete the action</param>
  25. public override void OnActionComplete(long elapsedTicks, bool exceptionThrown)
  26. {
  27. averageTimeCounter.Record(elapsedTicks, TimeUnit.Nanoseconds);
  28. }

Timer需要的参数处理Name之外还需要,持续时间单位跟比率时间单位,持续时间单位是要度量的时间的期间的一个单位,在上面的例子中,就是MILLISECONDS,表示这段周期内的数据会按照毫秒来进行度量。比率时间单位跟Meters的一致。

Health Checks(健康检查)

Meters提供一种一致的、统一的方法来对应用进行健康检查,健康检查是一个基础的对应用是否正常运行的自我检查。

Reporters报告

Reporters是将你的应用中所有的度量指标展现出来的一种方式,metrics.net中用了三种方法来导出你的度量指标,Http,Console跟CSV文件, Reporters是可定制的。例如可以使用Log4net进行输出,具体参见 https://github.com/nkot/Metrics.Log4Net

  1. Metric.Config.WithHttpEndpoint("http://localhost:1234/")
  2. .WithAllCounters()
  3. .WithReporting(config => config.WithCSVReports(@"c:\temp\csv", TimeSpan.FromSeconds(10))
  4. .WithTextFileReport(@"C:\temp\reports\metrics.txt", TimeSpan.FromSeconds(10)));
  1.  
  1. 上面我们介绍了基于Metrics.NET构建的ASP.NET MVC 应用程序的性能指标,如下表所示:
计数器名称 描述
Last Call Elapsed Time 已完成最后一次调用的所花费的时间。这是表示所有已完成请求的时间测量中的最新一个点。它不是平均值。
Request Timer 统计执行时间以及其分布情况
  1. POST & PUT Request Size histogram
POST/PUT请求中的内容大小
Global Error Meter ASP.NET引发 未捕获的异常的比率。如果此计数器增加时,它会显示与该应用程序的健康问题
  1. Delta Calls
最后一个采样周期内被调用的次数
  1. ActiveRequests
当前的并发请求数
  1.  

通过自定义Action Filter集成到ASP.NET MVC

定义一个MvcPerformanceAttribute,继承自ActionFilterAttribute:

  1. /// <summary>
  2. /// Custom action filter to track the performance of MVC actions
  3. /// </summary>
  4. public class MvcPerformanceAttribute : ActionFilterAttribute
  5. {
  6.  
  7. public MvcPerformanceAttribute()
  8. {
  9. }
  10.  
  11. /// <summary>
  12. /// Constant to identify MVC Action Types (used in the instance name)
  13. /// </summary>
  14. public const String ACTION_TYPE = "MVC";
  15.  
  16. /// <summary>
  17. /// Method called before the action method starts processing
  18. /// </summary>
  19. /// <param name="filterContext">An ActionExecutingContext object</param>
  20. public override void OnActionExecuting(ActionExecutingContext filterContext)
  21. {
  22. // First thing is to check if performance is enabled globally. If not, return
  23. if ( ConfigInfo.Value.PerformanceEnabled == false)
  24. {
  25. return;
  26. }
  27.  
  28. // Second thing, check if performance tracking has been turned off for this action
  29. // If the DoNotTrackAttribute is present, then return
  30. ActionDescriptor actionDescriptor = filterContext.ActionDescriptor;
  31.  
  32. if (actionDescriptor.GetCustomAttributes(typeof(DoNotTrackPerformanceAttribute), true).Length > 0
  33. || actionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(DoNotTrackPerformanceAttribute), true).Length > 0)
  34. {
  35. return;
  36. }
  37.  
  38. // ActionInfo encapsulates all the info about the action being invoked
  39. ActionInfo info = this.CreateActionInfo(filterContext);
  40.  
  41. // PerformanceTracker is the object that tracks performance and is attached to the request
  42. PerformanceTracker tracker = new PerformanceTracker(info);
  43.  
  44. // Store this on the request
  45. String contextKey = this.GetUniqueContextKey(filterContext.ActionDescriptor.UniqueId);
  46. HttpContext.Current.Items.Add(contextKey, tracker);
  47.  
  48. // Process the action start - this is what starts the timer and increments any
  49. // required counters before the action executes
  50. tracker.ProcessActionStart();
  51. }
  52.  
  53. /// <summary>
  54. /// Method called after the action method has completed executing
  55. /// </summary>
  56. /// <remarks>
  57. /// This method first checks to make sure we are indeed tracking performance. If so, it stops
  58. /// the stopwatch and then calls the OnActionComplete() method of all of the performance metric
  59. /// objects attached to this action filter
  60. /// </remarks>
  61. /// <param name="filterContext">An ActionExecutedConext object</param>
  62. public override void OnActionExecuted(ActionExecutedContext filterContext)
  63. {
  64. // This is the unique key the PerformanceTracker object would be stored under
  65. String contextKey = this.GetUniqueContextKey(filterContext.ActionDescriptor.UniqueId);
  66.  
  67. // Check if there is an object on the request. If not, must not be tracking performance
  68. // for this action, so just go ahead and return
  69. if (HttpContext.Current.Items.Contains(contextKey) == false)
  70. {
  71. return;
  72. }
  73.  
  74. // If we are here, we are tracking performance. Extract the object from the request and call
  75. // ProcessActionComplete. This will stop the stopwatch and update the performance metrics
  76. PerformanceTracker tracker = HttpContext.Current.Items[contextKey] as PerformanceTracker;
  77.  
  78. if (tracker != null)
  79. {
  80. bool exceptionThrown = (filterContext.Exception != null);
  81. tracker.ProcessActionComplete(exceptionThrown);
  82. }
  83. }
  84.  
  85. #region Helper Methdos
  86.  
  87. /// <summary>
  88. /// Helper method to create the ActionInfo object containing the info about the action that is getting called
  89. /// </summary>
  90. /// <param name="actionContext">The ActionExecutingContext from the OnActionExecuting() method</param>
  91. /// <returns>An ActionInfo object that contains all the information pertaining to what action is being executed</returns>
  92. private ActionInfo CreateActionInfo(ActionExecutingContext actionContext)
  93. {
  94. var parameters = actionContext.ActionDescriptor.GetParameters().Select(p => p.ParameterName);
  95. String parameterString = String.Join(",", parameters);
  96.  
  97. int processId = ConfigInfo.Value.ProcessId;
  98. String categoryName = ConfigInfo.Value.PerformanceCategoryName;
  99. String controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName;
  100. String actionName = actionContext.ActionDescriptor.ActionName;
  101. String httpMethod = HttpContext.Current.Request.HttpMethod;
  102. int contentLength = HttpContext.Current.Request.ContentLength;
  103.  
  104. ActionInfo info = new ActionInfo(processId, categoryName, ACTION_TYPE,
  105. controllerName, actionName, httpMethod, parameterString,contentLength);
  106.  
  107. return info;
  108. }
  109.  
  110. /// <summary>
  111. /// Helper method to form the key that will be used to store/retrieve the PerformanceTracker object
  112. /// off if the HttpContext
  113. /// </summary>
  114. /// <remarks>
  115. /// To minimize any chance of collisions, this method concatenates the full name of this class
  116. /// with the UniqueID of the MVC action to get a unique key to use
  117. /// </remarks>
  118. /// <param name="actionUniqueId">A String of the unique id assigned by ASP.NET to the MVC action</param>
  119. /// <returns>A Strin suitable to be used for the key</returns>
  120. private String GetUniqueContextKey(String actionUniqueId)
  121. {
  122. return this.GetType().FullName + ":" + actionUniqueId;
  123. }
  124.  
  125. #endregion
  126. }

首要任务是确定是否正在跟踪此控制器操作性能。首先,它会检查一个名为 ConfigInfo,看看是否在整个应用程序范围的基础上启用性能的单例类。如果 ConfigInfo 类不是能够在 Web.Config 文件中查找的AspNetPerformance.EnablePerformanceMonitoring,此调用将返回 false。

然后应该跟踪此控制器操作性能。辅助方法用于创建一个 ActionInfo 对象,它是一个对象,封装有关控制器操作的所有信息。然后创建 PerformanceTracker 对象,它是具有主要负责跟踪性能的控制器操作的对象。度量性能的每个请求将相关联的 PerformanceTracker 对象和关联的 PerformanceTracker 对象将需要再次检索在 OnActionExecuted() 方法中控制器动作完成后。PerformanceTracker 对象存储在当前的 HttpContext 对象项目字典中。对 HttpContext 项目字典是用于当数据需要在请求过程中不同的 Http 处理程序和模块之间共享而设计的。使用的诀窍是基于属性类型的完整名称和 ASP.NET 生成的唯一 id 的方法。通过将这些因素结合在一起,我们应该与其他模块的使用项目字典任何关键碰撞安全。最后,调用 PerformanceTracker 对象的 ProcessActionStart() 方法。

  1. internal void ProcessActionStart()
  2. {
  3. try
  4. {
  5. // Use the factory class to get all of the performance metrics that are being tracked
  6. // for MVC Actions
  7. this.performanceMetrics = PerformanceMetricFactory.GetPerformanceMetrics(actionInfo);
  8.  
  9. // Iterate through each metric and call the OnActionStart() method
  10. // Start off a task to do this so it can it does not block and minimized impact to the user
  11. Task t = Task.Factory.StartNew(() =>
  12. {
  13. foreach (PerformanceMetricBase m in this.performanceMetrics)
  14. {
  15. m.OnActionStart();
  16. }
  17. });
  18.  
  19. this.stopwatch = Stopwatch.StartNew();
  20. }
  21. catch (Exception ex)
  22. {
  23. String message = String.Format("Exception {0} occurred PerformanceTracker.ProcessActionStart(). Message {1}\nStackTrace {0}",
  24. ex.GetType().FullName, ex.Message, ex.StackTrace);
  25. Trace.WriteLine(message);
  26. }
  27. }

PerformanceMetricBase 对象和 PerformanceMetricFactory

更新实际性能计数器的任务是继承 PerformanceMetricBase 类的对象。这些对象作为PerformanceTracker 对象的中间人 ,并需要更新的任何性能计数器。代码分解为单独的一组对象允许要专注于管理全过程的测量性能的控制器操作和离开如何更新计数器对 PerformanceMetricBase 对象的详细信息的 PerformanceTracker 对象。如果我们想要添加额外的性能指标,可以通过简单地编写一个新的类,扩展了 PerformanceMetricBase 并不会受到 PerformanceTracker 的代码的干扰。

每个子类扩展 PerformanceMetricBase 负责更新对应的值到这篇文章前面定义的自定义性能计数器之一。因此,每个类将包含持有对 Metric.NET 的引用对象,他们是负责更新的成员变量。通常,这是一个单一的Metric.NET 对象。

PerformanceMetricBase 提供了两种虚拟方法,OnActionStart() 和 OnActionComplete() 子类别在哪里能够对性能计数器执行更新。需要覆盖至少一个方法实现或可重写这两种方法。 具体的实现代码放在Github: https://github.com/geffzhang/AspNetPerformanceMetrics

参考文章

Building Performance Metrics into ASP.NET MVC Applications

http://www.cnblogs.com/yangecnu/p/Using-Metrics-to-Profiling-WebService-Performance.html

使用Metrics.NET 构建 ASP.NET MVC 应用程序的性能指标的更多相关文章

  1. NHibernate构建一个ASP.NET MVC应用程序

    NHibernate构建一个ASP.NET MVC应用程序 什么是Nhibernate? NHibernate是一个面向.NET环境的对象/关系数据库映射工具.对象/关系数据库映射(object/re ...

  2. 测试驱动 ASP.NET MVC 和构建可测试 ASP.NET MVC 应用程序

    [测试驱动 ASP.NET MVC] http://t.cn/8kdi4Wl [构建可测试 ASP.NET MVC 应用程序]http://t.cn/8kdi4Wj

  3. 使用Razor Generator构建模块化ASP.NET MVC应用程序

    在构建Web应用程序的时候,我们很难做到模块化的开发,这是因为Web应用程序不仅仅包含编译的C#代码,还包含了js.css和aspx等资源. 在ASP.NET MVC中,我们发布应用程序的时候,还会包 ...

  4. [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序更新相关数据

    这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第八篇:为ASP.NET MVC应用程序 ...

  5. ASP.NET MVC应用程序执行过程分析

    ASP.NET MVC应用程序执行过程分析 2009-08-14 17:57 朱先忠 朱先忠的博客 字号:T | T   ASP.NET MVC框架提供了支持Visual Studio的工程模板.本文 ...

  6. ASP.NET MVC应用程序更新相关数据

    为ASP.NET MVC应用程序更新相关数据 这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译, ...

  7. 04 入门 - ASP.NET MVC应用程序的结构

    目录索引:<ASP.NET MVC 5 高级编程>学习笔记 用Visual Studio创建了一个新的ASP.NET MVC应用程序后,将自动向这个项目中添加一些文件和目录. 如图所示: ...

  8. asp.net MVC 应用程序的生命周期

    下面这篇文章总结了 asp.net MVC 框架程序的生命周期.觉得写得不错,故转载一下. 转载自:http://www.cnblogs.com/yplong/p/5582576.html       ...

  9. 学习ASP.NET MVC(一)——我的第一个ASP.NET MVC应用程序

    学习ASP.NET MVC系列: 学习ASP.NET MVC(一)——我的第一个ASP.NET MVC应用程序 学习ASP.NET MVC(二)——我的第一个ASP.NET MVC 控制器 学习ASP ...

随机推荐

  1. Angular2入门系列教程4-服务

    上一篇文章 Angular2入门系列教程-多个组件,主从关系 在编程中,我们通常会将数据提供单独分离出来,以免在编写程序的过程中反复复制粘贴数据请求的代码 Angular2中提供了依赖注入的概念,使得 ...

  2. C语言 · 字符转对比

    问题描述 给定两个仅由大写字母或小写字母组成的字符串(长度介于1到10之间),它们之间的关系是以下4中情况之一: 1:两个字符串长度不等.比如 Beijing 和 Hebei 2:两个字符串不仅长度相 ...

  3. C# 破解 Reflector8.5

    一.分析 破解.net .dll,可以使用reflector,但官方提供的reflector是需要购买的,因此,破解reflector势在必行. 二.破解Reflector具体步骤 下面为详细的破解步 ...

  4. MSDN文档篇

    很多人网上下载3~10G不等的MSDN文档,发现,下载完成了不会用 很多人每次都得在线下载文档,手上万千PC,都重新下载不是得疯了? so==> 先看几张图 推荐一个工具:https://vsh ...

  5. 从备考PMP到与项目经理同呼吸

    前言 PMP是什么梗? 项目管理专业人士资格认证.它是由美国项目管理协会(Project Management Institute(PMI)发起的,严格评估项目管理人员知识技能是否具有高品质的资格认证 ...

  6. 前端学HTTP之字符集

    前面的话 HTTP报文中可以承载以任何语言表示的内容,就像它能承载图像.影片或任何类型的媒体那样.对HTTP来说,实体主体只是二进制信息的容器而已.为了支持国际性内容,服务器需要告知客户端每个文档的字 ...

  7. .NET平台开源项目速览(17)FluentConsole让你的控制台酷起来

    从该系列的第一篇文章 .NET平台开源项目速览(1)SharpConfig配置文件读写组件 开始,不知不觉已经到第17篇了.每一次我们都是介绍一个小巧甚至微不足道的.NET平台的开源软件,或者学习,或 ...

  8. 学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup

    一个ASP.NET Core应用被启动之后就具有了针对请求的处理能力,而这个能力是由管道赋予的,所以应用的启动同时意味着管道的成功构建.由于管道是由注册的服务器和若干中间件构成的,所以应用启动过程中一 ...

  9. Visual Studio 2012远程调试中遇到的问题

    有的时候开发环境没问题的代码在生产环境中会某些开发环境无法重现的问题,或者需要对生产环境代码进行远程调试该怎么办? Vs已经提供给开发者远程调试的工具 下面简单讲讲该怎么用,前期准备:1.本地登录账户 ...

  10. Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

    Android XML shape 标签使用详解   一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...