PSCollectionView瀑布流实现
PSCollectionView是一个实现较简洁的仿Pinterest瀑布流iOS版实现,使用UIScrollView做容器,每列列宽固定,高度可变,使用方式类似UITableView。
其效果如图:
一.基本原理
其基本实现原理为:
- 列数固定,根据列数每列存储一个当前列的高度值。
- 每次插入数据块时,在当前最小高度的列里插入,然后更新当前列的高度为原有高度加上当前数据模块高度
- 重复2直到所有数据模块插入完毕
- 调整容器(UIScrollView)的高度为各列最大的高度值。
二.具体实现
1. 相关数据结构
公共属性
@property (nonatomic, retain) UIView *headerView;
@property (nonatomic, retain) UIView *footerView;
@property (nonatomic, retain) UIView *emptyView;
@property (nonatomic, retain) UIView *loadingView;
@property (nonatomic, assign, readonly) CGFloat colWidth;
@property (nonatomic, assign, readonly) NSInteger numCols;
@property (nonatomic, assign) NSInteger numColsLandscape;
@property (nonatomic, assign) NSInteger numColsPortrait;
@property (nonatomic, assign) id collectionViewDelegate;
@property (nonatomic, assign) id collectionViewDataSource;
- headerView,footerView,emptyView,loadingView分别对应列表头部,尾部,空白时,正在加载时要显示的视图。
numColsLandscape,numColsPortrait为横屏和竖屏时的列数。 - colWidth,numCols为只读属性,根据当前的视图方向,视图总大小,横屏和竖屏时的列数计算得出。
- collectionViewDelegate,collectionViewDataSource为Delegate和数据源。
私有属性
@property (nonatomic, assign, readwrite) CGFloat colWidth;
@property (nonatomic, assign, readwrite) NSInteger numCols;
@property (nonatomic, assign) UIInterfaceOrientation orientation;
@property (nonatomic, retain) NSMutableSet *reuseableViews;
@property (nonatomic, retain) NSMutableDictionary *visibleViews;
@property (nonatomic, retain) NSMutableArray *viewKeysToRemove;
@property (nonatomic, retain) NSMutableDictionary *indexToRectMap;
- 私有属性将colWidth,numCols定义为readwrite,便于内部赋值操作。
- orientation为当前视图的方向,从UIApplication的statusBarOrientation属性获取。
- reuseableViews数据集存储可重用的的数据块视图,在数据块移除可见范围时将其放入reuseableViews中,当DataSource调用dequeueReusableView时,从reuseableViews取出一个返回。
- visibleViews字典存储当前可见的数据块视图,key为数据块索引,当容器滚动时,将移除可见范围的数据块视图从visibleViews中移除,并放入reuseableViews中;当存在应该显示的数据块视图,但还未放入容器视图时,则从DataSource获取新的数据块视图,加入到容器视图中,同时将其加入到visibleViews中。
- viewKeysToRemove数组在遍历visibleViews时存储应该移除的数据块视图Key。
- indexToRectMap数据字典存储每个数据块(不管可不可见)在容器中的位置,将CGRect转换为NSString(NSStringFromCGRect)作为Value存储,Key为数据块的索引。
2.视图更新方式
- 在reloadData或视图方向发生变化时,需要重新计算所有数据块的位置并重新加载,见relayoutViews方法。
- 当滑动容器时,UIScrollView会调用其layoutSubviews方法,若方向未变化,则不需要重新加载所有数据块,仅仅需要移除非可见的数据块,载入进入可见范围的数据块,见removeAndAddCellsIfNecessary方法.
#pragma mark - DataSource - (void)reloadData {
[self relayoutViews];
} #pragma mark - View - (void)layoutSubviews {
[super layoutSubviews]; UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if (self.orientation != orientation) {
self.orientation = orientation;
[self relayoutViews];
} else {
[self removeAndAddCellsIfNecessary];
}
}3.relayoutViews方法
relayoutViews会将现有的所有数据块视图清除,重新从DataSource获取数据,重新计算所有数据块视图的位置,容器的高度等。
- 首先遍历可见数据块视图字典visibleViews,将所有数据块视图放入reuseableViews中,并清空visibleViews,indexToRectMap。
- 将emptyView,loadingView从容器视图中移除。
- 从DataSource获取数据块的个数numViews。
- 若headerView不为nil,则将headerView视图加入到容器,更新top索引。
- 若numViews不为0,则依次计算每个数据块的位置。使用colOffsets存储每一列的当前高度,每次增加数据块时将其添加到高度最小的列中,所处的列确定后,其orig坐标就确定了,宽度固定,再从DataSource获取此数据块的高度,那么当前数据块的frame位置就确定了,将其转换为NSString(使用setObject:NSStringFromCGRect)存储到indexToRectMap字典中,以数控块索引为key;同时将当前列的高度更新,再继续处理下一数据块,还是加入到高度最小的列中,直至所有数据块处理完毕。
- 这时的总高度即最高列的高度。
- 若numViews为0,则将emptyView增加到容器中,总高度则为添加emptyView的高度。
- 若footerView不为nil,则将footerView加入到容器中.
- 这时的总高度totalHeight即为最终容器内容的总高度,将其赋值的UIScrollView的contentSize属性。
- 这时headerView和footView已加入到容器中,但所有的数据块只是计算了其应该处于的位置,并未实际放入容器中,调用removeAndAddCellsIfNecessary将当前可见的数据块视图加入到容器中。
- (void)relayoutViews {
self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape; // Reset all state
[self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
PSCollectionViewCell *view = (PSCollectionViewCell *)obj;
[self enqueueReusableView:view];
}];
[self.visibleViews removeAllObjects];
[self.viewKeysToRemove removeAllObjects];
[self.indexToRectMap removeAllObjects]; if (self.emptyView) {
[self.emptyView removeFromSuperview];
}
[self.loadingView removeFromSuperview]; // This is where we should layout the entire grid first
NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; CGFloat totalHeight = 0.0;
CGFloat top = kMargin; // Add headerView if it exists
if (self.headerView) {
self.headerView.top = kMargin;
top = self.headerView.top;
[self addSubview:self.headerView];
top += self.headerView.height;
top += kMargin;
} if (numViews > 0) {
// This array determines the last height offset on a column
NSMutableArray *colOffsets = [NSMutableArray arrayWithCapacity:self.numCols];
for (int i = 0; i < self.numCols; i++) {
[colOffsets addObject:[NSNumber numberWithFloat:top]];
} // Calculate index to rect mapping
self.colWidth = floorf((self.width - kMargin * (self.numCols + 1)) / self.numCols);
for (NSInteger i = 0; i < numViews; i++) {
NSString *key = PSCollectionKeyForIndex(i); // Find the shortest column
NSInteger col = 0;
CGFloat minHeight = [[colOffsets objectAtIndex:col] floatValue];
for (int i = 1; i < [colOffsets count]; i++) {
CGFloat colHeight = [[colOffsets objectAtIndex:i] floatValue]; if (colHeight < minHeight) {
col = i;
minHeight = colHeight;
}
} CGFloat left = kMargin + (col * kMargin) + (col * self.colWidth);
CGFloat top = [[colOffsets objectAtIndex:col] floatValue];
CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:i];
if (colHeight == 0) {
colHeight = self.colWidth;
} if (top != top) {
// NaN
} CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); // Add to index rect map
[self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key]; // Update the last height offset for this column
CGFloat test = top + colHeight + kMargin; if (test != test) {
// NaN
}
[colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:test]];
} for (NSNumber *colHeight in colOffsets) {
totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight;
}
} else {
totalHeight = self.height; // If we have an empty view, show it
if (self.emptyView) {
self.emptyView.frame = CGRectMake(kMargin, top, self.width - kMargin * 2, self.height - top - kMargin);
[self addSubview:self.emptyView];
}
} // Add footerView if exists
if (self.footerView) {
self.footerView.top = totalHeight;
[self addSubview:self.footerView];
totalHeight += self.footerView.height;
totalHeight += kMargin;
} self.contentSize = CGSizeMake(self.width, totalHeight); [self removeAndAddCellsIfNecessary];
}4.removeAndAddCellsIfNecessary方法
removeAndAddCellsIfNecessary根据当前容器UIScrollView的contentOffset,将用户不可见的数据块视图从容器中移除,将用户可见的数据块视图加入到容器中。
- 获得当前容器的可见部分。
CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height);
- 逐个遍历visibleViews中的视图,使用CGRectIntersectsRect方法判断其frame与容器可见部分visibleRect是否有交集,若没有,则将其从visibleViews中去除,并添加到reuseableViews中。
- 对visibleViews剩余的数据块视图排序,获得其最小索引(topIndex)和最大索引(bottomIndex)。
- 将topIndex和bottomIndex分别向上和向下扩充bufferViewFactor*numCols个数据块索引。
- 从topIndex开始到bottomIndex判断索引对应的数据块视图的位置是否在容器的visibleRect范围内,以及其是否在visibleViews中。若其应该显示,而且不在visibleViews中,则向DataSource请求一个新的数据块视图,加到容器视图中,同时添加到visibleViews中。
这样新的ScrollView可见区域就可以被数据块填充满。
- (void)removeAndAddCellsIfNecessary {
static NSInteger bufferViewFactor = 5;
static NSInteger topIndex = 0;
static NSInteger bottomIndex = 0; NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; if (numViews == 0) return; // Find out what rows are visible
CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); // Remove all rows that are not inside the visible rect
[self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
PSCollectionViewCell *view = (PSCollectionViewCell *)obj;
CGRect viewRect = view.frame;
if (!CGRectIntersectsRect(visibleRect, viewRect)) {
[self enqueueReusableView:view];
[self.viewKeysToRemove addObject:key];
}
}]; [self.visibleViews removeObjectsForKeys:self.viewKeysToRemove];
[self.viewKeysToRemove removeAllObjects]; if ([self.visibleViews count] == 0) {
topIndex = 0;
bottomIndex = numViews;
} else {
NSArray *sortedKeys = [[self.visibleViews allKeys] sortedArrayUsingComparator:^(id obj1, id obj2) {
if ([obj1 integerValue] < [obj2 integerValue]) {
return (NSComparisonResult)NSOrderedAscending;
} else if ([obj1 integerValue] > [obj2 integerValue]) {
return (NSComparisonResult)NSOrderedDescending;
} else {
return (NSComparisonResult)NSOrderedSame;
}
}];
topIndex = [[sortedKeys objectAtIndex:0] integerValue];
bottomIndex = [[sortedKeys lastObject] integerValue]; topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols));
bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols));
}
// NSLog(@"topIndex: %d, bottomIndex: %d", topIndex, bottomIndex); // Add views
for (NSInteger i = topIndex; i < bottomIndex; i++) {
NSString *key = PSCollectionKeyForIndex(i);
CGRect rect = CGRectFromString([self.indexToRectMap objectForKey:key]); // If view is within visible rect and is not already shown
if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) {
// Only add views if not visible
PSCollectionViewCell *newView = [self.collectionViewDataSource collectionView:self viewAtIndex:i];
newView.frame = CGRectFromString([self.indexToRectMap objectForKey:key]);
[self addSubview:newView]; // Setup gesture recognizer
if ([newView.gestureRecognizers count] == 0) {
PSCollectionViewTapGestureRecognizer *gr = [[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)] autorelease];
gr.delegate = self;
[newView addGestureRecognizer:gr];
newView.userInteractionEnabled = YES;
} [self.visibleViews setObject:newView forKey:key];
}
}
}5.select方法
其定义了一个UITapGestureRecognizer的子类PSCollectionViewTapGestureRecognizer来检测每个数据块的点击操作。
从DataSource获取到一个新的数据块视图时,会检测里面是否已包含gesture recognizer对象,若没有则新创建一个PSCollectionViewTapGestureRecognizer对象放入,将delegate设为自身。// Setup gesture recognizer
if ([newView.gestureRecognizers count] == 0) {
PSCollectionViewTapGestureRecognizer *gr = [[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)] autorelease];
gr.delegate = self;
[newView addGestureRecognizer:gr];
newView.userInteractionEnabled = YES;
}手势识别检测到点击时会向Delegate询问此点是否可接受(gestureRecognizer:shouldReceiveTouch:),若手势识别对象是PSCollectionViewTapGestureRecognizer类型,则是我们添加进去的。若该点所属的数据块视图可见,则接受此点,若不可见,则忽略。若手势识别对象不是PSCollectionViewTapGestureRecognizer对象,就不是我们放入的,则一直返回YES。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES; NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
NSString *key = [matchingKeys lastObject]; if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
return YES;
} else {
return NO;
}
}当检测到点击操作时,调用didSelectView:方法,在其中调用delegate的collectionView:didSelectView:atIndex:方法,传递参数为self对象,选择的数据块视图以及选择的数据块索引;
- (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer {
NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
NSString *key = [matchingKeys lastObject];
if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) {
NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]);
[self.collectionViewDelegate collectionView:self didSelectView:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex];
}
}
}这种方式还存在各种问题
- 若DataSource返回的数据块视图中已加入自己的UITapGestureRecognizer对象,则[newView.gestureRecognizers count]就不为0,在判断时PSCollectionView内部定义的PSCollectionViewTapGestureRecognizer就不会加入, 这样选择数据块视图的操作就不会触发。
- 实现的gestureRecognizer:shouldReceiveTouch:方法对非PSCollectionViewTapGestureRecognizer的对象直接返回YES。这样,如果子类化PSCollectionView重写gestureRecognizer:shouldReceiveTouch:方法时,如果调用super的此方法,则会直接返回,不会执行自己的定制化操作;若不调用super的此方法,则选择功能就会出差错。
6.重用数据块视图机制
NSMutableSet *reuseableViews;
中存储可复用的数据块视图。dequeueReusableView从reuseableViews中任取一个视图返回,enqueueReusableView将数据块视图放入reuseableViews中。#pragma mark - Reusing Views - (PSCollectionViewCell *)dequeueReusableView {
PSCollectionViewCell *view = [self.reuseableViews anyObject];
if (view) {
// Found a reusable view, remove it from the set
[view retain];
[self.reuseableViews removeObject:view];
[view autorelease];
} return view;
} - (void)enqueueReusableView:(PSCollectionViewCell *)view {
if ([view respondsToSelector:@selector(prepareForReuse)]) {
[view performSelector:@selector(prepareForReuse)];
}
view.frame = CGRectZero;
[self.reuseableViews addObject:view];
[view removeFromSuperview];
}代码:
PSCollectionView.h
PSCollectionView.m
PSCollectionViewCell.h
PSCollectionViewCell.mgit工程:
https://github.com/ptshih/PSCollectionView三.使用方法
创建PSCollectionView对象
self.collectionView = [[[PSCollectionView alloc] initWithFrame:self.view.bounds] autorelease];
self.collectionView.delegate = self;
self.collectionView.collectionViewDelegate = self;
self.collectionView.collectionViewDataSource = self;
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;设置列数
// Specify number of columns for both iPhone and iPad
if (isDeviceIPad()) {
self.collectionView.numColsPortrait = 4;
self.collectionView.numColsLandscape = 5;
} else {
self.collectionView.numColsPortrait = 2;
self.collectionView.numColsLandscape = 3;
}添加header,footer,empty,loader等视图
UIView *loadingLabel = ...
self.collectionView.loadingView = loadingLabel;
UIView *emptyView = ...
self.collectionView.emptyView = emptyView;
UIView *headerView = ...
self.collectionView.headerView = headerView;
UIView *footerView = ...
self.collectionView.footerView = footerView;实现Delegate和DataSource
- (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndex:(NSInteger)index {
NSDictionary *item = [self.items objectAtIndex:index]; // You should probably subclass PSCollectionViewCell
PSCollectionViewCell *v = (PSCollectionViewCell *)[self.collectionView dequeueReusableView];
if (!v) {
v = [[[PSCollectionViewCell alloc] initWithFrame:CGRectZero] autorelease];
} [v fillViewWithObject:item] return v;
} - (CGFloat)heightForViewAtIndex:(NSInteger)index {
NSDictionary *item = [self.items objectAtIndex:index]; // You should probably subclass PSCollectionViewCell
return [PSCollectionViewCell heightForViewWithObject:item inColumnWidth:self.collectionView.colWidth];
} - (void)collectionView:(PSCollectionView *)collectionView didSelectView:(PSCollectionViewCell *)view atIndex:(NSInteger)index {
// Do something with the tap
}四.其他瀑布流实现
1.WaterflowView
2.上拉刷新瀑布流将PSCollectionView与EGOTableViewPullRefresh结合,增加上拉/下拉刷新效果。
3.瀑布效果,不同的实现方式参考:
PSCollectionView
When does layoutSubviews get called?
Overriding layoutSubviews when rotating UIView
iPhone开发笔记 – 瀑布流布局
瀑布流布局浅析
说说瀑布流式网站里那些可人的小细节
EGOTableViewPullRefresh
本文出自 清风徐来,水波不兴 的博客,转载时请注明出处及相应链接。
From: http://www.winddisk.com/2012/07/28/pscollectionview%E7%80%91%E5%B8%83%E6%B5%81%E5%A
PSCollectionView瀑布流实现的更多相关文章
- jquery瀑布流的制作
首先,还是来看一下炫酷的页面: 今天就边做边说了: 一.准备工作 新建css,js,img文件夹存放相应文件,并在demo.html文件中引入外部文件(注意要把jquery文件引入),这里就不过多描述 ...
- js瀑布流 原理实现揭秘 javascript 原生实现
web,js瀑布流揭秘 瀑布流再很久之前流行,可能如我一样入行晚的 ,可能就没有机会去使用.但是这个技术终究是个挺炫酷的东西,花了一个上午来研究,用原生js实现了一个,下面会附上源码,供大家解读. 说 ...
- CollectionView水平和竖直瀑布流的实现
最近在项目中需要实现一个水平的瀑布流(即每个Cell的高度是固定的,但是长度是不固定的),因为需要重写系统 UICollectionViewLayout中的一些方法通过计算去实现手动布局,所以本着代码 ...
- 用jquery实现瀑布流案例
一.瀑布流是我们常见的案例,这里主要讲述,用jquery的方式实现瀑布流的功能! 引言:我们经常见到很多网站的瀑布流功能,如淘宝.京东这些商品等等.. 实现它我们首先考虑几个问题:1.获取到数据 ...
- RecylerView完美实现瀑布流效果
RecylerView包含三种布局管理器,分别是LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager,对应实现单行列表,多行 ...
- 飞流直下的精彩 -- 淘宝UWP中瀑布流列表的实现
在淘宝UWP中,搜索结果列表是用户了解宝贝的重要一环,其中的图片效果对吸引用户点击搜索结果,查看宝贝详情有比较大的影响.为此手机淘宝特意在搜索结果列表上采用了2种表现方式:一种就是普通的列表模式,而另 ...
- iOS瀑布流实现(Swift)
这段时间突然想到一个很久之前用到的知识-瀑布流,本来想用一个简单的方法,发现自己走入了歧途,最终只能狠下心来重写UICollectionViewFlowLayout.下面我将用两种方法实现瀑布流,以及 ...
- 瀑布流StaggeredGridView 下拉刷新
1.项目中用到了瀑布流,之前用的是PinterestLikeAdapterView这个控件 然后上拉加载更多跟下拉刷新用的是XListView ,但是加载更多或者下拉刷新的时候闪屏,对用户体验很不好 ...
- iOS开发之窥探UICollectionViewController(四) --一款功能强大的自定义瀑布流
在上一篇博客中<iOS开发之窥探UICollectionViewController(三) --使用UICollectionView自定义瀑布流>,自定义瀑布流的列数,Cell的外边距,C ...
随机推荐
- 004_Gradle 笔记——Java构建入门
Gradle是一个通用的构建工具,通过它的构建脚本你可以构建任何你想要实现的东西,不过前提是你需要先写好构建脚本的代码.而大部分的项目,它 们的构建流程基本是一样的,我们不必为每一个工程都编写它的构建 ...
- centos7安装ssh服务
1.查看是否安装了相关软件: rpm -qa|grep -E "openssh" 显示结果含有以下三个软件,则表示已经安装,否则需要安装缺失的软件 openssh-ldap-6.6 ...
- 垃圾回收算法与 JVM 垃圾回收器综述(转)
垃圾回收算法与 JVM 垃圾回收器综述 我们常说的垃圾回收算法可以分为两部分:对象的查找算法与真正的回收方法.不同回收器的实现细节各有不同,但总的来说基本所有的回收器都会关注如下两个方面:找出所有的存 ...
- java经典面试题大全
基本概念 操作系统中 heap 和 stack 的区别 什么是基于注解的切面实现 什么是 对象/关系 映射集成模块 什么是 Java 的反射机制 什么是 ACID BS与CS的联系与区别 Cookie ...
- POJ 3169 Layout (spfa+差分约束)
题目链接:http://poj.org/problem?id=3169 题目大意:n头牛,按编号1~n从左往右排列,可以多头牛站在同一个点,给出ml行条件,每行三个数a b c表示dis[b]-dis ...
- LightOJ - 1179 Josephus Problem(约瑟夫环)
题目链接:https://vjudge.net/contest/28079#problem/G 题目大意:约瑟夫环问题,给你n和k(分别代表总人数和每次要数到k),求最后一个人的位置. 解题思路:因为 ...
- plt-3D打印1
plt-3D打印 import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D ...
- 【转】doxygen+graphviz生成工程中的类继承树及函数调用图
转自----hequn8128 在阅读代码量比较多的项目时,类的继承树和函数调用图能够直观地向我们显示类之间或者函数之间的各种关系,方便我们了解程序的整体框架,很多时候可以起到事半功倍的作用.这里尝试 ...
- IOS-优质应用推荐
壁纸应用 cuto 免费 点击下载 shots 收费 点击下载 Cutisan 锁屏壁纸制作下载地址 待办事项 TodayMind - 提醒事项触手可及 点击下载 滴答清单 点击下载 Microsof ...
- CTF中的EXP编写技巧 zio库的使用
zio库没有提供文档 这个是官方给出的一个例子程序 from zio import * io = zio('./buggy-server') # io = zio((pwn.server, 1337) ...