使用PyObjC与Apple Vision的VNRecognizeTextRequest出现内存泄漏
我在Mac M1设备上用PyCharm开发,通过Python的multiprocessing库结合Apple Vision库提取约10000张PNG图片的文本时,发现内存持续增长直至失控。为排查问题,我简化流程用单张图片重复处理3000次,仍出现相同情况。已尝试使用NSAutoreleasePool但无效果。
测试代码如下:
import time from Quartz import CIImage from Vision import VNImageRequestHandler, VNRecognizeTextRequest import multiprocessing from Cocoa import NSURL, NSAutoreleasePool import psutil import os def worker(job_queue): process = psutil.Process(os.getpid()) while True: # Create an autorelease pool to manage memory pool = NSAutoreleasePool.alloc().init() try: t1 = time.time() job = job_queue.get() if job is None: # Sentinel value to signal process to stop print(f"Worker {multiprocessing.current_process().name} exiting.") job_queue.task_done() memory_info = process.memory_info().rss print((multiprocessing.current_process().name, memory_info / 1024 / 1024)) break print(f"Worker {multiprocessing.current_process().name} Job Number = {job[1]} url = {job[0]}") url = NSURL.fileURLWithPath_(job[0]) ci_image = CIImage.imageWithContentsOfURL_(url) request = VNRecognizeTextRequest.alloc().init() handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None) success, error = handler.performRequests_error_([request], None) if error: print(f"Worker {multiprocessing.current_process().name} result = {success}, error = {error}") print(f"Worker {multiprocessing.current_process().name} done. Elapsed = {time.time() - t1}.") request = None handler = None # Get memory usage of the current process memory_info = process.memory_info().rss # Resident Set Size in bytes print((multiprocessing.current_process().name, memory_info / 1024 / 1024)) finally: # Ensure the autorelease pool is drained to release memory pool.drain() job_queue.task_done() memory_info = process.memory_info().rss print((multiprocessing.current_process().name, memory_info / 1024 / 1024)) if __name__ == '__main__': # Create a Queue for jobs job_queue = multiprocessing.JoinableQueue() # Number of worker processes num_workers = 4 # Create worker processes processes = [] for i in range(num_workers): p = multiprocessing.Process(target=worker, args=(job_queue,), name=f"Worker-{i + 1}") processes.append(p) p.start() # Put jobs into the queue num_jobs = 3000 for job_id in range(num_jobs): job_queue.put(["Path To File", job_id]) # Add sentinel values (None) to signal workers to stop after jobs are done for _ in range(num_workers): job_queue.put(None) # Wait for all jobs in the queue to be processed job_queue.join() # Wait for all worker processes to finish for p in processes: p.join() print("All jobs completed.")
程序启动4个进程,向队列加载3000个任务,每个进程从队列取任务执行文本识别后标记完成并取下一个任务。已移除文本识别结果处理部分,但内存仍会增长,每个进程内存从约250MB升至340MB。处理更多文件会出现内存耗尽问题,求解决方法。
解决方案
1. 显式释放Objective-C对象
Python的自动垃圾回收无法完全处理Objective-C对象的引用计数,需手动调用release()方法释放所有创建的Cocoa/Quartz/Vision对象,而非仅赋值为None。修改任务处理部分的代码:
# 在任务处理完成后添加 request.release() handler.release() ci_image.release() url.release() # 再置空对象 request = None handler = None ci_image = None url = None
2. 调整NSAutoreleasePool作用范围
将job_queue.get()移出Autorelease Pool,避免队列操作相关对象被纳入池管理;仅在处理图片识别的逻辑块内创建池,确保所有Objective-C对象都在池的生命周期内:
def worker(job_queue): process = psutil.Process(os.getpid()) while True: # 先取任务,再创建池 job = job_queue.get() if job is None: print(f"Worker {multiprocessing.current_process().name} exiting.") job_queue.task_done() memory_info = process.memory_info().rss print((multiprocessing.current_process().name, memory_info / 1024 / 1024)) break pool = NSAutoreleasePool.alloc().init() try: t1 = time.time() print(f"Worker {multiprocessing.current_process().name} Job Number = {job[1]} url = {job[0]}") url = NSURL.fileURLWithPath_(job[0]) ci_image = CIImage.imageWithContentsOfURL_(url) request = VNRecognizeTextRequest.alloc().init() handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None) success, error = handler.performRequests_error_([request], None) if error: print(f"Worker {multiprocessing.current_process().name} result = {success}, error = {error}") print(f"Worker {multiprocessing.current_process().name} done. Elapsed = {time.time() - t1}.") # 显式释放对象 request.release() handler.release() ci_image.release() url.release() request = None handler = None ci_image = None url = None memory_info = process.memory_info().rss print((multiprocessing.current_process().name, memory_info / 1024 / 1024)) finally: pool.drain() job_queue.task_done()
3. 改用multiprocessing.Pool管理进程
手动管理进程容易出现资源累积,改用multiprocessing.Pool可以自动回收进程资源,避免长期运行的内存泄漏:
import gc import time from Quartz import CIImage from Vision import VNImageRequestHandler, VNRecognizeTextRequest import multiprocessing from Cocoa import NSURL, NSAutoreleasePool import psutil import os def process_image(job): path, job_id = job process = psutil.Process(os.getpid()) pool = NSAutoreleasePool.alloc().init() try: t1 = time.time() print(f"Processing Job Number = {job_id}") url = NSURL.fileURLWithPath_(path) ci_image = CIImage.imageWithContentsOfURL_(url) request = VNRecognizeTextRequest.alloc().init() handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None) success, error = handler.performRequests_error_([request], None) if error: print(f"Job {job_id} result = {success}, error = {error}") print(f"Job {job_id} done. Elapsed = {time.time() - t1}.") # 释放对象 request.release() handler.release() ci_image.release() url.release() # 手动触发GC gc.collect() memory_info = process.memory_info().rss print((f"Job {job_id}", memory_info / 1024 / 1024)) return success, error finally: pool.drain() if __name__ == '__main__': num_workers = 4 num_jobs = 3000 jobs = [("Path To File", job_id) for job_id in range(num_jobs)] with multiprocessing.Pool(num_workers) as pool: results = pool.map(process_image, jobs) print("All jobs completed.")
4. 手动触发Python垃圾回收
在每次任务处理完成后,调用gc.collect()强制回收Python层面的垃圾,配合Objective-C对象的手动释放,进一步降低内存累积风险。记得先导入gc模块。
5. 限制任务队列长度
若坚持使用手动队列管理,设置队列的maxsize参数(比如maxsize=num_workers*2),避免一次性加载大量任务,减少内存占用压力:
job_queue = multiprocessing.JoinableQueue(maxsize=num_workers*2)
内容的提问来源于stack exchange,提问作者Gyun

