一个完整的Core Data应用
在这篇文章中,我们将建立一个小型但却全面支持Core Data的应用。应用允许你创建嵌套的列表;每个列表的item都可以有子列表,这将允许你创建非常深层次的item。为了让大家完整的了解发生了什么,我们将通过使用手动创建堆栈的方式来代替Xcode中Core Data的模板。这个应用的代码放到了GitHub上。
我们将怎么建立?
首先,我们创建一个PersistentStack对象,为其提供一个Core Data模型和一个文件名,PersistentStack会返回一个managed object context。然后我们将要创建我们的Core Data模型。接着,我们将创建一个简单的table view controller来显示使用fetched results controller取回的item根目录,并且通过增加items, sub-items的导航,删除items,增加undo支持,一步一步增加交互。
设置堆栈
我们将为主队列创建一个managed object context,在旧代码中,你可能见到[[NSManagedObjectContext alloc] init]。而目前,你应该用initWithConcurrencyType: 初始化,明确你是使用基于队列的并发模型。
1
2
3
4
5
6
7
8
9
10
11
|
- ( void )setupManagedObjectContext
{
self .managedObjectContext = [[ NSManagedObjectContext alloc]initWithConcurrencyType: NSMainQueueConcurrencyType ];
self .managedObjectContext.persistentStoreCoordinator = [[ NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: self .managedObjectModel];
NSError * error;
[ self .managedObjectContext.persistentStoreCoordinator addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: self .storeURL options: nil error:&error];
if (error) {
NSLog ( @"error: %@" , error);
}
self .managedObjectContext.undoManager = [[ NSUndoManager alloc] init];
}
|
检查错误是非常重要的,因为在开发过程中,这很有可能经常出错。当Core Data发现你改变了数据模型时,就会暂停操作。你也可以通过设置选项来告诉Core Data在遇到这种情况后怎么做。这个在Martin关于migrations的文章中彻底的解释了。注意,最后一行增加了一个undomanager;我们将在稍后用到。在iOS中,你需要明确的去增加一个undo manager,但是在Mac中,undo manager是默认有的。
这段代码建立了一个真正简单的Core Data堆栈:一个拥有持久化存储协调器的managed object context,拥有一个持久化存储的持久化存储协调器。更复杂的设置都是可能的;最常见的是拥有多个managed object context(每一个都在单独的队列中)。
创建一个模型
创建模型比较简单,我们只需要增加一个新文件到我们的项目,在Core Data选项中选择Data Model template。这个模型文件将会被编译成后缀名为.momd类型的文件,我们将会在运行时加载这个文件来为持久化存储创建需要用的NSManagedObjectModel,模型的源码是简单的XML,根据我们的经验,当你check到代码控制器中时,不会有任何合并冲突。如果你愿意,你还可以在代码中创建一个managed object model。
一旦你创建了模型,你就可以增加Item实体,这个实体有两个属性:字符串类型的title和integer类型的order。然后,增加两个关系:parent,关联一个item到它的父item,children,一个一对多的子关系。设置它们为彼此相反的关系,也就是说,你设置a的parent为b,那么b就会自动有一个children为a。
通常,你甚至可以完全抛开order属性,使用排序好的的关系。然而,他们并不能很好的和fetched results controllers(后面会用到)集成在一起工作。我们要么需要重新实现fetched results controller的一部分,要么重新实现排序,通常我们都会选择后者。
现在,从菜单中选择Editor > NSManagedObject,创建一个绑定到NSManagedObject实体的子类,这将会创建两个文件:Item.h和Item.m。在头文件中,会有一个额外的类,我们需要将其删除(这是遗留原因导致的)。
创建一个Store类
对于我们的模型,我们将创建一个根节点作为我们item树的开始。我们需要一个地方来创建这个根节点,并且方便以后容易找到。因此,我们可以通过创建一个简单的存储类来达到这个目的。存储类有一个managed object context,还有一个rootItem方法。在app delegate中,我们将会在运行时查找这个root item,并且传给了root view controller。作为一种优化,为了查找这个item变得更快,你可以将item对象的id存储到user default中:
1
2
3
4
5
6
7
8
9
10
|
- (Item*)rootItem {
NSFetchRequest * request = [ NSFetchRequest fetchRequestWithEntityName: @"Item" ];
request.predicate = [ NSPredicate predicateWithFormat: @"parent = %@" , nil ];
NSArray * objects = [ self .managedObjectContext executeFetchRequest:request error: NULL ];
Item* rootItem = [objects lastObject];
if (rootItem == nil ) {
rootItem = [Item insertItemWithTitle: nil parent: nil inManagedObjectContext: self .managedObjectContext];
}
return rootItem;
}
|
增加一个item大多数情况下都是简单的。然而,我们需要设置order属性比任何已经存在的item及其父类更大。我们将会设置第一个子节点的order为0,随后每一个子节点都会增加1.我们在Item类中创建一个自定义的方法来存放逻辑:
1
2
3
4
5
6
7
8
|
+ (instancetype)insertItemWithTitle:( NSString *)title parent:(Item*)parent inManagedObjectContext:( NSManagedObjectContext *)managedObjectContext {
NSUInteger order = parent.numberOfChildren;
Item* item = [ NSEntityDescription insertNewObjectForEntityForName: self .entityName inManagedObjectContext:managedObjectContext];
item.title = title;
item.parent = parent;
item.order = @(order);
return item;
}
|
获得子节点数量的方法很简单:
1
2
3
|
- ( NSUInteger )numberOfChildren {
return self .children.count;
}
|
为了支持自动更新我们的table view,我们需要使用fetched results controller。Fetched results controller是一个可以管理取出大量item请求的对象,同时对table view来说,也是一个完美的Core Data指南,在下一节中我们将会看到:
1
2
3
4
5
|
- ( NSFetchedResultsController *)childrenFetchedResultsController {
NSFetchRequest * request = [ NSFetchRequest fetchRequestWithEntityName:[ self . class entityName]]; request.predicate = [ NSPredicate predicateWithFormat: @"parent = %@" , self ];
request.sortDescriptors = @[[ NSSortDescriptor sortDescriptorWithKey: @"order" ascending: YES ]];
return [[ NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext: self .managedObjectContext sectionNameKeyPath: nil cacheName: nil ];
}
|
增加一个支持Table-View的Fetched Results Controller
我们下一步是创建一个root view controller:一个从NSFetchedResultsController读取数据的table view。Fetched results controller管理你的读取请求,如果你为它分配一个delegate,那么在managed object context中发生的任何改变都会通知你。实际上,这意味着如果你实现了delegate方法,当数据模型中发生相关变化时,你可以自动更新你的table view。比如,你在后台线程同步,并且把变化存储到数据库中,那么你的table view将会自动更新。
创建Table View的Data Source
在lighter view controllers这篇文章中,我们演示了怎么从table view中分离出data source。这里,我们将会用同样的方法创建一个fetched results controller。我们创建一个分离出的FetchedResultsControllerDataSource
类,它扮演了table view的data source,通过监听fetched results controller,自动更新table view。
我们初始化一个table view对象,初始化方法如下:
1
2
3
4
5
6
7
8
|
- ( id )initWithTableView:(UITableView*)tableView {
self = [ super init];
if ( self ) {
self .tableView = tableView;
self .tableView.dataSource = self ;
}
return self ;
}
|
当我们设置fetch results controller时,我们需要自己设置delegate,并且自己执行fetch的初始化。performFetch:方法经常容易被忘了调用,那么你将得不到结果(并且不会出错):
1
2
3
4
5
|
- ( void )setFetchedResultsController:( NSFetchedResultsController *)fetchedResultsController{
_fetchedResultsController = fetchedResultsController;
fetchedResultsController.delegate = self ;
[fetchedResultsController performFetch: NULL ];
}
|
因为我们的类实现了UITableViewDataSource协议,我们需要实现相关的方法。在这两个方法中,我们只需要向fetched results controller请求需要的信息:
1
2
3
4
5
6
7
|
- ( NSInteger )numberOfSectionsInTableView:(UITableView*)tableView {
return self .fetchedResultsController.sections.count;
}
- ( NSInteger )tableView:(UITableView*)tableView numberOfRowsInSection:( NSInteger )sectionIndex {
id < NSFetchedResultsSectionInfo > section = self .fetchedResultsController.sections[sectionIndex];
return section.numberOfObjects;
}
|
然而,当我们需要创建cell的时候,只需要一些简单的步骤:向fetched results controller请求正确的对象,从table view出列一个cell,然后告诉delegate用相应的对象配置这个cell。作为view controller,只会关心用模型对象更新cell:
1
2
3
4
5
6
|
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:( NSIndexPath *)indexPath{
id object = [ self .fetchedResultsController objectAtIndexPath:indexPath];
id cell = [tableView dequeueReusableCellWithIdentifier: self .reuseIdentifier forIndexPath:indexPath];
[ self .delegate configureCell:cell withObject:object];
return cell;
}
|
创建table view controller
现在,我们可以创建一个view controller,使用刚刚创建的类显示item。在示例程序中,我们创建一个Storyboard,并且增加一个拥有table view controller的navigation controller。这会自动设置view controller作为数据源,而这不是我们想要的效果。因此,在我们的viewDidLoad中,我们做下面的操作:
1
2
|
fetchedResultsControllerDataSource = [[FetchedResultsControllerDataSource alloc] initWithTableView: self .tableView];
self .fetchedResultsControllerDataSource.fetchedResultsController = self .parent.childrenFetchedResultsController; fetchedResultsControllerDataSource.delegate = self ; fetchedResultsControllerDataSource.reuseIdentifier = @"Cell" ;
|
在初始化fetched results controller data source时,table view的数据源可以被设置。reuse标识符匹配在Storyboard中相对应的对象。现在,我们需要实现delegate方法:
1
2
3
4
5
|
- ( void )configureCell:( id )theCell withObject:( id )object {
UITableViewCell* cell = theCell;
Item* item = object;
cell.textLabel.text = item.title;
}
|
当然,除了设置text的label外,你还可以做更多的事情,但是你应该明白了要领。现在我们已经为显示数据准备好了相当多的事情,但是却仍然没有增加数据的方法,这看起来有点空。
增加互动
我们将会增加两种和数据交互的方法。首先,我们需要实现增加items。然后我们需要实现fetched results controller的delegate方法去更新table view,并且增加删除和undo支持。
增加items
为了增加items,我们借鉴 Clear 的交互设计,这是我认为最漂亮的程序之一。我们增加一个text field作为table view的头,并修改table view的content inset,确保它默认保持隐藏,正如Joe在 scroll view article 这篇文章中解释一样。像往常一样,所有的代码都在github上,这里是插入item相关的代码,在textFieldShouldReturn中:
1
2
3
|
[Item insertItemWithTitle:title parent: self .parent inManagedObjectContext: self .parent.managedObjectContext];
textField.text = @"" ;
[textField resignFirstResponder];
|
监听改变
下一步是确保table view会为新创建的item插入一行。有好几种方法可以做到,但是我们将会使用fetched results controller的代理方法:
1
2
3
4
5
|
- ( void )controller:( NSFetchedResultsController *)controller didChangeObject:( id )anObject atIndexPath:( NSIndexPath *)indexPath forChangeType:( NSFetchedResultsChangeType )type newIndexPath:( NSIndexPath *)newIndexPath {
if (type == NSFetchedResultsChangeInsert ) {
[ self .tableView insertRowsAtIndexPaths:@[newIndexPath]withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
|
fetched results controller也会为删除、改变和移动调用一些方法(我们将在稍后实现)。如果你一次有很多改变,你可以多实现两个方法,那么table view将会同步的展现那些改变。对于单个item的插入和删除,这并不会有任何不同,但是如果你选择实现同时同步,那么将会变得更漂亮:
1
2
3
4
5
6
|
- ( void )controllerWillChangeContent:( NSFetchedResultsController *)controller {
[ self .tableView beginUpdates];
}
- ( void )controllerDidChangeContent:( NSFetchedResultsController *)controller {
[ self .tableView endUpdates];
}
|
使用Collection View
值得注意的是,fetched results controllers并非只能用于table view;你可以将它只用在任何view中。因为,他们都是index-path-based,同时它也能和collection views很好的一起工作,但是collection view并没有beginUpdates和endUpdates方法,却有一个performBatchUpdates。为了处理这种情况,你可以收集你得到的所有更新,然后在controllerDidChangeContent中,用block执行所有的更新。Ash Furrow写了一个关于 如何做的例子。
实现你自己的Fetched Results Controller
你不必使用NSFetchedResultsController。实际上,在很多情况下,为你的程序创建一个类似的类将显得更有意义。你可以做的是订阅NSManagedObjectContextObjectsDidChangeNotification(即注册监听通知)。然后你就可以得到一个notification,userInfo字典将会包含改变对象,插入对象,删除对象的列表,然后你可以按你喜欢的方式执行这些操作。
现在我们可以增加并且列出itmes了,现在我们需要确定能够创建sub-lists.在Storyboard中,你可以通过拖拽一个cell到view controller中来创建一个segue。这需要为segue指定一个名字,这样,如果一个view controller中有多个segues的话,我们就可以将其区分开了。
我处理segues的模式看起来像这样:首先,你尝试识别出这个segue,对于每一个segue,你为它的目标view controller单独写一个方法:
1
2
3
4
5
6
7
8
9
10
|
- ( void )prepareForSegue:(UIStoryboardSegue*)segue sender:( id )sender {
[ super prepareForSegue:segue sender:sender];
if ([segue.identifier isEqualToString:selectItemSegue]) {
[ self presentSubItemViewController:segue.destinationViewController];
}
}
- ( void )presentSubItemViewController:(ItemViewController*)subItemViewController {
Item* item = [ self .fetchedResultsControllerDataSource selectedItem];
subItemViewController.parent = item;
}
|
子view controller需要唯一的东西就是item。通过item,也可以得到managed object context。我们从data source中得到选中的item(通过table view选中item的index值,从fetched results controller中取出正确的item),就这么简单。
很不幸的是,在app delegate中,将managed object context作为一个属性,然后总是在任何地方访问它,这是很常见的一种模式,但这是一个坏主意,如果你想要为你view controller中的一部分使用一个不同的managed object context,这时,将很难重构,此外,你的代码将变得很难测试。
现在,尝试在sub-list中增加一个item,你很有可能得到一个crash。这是因为我们现在有两个fetched results controllers,一个是topmost view controller,还有一个是root view controller。后者尝试去更新它的table view,而它的table view是离屏的(offscreen),就这样所有的操作都crash了。解决方案是告诉我们的data source停止监听fetched results controller的代理方法:
1
2
3
4
5
6
7
8
|
- ( void )viewWillAppear:( BOOL )animated {
[ super viewWillAppear:animated];
self .fetchedResultsControllerDataSource.paused = NO ;
}
- ( void )viewWillDisappear:( BOOL )animated {
[ super viewWillDisappear:animated];
self .fetchedResultsControllerDataSource.paused = YES ;
}
|
一种方法就是在data source中设置fetched results controller的代理为nil,这样就再也不会收到更新通知了。当我们离开paused状态时,还需要加上去:
1
2
3
4
5
6
7
8
9
10
|
- ( void )setPaused:( BOOL )paused {
_paused = paused;
if (paused) {
self .fetchedResultsController.delegate = nil ;
} else {
self .fetchedResultsController.delegate = self ;
[ self .fetchedResultsController performFetch: NULL ];
[ self .tableView reloadData];
}
}
|
这样performFetch就会确保你的data source保持最新的。当然,更好的实现方法并不是设置代理为nil,而是保证每一个改变都是在paused状态下进行的,相应的,离开paused状态后,更新table view。
删除
为了支持删除,我们需要花费几步操作。首先,我们需要确信我们的table view支持删除,第二,我们需要从core data中删除对象,并且保证我们的排序是正确的。
为了支持滑动删除,我们需要在data source中实现两个方法:
1
2
3
4
5
6
7
8
9
|
- ( BOOL )tableView:(UITableView*)tableView canEditRowAtIndexPath:( NSIndexPath *)indexPath {
return YES ;
}
- ( void )tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:( NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
id object = [ self .fetchedResultsController objectAtIndexPath:indexPath];
[ self .delegate deleteObject:object];
}
}
|
我们需要通知代理删除对象,而不是立即删除。这样,我们不需要将store object分配给data source(data source在整个项目中都必须可重用),并且保持自定义操作的灵活性。view controller只需在managed object context中简单的调用deleteObject:。
然而,还有两个重要的问题需要被解决:我们怎么处理被删除item的子item,怎么强制我们的order变化?幸运的是,传播删除是很简单的:在我们的数据模型中我们可以选择Cascade作为子关系的删除规则。
为了强制我们的order变化,我们可以重写prepareForDeletion方法,用更高一层的order更新所有兄弟姐妹。
1
2
3
4
5
6
7
8
|
- ( void )prepareForDeletion {
NSSet * siblings = self .parent.children;
NSPredicate * predicate = [ NSPredicate predicateWithFormat: @"order > %@" , self .order];
NSSet * siblingsAfterSelf = [siblings filteredSetUsingPredicate:predicate];
[siblingsAfterSelf enumerateObjectsUsingBlock:^(Item* sibling, BOOL * stop){
sibling.order = @(sibling.order.integerValue - 1);
}];
}
|
现在我们几乎快完成了。我们可以和table view的cell交互,并且可以删除模型对象。最后一步是实现一旦模型对象被删除后,删除table view cell的代码。在我们data sources的控制器中:didChangeObject:….方法,我们增加另一个if分句:
1
2
3
|
else if (type == NSFetchedResultsChangeDelete ) {
[ self .tableView deleteRowsAtIndexPaths:@[indexPath]withRowAnimation:UITableViewRowAnimationAutomatic]; }
|
增加Undo支持
Core Data优点之一就是集成了undo支持。我们将为undo增加抖动的功能,第一步就是告诉程序,我们可以这样做:
1
|
application.applicationSupportsShakeToEdit = YES ;
|
现在,这个功能可以被任何摇动触发,程序将会为它的undo manager请求第一响应器,并且执行一次undo操作。在上个月的文章中,我们了解了,一个view controller也在响应链中(responder chain),这也正是我们将要使用的。在我们的view controller中,我们重写来自UIResponder类中的两个方法:
1
2
3
4
5
6
|
- ( BOOL )canBecomeFirstResponder {
return YES ;
}
- ( NSUndoManager *)undoManager {
return self .managedObjectContext.undoManager;
}
|
现在,当一个抖动发生时,managed object context的undo 管理器将会得到一个undo消息,并且撤销最后一次改变。记住,在iOS中,managed object context默认并没有一个undo manager,(Mac中,新建的managed object context默认是有的),所以我们需要在持久化堆栈中设置:
1
|
self .managedObjectContext.undoManager = [[ NSUndoManager alloc] init];
|
基本上就是这样了。现在,当你抖动时,你将得到iOS默认有两个按钮的提醒框:一个是undo按钮,一个cancel按钮。Core Data一个非常好的特性是将改变自动分组。比如,addItem:父节点将会记录这个操作,作为一个undo处理。关于删除,也是一样。
为了让用户管理undo操作更容易一些,我们可以给操作命名,并且将textFieldShouldReturn:的第一行修改成这样:
1
2
3
4
|
NSString * title = textField.text;
NSString * actionName = [ NSString stringWithFormat: NSLocalizedString ( @"add item \"%@\"" , @"Undo action name of add item" ), title];
[ self .undoManager setActionName:actionName];
[ self .store addItem:title parent: nil ];
|
现在,当用户抖动时,除了普通的“Undo”标签外,Ta将得到更多的上下文环境。
编辑
编辑目前在示例程序中并不支持,但是这只是一个改变对象属性的问题。比如,改变一个item的title,只需要设置title属性就好了。改变foo item的parent,只需要设置parent属性为一个新值bar,所有的东西都将得到更新:bar现在有一个children为foo,因为我们使用fetched results controllers,用户界面同样也会自动更新。
重新排序
重新排序cell,在现有程序中也是不可行的,但是这实现起来很简单。但是,还有一个需要注意的地方:如果你允许用户重新排序,你将需要在model中更新order属性,并且从fetched results controller得到一个delegate call(你需要忽略这个调用,因为cell已经被移动了)。这在NSFetchedResultsControllerDelegate documentation中解释了。
保存
保存和在managed object context中调用保存一样简单。因为我们并不直接访问managed object context,而是在store中保存。唯一的困难是什么时候去保存。Apple的示例代码在applicationWillTerminate:中执行这个操作,但是这取决于你使用情况,这也有可能在applicationDidEnterBackground:中,甚至当你程序运行时调用。
讨论
在写这篇文章和示例程序时,我犯了一个处级错误:我选择了一个不为空的根item,取而代之,让所有用户创建的item在根目录级别有一个指针为空的父item。这将造成很多问题:因为view controller中的父item可能是nil,我们需要将store传给每一个子view controller。同样的,强制order重新排序也非常困难,因为我们需要查找出所有item的兄弟姐妹,这样会迫使Core Data到磁盘上读取数据。不幸的是,当写这些代码时,问题并没有立刻弄明白,一些问题只是在写测试时才变得清晰。当我重新写代码的时候,我知道了将Store类中大部分代码移到Item类中,就这样,事情变得清楚多了。
原文链接:http://answerhuang.duapp.com/index.php/2013/09/17/一个完整的core-data应用/
转载于:https://www.cnblogs.com/yingkong1987/p/3325913.html
一个完整的Core Data应用相关推荐
- Core Data 教程入门
原文:Getting Started with Core Data Tutorial 作者:Pietro Rea 译者:kmyhy 这是<Core Data by Turoials>一书的 ...
- 手把手教你从Core Data迁移到Realm
来源:一缕殇流化隐半边冰霜 (@halfrost ) 链接:http://www.jianshu.com/p/d79b2b1bfa72 前言 看了这篇文章的标题,也许有些人还不知道Realm是什么,那 ...
- 深入理解Core Data
留给我这忘事精看 Core Data 是什么? 大概八年前,2005年的四月份,Apple 公布了 OS X 10.4,正是在这个版本号中 Core Data 框架公布了.那个时候 YouTube 也 ...
- Core Data概述
昨晚熬夜看发布会(本以为屌丝终于能买得起苹果了,谁知道...),因为看不了视频直播,所以就正好有空就把www.objc.io最新的一篇文章翻译了一下,同时感谢CocoaChina翻译组提供校对,以下为 ...
- Core Data 编程指南
一.技术概览 1. Core Data 功能初窥 对于处理诸如对象生命周期管理.对象图管理等日常任务,Core Data框架提供了广泛且自动化的解决方案.它有以下特性. (注:对象图-Object g ...
- Swift 3.0 使用Core Data
swift版本:3.0 Xcode版本:8.0 iOS版本:10.0 自iOS10 和swift3.0 之后,苹果的访问CoreData的方法发生了很大改变,简洁了许多,下面的内容是从0开始建立一个e ...
- core data使用教程
core data使用教程 从印象中记得还是在学校的时候老师讲过的时候用过,那时觉得好难,以至于工作2年多了一直没敢去看core data,前几天想了下,不去看不行,得都会用才行,于是那天6点下班后就 ...
- [OHIF-Viewers]医疗数字阅片-医学影像-cornerstone-core-Cornerstone.js提供了一个完整的基于Web的医学成像平台。...
[OHIF-Viewers]医疗数字阅片-医学影像-cornerstone-core-Cornerstone.js提供了一个完整的基于Web的医学成像平台. 还必须写中文,不然不让同步,蛋疼呀--- ...
- Core Data 教程(2): 如何预载/导入已有的数据
这是系列教程的第二部分,有助于你加快掌握基本的Core Data内容. 在系列教程一中,我们为对象建立了可视化数据模型,运行了快速肮脏测试并勾在一个表视图(table view)中来显示.而在这个教程 ...
最新文章
- Meta AI 新研究,统一模态的自监督新里程碑
- non-strictly-monotonic PTS
- Weblogic 12c中修改SERVER NAME的方法
- 【机器视觉】 HDevelop语言基础(二)-变量和表达式
- rmmod 提示 No such file or directory
- 现代软件工程 第十七章 【人、绩效和职业道德】 练习与讨论
- 磁盘IO性能监控(Linux 和 Windows)
- 也谈“避免使用虚函数作为库的接口”
- 关于java方向的思考
- python管道怎么使用_Python – 如何使用管道执行shell命令?
- 静态HTML网页设计作品 DIV布局 HTML5+CSS大作业——个人网页(6页) 网页制作期末大作业成品
- angr符号执行用例解析——0ctf_momo_3
- openwrt安装docker并启动
- 让程序跳转到指定地址执行(绝对地址赋值/强转)
- Kubernetes监控体系(12)-alertmanager配置钉钉和邮件告警
- java过滤xss_java处理XSS过滤的方法
- 多线程爬取免费代理ip池 (给我爬)
- GO语言开山篇(二):诞生小故事
- 资本市场低迷:快狗打车上市首日跌22% 市值仍超百亿港元
- 985高校吐槽大会……
热门文章
- OpenCvSharp 形态学操作(膨胀、腐蚀)
- linux 查看java进程_Linux进程查看及管理工具(ps, vmstat, dstat, glances等)
- 大专学java还是python_零基础应该选择学习 java、php、前端 还是 python?
- 安全研究 | Jenkins 任意文件读取漏洞分析
- 记MAVEN技巧 用maven坐标从公司nexus私有库上获取所在存储位置
- 区分JavaScript中slice与splice方法
- Spring源码系列:BeanDefinition源码解析
- python-冒泡排序
- 你还记得当初为什么进入IT行业吗?
- DedeCMS实现自定义表单提交后发送指定QQ邮箱法