Kivy A to Z -- Kivy的运行机制

时间:2022-11-18 15:57:52

 1. 当看到一个在Android平台上运行的Python程序时,我的第一个好奇的地方就是它究竟是怎么做到的。

2. 好,费话少说,我们通过源码来分析一下。

3. 首先从dist/default导入工程,如图所示:

 

Kivy A to Z -- Kivy的运行机制

 

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

 

(完)