在FreePascal/Lazarus中实现NSURLConnectionDataDelegate遇编译及回调问题
我来帮你梳理并解决这些在FreePascal/Lazarus中实现NSURLConnection异步代理的问题,这些坑我之前也踩过不少:
核心问题分析
你遇到的三个问题本质上都是FreePascal对Objective-C协议/方法签名的处理细节没摸透:
connectionDidFinishLoading不触发:大概率是代理对象被提前释放,或者方法签名完全错误,再或者没正确运行RunLoop处理异步事件;override关键字报错:NSObject本身并没有实现NSURLConnectionDelegate/DataDelegate的方法,这些是协议要求的方法,不需要加override,编译器的提示是对Objective-C协议机制的误解;- 符号重定义:多个代理方法用了相同的
message字符串,导致Objective-C选择器(SEL)冲突,每个方法的message必须严格对应Objective-C的原生选择器。
分步解决方案
1. 修正代理方法的message字符串(解决符号重定义)
Objective-C的选择器是精确匹配的,每个代理方法的message必须和原生选择器完全一致,不能随便简化。同时要显式声明类实现的协议:
{$mode objfpc} {$modeswitch objectivec1} {$modeswitch objectivec2} type TmsMacRequestDelegate = objcclass(NSObject, NSURLConnectionDelegate, NSURLConnectionDataDelegate) public // 对应Objective-C的 - (void)connectionDidFinishLoading:(NSURLConnection *)connection; procedure connectionDidFinishLoading(connection: NSURLConnection); message 'connectionDidFinishLoading:'; // 对应 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; procedure connection(connection: NSURLConnection; didReceiveResponse: NSURLResponse); message 'connection:didReceiveResponse:'; // 对应 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; procedure connection(connection: NSURLConnection; didReceiveData: NSData); message 'connection:didReceiveData:'; // 对应 - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite; procedure connection(connection: NSURLConnection; didSendBodyData: NSInteger; totalBytesWritten: NSInteger; totalBytesExpectedToWrite: NSInteger); message 'connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:'; // 对应 - (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; function connection(connection: NSURLConnection; willSendRequest: NSURLRequest; redirectResponse: NSURLResponse): NSURLRequest; message 'connection:willSendRequest:redirectResponse:'; // 对应 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; function connection(connection: NSURLConnection; willCacheResponse: NSCachedURLResponse): NSCachedURLResponse; message 'connection:willCacheResponse:'; end;
注意:带返回值的方法(比如重定向、缓存相关)要改成function,不能用procedure,否则会和Objective-C的方法签名不匹配。
2. 移除不必要的override关键字
这些方法是协议要求实现的,不是继承自NSObject的方法,所以完全不需要加override——加了反而会让编译器误以为你要重写父类不存在的方法,直接引发报错。
3. 确保代理对象的生命周期(解决方法不触发)
如果requestDelegate是局部变量,在urlConnection.start后很可能被FreePascal的内存管理机制释放,导致代理方法根本无法被调用。你需要把代理对象保存到持久化变量中(比如类成员变量、全局变量),或者手动retain它:
// 声明一个全局变量或者类成员变量来持有代理 var requestDelegate: TmsMacRequestDelegate; // 创建代理时手动保持引用 requestDelegate := TmsMacRequestDelegate.alloc.init; requestDelegate.retain; urlConnection := NSURLConnection.connectionWithRequest_delegate(urlRequest, requestDelegate); urlConnection.start;
然后在connectionDidFinishLoading方法里记得释放代理对象:
procedure TmsMacRequestDelegate.connectionDidFinishLoading(connection: NSURLConnection); begin // 处理你的完成逻辑 self.release; // 释放代理,避免内存泄漏 end;
4. 正确处理RunLoop(解决异步事件不触发)
NSURLConnection的异步操作依赖RunLoop来分发事件,如果你用自己的死循环等待,会阻塞RunLoop运行,导致代理方法无法被调用。正确的做法是让当前线程的RunLoop运行直到连接完成:
var isFinished: Boolean = False; // 启动连接后 isFinished := False; urlConnection.start; // 运行RunLoop,每次等待0.1秒,直到完成 while not isFinished do begin NSRunLoop.currentRunLoop.runUntilDate(NSDate.dateWithTimeIntervalSinceNow(0.1)); end;
然后在connectionDidFinishLoading里设置isFinished := True;来结束循环。
5. 实现重定向拦截(满足你的初始需求)
既然你不想自动跟随重定向,在connection:willSendRequest:redirectResponse:方法里返回nil即可阻止重定向:
function TmsMacRequestDelegate.connection(connection: NSURLConnection; willSendRequest: NSURLRequest; redirectResponse: NSURLResponse): NSURLRequest; begin if redirectResponse <> nil then begin // 这里可以记录重定向地址,方便后续手动处理 Writeln('拦截到重定向:', redirectResponse.URL.absoluteString.UTF8String); Result := nil; // 阻止自动重定向 end else begin Result := willSendRequest; // 正常请求直接返回原请求 end; end;
完整测试代码示例
{$mode objfpc} {$modeswitch objectivec1} {$modeswitch objectivec2} uses CocoaAll; var isFinished: Boolean = False; requestDelegate: TmsMacRequestDelegate; type TmsMacRequestDelegate = objcclass(NSObject, NSURLConnectionDelegate, NSURLConnectionDataDelegate) public procedure connectionDidFinishLoading(connection: NSURLConnection); message 'connectionDidFinishLoading:'; procedure connection(connection: NSURLConnection; didReceiveResponse: NSURLResponse); message 'connection:didReceiveResponse:'; procedure connection(connection: NSURLConnection; didReceiveData: NSData); message 'connection:didReceiveData:'; function connection(connection: NSURLConnection; willSendRequest: NSURLRequest; redirectResponse: NSURLResponse): NSURLRequest; message 'connection:willSendRequest:redirectResponse:'; end; procedure TmsMacRequestDelegate.connectionDidFinishLoading(connection: NSURLConnection); begin Writeln('连接完成!'); isFinished := True; self.release; end; procedure TmsMacRequestDelegate.connection(connection: NSURLConnection; didReceiveResponse: NSURLResponse); begin Writeln('收到响应'); end; procedure TmsMacRequestDelegate.connection(connection: NSURLConnection; didReceiveData: NSData); begin Writeln('收到数据,长度:', data.length); end; function TmsMacRequestDelegate.connection(connection: NSURLConnection; willSendRequest: NSURLRequest; redirectResponse: NSURLResponse): NSURLRequest; begin if redirectResponse <> nil then begin Writeln('拦截重定向到:', redirectResponse.URL.absoluteString.UTF8String); Result := nil; end else begin Result := willSendRequest; end; end; var urlRequest: NSURLRequest; urlConnection: NSURLConnection; begin urlRequest := NSURLRequest.requestWithURL(NSURL.URLWithString(NSString.stringWithUTF8String('https://example.com'))); requestDelegate := TmsMacRequestDelegate.alloc.init; requestDelegate.retain; urlConnection := NSURLConnection.connectionWithRequest_delegate(urlRequest, requestDelegate); if urlConnection = nil then begin Writeln('创建连接失败'); Exit; end; isFinished := False; urlConnection.start; while not isFinished do begin NSRunLoop.currentRunLoop.runUntilDate(NSDate.dateWithTimeIntervalSinceNow(0.1)); end; Writeln('所有操作完成'); end.
内容的提问来源于stack exchange,提问作者Tom

