NET Core的日志模型主要由三个核心对象构成,它们分别是Logger、LoggerProvider和LoggerFactory。总的来说,LoggerProvider提供一个具体的Logger对象将格式化的日志消息写入相应的目的地,但是我们在编程过程中使用的Logger对象则由LoggerFactory创建,这个Logger利用注册到LoggerFactory的LoggerProvider来提供真正具有日志写入功能的Logger,并委托后者来记录日志。

目录
一、Logger
    扩展方法LogXxx与BeginScope
    Logger<TCategoryName>
二、LoggerProvider
三、LoggerFactory
    Logger提供的同一性
    Logger类型
    LoggerFactory类型
    依赖注入

一、Logger

日志模型的Logger泛指所有实现了ILogger接口的所有类型以及对应对象,该接口定义在NuGet包“Microsoft.Extensions.Logging.Abstractions”中,这个NuGet包同时定义了分别代表LoggerProvider和LoggerFactory的接口ILoggerProvider和ILoggerFactory。ILogger接口中定义了如下三个方法Log、IsEnabled和BeginScope。

   1: public interface ILogger

   2: {

   3:     void Log(LogLevel logLevel, EventId eventId, object state, Exception exception, Func<object, Exception, string> formatter);

   4:     bool IsEnabled(LogLevel logLevel);

   5:     IDisposable BeginScope<TState>(TState state); 

   6: }

Logger对日志消息的写入实现在Log方法中。Log方法的logLevel代表写入日志消息的等级,而日志消息的原始内容通过参数state和exception这两个参数来承载承载,前者代表一个原始的日志条目(Log Entry),后者代表与之关联的异常。日志在被写入之前必须格式成一个字符串,由于日志原始信息分别由一个Object和Exception对象对象来表示,所以日志的“格式化器”自然体现为一个Func<object, Exception, string>类型的委托对象。

一条写入的日志消息会关联着一个日志记录事件,后者则通过一个EventId对象来标识,Log方法的eventId参数类型就是EventId。如下面的代码片段所示,EventId被定义成一个结构,它具有两个基本的属性Id和Name,前者代表必需的唯一标识,后者则是一个可选的名称。除此之外,整形到EventId类型之间还存在一个隐式类型转换,所以在需要使用EventId对象的地方,我们可以使用一个整数来代替。

   1: public struct EventId

   2: {

   3:     public int        Id { get; }

   4:     public string     Name{ get; }

   5:     public EventId(int id, string name = null);

   6:  

   7:     public static implicit operator EventId(int i);

   8: }

对于任意一次日志消息写入请求,Logger并不会直接调用Log方法将日志消息写入对应的目的地,它会根据提供日志消息的等级判断是否应该执行写入操作,判断的逻辑实现在IsEnabled方法中,只有当这个方法返回True的时候它的Log方法才会被执行。

在默认的情况下,每次调用Logger的Log方法所进行的日志记录操作都是相互独立的,但是有时候我们需要将相关的多次日志记录做一个逻辑关联,或者说我们需要为多次日志记录操作创建一个共同的上下文范围。这样一个关联上下文范围可以通过BeginScope<TState>方法来创建,该方法将该上下文范围与参数state表示的对象进行关联。被创建的这个关联上下文体现为一个IDisposable对象,我们需要调用其Dispose方法将其释放回收,也就是说被创建的关联上下文的生命周期终止于Dispose方法的调用。

扩展方法LogXxx与BeginScope

当我们调用Logger的Log方法记录日志时必须指定日志消息采用的等级,出于调用便利性考虑,日志模型还为ILogger接口定义了一系列针对不同日志等级的扩展方法,比如LogDebug、LogTrace、LogInformation、LogWarning、LogError和LogCritical等。下面的代码片段列出了整个日志等级Debug三个LogDebug方法重载的定义,针对其他日志等级的扩展方法的定义与之类似。对于这些扩展方法来说,如果它们没有定义表示日志事件ID的参数eventId,默认使用的事件ID为0。

   1: public static class LoggerExtensions

   2: {

   3:     public static void LogDebug(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args);

   4:     public static void LogDebug(this ILogger logger, EventId eventId, string message, params object[] args);

   5:     public static void LogDebug(this ILogger logger, string message, params object[] args);

   6: } 

