FDFullscreenPopGesture的原理

一 序言

由于Plus的出现,iphone的默认导航栏又是在屏幕顶部,对于app的返回操作大屏手机对于小手的用户来说操作显得不那么友好。iOS7为了提升app的返回体验,增加了边缘侧滑返回手势,但是对于小手用户来说,返回体验没有彻底得到改善。于是开发者们开始绞尽脑汁地想各种办法,其中一种办法,也就是今天要讲主角-将返回手势变为全屏侧滑返回的框架

二 剖析这个框架需要了解的知识点
  • 为分类添加属性 objc_setAssociatedObject,objc_getAssociatedObject
  • +(void)load方法执行的时机 ,在 main 函数调用之前被 ObjC 运行时会将所有类加载进内存,会调用每一个类的load方法。
  • NavigationController可以通过调用setViewController方法将画面的跳转历史路径(堆栈)完全替换

框架源码解析

1 文件目录结构

打开项目我们能看到,该框架只有一个.h 和.m文件。

  • .h中只暴露了UINavigationController 和 UIViewController的两个分类属性。
@interface UINavigationController (FDFullscreenPopGesture)
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *fd_fullscreenPopGestureRecognizer;
@property (nonatomic, strong, readonly) UIScreenEdgePanGestureRecognizer *fd_rtlFullscreenPopGestureRecognizer;
@property (nonatomic, assign) BOOL fd_viewControllerBasedNavigationBarAppearanceEnabled;
@end

@interface UIViewController (FDFullscreenPopGesture)
@property (nonatomic, assign) BOOL fd_interactivePopDisabled;
@property (nonatomic, assign) BOOL fd_prefersNavigationBarHidden;
@property (nonatomic, assign) CGFloat fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
@end

  • .m中包括四部分
@implementation _FDFullscreenPopGestureRecognizerDelegate : NSObject (私有)
@implementation UIViewController (FDFullscreenPopGesturePrivate)(私有)
@implementation UINavigationController (FDFullscreenPopGesture)
@implementation UIViewController (FDFullscreenPopGesture)

  • 下面为大家一一讲解下这四个implementation都干了一些什么事
1. _FDFullscreenPopGestureRecognizerDelegate

_FDFullscreenPopGestureRecognizerDelegate:定义了一个类遵循了手势代理协议,并且有一点navigationController的属性。自定义的手势是否被触发由这个类来控制。

@interface _FDFullscreenPopGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>
@property (nonatomic, weak) UINavigationController *navigationController;
@end

// 这个类实现了自定义手势的代理方法 
@implementation _FDFullscreenPopGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    // 当没有控制器入栈的时候,不触发手势
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }

    // 如果控制器的fd_interactivePopDisabled属性为NO不触发手势
    //(fd_interactivePopDisabled是作者对UIViewController添加的一个属性,下面会讲)
    UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
    if (topViewController.fd_interactivePopDisabled) {
        return NO;
    }

   //当手势开始的位置 超出了fd_interactivePopMaxAllowedInitialDistanceToLeftEdge所设定的值,那么就不触发手势
    CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
    CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
    if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
        return NO;
    }

    // 如果导航控制器正在执行转场动画,则不触发手势
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }

    // 1.这个比较神奇,当app语言设置为阿拉伯语等阅读顺序从右到左的语言,且app的布局适配了这个语种,
    // 2.那么导航控制器的入栈动画会由从右到左,调整为从左到右,从作者的代码上来看手势好像是不支持从左到右的app布局的。
    // 3.也就是说,当app语言设置为阿拉伯等语言并且app适配了这种布局,不触发手势
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
    BOOL isLeftToRight = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight;

    CGFloat multiplier = isLeftToRight ? 1 : - 1;
    if ((translation.x * multiplier) <= 0) {
        return NO;
    }

    return YES;
}
@end

