block在美团开发中的应用

本文探讨了Block在iOS开发中的应用,包括闭包的概念、Block如何实现闭包以及在实际开发中的应用场景,如组合请求和异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说到block,相信大部分iOS开发者都会想到retain cycle或是__block修饰的变量。但是本文将忽略这些老生常谈的讨论,而是将重点放在美团iOS在实践中对block的应用,希望能对同行有所助益。本文假设读者对block有一定的了解。

从闭包说起
在Lisp这样的语言中,有一个概念叫做闭包(closure1),指的是一个函数以及它所处的词法作用域(lexical scope2)构成的整体。为了理解闭包,我们首先来看看什么是词法作用域。

所谓词法作用域,顾名思义,是指一个符号引用的是其词法环境中的变量,而无关程序在运行时的状态。这么说可能有点抽象,让我来看一段Common Lisp3代码:

(defvar printer (let ((x 42))
          (lambda () (format t "~a" x))))
这里我们定义了一个变量printer,它的值是一个函数,这个函数会打印词法作用域中的变量x(其值为42)。
现在我们来调用这个函数:

CL-USER> (funcall printer)
42
可以看到,我们调用了printer中存放的函数之后,打印出来的数字是42,跟我们的预期相符。

接下来再让我们看一个可能会出乎意料的结果:

CL-USER> (let ((x 1))
       (funcall printer))
42
我们在调用之前把x设置为了1,但是打印的结果仍然是42。

为什么?因为printer中存放的函数在被调用时所引用的变量位于其词法作用域中, 即该函数被定义时所处的词法环境中,所以程序在运行时设置的变量x对函数不起作用。

前面我们讲过,所谓闭包,就是函数及其词法作用域的合称,具体到上例,那么匿名函数和x就构成了一个闭包,它会为函数保存一种状态,有点类似于全局变量,不过除了那个匿名函数,其他函数无法访问到x。

说了这么多,似乎跟block毫无关系?事实上,block为C带来了闭包。

Block
Apple从OS X 10.6和iOS 4以后开始支持block,让我们用C把上面的例子重写一下:

#include <stdio.h>

int main ()
{
    int x = 42;
    void (^block)() = ^() {
        printf("%d\n", x);
    };
    block();
    x = 1;
    block();

    return 0;
}
编译运行后得到的输出同样是两个42。

到了这里,相信读者对闭包已经有一个直观的认识了,但是它有什么用?有什么好处?
设想如下场景,我们要请求一个URL,并以block的形式传入回调函数,并在回调函数中用到刚才这个URL:

NSURL *someURL = …;
[SomeClass getURL:someURL finished:^(id responseObject) {
    // process responseObject with someURL
}];
这里网络请求是异步的,所以当block中代码执行时,getURL:finished:方法调用所在的栈很可能已经不存在了,但是因为回调block和someURL构成了closure,所以即使栈不存在,block仍然可以引用到someURL。

可能你会说,“我在block中增加一个NSURL类型的参数,把someURL传回来不也可以实现同样的目的吗?”不妨设想如果我们在block中要引用的对象有10个之多,用参数列表传递明显不再现实,用容器类或者专门定义一个类来传递虽然可以,但是前者没有编译器为我们检查错误,后者则相当繁琐。而利用闭包,可以轻易达到灵活性和简洁性的平衡。事实上,美团客户端就大量利用了闭包,在UI层发出请求,在回调中更新某些UI组件。

函数式编程4
在Lisp中,函数是一等公民,可以随时创建、作为参数传递、作为返回值返回,Objective C在没有block之前,没有类似的机制,有了block,Objective C也就具备了函数式编程的能力,block是对象,有自己的ISA指针,可以随时创建,作为参数传递,作为返回值返回。

先来看看block的经典用法:

[UIView animateWithDuration:0.25 animations:^{
            self.view.alpha = 1.0f;
        }];
UIView的animateWithDuration:animations:方法的第二个参数是一个block,它把跟动画相关的操作封装起来传递进去,以实现动画效果。

现在让我们发掘一下类似的用法:

