zoukankan      html  css  js  c++  java
  • ASP.NET Core中使用EasyCaching作为缓存抽象层

    ⒈是什么?

    CacheManager差不多,两者的定位和功能都差不多。

    EasyCaching主要提供了下面的几个功能

    1. 统一的抽象缓存接口
    2. 多种常用的缓存Provider(InMemory,Redis,Memcached,SQLite)
    3. 为分布式缓存的数据序列化提供了多种选择
    4. 二级缓存
    5. 缓存的AOP操作(able, put,evict)
    6. 多实例支持
    7. 支持Diagnostics
    8. Redis的特殊Provider

    ⒉示例(以InMemory为例)

      1.安装Nuget包

        EasyCaching.InMemory

      2.在Startup中配置服务及请求管道

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using EasyCaching.Core;
     6 using EasyCaching.InMemory;
     7 using Microsoft.AspNetCore.Builder;
     8 using Microsoft.AspNetCore.Hosting;
     9 using Microsoft.AspNetCore.Http;
    10 using Microsoft.AspNetCore.Mvc;
    11 using Microsoft.Extensions.Configuration;
    12 using Microsoft.Extensions.DependencyInjection;
    13 
    14 namespace Coreqi.EasyCaching
    15 {
    16     public class Startup
    17     {
    18         public Startup(IConfiguration configuration)
    19         {
    20             Configuration = configuration;
    21         }
    22 
    23         public IConfiguration Configuration { get; }
    24 
    25         // This method gets called by the runtime. Use this method to add services to the container.
    26         public void ConfigureServices(IServiceCollection services)
    27         {
    28             services.AddEasyCaching(option =>
    29             {
    30                 // 使用InMemory最简单的配置
    31                 option.UseInMemory("default");
    32 
    33                 //// 使用InMemory自定义的配置
    34                 //option.UseInMemory(options => 
    35                 //{
    36                 //     // DBConfig这个是每种Provider的特有配置
    37                 //     options.DBConfig = new InMemoryCachingOptions
    38                 //     {
    39                 //         // InMemory的过期扫描频率,默认值是60秒
    40                 //         ExpirationScanFrequency = 60, 
    41                 //         // InMemory的最大缓存数量, 默认值是10000
    42                 //         SizeLimit = 100 
    43                 //     };
    44                 //     // 预防缓存在同一时间全部失效,可以为每个key的过期时间添加一个随机的秒数,默认值是120秒
    45                 //     options.MaxRdSecond = 120;
    46                 //     // 是否开启日志,默认值是false
    47                 //     options.EnableLogging = false;
    48                 //     // 互斥锁的存活时间, 默认值是5000毫秒
    49                 //     options.LockMs = 5000;
    50                 //     // 没有获取到互斥锁时的休眠时间,默认值是300毫秒
    51                 //     options.SleepMs = 300;
    52                 // }, "m2");         
    53 
    54                 //// 读取配置文件
    55                 //option.UseInMemory(Configuration, "m3");
    56             });
    57 
    58             services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    59         }
    60 
    61         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    62         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    63         {
    64             if (env.IsDevelopment())
    65             {
    66                 app.UseDeveloperExceptionPage();
    67             }
    68             else
    69             {
    70                 app.UseExceptionHandler("/Home/Error");
    71             }
    72 
    73             app.UseStaticFiles();
    74             app.UseCookiePolicy();
    75 
    76             // 如果使用的是Memcached或SQLite,还需要下面这个做一些初始化的操作
    77             app.UseEasyCaching();
    78 
    79 
    80             app.UseMvc(routes =>
    81             {
    82                 routes.MapRoute(
    83                     name: "default",
    84                     template: "{controller=Home}/{action=Index}/{id?}");
    85             });
    86         }
    87     }
    88 }

      3.创建一个实体类

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 
     6 namespace Coreqi.EasyCaching.Models
     7 {
     8     [Serializable]
     9     public class User
    10     {
    11         public int id { get; set; }
    12         public string username { get; set; }
    13         public string password { get; set; }
    14         public int enabled { get; set; }
    15     }
    16 }

      4.模拟一个服务层

     1 using Coreqi.EasyCaching.Models;
     2 using System;
     3 using System.Collections.Generic;
     4 using System.Linq;
     5 using System.Threading.Tasks;
     6 
     7 namespace Coreqi.EasyCaching.Services
     8 {
     9     public class UserService:IUserService
    10     {
    11         public static IList<User> users;
    12         public UserService()
    13         {
    14             users = new List<User>
    15             {
    16                 new User{ id = 1,username = "fanqi",password = "admin",enabled = 1},
    17                 new User{ id = 2,username = "gaoxing",password = "admin",enabled = 1}
    18             };
    19         }
    20         public IList<User> getAll()
    21         {
    22             return users;
    23         }
    24         public User getById(int id)
    25         {
    26             return users.FirstOrDefault(f => f.id == id);
    27         }
    28         public User add(User user)
    29         {
    30             users.Add(user);
    31             return user;
    32         }
    33         public User modify(User user)
    34         {
    35             delete(user.id);
    36             add(user);
    37             return user;
    38         }
    39         public void delete(int id)
    40         {
    41             users.Remove(getById(id));
    42         }
    43     }
    44 }

      5.控制器中使用缓存

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using Coreqi.EasyCaching.Models;
     6 using Coreqi.EasyCaching.Services;
     7 using EasyCaching.Core;
     8 using Microsoft.AspNetCore.Mvc;
     9 
    10 namespace Coreqi.EasyCaching.Controllers
    11 {
    12     [Route("api/[controller]")]
    13     public class UserController : Controller
    14     {
    15         private readonly IEasyCachingProvider _cache;
    16         private readonly IUserService _service;
    17         public UserController(IEasyCachingProvider cache, IUserService service)
    18         {
    19             this._cache = cache;
    20             this._service = service;
    21         }
    22 
    23         [HttpGet]
    24         [Route("add")]
    25         public async Task<IActionResult> Add()
    26         {
    27             IList<User> users = _service.getAll();
    28             _cache.Set("users", users, TimeSpan.FromMinutes(2));
    29             await _cache.SetAsync("users2", users, TimeSpan.FromMinutes(3));
    30             return await Task.FromResult(new JsonResult(new { message = "添加成功!" }));
    31         }
    32 
    33         [HttpGet]
    34         [Route("remove")]
    35         public async Task<IActionResult> Remove()
    36         {
    37             _cache.Remove("users");
    38             await _cache.RemoveAsync("users2");
    39             return await Task.FromResult(new JsonResult(new { message = "删除成功!" }));
    40         }
    41 
    42         [HttpGet]
    43         [Route("get")]
    44         public async Task<IActionResult> Get()
    45         {
    46             var users = _cache.Get<List<User>>("users");
    47             var users2 = await _cache.GetAsync<List<User>>("users2");
    48             return await Task.FromResult(new JsonResult(new { users1 = users.Value,users2 = users2.Value }));
    49         }
    50     }
    51 }

     ⒊改造为Redis示例

      1.安装Nuget包

        EasyCaching.Redis

      2.在Startup中配置服务及请求管道

     1         // This method gets called by the runtime. Use this method to add services to the container.
     2         public void ConfigureServices(IServiceCollection services)
     3         {
     4             services.AddSingleton<IUserService, UserService>();
     5             services.AddEasyCaching(option =>
     6             {
     7                 //使用redis
     8                 option.UseRedis(config =>
     9                 {
    10                     config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    11                 }, "localhostRedis");
    12             });
    13 
    14             services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    15         }

      3.启动程序

        然后就发现数据缓存到Redis中了,不过,

    对于序列化,一般都会有一个基于BinaryFormatter的默认实现,因为这个并不依赖于第三方类库,如果我们没有指定其它的,EasyCaching就会使用这个去进行数据的序列化,除了这个默认的实现,EasyCaching还提供了三种额外的选择。Newtonsoft.Json,MessagePack和Protobuf。切换方法如下:

        1.安装以下Nuget包(你用那个序列化就装那个)

    1 EasyCaching.Serialization.Json
    2 EasyCaching.Serialization.MessagePack
    3 EasyCaching.Serialization.Protobuf

        2.配置序列化方式

     1         // This method gets called by the runtime. Use this method to add services to the container.
     2         public void ConfigureServices(IServiceCollection services)
     3         {
     4             services.AddSingleton<IUserService, UserService>();
     5             services.AddEasyCaching(option =>
     6             {
     7                 //使用redis
     8                 option.UseRedis(config =>
     9                 {
    10                     config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    11                 }, "localhostRedis")
    12                 //.WithMessagePack(); //使用MessagePack替换BinaryFormatter
    13                 //.WithProtobuf();    //使用Protobuf替换BinaryFormatter
    14                 .WithJson();    //使用Newtonsoft.Json替换BinaryFormatter
    15             });
    16 
    17             services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    18         }

     ⒋多示例支持

      多实例指在同一个项目中同时使用多个缓存提供者,包括多个同一类型的缓存提供者或着是不同类型的缓存提供者。

      在代码中借助IEasyCachingProviderFactory来指定使用那个缓存提供者。

      1.先添加两个缓存提供者

     1         // This method gets called by the runtime. Use this method to add services to the container.
     2         public void ConfigureServices(IServiceCollection services)
     3         {
     4             services.AddSingleton<IUserService, UserService>();
     5             services.AddEasyCaching(option =>
     6             {
     7                 option.UseInMemory("m1");   //配置一个InMemory,名称为m1
     8                 //使用redis
     9                 option.UseRedis(config =>
    10                 {
    11                     config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    12                 }, "localhostRedis")
    13                 //.WithMessagePack(); //使用MessagePack替换BinaryFormatter
    14                 //.WithProtobuf();    //使用Protobuf替换BinaryFormatter
    15                 .WithJson();    //使用Newtonsoft.Json替换BinaryFormatter
    16             });
    17 
    18             services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    19         }

      2.在代码中使用

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using Coreqi.EasyCaching.Models;
     6 using Coreqi.EasyCaching.Services;
     7 using EasyCaching.Core;
     8 using Microsoft.AspNetCore.Mvc;
     9 
    10 namespace Coreqi.EasyCaching.Controllers
    11 {
    12     [Route("api/[controller]")]
    13     public class LoginController : Controller
    14     {
    15         private readonly IEasyCachingProviderFactory _cacheFactory;
    16         private readonly IUserService _userService;
    17         public LoginController(IEasyCachingProviderFactory cacheFactory, IUserService userService)
    18         {
    19             this._cacheFactory = cacheFactory;
    20             this._userService = userService;
    21         }
    22 
    23         [HttpGet]
    24         [Route("add")]
    25         public async Task<IActionResult> Add()
    26         {
    27             var _cache1 = _cacheFactory.GetCachingProvider("m1");   //获取名字为m1的provider
    28             var _cache2 = _cacheFactory.GetCachingProvider("localhostRedis");   //获取名字为localhostRedis的provider
    29             IList<User> users = _userService.getAll();
    30             IList<string> loginNames = users.Select(s => s.username).ToList();
    31             _cache1.Set("loginNames", loginNames, TimeSpan.FromMinutes(2));
    32             await _cache2.SetAsync("users", users, TimeSpan.FromMinutes(2));
    33             return await Task.FromResult(new JsonResult(new { message = "添加成功!" }));
    34         }
    35 
    36         [HttpGet]
    37         [Route("get")]
    38         public async Task<IActionResult> Get()
    39         {
    40             var _cache1 = _cacheFactory.GetCachingProvider("m1");   //获取名字为m1的provider
    41             var _cache2 = _cacheFactory.GetCachingProvider("localhostRedis");   //获取名字为localhostRedis的provider
    42             IList<string> loginNames = _cache1.Get<List<string>>("loginNames").Value;
    43             IList<User> users = (await _cache2.GetAsync<List<User>>("users")).Value;
    44             return await Task.FromResult(new JsonResult(new { loginNames = loginNames,users = users}));
    45         }
    46     }
    47 }

     ⒌缓存的Aop操作

      一句话,提供和Java Spring中的@Cacheable、@CacheEvict、@CachePut等注解类似的Aop操作

      例如,我们以前的查询代码一般是这样的

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using Coreqi.EasyCaching.Models;
     6 using Coreqi.EasyCaching.Services;
     7 using EasyCaching.Core;
     8 using Microsoft.AspNetCore.Mvc;
     9 
    10 namespace Coreqi.EasyCaching.Controllers
    11 {
    12     [Route("api/[controller]")]
    13     public class CacheAopController : Controller
    14     {
    15         private readonly IEasyCachingProvider _cache;
    16         private readonly IUserService _service;
    17         public CacheAopController(IEasyCachingProvider cache, IUserService service)
    18         {
    19             this._cache = cache;
    20             this._service = service;
    21         }
    22 
    23         [HttpGet]
    24         [Route("get/{id}")]
    25         public async Task<User> GetUserByIdAsync(int id)
    26         {
    27             string cacheKey = $"user:{id}";
    28             var userInfo = await _cache.GetAsync<User>(cacheKey);
    29             if (userInfo.HasValue)
    30             {
    31                 return userInfo.Value;
    32             }
    33             var user = _service.getById(id);
    34             if (user != null)
    35             {
    36                 _cache.Set<User>(cacheKey, user, TimeSpan.FromHours(2));
    37             }
    38             return user;
    39         }
    40     }
    41 }

      先去查询缓存数据,没有的话再去查库然后保存到缓存中,一个查询是这样,那么多的CRUD都这样岂不是要写到吐?

       而我们使用EasyCaching的缓存AOP来简化这一操作。

      1.Nuget包

      EasyCaching.Interceptor.AspectCore

      2.在接口的定义上添加一个Attribute标识。

     1 using Coreqi.EasyCaching.Models;
     2 using EasyCaching.Core.Interceptor;
     3 using System;
     4 using System.Collections.Generic;
     5 using System.Linq;
     6 using System.Threading.Tasks;
     7 
     8 namespace Coreqi.EasyCaching.Services
     9 {
    10     public interface IUserService
    11     {
    12         IList<User> getAll();
    13 
    14         [EasyCachingAble(Expiration =10)]
    15         User getById(int id);
    16 
    17         User add(User user);
    18 
    19         User modify(User user);
    20 
    21         void delete(int id);
    22     }
    23 }

      3.配置服务使其生效

     1         // This method gets called by the runtime. Use this method to add services to the container.
     2         public IServiceProvider ConfigureServices(IServiceCollection services)
     3         {
     4             services.AddSingleton<IUserService, UserService>(); //必须将Service添加到IOC中,否则AOP不成功
     5             services.AddEasyCaching(option =>
     6             {
     7                 option.UseInMemory("m1");   //配置一个InMemory,名称为m1
     8                 //使用redis
     9                 option.UseRedis(config =>
    10                 {
    11                     config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    12                 }, "localhostRedis")
    13                 //.WithMessagePack(); //使用MessagePack替换BinaryFormatter
    14                 //.WithProtobuf();    //使用Protobuf替换BinaryFormatter
    15                 .WithJson();    //使用Newtonsoft.Json替换BinaryFormatter
    16             });
    17 
    18             services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    19 
    20             return services.ConfigureAspectCoreInterceptor(options =>
    21             {
    22                 options.CacheProviderName = "localhostRedis";   //指定要使用的缓存提供者名称
    23             });
    24         }

      *也可以在Attribute特性上指定

     1 using Coreqi.EasyCaching.Models;
     2 using EasyCaching.Core.Interceptor;
     3 using System;
     4 using System.Collections.Generic;
     5 using System.Linq;
     6 using System.Threading.Tasks;
     7 
     8 namespace Coreqi.EasyCaching.Services
     9 {
    10     public interface IUserService
    11     {
    12         IList<User> getAll();
    13 
    14         [EasyCachingAble(Expiration =10,CacheProviderName ="m1")]
    15         User getById(int id);
    16 
    17         User add(User user);
    18 
    19         User modify(User user);
    20 
    21         void delete(int id);
    22     }
    23 }

     

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using Coreqi.EasyCaching.Models;
     6 using Coreqi.EasyCaching.Services;
     7 using EasyCaching.Core;
     8 using Microsoft.AspNetCore.Mvc;
     9 
    10 namespace Coreqi.EasyCaching.Controllers
    11 {
    12     [Route("api/[controller]")]
    13     public class CacheAopController : Controller
    14     {
    15         private readonly IEasyCachingProvider _cache;
    16         private readonly IUserService _service;
    17         public CacheAopController(IEasyCachingProvider cache, IUserService service)
    18         {
    19             this._cache = cache;
    20             this._service = service;
    21         }
    22 
    23         [HttpGet]
    24         [Route("get/{id}")]
    25         public User GetUserById(int id)
    26         {
    27             return _service.getById(id);
    28         }
    29     }
    30 }

    完成上面的操作后可以在调用方法的时候优先取缓存,没有缓存的时候才会去执行方法。

    EasyCaching提供的AOP Attritebute有一些通用的属性。

    配置名说明
    CacheKeyPrefix 指定生成缓存键的前缀,正常情况下是用在修改和删除的缓存上
    CacheProviderName 可以指定特殊的provider名字
    IsHightAvailability 缓存相关操作出现异常时,是否还能继续执行业务方法

    EasyCachingAble和EasyCachingPut还有一个同名和配置。

    配置名说明
    Expiration key的过期时间,单位是秒

    EasyCachingEvict有两个特殊的配置。

    配置名说明
    IsAll 这个要搭配CacheKeyPrefix来用,就是删除这个前缀的所有key
    IsBefore 在业务方法执行之前删除缓存还是执行之后

    ⒍支持Diagnostics【诊断系统】

      提供了Diagnostics的支持方便接入第三方的APM,实现追踪。

      有一个接入Jaeger的一个案例。大家可以去看看。

    ⒎二级缓存

    二级缓存,多级缓存,其实在缓存的小世界中还算是一个比较重要的东西!

    一个最为头疼的问题就是不同级的缓存如何做到近似实时的同步。

    在EasyCaching中,二级缓存的实现逻辑大致就是下面的这张图。

    如果某个服务器上面的本地缓存被修改了,就会通过缓存总线去通知其他服务器把对应的本地缓存移除掉

      1.添加Nuget包。

    1 EasyCaching.InMemory
    2 EasyCaching.Redis
    3 EasyCaching.HybridCache
    4 EasyCaching.Bus.Redis

      2.添加配置

     1             services.AddEasyCaching(option =>
     2             {
     3                 option.UseInMemory("m1");   //配置一个InMemory,名称为m1
     4                 //使用redis
     5                 option.UseRedis(config =>
     6                 {
     7                     config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
     8                     config.DBConfig.Database = 5;
     9                 }, "localhostRedis")
    10                 //.WithMessagePack(); //使用MessagePack替换BinaryFormatter
    11                 //.WithProtobuf();    //使用Protobuf替换BinaryFormatter
    12                 .WithJson();    //使用Newtonsoft.Json替换BinaryFormatter
    13 
    14                 //使用Hybird
    15                 option.UseHybrid(config =>
    16                 {
    17                     config.EnableLogging = false;   //是否开启日志
    18                     config.TopicName = "test_topic";    //缓存总线的订阅主题
    19                     config.LocalCacheProviderName = "m1";   //本地缓存的名字
    20                     config.DistributedCacheProviderName = "localhostRedis"; //分布式缓存的名字
    21                 });
    22 
    23                 //使用redis作为缓存总线
    24                 option.WithRedisBus(config =>
    25                 {
    26                     config.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    27                     config.Database = 6;
    28                 });
    29 
    30                 //使用CSRedis作为缓存总线
    31                 //option.WithCSRedisBus(config =>
    32                 //{
    33                 //    config.ConnectionStrings = new List<string>
    34                 //    {
    35                 //        "127.0.0.1:6379,defaultDatabase=6,poolsize=10"
    36                 //    };
    37                 //});
    38 
    39                 //使用RabbitMq作为缓存总线
    40                 //option.WithRabbitMQBus(config =>
    41                 //{
    42                 //    config = new RabbitMQBusOptions();
    43                 //});
    44             });

      3.使用

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using EasyCaching.Core;
     6 using Microsoft.AspNetCore.Mvc;
     7 
     8 namespace Coreqi.EasyCaching.Controllers
     9 {
    10     [Route("api/[controller]")]
    11     public class HybridCachingController : Controller
    12     {
    13         private readonly IHybridCachingProvider _provider;
    14         public HybridCachingController(IHybridCachingProvider provider)
    15         {
    16             this._provider = provider;
    17         }
    18 
    19         [HttpGet]
    20         [Route("add")]
    21         public string Add()
    22         {
    23             string cacheKey = "coreqiTest";
    24             _provider.Set(cacheKey, "1111111", TimeSpan.FromSeconds(30));
    25             return "添加成功!";
    26         }
    27     }
    28 }

    ⒏特殊的Redis缓存提供者

    Redis支持多种数据结构,还有一些原子递增递减的操作等等。为了支持这些操作,EasyCaching提供了一个独立的接口,IRedisCachingProvider。

    这个接口,目前也只支持了百分之六七十常用的一些操作,还有一些可能用的少的就没加进去。

    同样的,这个接口也是支持多实例的,也可以通过IEasyCachingProviderFactory来获取不同的provider实例。

    在注入的时候,不需要额外的操作,和添加Redis是一样的。不同的是,在使用的时候,不再是用IEasyCachingProvider,而是要用IRedisCachingProvider

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Threading.Tasks;
     5 using EasyCaching.Core;
     6 using Microsoft.AspNetCore.Mvc;
     7 
     8 namespace Coreqi.EasyCaching.Controllers
     9 {
    10     [Route("api/[controller]")]
    11     public class MultiRedisController : Controller
    12     {
    13         private readonly IRedisCachingProvider _redis1;
    14         private readonly IRedisCachingProvider _redis2;
    15 
    16         public MultiRedisController(IEasyCachingProviderFactory factory)
    17         {
    18             this._redis1 = factory.GetRedisProvider("redis1");
    19             this._redis2 = factory.GetRedisProvider("redis2");
    20         }
    21 
    22         [HttpGet]
    23         [Route("get")]
    24         public string Get()
    25         {
    26             _redis1.StringSet("keyredis1", "val");
    27 
    28             var res1 = _redis1.StringGet("keyredis1");
    29             var res2 = _redis2.StringGet("keyredis1");
    30 
    31             return $"redis1 cached value: {res1}, redis2 cached value : {res2}";
    32         }
    33     }
    34 }

    ⒐EasyCaching扩展性功能

    除了这些基础功能,还有一些扩展性的功能,在这里要非常感谢yrinleung,他把EasyCaching和WebApiClient,CAP等项目结合起来了。感兴趣的可以看看这个项目EasyCaching.Extensions

    参考文章:一篇短文带您了解一下EasyCaching-----作者@Catcher ( 黄文清 )

  • 相关阅读:
    Android Studio在项目中添加assets资源目录
    Android Studio向项目中导入jar包的方法
    Android Studio 配置 androidAnnotations框架详细步骤
    Android退出所有Activity最优雅的方式
    Android数据存储之SQLite使用
    Android数据存储之SharedPreferences使用
    三种实现Android主界面Tab的方式
    Android热门网络框架Volley详解
    Android必学之数据适配器BaseAdapter
    23种设计模式UML图
  • 原文地址:https://www.cnblogs.com/fanqisoft/p/10877751.html
Copyright © 2011-2022 走看看