2.UIViewController (FDFullscreenPopGesturePrivate)
  1. 在该分类中定义了一个block,并且在viewWillAppear:的时候被注入
  2. main函数之前,交换系统的两个实现方法viewWillAppear:viewWillDisappear:

具体代码如下

typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);

@interface UIViewController (FDFullscreenPopGesturePrivate)
@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
@end

@implementation UIViewController (FDFullscreenPopGesturePrivate)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        //viewWillAppear
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(fd_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            // 主类本身没有实现需要替换的方法,而是继承了父类的实现,即 class_addMethod 方法返回 YES 。
            // 这时使用 class_getInstanceMethod 函数获取到的 originalSelector 指向的就是父类的方法,我们再通过执行 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            // 将父类的实现替换到我们自定义的 mrc_viewWillAppear 方法中。这样就达到了在 mrc_viewWillAppear 方法的实现中调用父类实现的目的。
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }

        //viewWillDisappear
        SEL originalSelector2 = @selector(viewWillDisappear:);
        SEL swizzledSelector2 = @selector(fd_viewWillDisappear:);

        Method originalMethod2 = class_getInstanceMethod(class, originalSelector2);
        Method swizzledMethod2 = class_getInstanceMethod(class, swizzledSelector2);

        BOOL success2 = class_addMethod(class, originalSelector2, method_getImplementation(swizzledMethod2), method_getTypeEncoding(swizzledMethod2));
        if (success2) {
            class_replaceMethod(class, swizzledSelector2, method_getImplementation(originalMethod2), method_getTypeEncoding(originalMethod2));
        } else {
            method_exchangeImplementations(originalMethod2, swizzledMethod2);
        }
    });
}

交换方法后做的事情

- (void)fd_viewWillAppear:(BOOL)animated {
    // Forward to primary implementation.
    // 为了不破坏原本的业务逻辑,先执行原来的viewWillAppear方法
    [self fd_viewWillAppear:animated];

    // 执行注入的block 这个block到底干了什么事情,会在后面讲到
    if (self.fd_willAppearInjectBlock) {
        self.fd_willAppearInjectBlock(self, animated);
    }

    //设置导航的显示/隐藏
    // 根据导航栏栈顶控制的fd_prefersNavigationBarHidden这个分类属性,- 控制导航栏是否需要隐藏
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(CGFLOAT_MIN * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        UIViewController *vc = [self.navigationController.viewControllers lastObject];
        if (vc.fd_prefersNavigationBarHidden) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
        } else {
            [self.navigationController setNavigationBarHidden:NO animated:NO];
        }
    });
}

- (void)fd_viewWillDisappear:(BOOL)animated{

    [self fd_viewWillDisappear:animated];

    //设置导航的显示/隐藏
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(CGFLOAT_MIN * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        UIViewController *vc = [self.navigationController.viewControllers lastObject];
        if (vc.fd_prefersNavigationBarHidden) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
        } else {
            [self.navigationController setNavigationBarHidden:NO animated:NO];
        }
    });
}

block的使用

- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
    // 1\. 调用fd_willAppearInjectBlock属性的get方法的时候
    // 2\. 会在本类中以该get方法的名称为key,找到对应的value,也就是该block的值
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
    // 1\. 当调用了fd_willAppearInjectBlock这个分类属性的set方法时候,
    // 2\. 会以block为value 以该属性的get方法为key将block存储起来
    // 3\. 以后就可以通过调用fd_willAppearInjectBlock属性的get方法,获取block
    objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

3.UINavigationController (FDFullscreenPopGesture)

全屏手势的核心返回功能在此实现,交换push方法后,将系统返回手势替换为自定义手势,设置代理,如果允许用户根据控制器的分类属性控制导航栏显示或者隐藏,则给入栈的控制器的block赋值。

  • 交换方法实现