[SAKBaseModel comboRequest:^() {
 [dealModel fetchDealByID:123456
               withFields:nil
               completion:^(MTDeal *deal, NSError *error) {
                   ...
               }];
 [orderModel fetchOrderByID:654321
             withDealFields:nil
                 completion:^(MTOrder *order, NSError *error) {
                ...
             }];
}];
这里我们为SAKBaseModel设计了一个类似于UIView的接口叫comboRequest,它会接受一个block作为参数,在这个block中发出的请求都会作为combo请求的一部分。如果dealModel或者orderModel的任何一个请求不是出现在block中,那么它就是一个普通的请求。这样做的好处是dealModel和orderModel的接口不需要关心自己是不是属于一个combo请求,调用者则可以灵活地调整代码。

那么怎么实现这样的接口呢?还是从UIView上获取灵感。我们知道UIView有个方法setAnimationsEnabled:,实际上SAKBaseModel也可以有这么一个方法:setComboRequestEnabled:,而在comboRequest方法的实现中,在调用传进来的block之前先setComboRequestEnabled:YES,调用完后再恢复为原状态。相应的,在实际的model接口中,检查comboRequest是否为YES,如果是,则把自己作为一个combo请求的一部分,否则正常发出请求即可。

Think Big
Lisp最强大的特性之一是condition系统,它可以分离异常的检测、异常的解决和异常解决方式的决策,看一段示例代码:

(define-condition network-timeout-error (error)
    ((url :initarg :url :accessor url)))

(defun try-again (condition)
    (let ((restart (find-restart ‘try-again)))
      (when restart (invoke-restart restart))))

