使用iOS导航栈的注意点

294 阅读4分钟

说在前面

此案例的目标读者为所有iOS开发的同学,与项目是否使用mPaas无关。

问题描述

App上使用发布文章的功能时,点击发布后,页面未退回到App首页。关闭App重新打开后,再使用发布文章的功能则一切正常,问题不再出现。

问题分析

第一步,先看一下原生代码中,是使用什么方式退回到app的首页的:

[navigationController popToRootViewControllerAnimated:YES];

咋一看,很简单,popToRootViewControllerAnimated就是系统导航控制器提供的方法,可直接将导航栈中除根控制器以外的所有控制器出栈,不应该有问题。打断点仔细看看有没有什么有用的线索:

image-20211214101530562

通过对比发现,navigationController在出问题和不出问题时的状态并没有什么不同。但是可以看到,这个导航栈控制器并不是UINavigationController,而是一个叫做DFNavigationController的类,经确认,它是mPaas提供的导航栈,那么就有理由推断,这个UINavigationController的子类可能是重写了popToRootViewControllerAnimated方法,通过class-dump导出的DFNavigationController.h也验证了这个推断。那么很有可能在某个场景下,该方法提前退出,并没有真正进行pop操作。进一步大胆猜测,DFNavigationController内部维护了一个Controller列表,由于某种原因,导致该列表和导航栈中的Controller列表不一致了,最终导致popToRoot行为异常,由于DFNavigationController会存在于app的整个声明周期,所以此问题无法恢复,直到重启app后,不一致性不再出现,popToRoot行为又恢复正常。不过由于看不到DFNavigationController的源码,所以不能直接对DFNavigationController继续分析,同时也不敢贸然替换它内部的方法实现,问题似乎无法继续分析了。那么只能从我们的调用逻辑上进行排查。

第二步,分析文章发布流程。文章的发布流程也不复杂,用户发布文章前,需要先进行视频或者图片的选择,所以会拉起一个photo组件提供的页面,从表现上和navigationController的items看,是push了一个ViewController,用户选好图片或视频后,点击下一步,该ViewController自己会pop掉,并给调用者返回用户选择的图片url,调用者将图片url拼接到mpaas url中,交给vdn,最终打开一个h5的文章编辑界面,用户编辑好文章后,就可以点击发布了。

847FD8E4-0A90-4E05-AE67-4D812B29DA6B_1_105_c

在复现问题和分析流程的过程中发现,选择完图片到拉起H5页面期间,会有短暂的停顿,同时控制台日志输出了ugcsyn资源包的下载进度的信息,原来第一次加载这个h5页面时,app会下载最新的资源包,但是界面应该会弹出加载进度的提示框才对,貌似没看到。隐约感觉这里不对劲,同时联想到原问题也是在第一次使用文章发布功能的时候必现,更加让我怀疑问题和资源包的加载有关系。继续对这里的逻辑进行分析,在弹出加载进度前,刚好是图片选择界面pop自身,这期间几乎没有任何时间间隔。有没有可能是两个操作间隔太短导致出问题呢?带着这个疑问,我们尝试在打开h5的操作前加一个1秒的延时,足以让图片选择界面完全pop掉,再重新走一遍流程,发现加载进度框出来了,同时问题也不见了,文章发布后,可正常退回首页了。

第三步,根因分析。虽然看不到DFNavigationController的内部实现,但是从文章发布的流程上已经发现了问题,经过确认,加载进度显示框是在导航栈的栈顶弹出的,而一个栈顶Controller从执行pop到它自身从栈中移除,是有一个耗时过程的。经过编写样例程序,让一个控制器A 在pop后立刻present控制器B,发现控制器B是无法正常显示出来的,因为B弹的时候,A可能已经被移除了。系统log也会提示:

Attempt to present <UIDocumentPickerViewController: 0x10207a310> on <SwiftNavigation.PaceViewController: 0x100f34600> (from <SwiftNavigation.PaceViewController: 0x100f34600>) whose view is not in the window hierarchy.

总结

至此,问题基本清楚了。我认为此问题的较合理解法时,photo组件先完成pop,然后在给调用者返回url,避免调用者收到url后,立刻触发其他导航栈的修改操作。如果后续我们遇到controller present不出来,或者pop不出去,都可以检查一下是否有导航栈段时间内的频繁操作,说不定问题就出现在这里。