Netflix中的负载均衡策略
Spring Cloud的负载均衡策略可以通过配置Ribbon搞定,也就是注入实现com.netflix.loadbalancer.IRule的类,当前包含的策略包括
1.RandomRule 随机策略 在while循环内,如果服务地址不为空会不停的循环直到随机出一个可用的服务。
@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
} else {
Server server = null; while(server == null) {
if (Thread.interrupted()) {
return null;
} List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == ) {
return null;
} int index = this.rand.nextInt(serverCount);
server = (Server)upList.get(index);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
} server = null;
Thread.yield();
}
} return server;
}
}
不过感觉怎么第一个就有坑呢。。upList表示当前可用的服务实例集合,这个集合可以由客户端开启定时任务定期对调用服务进行ping来更新,allList表示当前所有服务实例的集合。
也就是说当存在。通过com.netflix.loadbalancer.BaseLoadBalancer中可见:
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
BaseLoadBalancer.logger.debug("LoadBalancer: PingTask executing [{}] servers configured", numCandidates); for(int i = ; i < numCandidates; ++i) {
results[i] = false; try {
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} catch (Exception var7) {
BaseLoadBalancer.logger.error("Exception while pinging Server: '{}'", servers[i], var7);
}
} return results;
}
public void runPinger() throws Exception {
if (BaseLoadBalancer.this.pingInProgress.compareAndSet(false, true)) {
Server[] allServers = null;
boolean[] results = null;
Lock allLock = null;
Lock upLock = null; try {
allLock = BaseLoadBalancer.this.allServerLock.readLock();
allLock.lock();
allServers = (Server[])BaseLoadBalancer.this.allServerList.toArray(new Server[BaseLoadBalancer.this.allServerList.size()]);
allLock.unlock();
int numCandidates = allServers.length;
boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
List<Server> newUpList = new ArrayList();
List<Server> changedServers = new ArrayList(); for(int i = ; i < numCandidates; ++i) {
boolean isAlive = resultsx[i];
Server svr = allServers[i];
boolean oldIsAlive = svr.isAlive();
svr.setAlive(isAlive);
if (oldIsAlive != isAlive) {
changedServers.add(svr);
BaseLoadBalancer.logger.debug("LoadBalancer [{}]: Server [{}] status changed to {}", new Object[]{BaseLoadBalancer.this.name, svr.getId(), isAlive ? "ALIVE" : "DEAD"});
} if (isAlive) {
newUpList.add(svr);
}
} upLock = BaseLoadBalancer.this.upServerLock.writeLock();
upLock.lock();
BaseLoadBalancer.this.upServerList = newUpList;
upLock.unlock();
BaseLoadBalancer.this.notifyServerStatusChangeListener(changedServers);
} finally {
BaseLoadBalancer.this.pingInProgress.set(false);
}
}
}
如此看来,当upList数量不等于allList数量时,这个server = (Server)upList.get(index);就出问题了!?当然,默认情况下ping的方法是不进行真实健康监测的,即所有服务都是健康的,保证allList.size()=upList.size();不过感觉很怪异。
2.RoundRobinRule 轮询策略,但是有个查找次数的限制,也就是说查了10次都是不可用的服务的话就会警告没有可用服务并返回null了,选择的方式是很简单,取余运算。
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = ; while(true) {
if (server == null && count++ < ) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if (upCount != && serverCount != ) {
int nextServerIndex = this.incrementAndGetModulo(serverCount);
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive() && server.isReadyToServe()) {
return server;
} server = null;
}
continue;
} log.warn("No up servers available from load balancer: " + lb);
return null;
} if (count >= ) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
} return server;
}
}
} private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
next = (current + ) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next)); return next;
}
此处的upCount依然是个摆设。。。
3.ClientConfigEnabledRoundRobinRule 默认使用RoundRobinRule 策略 不过字面意思,客户端可配置的,所以可以作为父类扩展
public void initWithNiwsConfig(IClientConfig clientConfig) {
this.roundRobinRule = new RoundRobinRule();
}
public Server choose(Object key) {
if (this.roundRobinRule != null) {
return this.roundRobinRule.choose(key);
} else {
throw new IllegalArgumentException("This class has not been initialized with the RoundRobinRule class");
}
}
4.WeightedResponseTimeRule 实例初始化的时候会开启一个定时任务,通过定时任务来获取服务响应时间定期维护每个服务的权重
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
} else {
Server server = null; while(server == null) {
List<Double> currentWeights = this.accumulatedWeights;
if (Thread.interrupted()) {
return null;
} List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == ) {
return null;
} int serverIndex = ;
double maxTotalWeight = currentWeights.size() == ? 0.0D : ((Double)currentWeights.get(currentWeights.size() - )).doubleValue();
if (maxTotalWeight < 0.001D) {
server = super.choose(this.getLoadBalancer(), key);
if (server == null) {
return server;
}
} else {
double randomWeight = this.random.nextDouble() * maxTotalWeight;
int n = ; for(Iterator var13 = currentWeights.iterator(); var13.hasNext(); ++n) {
Double d = (Double)var13.next();
if (d.doubleValue() >= randomWeight) {
serverIndex = n;
break;
}
} server = (Server)allList.get(serverIndex);
} if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
} server = null;
}
} return server;
}
}
public void maintainWeights() {
ILoadBalancer lb = WeightedResponseTimeRule.this.getLoadBalancer();
if (lb != null) {
if (WeightedResponseTimeRule.this.serverWeightAssignmentInProgress.compareAndSet(false, true)) {
try {
WeightedResponseTimeRule.logger.info("Weight adjusting job started");
AbstractLoadBalancer nlb = (AbstractLoadBalancer)lb;
LoadBalancerStats stats = nlb.getLoadBalancerStats();
if (stats != null) {
double totalResponseTime = 0.0D;//所有实例的响应时间总和 ServerStats ss;
for(Iterator var6 = nlb.getAllServers().iterator(); var6.hasNext(); totalResponseTime += ss.getResponseTimeAvg()) {
//通过ss.getResponseTimeAvg()获取每个服务的平均响应时间 然后累加到totalResponseTime中
Server server = (Server)var6.next();
ss = stats.getSingleServerStat(server);
} Double weightSoFar = 0.0D;
List<Double> finalWeights = new ArrayList();
Iterator var20 = nlb.getAllServers().iterator(); while(var20.hasNext()) {
Server serverx = (Server)var20.next();
ServerStats ssx = stats.getSingleServerStat(serverx);
double weight = totalResponseTime - ssx.getResponseTimeAvg();//所有服务的平均响应时间的和-该服务的平均响应时间=该服务的权重
weightSoFar = weightSoFar.doubleValue() + weight;
//由于通过集合存储 所以此处采取区间的模式 也就是从0到n,n到...的模式
//比如 三个服务 响应时间分别为10,20,30 则权重分别为(0-50)(50-90)(90-120)
finalWeights.add(weightSoFar);
} WeightedResponseTimeRule.this.setWeights(finalWeights);
return;
}
} catch (Exception var16) {
WeightedResponseTimeRule.logger.error("Error calculating server weights", var16);
return;
} finally {
WeightedResponseTimeRule.this.serverWeightAssignmentInProgress.set(false);
} }
}
}
不过当权重的集合中没有数据的时候,这个类继承了RoundRobinRule 类,就使用轮询的方式选择了。如果存在权重信息则使用this.random.nextDouble() * maxTotalWeight的方式也就是1以内小数*最大权重值区间内的随机数来选取服务索引的方式。跟RandomRule 的模式一样,当选取的服务状态异常的时候会While循环走下去。直到。。。死循环。
5.BestAvailableRule 对所有实例进行迭代,首先过滤掉不可用的服务,然后选出连接数最少的服务返回,继承了ClientConfigEnabledRoundRobinRule类也就是使用了RoundRobinRule策略,也就是loadBalancerStats进行统计服务连接信息为空的时候先采用轮询策略过渡。
public Server choose(Object key) {
if (this.loadBalancerStats == null) {
return super.choose(key);
} else {
List<Server> serverList = this.getLoadBalancer().getAllServers();
int minimalConcurrentConnections = ;
long currentTime = System.currentTimeMillis();
Server chosen = null;
Iterator var7 = serverList.iterator(); while(var7.hasNext()) {
Server server = (Server)var7.next();
ServerStats serverStats = this.loadBalancerStats.getSingleServerStat(server);
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
} if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}
}
6.RetryRule 采用了轮询策略(内部直接实例化RoundRobinRule使用)的重试策略来获取可用的服务实例。这里有个maxRetryMillis属性用来限定重试的时间,如果首次获取服务实例为空,则开启一个定指定关闭时间的定时线程,在该指定时间内如果没有找到可用的实例就返回null了。默认为500毫秒。(轮询策略内不是10次以内不管找到可用实例与否都返回结果,所以此处可以看成一个次数微微可控的加强版)
public Server choose(ILoadBalancer lb, Object key) {
long requestTime = System.currentTimeMillis();
long deadline = requestTime + this.maxRetryMillis;
Server answer = null;
answer = this.subRule.choose(key);
if ((answer == null || !answer.isAlive()) && System.currentTimeMillis() < deadline) {
InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis()); while(!Thread.interrupted()) {
answer = this.subRule.choose(key);
if (answer != null && answer.isAlive() || System.currentTimeMillis() >= deadline) {
break;
} Thread.yield();
} task.cancel();
} return answer != null && answer.isAlive() ? answer : null;
}
7.PredicateBasedRule 继承自ClientConfigEnabledRoundRobinRule的一个抽象类。
public abstract AbstractServerPredicate getPredicate(); public Server choose(Object key) {
ILoadBalancer lb = this.getLoadBalancer();
Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
return server.isPresent() ? (Server)server.get() : null;
}
使用的时候需要重写getPredicate方法,目测是先过滤一部分服务然后在选择一个服务。
//上边方法this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);调用到这
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
return eligible.size() == ? Optional.absent() :
Optional.of(eligible.get(this.nextIndex.getAndIncrement() % eligible.size()));
//此处可见过滤后的集合为空则返回 Optional.absent()表示不存在对象集合(通过isPresent()方法默认就是false),集合不为空则还是如同轮询算法般取余
}
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));//loadBalancerKey 如果为null的话 则返回当前即可(这个filter的过滤条件是不过滤。。。)
} else {
List<Server> results = Lists.newArrayList();
Iterator var4 = servers.iterator(); while(var4.hasNext()) {
Server server = (Server)var4.next();
//此处进行条件判断 将满足条件的集合返回
if (this.apply(new PredicateKey(loadBalancerKey, server))) {
results.add(server);
}
} return results;
}
}
当然这个抽象类需要我们实现getPredicate()返回AbstractServerPredicate过滤条件(默认全部返回为true,也就是等价于采用轮询的模式了)。
8.AvailabilityFilteringRule实现PredicateBasedRule类,如代码所示,组合条件是一个new AvailabilityPredicate().
private AbstractServerPredicate predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, (IClientConfig)null)).addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build();
public void initWithNiwsConfig(IClientConfig clientConfig) {
this.predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, clientConfig)).addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build();
}
查看过滤条件
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = this.getLBStats();
if (stats == null) {
return true;
} else {
return !this.shouldSkipServer(stats.getSingleServerStat(input.getServer()));//下边返回true则这块会把该服务实例过滤掉 返回为!true
}
}
//也就是这块 可以看出 如果断路器当前是开启状态或者当前服务实例的请求连接数大于配置的连接数阈值则进行过滤(默认是2147483647,可以通过clientConfig进行配置 Spring Cloud中也就是<clientName>.<nameSpace>.ActiveConnectionsLimit进行配置)
private boolean shouldSkipServer(ServerStats stats) {//满足其一条件则会返回true
return CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped() || stats.getActiveRequestsCount() >= ((Integer)this.activeConnectionsLimit.get()).intValue();
}
public Server choose(Object key) {
int count = ; for(Server server = this.roundRobinRule.choose(key); count++ <= ; server = this.roundRobinRule.choose(key)) {
if (this.predicate.apply(new PredicateKey(server))) {
return server;
}
} return super.choose(key);
}
筛选的条件可以发现是先使用轮询的方式挑选出一个服务实例,然后再进行过滤查看是否满足可以的条件,不满足再轮询下一条。
8.ZoneAvoidanceRule实现PredicateBasedRule类,此处的过滤条件通过构造函数可以看出,字面意思,第一个是根据区域进行筛选,第二个是根据可用性进行筛选
public void initWithNiwsConfig(IClientConfig clientConfig) {
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, clientConfig);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, clientConfig);
this.compositePredicate = this.createCompositePredicate(zonePredicate, availabilityPredicate);
}
ZoneAvoidancePredicate的过滤条件如下:
public boolean apply(@Nullable PredicateKey input) {
if (!ENABLED.get()) {//查看niws.loadbalancer.zoneAvoidanceRule.enabled配置的熟悉是否为true(默认为true)如果为false没有开启分片过滤 则不进行过滤
return true;
} else {
String serverZone = input.getServer().getZone();//获取配置的分片字符串 默认为UNKNOWN
if (serverZone == null) {
return true;
} else {
LoadBalancerStats lbStats = this.getLBStats();
if (lbStats == null) {//无负载均衡的要求
return true;
} else if (lbStats.getAvailableZones().size() <= ) {
return true;//可用的分片(处于Up状态)<=1 当然就没必要再过滤了
} else {
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);//key为服务实例配置的Zone
if (!zoneSnapshot.keySet().contains(serverZone)) {
return true;//如果所有分片的配置都不符合规则 那就没必要继续筛选了 不进行过滤 也就表示当前的分片设置没啥意义了
} else {
logger.debug("Zone snapshots: {}", zoneSnapshot);
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get());//此处开始挑选可用的区域
logger.debug("Available zones: {}", availableZones);
return availableZones != null ? availableZones.contains(input.getServer().getZone()) : false;
}
}
}
}
}
对两个过滤条件进行实例化后会通过this.compositePredicate = this.createCompositePredicate(zonePredicate, availabilityPredicate);将过滤条件合并。
private List<AbstractServerPredicate> fallbacks = Lists.newArrayList();//也就是所有过滤条件都存到这个fallback里了
public static CompositePredicate.Builder withPredicate(AbstractServerPredicate primaryPredicate) {
return new CompositePredicate.Builder(primaryPredicate);
}
public CompositePredicate.Builder addFallbackPredicate(AbstractServerPredicate fallback) {
this.toBuild.fallbacks.add(fallback);
return this;
}
ZoneAvoidanceRule实现PredicateBasedRule类所以还是会通过父类的choose方法进行选择。
public Server choose(Object key) {
ILoadBalancer lb = this.getLoadBalancer();
Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
return server.isPresent() ? (Server)server.get() : null;
} public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
return eligible.size() == ? Optional.absent() : Optional.of(eligible.get(this.nextIndex.getAndIncrement() % eligible.size()));
}
getEligibleServers方法在AbstractServerPredicate的子类CompositePredicate中进行了重写。
public class CompositePredicate extends AbstractServerPredicate {
private List<AbstractServerPredicate> fallbacks = Lists.newArrayList();
private int minimalFilteredServers = ;
private float minimalFilteredPercentage = 0.0F;
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
List<Server> result = super.getEligibleServers(servers, loadBalancerKey); AbstractServerPredicate predicate;
for(Iterator i = this.fallbacks.iterator(); (result.size() < this.minimalFilteredServers || result.size() <= (int)((float)servers.size() * this.minimalFilteredPercentage)) && i.hasNext(); result = predicate.getEligibleServers(servers, loadBalancerKey)) {
predicate = (AbstractServerPredicate)i.next();
} return result;
}
}
先使用父类的getEligibleServers进行过滤一遍( 默认情况下也就是没过滤)
然后按照fallbacks中存储的过滤器顺序进行过滤(此处就行先ZoneAvoidancePredicate然后AvailabilityPredicate)
当然进行下一条过滤是存在条件的 也就是:
(result.size() < this.minimalFilteredServers || result.size() <= (int)((float)servers.size() * this.minimalFilteredPercentage)) && i.hasNext()
当前过滤后的实例结果集大小小于最小过滤集合总数了(此处小于默认值1也就是0了)或者过滤后的结果集大小小于实例总数的最小过滤集合百分比了(此处比例因子是0所有相当于结果集大小还是0了)也就是当前服务示例的结果集以及不满足继续过滤的需求了 但这时候&& i.hasNext() 也就是过滤条件还没结束。。则继续进行过滤。
反复琢磨了会。。没看懂啊!!什么情况,假如父类过滤后result.size()>0的话,那循环条件中直接就(xx;false&&true;xxxx)了直接就退出了,那过滤条件是摆设么。。。换句话说,result.size()=0了 满足(xx;true&&true;xxxx)然后开始执行xxxx的过滤条件了。。问题上result都为空了。还过滤什么?!
总结:
Spring Cloud使用Feign+Ribbon可以方便的实现客户端负载均衡策略,而且提供多种负载规则,当然也可以通过实现AbstractLoadBalancerRule抽象类或者IRule进行扩展。简单方便。
Netflix中的负载均衡策略的更多相关文章
- Spring Cloud中的负载均衡策略
在上篇博客(Spring Cloud中负载均衡器概览)中,我们大致的了解了一下Spring Cloud中有哪些负载均衡器,但是对于负载均衡策略我们并没有去详细了解,我们只是知道在BaseLoadBal ...
- springcloud中的负载均衡策略
IRule 这是所有负载均衡策略的父接口,里边的核心方法就是choose方法,用来选择一个服务实例. AbstractLoadBalancerRule AbstractLoadBalancerRule ...
- spring cloud中通过配置文件自定义Ribbon负载均衡策略
一.Ribbon中的负载均衡策略 1.Ribbon中支持的负载均衡策略 AvailabilityFilteringRule:过滤掉那些因为一直连接失败的被标记为circuit tripped的后端se ...
- 【SpringCloud】Netflix源码解析之Ribbon:负载均衡策略的定义和实现
Ribbon负载均衡策略定义 IRule其实就只做了一件事情Server choose(Object key),可以看到这个功能是在LB中定义(要求)的,LB把这个功能委托给IRule来实现.不同的I ...
- Spring Cloud Ribbon 中的 7 种负载均衡策略
负载均衡通器常有两种实现手段,一种是服务端负载均衡器,另一种是客户端负载均衡器,而我们今天的主角 Ribbon 就属于后者--客户端负载均衡器. 服务端负载均衡器的问题是,它提供了更强的流量控制权,但 ...
- 撸一撸Spring Cloud Ribbon的原理-负载均衡策略
在前两篇<撸一撸Spring Cloud Ribbon的原理>,<撸一撸Spring Cloud Ribbon的原理-负载均衡器>中,整理了Ribbon如何通过负载均衡拦截器植 ...
- 每天学点SpringCloud(三):自定义Eureka集群负载均衡策略
相信看了 每天学点SpringCloud(一):简单服务提供者消费者调用,每天学点SpringCloud(二):服务注册与发现Eureka这两篇的同学都了解到了我的套路,没错,本篇博客同样是为了解决上 ...
- SpringCloud Netflix Ribbon(负载均衡)
⒈Ribbon是什么? Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具. Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负 ...
- Spring Cloud微服务开发笔记5——Ribbon负载均衡策略规则定制
上一篇文章单独介绍了Ribbon框架的使用,及其如何实现客户端对服务访问的负载均衡,但只是单独从Ribbon框架实现,没有涉及spring cloud.本文着力介绍Ribbon的负载均衡机制,下一篇文 ...
随机推荐
- 那些年我们对npm 和 cnpm 的误区
1. npm 和 cnpm 的区别 相信很多人都不太明白 npm 和 cnpm 到底是什么东东, 为啥在国内要用 淘宝镜像使用 cnpm, (1) 两者之间只是 node 中包管理器的不同哟, (2) ...
- Json反序列化为动态类型(dynamic)
方法(依赖于Newtonsoft.Json): /// <summary> /// 反序列化json字符串 /// </summary> /// <typeparam n ...
- excel导入 服务器忘了装组件了。。。
excel导入 本地没问题 一直在找权限问题 最后发现服务器忘了装组件了... 郁闷 记录下 http://www.microsoft.com/zh-cn/download/confirmation ...
- Pig order by用法举例
sorted = order data by $0; 数值类型按照数值大小比较 chararray类型按照字符的字典顺序比较 bytearray按照字节的字典顺序比较 复杂类型(map.tuple ...
- Multiple Database Block Sizes and the Buffer Cache
In oracle 10g we can have multiple block sizes at the same time. When a tablespace is created we can ...
- eclipse maven构建的java web工程项目 在修改了工程项目名时,tomcat启动异常java.lang.IllegalArgumentException: Can't convert argument:null
问题 我修改了前一个项目的名称.重新启动该项目至tomcat,报错:Java.lang.IllegalArgumentException: Can't convert argument: null 因 ...
- clean-css
What is clean-css? Clean-css is a fast and efficient Node.js library for minifying CSS files. Accord ...
- [翻译] SCRecorder
SCRecorder https://github.com/rFlex/SCRecorder An easy Vine/Instagram like video and/or audio record ...
- UINavigationController便于pop的category
UINavigationController便于pop的category 效果图: 这个category是为了方便UINavigationController用于跳转到指定的控制器当中,用于跳级,如果 ...
- 详细讲解WaterRefreshLoadMoreView的使用
详细讲解WaterRefreshLoadMoreView的使用 效果图: 加载控件的源码在如下网址中:上拉加载下拉刷新控件WaterRefreshLoadMoreView 使用的源码: // // V ...