• 开源库UITableView+FDTemplateLayoutCell学习


    摘自:优化UITableViewCell高度计算Swift版优化UITableViewCell高度计算的那些事

    本文带大家详细探索那篇文章所对应的库(1.2版),这个库就是利用缓存tableviewcell的高度提高滑动的流畅性。

    主要是利用Runloop在空闲状态时,后台计算tableviewcell的高度并缓存起来。然后在使用的时候就直接从缓存中去,这里都放在一个数组里存在内存。

    对Runloop以及几个mode不懂的可以看sunnyxx blog中的视频 视频可戳 , 文章的话可以看看 深入理解RunLoop、 【iOS程序启动与运转】- RunLoop个人小结

    其实就是在kCFRunLoopDefaultMode模式下BeforWaitting状态去执行计算的。

    下面来探究源码。首先在UITableView+FDTemplateLayoutCell 下载源码,下载1.2版本。

    然后你得到的库就只有两个文件:

    .m文件大概只有500行代码。

    下面看下作者的视线思路:

    1.  创建了一个_FDTemplateLayoutCellHeightCache类,就是管理Cache的一个类,里面有两个属性四个方法。

    属性:

    • sections 这个变量就是用来存储缓存的height的一个二维数组。(因为tableview有section和row组成所以必须二维)

    • _FDTemplateLayoutCellHeightCacheAbsentValue 这个是一个静态常量,就是用来标记没有缓存高度的row 。

    方法:

    • buildHeightCachesAtIndexPathsIfNeeded:indexPaths
      这个方法传入indexPaths数组来给sections中还没有初始化的元素进行初始化
    • hasCachedHeightAtIndexPath:indexPath 根据下标索引判断是否有缓存(其实就是判断是否等于上面那个静态常量)
    • cacheHeight:height:byIndexPath 根据indexPath给sections赋值。
    • cachedHeightAtIndexPath:indexPath 根据indexPath取值

    这个类主要是操作和存储缓存的。这个类的代码如下:

    @interface _FDTemplateLayoutCellHeightCache : NSObject
    @property (nonatomic, strong) NSMutableArray *sections;  
    @end
    
    static CGFloat const _FDTemplateLayoutCellHeightCacheAbsentValue = -1;
    
    @implementation _FDTemplateLayoutCellHeightCache
    
    - (void)buildHeightCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths {
        if (indexPaths.count == 0) {
            return;
        }
        
        if (!self.sections) {
            self.sections = @[].mutableCopy;
        }
        
        // Build every section array or row array which is smaller than given index path.
        [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
            for (NSInteger section = 0; section <= indexPath.section; ++section) {
                if (section >= self.sections.count) {
                    self.sections[section] = @[].mutableCopy;
                }
            }
            NSMutableArray *rows = self.sections[indexPath.section];
            for (NSInteger row = 0; row <= indexPath.row; ++row) {
                if (row >= rows.count) {
                    rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
                }
            }
        }];
    }
    
    - (BOOL)hasCachedHeightAtIndexPath:(NSIndexPath *)indexPath
    {
        [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
        NSNumber *cachedNumber = self.sections[indexPath.section][indexPath.row];
        return ![cachedNumber isEqualToNumber:@(_FDTemplateLayoutCellHeightCacheAbsentValue)];
    }
    
    - (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath
    {
        [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
        self.sections[indexPath.section][indexPath.row] = @(height);
    }
    
    - (CGFloat)cachedHeightAtIndexPath:(NSIndexPath *)indexPath
    {
        [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
    #if CGFLOAT_IS_DOUBLE
        return [self.sections[indexPath.section][indexPath.row] doubleValue];
    #else
        return [self.sections[indexPath.section][indexPath.row] floatValue];
    #endif
    }
    
    @end

    2. 接下来是UITableView的一个扩展UITableView + FDTemplateLayoutCellPrivate

    • 第一个方法fd_templateCellForReuseIdentifier:identifier,这个方法主要是通过你传入的一个identifier(就是复用的id)获取cell。

        第一句是这样的 NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);

        OC中的 _cmd 代表的就是本方法,objc_getAssociatedObject 获取一个关联对象的属性。

    • 接下来提供了一个方法来获取管理Cache的_FDTemplateLayoutCellHeightCache的对象fd_cellHeightCache。
    • 属性:fd_autoCacheInvalidationEnabled 记录是否自动缓存高度

    • 属性:fd_precacheEnabled

    这是一个私有类,下面给出这个类的完整代码:

    @interface UITableView (FDTemplateLayoutCellPrivate)
    
    /// Returns a template cell created by reuse identifier, it has to be registered to table view.
    /// Lazy getter, and associated to table view.
    - (id)fd_templateCellForReuseIdentifier:(NSString *)identifier;
    
    /// A private height cache data structure.
    @property (nonatomic, strong, readonly) _FDTemplateLayoutCellHeightCache *fd_cellHeightCache;
    
    /// This is a private switch that I don't think caller should concern.
    /// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration".
    @property (nonatomic, assign) BOOL fd_autoCacheInvalidationEnabled;
    
    /// It helps to improve scroll performance by "pre-cache" height of cells that have not
    /// been displayed on screen. These calculation tasks are collected and performed only
    /// when "RunLoop" is in "idle" time.
    ///
    /// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration".
    @property (nonatomic, assign) BOOL fd_precacheEnabled;
    
    /// Debug log controlled by "fd_debugLogEnabled".
    - (void)fd_debugLog:(NSString *)message;
    
    @end
    
    @implementation UITableView (FDTemplateLayoutCellPrivate)
    
    - (id)fd_templateCellForReuseIdentifier:(NSString *)identifier
    {
        NSAssert(identifier.length > 0, @"Expects a valid identifier - %@", identifier);
        
        NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
        if (!templateCellsByIdentifiers) {
            templateCellsByIdentifiers = @{}.mutableCopy;
            objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        
        UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
        
        if (!templateCell) {
            templateCell = [self dequeueReusableCellWithIdentifier:identifier];
            NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
            templateCell.fd_isTemplateLayoutCell = YES;
            templateCellsByIdentifiers[identifier] = templateCell;
            [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
        }
        
        return templateCell;
    }
    
    - (_FDTemplateLayoutCellHeightCache *)fd_cellHeightCache {
        _FDTemplateLayoutCellHeightCache *cache = objc_getAssociatedObject(self, _cmd);
        if (!cache) {
            cache = [_FDTemplateLayoutCellHeightCache new];
            objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN);
        }
        return cache;
    }
    
    - (BOOL)fd_autoCacheInvalidationEnabled
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    
    - (void)setFd_autoCacheInvalidationEnabled:(BOOL)enabled
    {
        objc_setAssociatedObject(self, @selector(fd_autoCacheInvalidationEnabled), @(enabled), OBJC_ASSOCIATION_RETAIN);
    }
    
    - (BOOL)fd_precacheEnabled
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    
    - (void)setFd_precacheEnabled:(BOOL)precacheEnabled
    {
        objc_setAssociatedObject(self, @selector(fd_precacheEnabled), @(precacheEnabled), OBJC_ASSOCIATION_RETAIN);
    }
    
    - (void)fd_debugLog:(NSString *)message
    {
        if (!self.fd_debugLogEnabled) {
            return;
        }
        NSLog(@"** FDTemplateLayoutCell ** %@", message);
    }
    
    @end

    3. 下面又是一个分类,(这个是重点计算高度,调用缓存管理方法的分类)UITableView + FDTemplateLayoutCellPrecache

    这个里面的方法在他blog中也有提到就是在NSDefaultRunLoopMode下当状态将要进入休眠的时候把计算方法分解成多个RunLoop Source任务(source0) 

    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

    这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件.

    主要逻辑就是先通过遍历所有section和row找到还没有缓存的row,然后加入到待缓存数组 ,创建一个observer去监听Runloop的状态 ,如果空闲了去创建source0任务,执行计算方法并缓存起来。如果预缓存任务完成了就把监听的Observer移除了。

    下面给出这个类的代码:

    @implementation UITableView (FDTemplateLayoutCellPrecache)
    
    - (void)fd_precacheIfNeeded
    {
        if (!self.fd_precacheEnabled) {
            return;
        }
        
        // Delegate could use "rowHeight" rather than implements this method.
        if (![self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
            return;
        }
        
        CFRunLoopRef runLoop = CFRunLoopGetCurrent();
        
        // This is a idle mode of RunLoop, when UIScrollView scrolls, it jumps into "UITrackingRunLoopMode"
        // and won't perform any cache task to keep a smooth scroll.
        CFStringRef runLoopMode = kCFRunLoopDefaultMode;
        
        // Collect all index paths to be precached.
        NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
        
        // Setup a observer to get a perfect moment for precaching tasks.
        // We use a "kCFRunLoopBeforeWaiting" state to keep RunLoop has done everything and about to sleep
        // (mach_msg_trap), when all tasks finish, it will remove itself.
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
        (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
            // Remove observer when all precache tasks are done.
            if (mutableIndexPathsToBePrecached.count == 0) {
                CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
                return;
            }
            // Pop first index path record as this RunLoop iteration's task.
            NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
            [mutableIndexPathsToBePrecached removeObject:indexPath];
            
            // This method creates a "source 0" task in "idle" mode of RunLoop, and will be
            // performed in a future RunLoop iteration only when user is not scrolling.
            [self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
                         onThread:[NSThread mainThread]
                       withObject:indexPath
                    waitUntilDone:NO
                            modes:@[NSDefaultRunLoopMode]];
        });
        
        CFRunLoopAddObserver(runLoop, observer, runLoopMode);
    }
    
    - (void)fd_precacheIndexPathIfNeeded:(NSIndexPath *)indexPath
    {
        if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
            CGFloat height = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
            [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath];
            [self fd_debugLog:[NSString stringWithFormat:
                               @"precached - [%@:%@] %@",
                               @(indexPath.section),
                               @(indexPath.row),
                               @(height)]];
        }
    }
    
    - (NSArray *)fd_allIndexPathsToBePrecached
    {
        NSMutableArray *allIndexPaths = @[].mutableCopy;
        for (NSInteger section = 0; section < [self numberOfSections]; ++section) {
            for (NSInteger row = 0; row < [self numberOfRowsInSection:section]; ++row) {
                NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
                if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
                    [allIndexPaths addObject:indexPath];
                }
            }
        }
        return allIndexPaths.copy;
    }
    
    @end

    4. 下面又是一个分类UITableView + FDTemplateLayoutCellAutomaticallyCacheInvalidation 

      因为我们会有一些操作导致cell的改变,所以这里作者要保证在每次cell改变的时候把sections数组改掉,然后如果新增或者修改了 需要重新计算高度。用到了methodSwizzle 黑魔法。这里作者把swizzle放在了UITableView的load类方法中。需要使用methodSwizzle的方法有:

      SEL selectors[] = {
            @selector(reloadData),
            @selector(insertSections:withRowAnimation:),
            @selector(deleteSections:withRowAnimation:),
            @selector(reloadSections:withRowAnimation:),
            @selector(moveSection:toSection:),
            @selector(insertRowsAtIndexPaths:withRowAnimation:),
            @selector(deleteRowsAtIndexPaths:withRowAnimation:),
            @selector(reloadRowsAtIndexPaths:withRowAnimation:),
            @selector(moveRowAtIndexPath:toIndexPath:)
        };

    这个类的代码:

    @implementation UITableView (FDTemplateLayoutCellAutomaticallyCacheInvalidation)
    
    + (void)load
    {
        // All methods that trigger height cache's invalidation
        SEL selectors[] = {
            @selector(reloadData),
            @selector(insertSections:withRowAnimation:),
            @selector(deleteSections:withRowAnimation:),
            @selector(reloadSections:withRowAnimation:),
            @selector(moveSection:toSection:),
            @selector(insertRowsAtIndexPaths:withRowAnimation:),
            @selector(deleteRowsAtIndexPaths:withRowAnimation:),
            @selector(reloadRowsAtIndexPaths:withRowAnimation:),
            @selector(moveRowAtIndexPath:toIndexPath:)
        };
        
        for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
            SEL originalSelector = selectors[index];
            SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
            
            Method originalMethod = class_getInstanceMethod(self, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    - (void)fd_reloadData
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [self.fd_cellHeightCache.sections removeAllObjects];
        }
        [self fd_reloadData]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
                [self.fd_cellHeightCache.sections insertObject:@[].mutableCopy atIndex:idx];
            }];
        }
        [self fd_insertSections:sections withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
                [self.fd_cellHeightCache.sections removeObjectAtIndex:idx];
            }];
        }
        [self fd_deleteSections:sections withRowAnimation:animation]; // Primary call
    }
    
    - (void)fd_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [sections enumerateIndexesUsingBlock: ^(NSUInteger idx, BOOL *stop) {
                NSMutableArray *rows = self.fd_cellHeightCache.sections[idx];
                for (NSInteger row = 0; row < rows.count; ++row) {
                    rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
                }
            }];
        }
        [self fd_reloadSections:sections withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_moveSection:(NSInteger)section toSection:(NSInteger)newSection
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [self.fd_cellHeightCache.sections exchangeObjectAtIndex:section withObjectAtIndex:newSection];
        }
        [self fd_moveSection:section toSection:newSection]; // Primary call
    }
    
    - (void)fd_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
                NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section];
                [rows insertObject:@(_FDTemplateLayoutCellHeightCacheAbsentValue) atIndex:indexPath.row];
            }];
        }
        [self fd_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
                [self.fd_cellHeightCache.sections[indexPath.section] removeObjectAtIndex:indexPath.row];
            }];
        }
        [self fd_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
    }
    
    - (void)fd_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
                NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section];
                rows[indexPath.row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
            }];
        }
        [self fd_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            NSMutableArray *sourceRows = self.fd_cellHeightCache.sections[sourceIndexPath.section];
            NSMutableArray *destinationRows = self.fd_cellHeightCache.sections[destinationIndexPath.section];
            
            NSNumber *sourceValue = sourceRows[sourceIndexPath.row];
            NSNumber *destinationValue = destinationRows[destinationIndexPath.row];
            
            sourceRows[sourceIndexPath.row] = destinationValue;
            destinationRows[destinationIndexPath.row] = sourceValue;
        }
        [self fd_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; // Primary call
    }
    
    @end

    5. 下面还有一个分类UITableView + FDTemplateLayoutCell,这个类提供外界获取cell高度的方法

    • fd_heightForCellWithIdentifier:configuration:configuration
    • fd_heightForCellWithIdentifier:cacheByIndexPath:configuration:configuration

      这个类的方法如下:

    @implementation UITableView (FDTemplateLayoutCell)
    
    - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration
    {
        if (!identifier) {
            return 0;
        }
        
        // Fetch a cached template cell for `identifier`.
        UITableViewCell *cell = [self fd_templateCellForReuseIdentifier:identifier];
        
        // Manually calls to ensure consistent behavior with actual cells (that are displayed on screen).
        [cell prepareForReuse];
        
        // Customize and provide content for our template cell.
        if (configuration) {
            configuration(cell);
        }
        
        // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
        // of growing horizontally, in a flow-layout manner.
        NSLayoutConstraint *tempWidthConstraint =
        [NSLayoutConstraint constraintWithItem:cell.contentView
                                     attribute:NSLayoutAttributeWidth
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:nil
                                     attribute:NSLayoutAttributeNotAnAttribute
                                    multiplier:1.0
                                      constant:CGRectGetWidth(self.frame)];
        [cell.contentView addConstraint:tempWidthConstraint];
        
        // Auto layout engine does its math
        CGSize fittingSize = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
        
        [cell.contentView removeConstraint:tempWidthConstraint];
        
        // Add 1px extra space for separator line if needed, simulating default UITableViewCell.
        if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
            fittingSize.height += 1.0 / [UIScreen mainScreen].scale;
        }
        
        return fittingSize.height;
    }
    
    - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id))configuration
    {
        if (!identifier || !indexPath) {
            return 0;
        }
        
        // Enable auto cache invalidation if you use this "cacheByIndexPath" API.
        if (!self.fd_autoCacheInvalidationEnabled) {
            self.fd_autoCacheInvalidationEnabled = YES;
        }
        // Enable precache if you use this "cacheByIndexPath" API.
        if (!self.fd_precacheEnabled) {
            self.fd_precacheEnabled = YES;
            // Manually trigger precache only for the first time.
            [self fd_precacheIfNeeded];
        }
        
        // Hit the cache
        if ([self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
            [self fd_debugLog:[NSString stringWithFormat:
                               @"hit cache - [%@:%@] %@",
                               @(indexPath.section),
                               @(indexPath.row),
                               @([self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath])]];
            return [self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath];
        }
        
        // Do calculations
        CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
        [self fd_debugLog:[NSString stringWithFormat:
                           @"calculate - [%@:%@] %@",
                           @(indexPath.section),
                           @(indexPath.row),
                           @(height)]];
        
        // Cache it
        [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath];
        
        return height;
    }
    
    - (BOOL)fd_debugLogEnabled
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    
    - (void)setFd_debugLogEnabled:(BOOL)debugLogEnabled
    {
        objc_setAssociatedObject(self, @selector(fd_debugLogEnabled), @(debugLogEnabled), OBJC_ASSOCIATION_RETAIN);
    }
    
    @end
  • 相关阅读:
    链接
    Java垃圾收集算法1
    Java基础知识常见面试题汇总第一篇
    Java基础知识
    线程间通信
    System.Span, System.Memory,还有System.IO.Pipelines
    微服务构件
    定位恶意软件
    Spring MVC-处理程序映射(Handler Mapping)-简单的Url处理程序映射(Simple Url Handler Mapping)示例(转载实践)
    Spring MVC-处理程序映射(Handler Mapping)-控制器类名称处理程序映射(Controller Class Name Handler Mapping)示例(转载实践)
  • 原文地址:https://www.cnblogs.com/muzijie/p/7391012.html
Copyright © 2020-2023  润新知