1. 当看到一个在Android平台上运行的Python程序时,我的第一个好奇的地方就是它究竟是怎么做到的。
2. 好,费话少说,我们通过源码来分析一下。
3. 首先从dist/default导入工程,如图所示:
4. 接下来我们来理顺一下整个的Python程序的引导过程
5. PythonActivity的onCreate中:
mView =new SDLSurfaceView( this, mPath.getAbsolutePath()); Hardware.view =mView; setContentView(mView);
6. SDLSurfaceView实现了:SurfaceHolder.Callback,所以接下来SDLSurfaceView.surfaceChanged将被调用
7. 来看看surfaceChanged做了什么:
if (!mRunning) { mRunning =true; new Thread(this).start(); }else { mChanged =true; if (mStarted) { nativeExpose(); } }
从new Thread(this).start()我们知道会创建一个线程,在这个线程里,SDLSurfaceView的run方法将被调用
8. 接下来再看看SDLSurfaceView.run
这个函数主要是完成一些OPENGL的初始化工作,然后调用 waitForStart();
waitForStart()在等什么呢?答案是,在等待Python的初始化工作完成。
9. PythonActivity的onResume中:
if (!mLaunchedThread) { mLaunchedThread =true; new Thread(this).start(); } if (mView !=null) { mView.onResume(); }
该函数的主要作用就是创建了一个线程
另外,当PythonActivyt被创建的时候,mView.onResume什么事也没做。
10. 再来看看PythonActivity这个线程的run函数看看做了什么
public void run() { unpackData("private", getFilesDir()); unpackData("public",externalStorage); System.loadLibrary("sdl"); System.loadLibrary("sdl_image"); System.loadLibrary("sdl_ttf"); System.loadLibrary("sdl_mixer"); System.loadLibrary("python2.7"); System.loadLibrary("application"); System.loadLibrary("sdl_main"); System.load(getFilesDir() +"/lib/python2.7/lib-dynload/_io.so" ); System.load(getFilesDir() +"/lib/python2.7/lib-dynload/unicodedata.so" ); try { System.loadLibrary("sqlite3"); System.load(getFilesDir() +"/lib/python2.7/lib-dynload/_sqlite3.so" ); }catch(UnsatisfiedLinkError e) { } try { System.load(getFilesDir() +"/lib/python2.7/lib-dynload/_imaging.so" ); System.load(getFilesDir() +"/lib/python2.7/lib-dynload/_imagingft.so" ); System.load(getFilesDir() +"/lib/python2.7/lib-dynload/_imagingmath.so" ); }catch(UnsatisfiedLinkError e) { } if (mAudioThread ==null ) { Log.i("python","Starting audio thread"); mAudioThread =new AudioThread(this); } runOnUiThread(new Runnable () { public void run() { mView.start(); } }); }
首先是从private.mp3中解压出python的运行环境
unpackData("private", getFilesDir());
解压的路径是程序所在的data目录的files文件文件夹下
再解压出Kivy的代码
unpackData("public", externalStorage);
解压的路径是在内部SD卡中。
在这里主要要是加载了一堆的动态库,然后调用
runOnUiThread(new Runnable () { public void run() { mView.start(); } });
转到SDLSurfaceView.start中运行了。
11. 再来看看SDLSurfaceView.start做了什么:
this.setFocusableInTouchMode(true); this.setFocusable(true); this.requestFocus(); synchronized (this) { mStarted =true; this.notify(); }
这里我们比较关注的是:this.notify(),这样SDLSurfaceView.run里的waitForStart该返回了
12. 接着往下看SDLSurface.run里的代码:
nativeResize(mWidth,mHeight); nativeInitJavaCallbacks(); nativeSetEnv("ANDROID_PRIVATE",mFilesDirectory); nativeSetEnv("ANDROID_ARGUMENT",mArgument); nativeSetEnv("PYTHONOPTIMIZE","2"); nativeSetEnv("PYTHONHOME",mFilesDirectory); nativeSetEnv("PYTHONPATH",mArgument +":" +mFilesDirectory +"/lib"); //XXX Using SetOpenFile make a crash in nativeSetEnv. I don't // understand why, maybe because the method is static or something. // Anyway, if you remove that part of the code, ensure the Laucher // (ProjectChooser) is still working. final android.content.Intent intent =mActivity.getIntent(); if (intent !=null) { final android.net.Uri data = intent.getData(); if (data !=null && data.getEncodedPath() !=null) nativeSetEnv("PYTHON_OPENFILE", data.getEncodedPath()); } nativeSetMultitouchUsed(); nativeInit();
这里做了几件事:
设置Python的路径
设置Kivy代码所在的路径
最后调用native函数nativeInit即开始进行Python的初始化了。
13. 最后,来看下nativeInit做了什么动作
我们先找到src\jni\sdl_main下的sdl_main.c
extern C_LINKAGE void JAVA_EXPORT_NAME(SDLSurfaceView_nativeInit) ( JNIEnv* env, jobject thiz ) { int argc = 1; char * argv[] = { "sdl" }; main( argc, argv ); }; extern C_LINKAGE void JAVA_EXPORT_NAME(SDLSurfaceView_nativeIsSdcardUsed) ( JNIEnv* env, jobject thiz, jint flag ) { isSdcardUsed = flag; } extern C_LINKAGE void JAVA_EXPORT_NAME(SDLSurfaceView_nativeSetEnv) ( JNIEnv* env, jobject thiz, jstring j_name, jstring j_value ) { jboolean iscopy; const char *name = (*env)->GetStringUTFChars(env, j_name, &iscopy); const char *value = (*env)->GetStringUTFChars(env, j_value, &iscopy); setenv(name, value, 1); (*env)->ReleaseStringUTFChars(env, j_name, name); (*env)->ReleaseStringUTFChars(env, j_value, value); }
我们重点关注一下:SDLSurfaceView_nativeInit
JAVA_EXPORT_NAME(SDLSurfaceView_nativeInit) ( JNIEnv* env, jobject thiz ) { int argc = 1; char * argv[] = { "sdl" }; main( argc, argv ); };
这里的main是在哪里实现的呢?
先来看看sdl_main.c包含的头文件:sdl_main.h
#define main SDL_main
哦,原来这个main原名叫SDL_main
那么我们接下来搜索一下SDL_main在什么地方定义的吧,你会发现死活搜索不到
再来看下jni/sdl_main/Android.mk
LOCAL_SHARED_LIBRARIES := sdl application
这里引用了application这个动态库
application在哪里呢?我们找到src\jni\application\src\start.c,这里有一个main,但是我们要找的是SDL_main
我们接着看start.c引用的头文件SDL.h,在SDL.h里引用了SDL_main.h,而SDL_main.h里定义了:#define main SDL_main
嗯,很清楚了,start.c里的main就是SDL_main
最后,来看下main的代码,做了哪些工作:
int main(int argc, char **argv) { char *env_argument = NULL; int ret = 0; FILE *fd; LOG("Initialize Python for Android"); env_argument = getenv("ANDROID_ARGUMENT"); setenv("ANDROID_APP_PATH", env_argument, 1); //setenv("PYTHONVERBOSE", "2", 1); Py_SetProgramName(argv[0]); Py_Initialize(); PySys_SetArgv(argc, argv); /* ensure threads will work. */ PyEval_InitThreads(); /* our logging module for android */ initandroidembed(); /* inject our bootstrap code to redirect python stdin/stdout * replace sys.path with our path */ PyRun_SimpleString( "import sys, posix\n" \ "private = posix.environ['ANDROID_PRIVATE']\n" \ "argument = posix.environ['ANDROID_ARGUMENT']\n" \ "sys.path[:] = [ \n" \ " private + '/lib/python27.zip', \n" \ " private + '/lib/python2.7/', \n" \ " private + '/lib/python2.7/lib-dynload/', \n" \ " private + '/lib/python2.7/site-packages/', \n" \ " argument ]\n" \ "import androidembed\n" \ "class LogFile(object):\n" \ " def __init__(self):\n" \ " self.buffer = ''\n" \ " def write(self, s):\n" \ " s = self.buffer + s\n" \ " lines = s.split(\"\\n\")\n" \ " for l in lines[:-1]:\n" \ " androidembed.log(l)\n" \ " self.buffer = lines[-1]\n" \ " def flush(self):\n" \ " return\n" \ "sys.stdout = sys.stderr = LogFile()\n" \ "import site; print site.getsitepackages()\n"\ "print 'Android path', sys.path\n" \ "print 'Android kivy bootstrap done. __name__ is', __name__"); /* run it ! */ LOG("Run user program, change dir and execute main.py"); chdir(env_argument); /* search the initial main.py */ char *main_py = "main.pyo"; if ( file_exists(main_py) == 0 ) { if ( file_exists("main.py") ) main_py = "main.py"; else main_py = NULL; } if ( main_py == NULL ) { LOG("No main.pyo / main.py found."); return -1; } fd = fopen(main_py, "r"); if ( fd == NULL ) { LOG("Open the main.py(o) failed"); return -1; } /* run python ! */ ret = PyRun_SimpleFile(fd, main_py); if (PyErr_Occurred() != NULL) { ret = 1; PyErr_Print(); /* This exits with the right code if SystemExit. */ if (Py_FlushLine()) PyErr_Clear(); } /* close everything */ Py_Finalize(); fclose(fd); LOG("Python for android ended."); return ret; }
首先,当然是初始化Python:Py_Initialize()
接下来允许Python多线程:PyEval_InitThreads()
PyRun_SimpleString里运行的代码的作用是将Python的输出重定向到Android的log输出,这样我们就可以在logcat里看到Python的日志输出了。
最后,调用PyRun_SimpleFile运行Kivy代码
14. 好,整个过程讲完了,不算复杂,但是确实有些绕。
(完)