对于定义在ILogger接口中的Log方法来说,原始日志消息的内容通过Object类型的参数state和Exception类型的参数exception来承载,并通过一个Func<object, Exception, string>类型的委托对象来将它们格式化成可以写入的字符串。上述这些扩展方法对此作了简化,它利用一个包含占位符的字符串模板(对应参数message)和用于替换占位符的参数列表(对应参数args)来承载原始的日志消息,日志消息的格式化体现在如何使用提供的参数替换模板中相应的占位符进而生成一个完整的消息。值得一提的是,定义在模板中的占位符通过花括号括起来,可以使用零基连续整数(比如“{0}”、“{1}”和“{2}”等),也可以使用任意字符串(比如“{Minimum}”和“Maximum”等)。

定义在ILogger接口的泛型方法BeginScope<TState>为多次相关的日志记录操作创建一个相同的执行上下文范围,并将其上下文范围与一个TState对象进行关联。ILogger接口还具有如下一个同名的扩展方法,它采用与上面类似的方式将创建的上下文范围与一个字符串进行关联,该字符串是指定的模板与参数列表格式化后的结果。

   1: public static class LoggerExtensions

   2: {

   3:     public static IDisposable BeginScope(this ILogger logger, string messageFormat, params object[] args);

   4: }

Logger<TCategoryName>

每条日志消息都关联着一个具体的类型(Category),这个类型实际上创建这条日志消息的“源”,我们一般将日志记录所在的应用或者组件名称作为类型。除了ILogger这个基本的接口,日志模型中还定义了如下一个泛型的ILogger <TCategoryName>接口,它派生与ILogger接口并将泛型参数的类型名称作为由它写入的日志消息的类型。

   1: public interface ILogger<out TCategoryName> : ILogger

   2: {}

Logger<TCategoryName>实现了ILogger <TCategoryName>接口。一个Logger<TCategoryName>对象可以视为是对另一个Logger对象的封装,它使用泛型参数类型来确定写入日志的类型,而采用这个内部封装的Logger对象完成具体的日志写入操作。如下面的代码片段所示,Logger<TCategoryName>的构造函数接受一个LoggerFactory作为输入参数,上述的这个内部封装的Logger对象就是由它创建的。

   1: public class Logger<TCategoryName> : ILogger<TCategoryName>

   2: {

   3:     public Logger(ILoggerFactory factory) ;

   4:  

   5:     IDisposable ILogger.BeginScope<TState>(TState state;

   6:     void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, 

   7:     Exception exception, Func<TState, Exception, string> formatter) ;

   8: }

在利用指定的LoggerFactory创建Logger对象时,泛型参数TCategoryName的类型被用来计算日志类型。对于具有简写形式的基元类型(比如Int32、Boolean和Decimal等)来说,类型的简写形式(比如int、bool和decimal等)直接作为日志类型名称。对于一般的类型来说,日志类型名称就是该类型的全名(命名空间+类型名)。如果该类型内嵌于另一个类型之中(比如“Foo.Bar+Baz”),表示内嵌的“+”需要替换成“.”(比如“Foo.Bar.Baz”)。如果该类型是一个泛型类型(比如Foobar<T1,T2>),泛型参数部分将不包含在日志类型名称中(日志类型为“Foobar”)。

除了调用构造函数创建一个Logger<TCategoryName>对象之外,我们还可以调用针对ILoggerFactory接口的扩展方法CreateLogger<T>来创建它。如下面的代码片段所示,除了这个CreateLogger<T>方法之外,另一个CreateLogger方法直接指定一个Type类型的参数,虽然返回类型不同,但是由此两个方法创建的Logger在日志记录行为上是等效的。

   1: public static class LoggerFactoryExtensions

   2: {

   3:     public static ILogger<T> CreateLogger<T>(this ILoggerFactory factory) 

   4:     public static ILogger CreateLogger(this ILoggerFactory factory, Type type);

   5: }

二、LoggerProvider

