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. 好,整个过程讲完了,不算复杂,但是确实有些绕。
(完)