iOS 项目中使用 Python下
上一节最后我们通过将 Python 所依赖的 Resources 通过 Bundle 成功引入项目。并通过 fopen 方式结合 PyRun_SimpleFileEx 执行成功,对于执行单一方法的 Py 文件来说还是很方便的,但是我们的业务往往比较复杂,需要被拆分成多个文件类,除系统自带的库之外我们也需要引入一些其他的第三方库,同时我们也需要实例化某个具体类,并调用这些类的中的具体方法并且传入参数,那么我们该如何实现呢?
Py代码/第三方库的引入
直接引入
第一种方式比较简单粗暴,我们将自己开发的文件统一放在一个文件目录下,并命名为 ocpy 。并将该文件目录直接放入 Resources 目录中的 python 环境目录下,如下图所示
7.png同样的,我们把运行所依赖的其他第三方资源直接也放在该目录下,如 requests 库
8.png这种方式比较简单,但是同时将 Python环境自有的库、第三方库、和自己的库混合在一起不方便今后的管理。有没有更好的方式来实现当然有,我们可以通过设置 PYTHONPATH 来实现。
设置 PYTHONPATH
我们新建一个 PythonCon.bundle 资源文件,把我们自己开发的文件放入里面如下图所示
9.png在设置完 PythonHome 路径后,即可设置 PYTHONPATH 的路径为PythonCon.bundle
NSString *python_path = [NSString stringWithFormat:@"PYTHONPATH=%@/PythonCon.bundle/", resourcePath, nil];
putenv((char *)[python_path UTF8String]);
同样的我们可以用该方法设置我们所依赖的第三方库(此处未把第三方库从PythonEnv.bundle中独立出来)。
Python 类中方法的调用
模块的导入&类的创建
以上准备工作完成后,我们便具备对文件类中的方法进行调用了,首先我们需要引入我们需要调用的模块,然后获取该模块中所有的类和方法,获取并初始化我们需要的类
// 模块的引入
PyObject *pModule = PyImport_ImportModule([@"ocpy.main" UTF8String]);
// 获取模块中的类/方法
PyObject *pDict = PyModule_GetDict(pModule);
// 获取某个具体的e类/方法
PyObject *pClass = PyDict_GetItemString(pDict, "JDGFileObject");
// 创建&初始化该类
PyObject *pyClass = PyInstanceMethod_New(pClass);
_pyInstance = PyObject_CallObject(pyClass, NULL);
方法调用
我们可以使用 PyObject_CallMethod 方法来调用我们所创建类中的方法,PyObject_CallMethod 需要至少传入三个参数,
第一个即是我们刚刚初始化的类 _pyInstance,第二个参数为我们需要调用的该类中的方法,第三个及以后是调用该方法中需要传入的参数类型(如果没有可直接为 NULL)
例如我们需要调用 main 文件中的 test 方法,该方法需要两个字符串类型 arg1和arg2参数,并最终返回一个 "success" 结果,最终我们通过使用PyArg_Parse 从 result 对象中成功解析出我们所需要的内容 "success"。
PyObject *result = PyObject_CallMethod(_pyInstance, [func UTF8String], "(s,s)", [arg1 UTF8String], [arg2 UTF8String]);
char *resultCString = NULL;
PyArg_Parse(result, "s", &resultCString);
NSString *content = [NSString stringWithFormat:@"%s", resultCString];
NSLog(@"========%@", content);
以上便是整个py类设置调用的过程,完整的代码见文末
Pod 发布过程中的问题
当所有工作完成后,我们可以将该工具发布出去供他人使用,而公司内部也有成熟的组件管理工具,非常方便,我们只需要把相关模块发布到系统中,其他人就可以直接在他的模块中使用。我准备把这个检测工具包含 Python 和依赖的 OpenSSL、BZip2、XZ等通过该工具发布到系统中。
项目编译 Must define SIZEOF_WECHSR_T 问题
以前发布的组件都是比较常规类型的,没发布过这种依赖二进制文件类型的组件,因此也就没研究过这个工具的发布过程,没想到这一次发布就出现问题了
1.png该工具在发布时,有一个检验的过程,和工具维护人员沟通后了解到公司内部所发布的组件需要同时支持 armv7、arm64 、i386、 x86_64 这四种类型,而作者提供的默认 .a 只包含 arm64 和 x86_64 这两种类型,而我在做测试时一直使用的是 iphone6 及其以上的模拟器,因此一直没有出现这个错误,当我换成 iphone5模拟器去执行时,出现了下面这个错误提示
4.png和上面的错误日志是一样的,还好作者提供了编译脚本项目,通过查看作者提供的脚本 中makefile 文件,打包的时候只支持 arm64 和 x86_64 这两种类型,因此我们只需要对 makefile 进行简单的修改这样就可以解决这个问题。因为我只使用了iOS模块因此我只修改了该模块
2.png修改完成后在该文件的同目录下执行 make iOS 命令完成重新编译。support 目录下的文件就是我们需要的最新文件如下图所示
3.png发布 OpenSSL 问题
以上问题解决后,再次执行发布功能,这次出现了另外一个错误
6.png错误提示比较少,看不出具体问题,在不引入其他模块进行项目编译、发布等都是是正常的,和工具维护人员也进行了相关沟通,他也进行了尝试,也出现了相同的错误。
既然常规方式不行,只能通过各种其他方式进行问题排查,拆分各个模块,每次只发布其中一个模块,最后发现只有在发布 openssl 这个模块时出现了这个错误,其他模块单独和一起发布都是正常的,问题缩小就容易解决了,经过了解,openssl 这个模块已经有相关成功组件,那就是自己的原因了,询问了对方是如何进行发布操作的,但是他也不知道,最后把对方发布成功的包给要过来了,经过对比发现,我的文件比对方的要多几个 __DECC_INCLUDE_ 开头的文件,如下图
5.png看了下这些文件,里面无相关代码,只有一些注释,其中有一句 This file is only used by HP C on VMS, and is included automatically,既然在iOS里面用不到,那就直接删了(注意删除其他目录下类似文件),删除后再次发布,这次终于发布成功了。
完整的代码
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
NSLog(@"resourcePath === %@",resourcePath);
NSString *python_home;
putenv("PYTHONOPTIMIZE=1");
putenv("PYTHONDONTWRITEBYTECODE=1");
putenv("PYTHONUNBUFFERED=1");
// Set the home for the Python interpreter
python_home = [NSString stringWithFormat:@"%@/PythonEnv.bundle/Resources/", resourcePath, nil];
NSLog(@"PythonHome is: %@", python_home);
Py_SetPythonHome(Py_DecodeLocale([python_home UTF8String], NULL));
NSString *python_path = [NSString stringWithFormat:@"PYTHONPATH=%@/PythonCon.bundle/", resourcePath, nil];
putenv((char *)[python_path UTF8String]);
NSLog(@"PYTHONPATH is: %@", python_path);
NSLog(@"Initializing Python runtime...");
Py_Initialize();
PyEval_InitThreads();
// 模块的引入
PyObject *pModule = PyImport_ImportModule([@"ocpy.main" UTF8String]);
// 获取模块中的类/方法
PyObject *pDict = PyModule_GetDict(pModule);
// 获取某个具体的e类/方法
PyObject *pClass = PyDict_GetItemString(pDict, "JDGFileObject");
// 创建&初始化
PyObject *pyClass = PyInstanceMethod_New(pClass);
PyObject *pyInstance = PyObject_CallObject(pyClass, NULL);
// 方法的调用
PyObject *result = PyObject_CallMethod(pyInstance, [func UTF8String], "(s,s)", [arg1 UTF8String], [arg2 UTF8String]);
char *resultCString = NULL;
PyArg_Parse(result, "s", &resultCString);
NSString *content = [NSString stringWithFormat:@"%s", resultCString];
NSLog(@"========%@", content);
以上内容参考以下文章