日志模型的LoggerProvider泛指所有实现了接口ILoggerProvider的类型和对应的对象,从其命名我们不难看出LoggerProvider的目的在于“提供”真正具有日志写入功能的Logger。如下面的代码片段所示,ILoggerProvider继承了IDisposable,如果某个具体的LoggerProvider需要释放某种资源,可以将相关的操作实现在Dispose方法中。

   1: public interface ILoggerProvider : IDisposable

   2: {

   3:     ILogger CreateLogger(string categoryName);

   4: }

LoggerProvider针对Logger的提供实现在唯一的方法CreateLogger中,该方法的参数categoryName自然代表上面我们所说的日志消息的类型。这个CreateLogger方法返回类型为ILogger,代表根据指定日志类型创建的Logger对象。

三、LoggerFactory

从命名的角度来讲,LoggerProvider和LoggerFactory最终都是为了提供一个Logger对象,但是两者提供的Logger对象在本质上是不同的。一个LoggerProvider一般针对某种具体的日志目的地类型(比如控制台、文件或者Event Log等)提供对应的Logger,而LoggerFactory仅仅为我们创建日志编程所用的那个Logger对象。

日志模型中的LoggerFactory泛指所有实现了ILoggerFactory接口的所有类型及其对应的对象。如下面的代码片段所示,ILoggerFactory具有两个简单的方法,针对Logger的创建实现在CreateLogger方法中。我们通过调用AddProvider方法将某个LoggerProvider对象注册到LoggerFactory之上,CreateLogger方法创建的Logger需要利用这些注册的LoggerProvider来提供真正具有日志写入功能的Logger对象,并借助后者来完成对日志的写入操作。

   1: public interface ILoggerFactory : IDisposable

   2: {

   3:     ILogger CreateLogger(string categoryName);

   4:     void AddProvider(ILoggerProvider provider);

   5: }

日志模型中定义了一个实现了ILoggerFactory接口的类型,这就是我们在上面演示实例中使用的LoggerFactory类,由它创建的是一个类型为Logger的对象,这两个类型均定义在NuGet包“Microsoft.Extensions.Logging”之中。到目前为止,我们认识了日志模型中的三个接口(ILogger、ILoggerProvider和ILoggerFactory)和其中两个的实现者(Logger和LoggerFactory),右图所示的UML体现了它们之间的关系。

Logger提供的同一性

上图所示的UML基本上体现了Logger和LoggerFactory这两个类型的实现逻辑,这个逻辑我们在上面已经提到过多次,现在我们通过代码实现的方式来对它做进一步地说明。在这之前,我们有必要了解LoggerFactory类型创建Logger过程中所体现出的一个重要特性,即对于CreateLogger方法的多次调用,如果我们指定的日志类型(categoryName参数)相同(不区分大小写),该方法返回的实际是同一个对象。

   1: LoggerFactory loggerFactory = new LoggerFactory();

   2: ILogger logger1 = loggerFactory.CreateLogger("App");

   3:  

   4: loggerFactory.AddConsole();

   5: ILogger logger2 = loggerFactory.CreateLogger("App");

   6:  

   7: loggerFactory.AddDebug();

   8: ILogger logger3 = loggerFactory.CreateLogger("App");

   9:  

  10: Debug.Assert(ReferenceEquals(logger1, logger2) && ReferenceEquals(logger2, logger3));

如上面的代码片段所示,我们利用同一个LoggerFactory对象针对相同的日志类型(“App”)先后得到三个Logger对象,虽然这三个Logger被创建的时候LoggerFactory具有不同的状态(注册到它上面的LoggerProvider逐次增多),但是它们其实是同一个对象。换句话说,LoggerFactory和由它创建的Logger对象并不是两个孤立的对象,它们之间存在着一种动态的关联,当LoggerFactory自身的状态发生改变时(注册新的LoggerProvider),它会主动改变Logger的状态使之与自身同步。

Logger类型