+ (void)load {
    // Inject "-pushViewController:animated:"
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(pushViewController:animated:);
        SEL swizzledSelector = @selector(fd_pushViewController:animated:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // 1.这里要注意,class_addMethod是为了检查本类是否实现了这个方法
        // 2.如果方法添加成功,代表本类没有实现该方法(该方法在父类中实现,却没有在子类中实现)
        // 3.如果实现了,那很好,直接交换
        // 4.如果没实现,那么class_addMethod已经把push方法 (对应的实现是fd_push)添加到了本类
        // 5.我们只需要再调用class_replaceMethod方法添加fd_push(对应的实现是push) 添加到本类
        // 6.这样,就达到了方法交换的目的
        // 7.pushViewController:animated: 的内部实现为fd_pushViewController:animated:
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

  • 添加手势
- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        //打印self.interactivePopGestureRecognizer.view我们会发现它的类型是UILayoutContainerView
        // (UILayoutContainerView就是window 上的第一个 subview)
        //判断自定义手势是否已经加在了UILayoutContainerView上
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
        // 使用自定义手势替换系统边缘返回的手势,
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];
        // 关闭导航控制器自带的边缘返回手势(因为它已经被自定义手势取而代之了)
        self.interactivePopGestureRecognizer.enabled = NO;
    }

    // 这个方法控制了导航控制器中的子控制器是否有独立控制导航栏显示或者隐藏的权利(下面会讲)
    // fd_viewControllerBasedNavigationBarAppearanceEnabled属性默认为YES
    // 也就是说,默认会根据控制的分类属性fd_prefersNavigationBarHidden来控制栏的隐藏或者显示
    // 如果fd_viewControllerBasedNavigationBarAppearanceEnabled为NO
    // 那么导航控制器的导航栏的显示与否,控制器无权决定
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];

    // 入栈
    if (![self.viewControllers containsObject:viewController]) {
        // 调用push方法,将控制器入栈
        [self fd_pushViewController:viewController animated:animated];
    }
}

  • 设置当前即将要push的ViewController的当要处理隐藏导航栏时的block
- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController {
   // 上面已经说了,fd_viewControllerBasedNavigationBarAppearanceEnabled为NO,则直接return
    if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
        return;
    }
    // 前面在2.中我们提到过
    // UIViewController (FDFullscreenPopGesturePrivate) 定义了一个block
    // 从这里我们可以看到,只有在 fd_viewControllerBasedNavigationBarAppearanceEnabled == YES的时候
    // 才会给block赋值,才会执行block,
    // block中会根据fd_prefersNavigationBarHidden 判断是否要显示或者隐藏导航栏
    __weak typeof(self) weakSelf = self;
    _FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            [strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
        }
    };

    // 1.对即将入栈的控制器的fd_willAppearInjectBlock属性进行赋值
    // 2.在push前,也对栈顶的控制器fd_willAppearInjectBlock赋值
    // 3.请注意,这个时候栈顶的控制器不一定是push入栈的,也有可能是通过-setViewControllers:方法入栈
    // 4.具体请看我的“框架知识储备",了解NavigationController的setViewControllers方法
    appearingViewController.fd_willAppearInjectBlock = block;
    UIViewController *disappearingViewController = self.viewControllers.lastObject;
    if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
        // 在有新的控制器入栈前,检查栈顶控制器block属性是否有值,如果没有,就赋值
        disappearingViewController.fd_willAppearInjectBlock = block;
    }
}

  • 手势代理