(defun deal-requester (deal-id)
    (handler-bind ((network-timeout-error #’try-again))
      (request-from-url (format nil “http://api.mobile.meituan.com/deal/~a” deal-id)
        (lambda (deal error)
        (if error
            (format t “error: ~a”, error)
            (process-deal))))))
(defun request-from-url (url finished)
    (let ((callback (lambda (response error)
              (if (network-timeout-error-p error)
                (error ‘network-timeout-error :url url)
                (funcall finished (parse-deal response) error)))))
      (restart-bind
        ((try-again (lambda () (http-request url callback))))
        (http-request url callback))))
可以看到,condition系统对于代码的分层提供了良好的支持,请求超时的错误在底层代码被检测到,在发出请求前注册一个restart,而在业务层去决定要不要调用restart。

一直以来,C语言要实现优雅的异常处理就是一件不简单的事情,而Objective-C虽然加入了try-catch支持,但是苹果并不鼓励使用,那么能否实现类似于condition系统这样的异常处理机制呢?

答案是能。让我们来看看接口设计:

typedef void (^RESTART)(id userInfo);
typedef void (^HANDLER)(id condition);

void restart_bind(void (^body)(), NSString *restartName, RESTART restart, ...) NS_REQUIRES_NIL_TERMINATION;

void handler_bind(void (^body)(), Class class, HANDLER handler, ...) NS_REQUIRES_NIL_TERMINATION;

void notify(id condition);

RESTART find_restart(NSString *restartName);
如下图所示,handler_bind首先在栈中注册好handler,而restart_bind则在handler有效的环境中注册restart,当有异常发生时,notify函数会在当前环境中寻找handler,找到后,控制会转移到上层的handler代码中,这时handler可以用find_restart在栈中搜索restart,找到之后可以调用,从而实现异常的恢复,做完这一切,控制回到notify发生的点继续向下执行。



完整的代码敬请期待美团iOS的开源项目。

有了SAKCondition,我们可以实现任意底层代码的逻辑穿透到上层代码,比如网络层和UI层,使得上层代码可以在不了解下层代码实现细节的情况下调用恢复机制。事实上,美团的iPhone客户端就是利用SAKCondition实现了美团账户的安全解锁功能。

总结
block给Objective C带来了无穷的可能性。本文只讨论了美团iOS在实践中的一些用法,更多想法还在等待挖掘。

资源下载链接为: https://pan.quark.cn/s/9648a1f24758 这个HTML文件是一个专门设计的网页,适合在告白或纪念日这样的特殊时刻送给女朋友,给她带来惊喜。它通过HTML技术,将普通文字转化为富有情感和创意的表达方式,让数字媒体也能传递深情。HTML(HyperText Markup Language)是构建网页的基础语言,通过标签描述网页结构和内容,让浏览器正确展示页面。在这个特效网页中,开发者可能使用了HTML5的新特性,比如音频、视频、Canvas画布或WebGL图形,来提升视觉效果和交互体验。 原本这个文件可能是基于ASP.NET技术构建的,其扩展名是“.aspx”。ASP.NET是微软开发的一个服务器端Web应用程序框架,支持多种编程语言(如C#或VB.NET)来编写动态网页。但为了在本地直接运行,不依赖服务器,开发者将其转换为纯静态的HTML格式,只需浏览器即可打开查看。 在使用这个HTML特效页时,建议使用Internet Explorer(IE)浏览器,因为一些老的或特定的网页特效可能只在IE上表现正常,尤其是那些依赖ActiveX控件或IE特有功能的页面。不过,由于IE逐渐被淘汰,现代网页可能不再对其进行优化,因此在其他现代浏览器上运行可能会出现问题。 压缩包内的文件“yangyisen0713-7561403-biaobai(html版本)_1598430618”是经过压缩的HTML文件,可能包含图片、CSS样式表和JavaScript脚本等资源。用户需要先解压,然后在浏览器中打开HTML文件,就能看到预设的告白或纪念日特效。 这个项目展示了HTML作为动态和互动内容载体的强大能力,也提醒我们,尽管技术在进步,但有时复古的方式(如使用IE浏览器)仍能唤起怀旧之情。在准备类似的个性化礼物时,掌握基本的HTML和网页制作技巧非常
### 如何用 UniApp 实现美团风格的点餐页面 #### 项目初始化与环境配置 为了创建一个类似于美团点餐的应用程序,首先需要安装并配置好开发环境。确保已经安装 Node.js 和 npm 后,在命令行工具中执行如下操作来新建一个 UniApp 工程: ```bash npm install -g @vue/cli vue create my-waimai-app cd my-waimai-app npm run serve ``` 接着引入 `uni-app` 插件以便能够访问其特有的 API 及组件。 #### 页面布局设计 构建首页时采用响应式网格系统以适应各种尺寸屏幕,并且合理安排商品展示区、分类菜单以及其他交互元素的位置。下面是一个简单的 HTML 骨架结构[^1]: ```html <template> <view class="container"> <!-- 导航栏 --> <nav-bar></nav-bar> <!-- 主体部分 --> <scroll-view scroll-y style="height: calc(100vh - var(--status-bar-height));"> <block v-for="(category, index) in categories" :key="index"> <text>{{ category.name }}</text> <!-- 商品列表 --> <view class="product-list"> <block v-for="item in category.items" :key="item.id"> <card-item :data="item"></card-item> </block> </view> </block> <!-- 加载更多提示符 --> <loading-indicator></loading-indicator> </scroll-view> <!-- 购物车浮窗按钮 --> <cart-float-button></cart-float-button> </view> </template> ``` 这里使用了 `<scroll-view>` 组件来自定义滚动行为,解决了小程序中原生滚动条无法隐藏的问题[^2];同时通过 CSS 变量调整视口高度减去状态栏占用的空间大小,使得整体界面更加美观和谐。 #### 功能模块实现 针对具体业务逻辑如菜品详情查看、加入购物车等功能,则需单独封装成可复用的小部件(Component),并通过 props 或者事件机制与其他组件通信协作完成相应动作。以下是简化版的商品卡片组件示例代码片段: ```javascript // components/CardItem.vue export default { name: 'CardItem', props: ['data'], methods: { addToCart() { console.log('Add to cart:', this.data); // 发送请求至服务器更新订单信息... } }, }; ``` 此外还需注意处理网络请求失败情况下的异常捕获及重试策略等问题,保证用户体验流畅稳定。 #### 性能优化建议 考虑到移动互联网环境下流量成本较高,应该尽可能压缩图片资源体积并开启懒加载特性;对于频繁变动的数据则考虑启用缓存机制降低带宽消耗。最后记得定期审查应用程序性能指标找出潜在瓶颈所在加以改进[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值