我们定义了一个精简版本的同名类型来模拟真实Logger类的实现逻辑。如下面的代码片段所示,我们创建一个Logger对象的时候需要指定创建它的LoggerFactory对象和日志类型。它的字段loggers代表由它封装的一组具有真正日志写入功能的Logger对象,它们由注册到LoggerFactory的LoggerProvider(体现为LoggerFactory的LoggerProviders属性)来提供。

   1: public class Logger : ILogger

   2: {

   3:     private LoggerFactory     loggerFactory;

   4:     private IList<ILogger>     loggers;

   5:     private string         categoryName;

   6:  

   7:     public Logger(LoggerFactory loggerFactory, string categoryName)

   8:     {

   9:         this.loggerFactory    = loggerFactory;

  10:         this.categoryName     = categoryName;

  11:         loggers               = loggerFactory.LoggerProviders.Select(provider => provider.CreateLogger(categoryName)).ToList();

  12:     }

  13:  

  14:     public bool IsEnabled(LogLevel logLevel) => loggers.Any(logger => logger.IsEnabled(logLevel));

  15:  

  16:     public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)

  17:         => loggers.ForEach(logger => logger.Log(logLevel, eventId, state, exception, formatter));

  18:  

  19:     internal void AddProvider(ILoggerProvider provider) => loggers.Add(provider.CreateLogger(categoryName));

  20:     //其他成员

  21: }

IsEnabled方法实现了针对等级的日志过滤,如果指定的日志等级能够通过任一Logger的过滤条件,该方法就返回True。至于真正用于实现日志消息记录的Log方法,它只需要调用每个Logger对象的同名方法即可。除此之外,Logger类还定义了一个AddProvider方法,它利用指定的LoggerProvider来创建对应的Logger,并将后者添加到封装的Logger列表中。一旦新的LoggerProvider注册到LoggerFactory之上,LoggerFactory正是调用这个方法将新注册的LoggerProvider应用到由它创建的Logger对象之上。

一个Logger对象是对一组具有真正日志写入功能的Logger对象的封装,由它的BeginScope方法创建的日志上下文范围则是对这组Logger创建的上下文范围的封装。当这个日志上下文范围因调用Dispose方法被释放的时候,这些内部封装的上下文范围同时被释放。如下所示的代码基本体现了定义在BeginScope方法中创建日志上下文范围的逻辑。

   1: public class ConsoleLogger : ILogger

   2: {

   3:     private IList<ILogger> loggers;

   4:     public IDisposable BeginScope<TState>(TState state)

   5:     {

   6:         return new Scope(loggers.Select(logger => logger.BeginScope(state)));

   7:     }

   8:  

   9:     private class Scope : IDisposable

  10:     {

  11:         private readonly IDisposable[] scopes;

  12:         public Scope(IEnumerable<IDisposable> scopes)

  13:         {

  14:             this.scopes = scopes.ToArray();

  15:         }

  16:         public void Dispose() => scopes.ForEach(scope => scope.Dispose());

  17:     }

  18: }

LoggerFactory类型

我们同样采用最精简的代码来模拟实现在LoggerFactory类型中的Logger创建逻辑。如下面的代码片段所示,处于线程安全方面的考虑,我们定义了一个ConcurrentBag<ILoggerProvider>类型的属性LoggerProviders来保存注册到LogggerFactory上的LoggerProvider。另一个ConcurrentDictionary<string, Logger>类型的字段loggers则用来保存自身创建的Logger对象,该对象的Key表示日志消息类型。

   1: public class LoggerFactory : ILoggerFactory

   2: {

   3:     internal ConcurrentBag<ILoggerProvider> LoggerProviders { get; private set; }

   4:     private readonly ConcurrentDictionary<string, Logger> loggers  = new ConcurrentDictionary<string, Logger>(StringComparer.OrdinalIgnoreCase);

   5:  

   6:     public void AddProvider(ILoggerProvider provider)

   7:     {

   8:         this.LoggerProviders = new ConcurrentBag<ILoggerProvider>();

   9:         this.LoggerProviders.Add(provider);

  10:         loggers.ForEach(it => it.Value.AddProvider(provider));

  11:     }

  12:  

  13:     public ILogger CreateLogger(string categoryName)

  14:     {

  15:         Logger logger;

  16:         return loggers.TryGetValue(categoryName, out logger) 

  17:             ? logger 

  18:             : loggers[categoryName] = new Logger(this, categoryName);

  19:     }

  20:  

  21:     public void Dispose() => LoggerProviders.ForEach(provider => provider.Dispose());

  22: }