- (_FDFullscreenPopGestureRecognizerDelegate *)fd_popGestureRecognizerDelegate {
   // 1.这是我们在1.中第一个提到的类,自定义的pan手势代理,在这个类实现
   // 2.由于该类在判断手势是否满足触发条件时,需要根据导航控制器的情况来做判断
   // 3.所以将导航控制器交给该类引用(记得用weak,不然会循环引用)
    _FDFullscreenPopGestureRecognizerDelegate *delegate = objc_getAssociatedObject(self, _cmd);
    if (!delegate) {
        delegate = [[_FDFullscreenPopGestureRecognizerDelegate alloc] init];
        delegate.navigationController = self;
        objc_setAssociatedObject(self, _cmd, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return delegate;
}

- (UIPanGestureRecognizer *)fd_fullscreenPopGestureRecognizer {
    // "懒加载"自定义手势
    // 先获取该手势,如果获取不到,再创建,获取到了 直接返回
    UIPanGestureRecognizer *panGestureRecognizer = objc_getAssociatedObject(self, _cmd);
    if (!panGestureRecognizer) {
        panGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
        panGestureRecognizer.maximumNumberOfTouches = 1;
        objc_setAssociatedObject(self, _cmd, panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return panGestureRecognizer;
}

  • A view controller is able to control navigation bar's appearance by itself
- (BOOL)fd_viewControllerBasedNavigationBarAppearanceEnabled {
   // 获取NSNumber对象,注意了,如果NSnumber的value为0的时候,
   // if条件也会判断为真,因为NSnumber是对象,对象空的时候为nil而不是0
    NSNumber *number = objc_getAssociatedObject(self, _cmd);
    if (number) {
       // 如果number为0,那么boolValue得到的结果就为NO,反之YES
        return number.boolValue;
    }
    // 代码如果执行到这,说明没设置该属性,默认为YES
    self.fd_viewControllerBasedNavigationBarAppearanceEnabled = YES;
    return YES;
}

- (void)setFd_viewControllerBasedNavigationBarAppearanceEnabled:(BOOL)enabled {
   // 注意,这里@(enable)是将bool值包装成一个NSNumber类型的对象
    SEL key = @selector(fd_viewControllerBasedNavigationBarAppearanceEnabled);
    objc_setAssociatedObject(self, key, @(enabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

4. UIViewController (FDFullscreenPopGesture)

这里添加了框架的功能属性:手势的触发位置、该控制器是否支持手势,导航栏是否隐藏

@implementation UIViewController (FDFullscreenPopGesture)

- (BOOL)fd_interactivePopDisabled
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_interactivePopDisabled:(BOOL)disabled
{
    objc_setAssociatedObject(self, @selector(fd_interactivePopDisabled), @(disabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)fd_prefersNavigationBarHidden
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_prefersNavigationBarHidden:(BOOL)hidden
{
    objc_setAssociatedObject(self, @selector(fd_prefersNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (CGFloat)fd_interactivePopMaxAllowedInitialDistanceToLeftEdge
{
#if CGFLOAT_IS_DOUBLE
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
#else
    return [objc_getAssociatedObject(self, _cmd) floatValue];
#endif
}

- (void)setFd_interactivePopMaxAllowedInitialDistanceToLeftEdge:(CGFloat)distance
{
    SEL key = @selector(fd_interactivePopMaxAllowedInitialDistanceToLeftEdge);
    objc_setAssociatedObject(self, key, @(MAX(0, distance)), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

三 FDFullscreenPopGesture工作流程图

下面参考别人绘制的一张流程图

image
四 使用

在使用FDFullscreenPopGesture时,在需要隐藏系统导航栏的页面的viewDidLoad方法里设置下fd_prefersNavigationBarHiddenYES属性,需要显示导航栏的页面什么都不处理,使用起来非常简单。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.fd_prefersNavigationBarHidden = YES;
}


本文参考FDFullscreenPopGesture全屏手势返回源码结构及解析,非常感谢该作者.


  • 如有错误,欢迎指正,多多点赞,打赏更佳,您的支持是我写作的动力。

项目连接地址 - FDFullScreenPopGestureDemo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,520评论 28 53
  • 首先介绍下自己的背景: 我11年左右入市到现在,也差不多有4年时间,看过一些关于股票投资的书籍,对于巴菲特等股神的...
    瞎投资阅读 5,654评论 3 8
  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    极客学院Wiki阅读 7,229评论 0 3
  • 不知不觉易趣客已经在路上走了快一年了,感觉也该让更多朋友认识知道易趣客,所以就谢了这篇简介,已做创业记事。 易趣客...
    Physher阅读 3,407评论 1 2