当LoggerFactory的CreateLogger方法的时候,如果根据指定的日志类型能够在loggers字段表示的字典中找到一个Logger对象,则直接将它作为返回值。只有在根据指定的日志类型找不到 对应的Logger的情况下,LoggerFactory才会真正去创建一个新的Logger对象,并在返回之前将它添加到该字典之中。针对相同的日志类型,LoggerFactory之所以总是返回同一个Logger,根源就在于此。

对于用于注册LoggerProvider的AddProvider方法来说,LoggerFactory除了将指定的LoggerProvider添加到LoggerProviders属性表示的列表之中,它还会调用每个已经创建的Logger对象的AddProvider方法。正是源于对这个方法的调用,我们新注册到LoggerFactory上的LoggerProvider才会自动应用到所有已经创建的Logger对象中。

LoggerProvider类型都实现了IDisposable接口,针对它们的Dispose方法的调用被放在LoggerFactory的同名方法中。换句话说,当LoggerFactory被释放的时候,注册到它之上的所有LoggerProvider会自动被释放。

依赖注入

在一个真正的.NET Core应用中,框架内部会借助ServiceProvider以依赖注入的形式向我们提供用于创建Logger对象的LoggerFactory。这样一个ServiceProvider在根据一个ServiceCollection对象构建之前,我们必然需要在后者之上实施针对LoggerFactory的服务注册,这样的服务注册可以通过针对接口IServiceCollection的扩展方法AddLogging来完成。

   1: public static class LoggingServiceCollectionExtensions

   2: {

   3:     public static IServiceCollection AddLogging(this IServiceCollection services)

   4:     {

   5:         services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());

   6:         services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));

   7:         return services;

   8:     }

   9: }

如上面的代码片段所示,扩展方法AddLogging除了以Singleton模式注册了ILoggerFactory接口与实现它的LoggerFactory类型之间的映射之外,还以同样的模式注册了ILogger<>接口和Logger<>类型的映射。如果创建ServiceProvider的ServiceCollection具有这两个服务注册,我们可以利用ServiceProvider直接提供一个Logger<T>,而不需要间接地利用ServiceProvider提供的LoggerFactory来创建它。下面的代码片段展示了Logger<T>的这两种创建方式。

   1: IServiceProvider serviceProvider = new ServiceCollection()

   2:     .AddLogging()

   3:     .BuildServiceProvider();

   4:  

   5: ILogger<Foobar> logger1 = serviceProvider.GetService<ILoggerFactory>().CreateLogger<Foobar>();

   6: ILogger<Foobar> logger2 = serviceProvider.GetService<ILogger<Foobar>>();

.NET Core下的日志(2):日志模型详解的更多相关文章

  1. 《手把手教你》系列基础篇(九十一)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-下篇(详解教程)

    1.简介 为了方便查看和归档:(1)不同包的日志可能要放到不同的文件中,如service层和dao层的日志:(2)不同日志级别:调试.信息.警告和错误等也要分文件输出.所以宏哥今天主要介绍和分享的是: ...

  2. ASP.NET Core的配置(2):配置模型详解

    在上面一章我们以实例演示的方式介绍了几种读取配置的几种方式,其中涉及到三个重要的对象,它们分别是承载结构化配置信息的Configuration,提供原始配置源数据的ConfigurationProvi ...

  3. WAL日志文件名称格式详解

    转自:http://blog.osdba.net/534.html WAL日志文件名称格式详解 PostgreSQL的WAL日志文件在pg_xlog目录下,一般情况下,每个文件为16M大小: osdb ...

  4. 【Android 应用开发】Ubuntu 下 Android Studio 开发工具使用详解 (旧版本 | 仅作参考)

    . 基本上可以导入项目开始使用了 ... . 作者 : 万境绝尘 转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/21035637 ...

  5. Ubuntu下Git从搭建到使用详解

    Ubuntu下Git从搭建到使用详解 一.git的搭建 (1).sudo apt-get update (2).sudo apt-get -y install git 符:安装最新版本方法: add- ...

  6. 【Android 应用开发】Ubuntu 下 Android Studio 开发工具使用详解

    . 基本上可以导入项目开始使用了 ... . 作者 : 万境绝尘 转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/21035637 ...

  7. Java生产环境下性能监控与调优详解视频教程 百度云 网盘

    集数合计:9章Java视频教程详情描述:A0193<Java生产环境下性能监控与调优详解视频教程>软件开发只是第一步,上线后的性能监控与调优才是更为重要的一步本课程将为你讲解如何在生产环境 ...

  8. ISO七层模型详解

    ISO七层模型详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 在我刚刚接触运维这个行业的时候,去面试时总是会做一些面试题,笔试题就是看一个运维工程师的专业技能的掌握情况,这个很 ...

  9. 28、vSocket模型详解及select应用详解

    在上片文章已经讲过了TCP协议的基本结构和构成并举例,也粗略的讲过了SOCKET,但是讲解的并不完善,这里详细讲解下关于SOCKET的编程的I/O复用函数. 1.I/O复用:selec函数 在介绍so ...

  10. JVM的类加载过程以及双亲委派模型详解

    JVM的类加载过程以及双亲委派模型详解 这篇文章主要介绍了JVM的类加载过程以及双亲委派模型详解,类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象 ...

随机推荐

  1. 从直播编程到直播教育:LiveEdu.tv开启多元化的在线学习直播时代

    2015年9月,一个叫Livecoding.tv的网站在互联网上引起了编程界的注意.缘于Pingwest品玩的一位编辑在上网时无意中发现了这个网站,并写了一篇文章<一个比直播睡觉更奇怪的网站:直 ...

  2. [Java 缓存] Java Cache之 DCache的简单应用.

    前言 上次总结了下本地缓存Guava Cache的简单应用, 这次来继续说下项目中使用的DCache的简单使用. 这里分为几部分进行总结, 1)DCache介绍; 2)DCache配置及使用; 3)使 ...

  3. 猖獗的假新闻:2017年1月1日起iOS的APP必须使用HTTPS

    一.假新闻如此猖獗 刚才一位老同事 打电话问:我们公司还是用的HTTP,马上就到2017年了,提交AppStore会被拒绝,怎么办? 公司里已经有很多人问过这个问题,回答一下: HTTP还是可以正常提 ...

  4. Angular企业级开发(1)-AngularJS简介

    AngularJS介绍 AngularJS是一个功能完善的JavaScript前端框架,同时是基于MVC(Model-View-Controller理念的框架,使用它能够高效的开发桌面web app和 ...

  5. 算法与数据结构(十四) 堆排序 (Swift 3.0版)

    上篇博客主要讲了冒泡排序.插入排序.希尔排序以及选择排序.本篇博客就来讲一下堆排序(Heap Sort).看到堆排序这个名字我们就应该知道这种排序方式的特点,就是利用堆来讲我们的序列进行排序.&quo ...

  6. UWP开发之Mvvmlight实践七:如何查找设备(Mobile模拟器、实体手机、PC)中应用的Log等文件

    在开发中或者后期测试乃至最后交付使用的时候,如果应用出问题了我们一般的做法就是查看Log文件.上章也提到了查看Log文件,这章重点讲解下如何查看Log文件?如何找到我们需要的Packages安装包目录 ...

  7. JDBC Tutorials: Commit or Rollback transaction in finally block

    http://skeletoncoder.blogspot.com/2006/10/jdbc-tutorials-commit-or-rollback.html JDBC Tutorials: Com ...

  8. cesium自定义气泡窗口infoWindow

    一.自定义气泡窗口与cesium默认窗口效果对比: 1.cesium点击弹出气泡窗口显示的位置固定在地图的右上角,默认效果: 2.对于习惯arcgis或者openlayer气泡窗口样式的giser来说 ...

  9. 【从零开始学BPM,Day1】工作流管理平台架构学习

    [课程主题] 主题:5天,一起从零开始学习BPM [课程形式] 1.为期5天的短任务学习 2.每天观看一个视频,视频学习时间自由安排. [第一天课程] Step 1 软件下载:H3 BPM10.0全开 ...

  10. Android—ListView条目背景为图片时,条目间距问题解决

    ListView是android开发中使用最普遍的控件了,可有的listView条目的内容颇为丰富,甚至为了美观,背景用指定图片,如下图: