OpenGL—Android 开机动画源码分析二
引自http://blog.csdn.net/luoshengyang/article/details/7691321/
BootAnimation类的成员函数的实现比较长,我们分段来阅读:
第三个开机画面是由应用程序bootanimation来负责显示的。应用程序bootanimation在启动脚本init.rc中被配置成了一个服务,如下所示:
service bootanim /system/bin/bootanimation
user graphics
group graphics
disabled
oneshot
应用程序bootanimation的用户和用户组名称分别被设置为graphics。注意, 用来启动应用程序bootanimation的服务是disable的,即init进程在启动的时候,不会主动将应用程序bootanimation启动起来。当SurfaceFlinger服务启动的时候,它会通过修改系统属性ctl.start的值来通知init进程启动应用程序bootanimation,以便可以显示第三个开机画面,而当System进程将系统中的关键服务都启动起来之后,ActivityManagerService服务就会通知SurfaceFlinger服务来修改系统属性ctl.stop的值,以便可以通知init进程停止执行应用程序bootanimation,即停止显示第三个开机画面。接下来我们就分别分析第三个开机画面的显示过程和停止过程。
BootAnimation类间接地继承了RefBase类,并且重写了RefBase类的成员函数onFirstRef,因此,当一个BootAnimation对象第一次被智能指针引用的时,这个BootAnimation对象的成员函数onFirstRef就会被调用。
BootAnimation类的成员函数onFirstRef实现在文件frameworks/base/cmds/bootanimation/BootAnimation.cpp中,如下所示:
void BootAnimation::onFirstRef() {
status_t err = mSession->linkToComposerDeath(this);
LOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err));
if (err == NO_ERROR) {
run("BootAnimation", PRIORITY_DISPLAY);
}
}
mSession是BootAnimation类的一个成员变量,它的类型为SurfaceComposerClient,是用来和SurfaceFlinger执行Binder进程间通信的,它是在BootAnimation类的构造函数中创建的,如下所示:
BootAnimation::BootAnimation() : Thread(false)
{
mSession = new SurfaceComposerClient();
}
SurfaceComposerClient类内部有一个实现了ISurfaceComposerClient接口的Binder代理对象mClient,这个Binder代理对象引用了SurfaceFlinger服务,SurfaceComposerClient类就是通过它来和SurfaceFlinger服务通信的。
回到BootAnimation类的成员函数onFirstRef中,由于BootAnimation类引用了SurfaceFlinger服务,因此,当SurfaceFlinger服务意外死亡时,BootAnimation类就需要得到通知,这是通过调用成员变量mSession的成员函数linkToComposerDeath来注册SurfaceFlinger服务的死亡接收通知来实现的。
BootAnimation类继承了Thread类,因此,当BootAnimation类的成员函数onFirstRef调用了父类Thread的成员函数run之后,系统就会创建一个线程,这个线程在第一次运行之前,会调用BootAnimation类的成员函数readyToRun来执行一些初始化工作,后面再调用BootAnimation类的成员函数htreadLoop来显示第三个开机画面。
BootAnimation类的成员函数readyToRun的实现如下所示:
status_t BootAnimation::readyToRun() {
mAssets.addDefaultAssets(); DisplayInfo dinfo;
status_t status = session()->getDisplayInfo(, &dinfo);
if (status)
return -; // create the native surface
sp<SurfaceControl> control = session()->createSurface(
getpid(), , dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565);
session()->openTransaction();
control->setLayer(0x40000000);
session()->closeTransaction(); sp<Surface> s = control->getSurface(); // initialize opengl and egl
const EGLint attribs[] = {
EGL_DEPTH_SIZE, ,
EGL_NONE
};
EGLint w, h, dummy;
EGLint numConfigs;
EGLConfig config;
EGLSurface surface;
EGLContext context; EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(display, , );
EGLUtils::selectConfigForNativeWindow(display, attribs, s.get(), &config);
surface = eglCreateWindowSurface(display, config, s.get(), NULL);
context = eglCreateContext(display, config, NULL, NULL);
eglQuerySurface(display, surface, EGL_WIDTH, &w);
eglQuerySurface(display, surface, EGL_HEIGHT, &h); if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)
return NO_INIT; mDisplay = display;
mContext = context;
mSurface = surface;
mWidth = w;
mHeight = h;
mFlingerSurfaceControl = control;
mFlingerSurface = s; mAndroidAnimation = true;
if ((access(USER_BOOTANIMATION_FILE, R_OK) == ) &&
(mZip.open(USER_BOOTANIMATION_FILE) == NO_ERROR) ||
(access(SYSTEM_BOOTANIMATION_FILE, R_OK) == ) &&
(mZip.open(SYSTEM_BOOTANIMATION_FILE) == NO_ERROR))
mAndroidAnimation = false; return NO_ERROR;
}
BootAnimation类的成员函数session用来返回BootAnimation类的成员变量mSession所描述的一个SurfaceComposerClient对象。通过调用SurfaceComposerClient对象mSession的成员函数createSurface可以获得一个SurfaceControl对象control。
SurfaceComposerClient类的成员函数createSurface首先调用内部的Binder代理对象mClient来请求SurfaceFlinger返回一个类型为SurfaceLayer的Binder代理对象,接着再使用这个Binder代理对象来创建一个SurfaceControl对象。创建出来的SurfaceControl对象的成员变量mSurface就指向了从SurfaceFlinger返回来的类型为SurfaceLayer的Binder代理对象。有了这个Binder代理对象之后,SurfaceControl对象就可以和SurfaceFlinger服务通信了。
调用SurfaceControl对象control的成员函数getSurface会返回一个Surface对象s。这个Surface对象s内部也有一个类型为SurfaceLayer的Binder代理对象mSurface,这个Binder代理对象与前面所创建的SurfaceControl对象control的内部的Binder代理对象mSurface引用的是同一个SurfaceLayer对象。这样,Surface对象s也可以通过其内部的Binder代理对象mSurface来和SurfaceFlinger服务通信。
Surface类继承了ANativeWindow类。ANativeWindow类是连接OpenGL和Android窗口系统的桥梁,即OpenGL需要通过ANativeWindow类来间接地操作Android窗口系统。这种桥梁关系是通过EGL库来建立的,所有以egl为前缀的函数名均为EGL库提供的接口。
为了能够在OpenGL和Android窗口系统之间的建立一个桥梁,我们需要一个EGLDisplay对象display,一个EGLConfig对象config,一个EGLSurface对象surface,以及一个EGLContext对象context,其中,EGLDisplay对象display用来描述一个EGL显示屏,EGLConfig对象config用来描述一个EGL帧缓冲区配置参数,EGLSurface对象surface用来描述一个EGL绘图表面,EGLContext对象context用来描述一个EGL绘图上下文(状态),它们是分别通过调用egl库函数eglGetDisplay、EGLUtils::selectConfigForNativeWindow、eglCreateWindowSurface和eglCreateContext来获得的。注意,EGLConfig对象config、EGLSurface对象surface和EGLContext对象context都是用来描述EGLDisplay对象display的。有了这些对象之后,就可以调用函数eglMakeCurrent来设置当前EGL库所使用的绘图表面以及绘图上下文。
还有另外一个地方需要注意的是,每一个EGLSurface对象surface有一个关联的ANativeWindow对象。这个ANativeWindow对象是通过函数eglCreateWindowSurface的第三个参数来指定的。在我们这个场景中,这个ANativeWindow对象正好对应于前面所创建的 Surface对象s。每当OpenGL需要绘图的时候,它就会找到前面所设置的绘图表面,即EGLSurface对象surface。有了EGLSurface对象surface之后,就可以找到与它关联的ANativeWindow对象,即Surface对象s。有了Surface对象s之后,就可以通过其内部的Binder代理对象mSurface来请求SurfaceFlinger服务返回帧缓冲区硬件设备的一个图形访问接口。这样,OpenGL最终就可以将要绘制的图形渲染到帧缓冲区硬件设备中去,即显示在实际屏幕上。屏幕的大小,即宽度和高度,可以通过函数eglQuerySurface来获得。
BootAnimation类的成员变量mAndroidAnimation是一个布尔变量。当它的值等于true的时候,那么就说明需要显示的第三个开机画面是Android系统默认的开机动画,否则的话,第三个开机画面就是由用户自定义的开机动画。
自定义的开机动画是由文件USER_BOOTANIMATION_FILE或者文件SYSTEM_BOOTANIMATION_FILE来描述的。只要其中的一个文件存在,那么第三个开机画面就会使用用户自定义的开机动画。USER_BOOTANIMATION_FILE和SYSTEM_BOOTANIMATION_FILE均是一个宏,它们的定义如下所示:
#define USER_BOOTANIMATION_FILE "/data/local/bootanimation.zip"
#define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip"
这一步执行完成之后,用来显示第三个开机画面的线程的初始化工作就执行完成了,接下来,就会执行这个线程的主体函数,即BootAnimation类的成员函数threadLoop。
BootAnimation类的成员函数threadLoop的实现如下所示:
bool BootAnimation::threadLoop()
{
bool r;
if (mAndroidAnimation) {
r = android();
} else {
r = movie();
} eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(mDisplay, mContext);
eglDestroySurface(mDisplay, mSurface);
mFlingerSurface.clear();
mFlingerSurfaceControl.clear();
eglTerminate(mDisplay);
IPCThreadState::self()->stopProcess();
return r;
}
如果BootAnimation类的成员变量mAndroidAnimation的值等于true,那么接下来就会调用BootAnimation类的成员函数android来显示系统默认的开机动画,否则的话,就会调用BootAnimation类的成员函数movie来显示用户自定义的开机动画。显示完成之后,就会销毁前面所创建的EGLContext对象mContext、EGLSurface对象mSurface,以及EGLDisplay对象mDisplay等。
接下来,我们就分别分析BootAnimation类的成员函数android和movie的实现。
BootAnimation类的成员函数android的实现如下所示:
bool BootAnimation::android()
{
initTexture(&mAndroid[], mAssets, "images/android-logo-mask.png");
initTexture(&mAndroid[], mAssets, "images/android-logo-shine.png"); // clear screen
glShadeModel(GL_FLAT);
glDisable(GL_DITHER);
glDisable(GL_SCISSOR_TEST);
glClear(GL_COLOR_BUFFER_BIT);
eglSwapBuffers(mDisplay, mSurface); glEnable(GL_TEXTURE_2D);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); const GLint xc = (mWidth - mAndroid[].w) / ;
const GLint yc = (mHeight - mAndroid[].h) / ;
const Rect updateRect(xc, yc, xc + mAndroid[].w, yc + mAndroid[].h); // draw and update only what we need
mFlingerSurface->setSwapRectangle(updateRect); glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
updateRect.height()); // Blend state
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); const nsecs_t startTime = systemTime();
do {
nsecs_t now = systemTime();
double time = now - startTime;
float t = 4.0f * float(time / us2ns()) / mAndroid[].w;
GLint offset = ( - (t - floorf(t))) * mAndroid[].w;
GLint x = xc - offset; glDisable(GL_SCISSOR_TEST);
glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, mAndroid[].name);
glDrawTexiOES(x, yc, , mAndroid[].w, mAndroid[].h);
glDrawTexiOES(x + mAndroid[].w, yc, , mAndroid[].w, mAndroid[].h); glEnable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, mAndroid[].name);
glDrawTexiOES(xc, yc, , mAndroid[].w, mAndroid[].h); EGLBoolean res = eglSwapBuffers(mDisplay, mSurface);
if (res == EGL_FALSE) {
break;
} // 12fps: don't animate too fast to preserve CPU
const nsecs_t sleepTime = - ns2us(systemTime() - now);
if (sleepTime > )
usleep(sleepTime);
} while (!exitPending()); glDeleteTextures(, &mAndroid[].name);
glDeleteTextures(, &mAndroid[].name);
return false;
}
Android系统默认的开机动画是由两张图片android-logo-mask.png和android-logo-shine.png中。这两张图片保存在frameworks/base/core/res/assets/images目录中,它们最终会被编译在framework-res模块(frameworks/base/core/res)中,即编译在framework-res.apk文件中。编译在framework-res模块中的资源文件可以通过AssetManager类来访问。
BootAnimation类的成员函数android首先调用另外一个成员函数initTexture来将根据图片android-logo-mask.png和android-logo-shine.png的内容来分别创建两个纹理对象,这两个纹理对象就分别保存在BootAnimation类的成员变量mAndroid所描述的一个数组中。通过混合渲染这两个纹理对象,我们就可以得到一个开机动画,这是通过中间的while循环语句来实现的。
图片android-logo-mask.png用作动画前景,它是一个镂空的“ANDROID”图像。图片android-logo-shine.png用作动画背景,它的中间包含有一个高亮的呈45度角的条纹。在每一次循环中,图片android-logo-shine.png被划分成左右两部分内容来显示。左右两个部分的图像宽度随着时间的推移而此消彼长,这样就可以使得图片android-logo-shine.png中间高亮的条纹好像在移动一样。另一方面,在每一次循环中,图片android-logo-shine.png都作为一个整体来渲染,而且它的位置是恒定不变的。由于它是一个镂空的“ANDROID”图像,因此,我们就可以通过它的镂空来看到它背后的图片android-logo-shine.png的条纹一闪一闪地划过。
这个while循环语句会一直被执行,直到应用程序/system/bin/bootanimation被结束为止,后面我们再分析。
BootAnimation类的成员函数movie的实现比较长,我们分段来阅读:
bool BootAnimation::movie()
{
ZipFileRO& zip(mZip); size_t numEntries = zip.getNumEntries();
ZipEntryRO desc = zip.findEntryByName("desc.txt");
FileMap* descMap = zip.createEntryFileMap(desc);
LOGE_IF(!descMap, "descMap is null");
if (!descMap) {
return false;
} String8 desString((char const*)descMap->getDataPtr(),
descMap->getDataLength());
char const* s = desString.string(); Animation animation; // Parse the description file
for (;;) {
const char* endl = strstr(s, "\n");
if (!endl) break;
String8 line(s, endl - s);
const char* l = line.string();
int fps, width, height, count, pause;
char path[];
if (sscanf(l, "%d %d %d", &width, &height, &fps) == ) {
//LOGD("> w=%d, h=%d, fps=%d", fps, width, height);
animation.width = width;
animation.height = height;
animation.fps = fps;
}
if (sscanf(l, "p %d %d %s", &count, &pause, path) == ) {
//LOGD("> count=%d, pause=%d, path=%s", count, pause, path);
Animation::Part part;
part.count = count;
part.pause = pause;
part.path = path;
animation.parts.add(part);
}
s = ++endl;
}
从前面BootAnimation类的成员函数readyToRun的实现可以知道,如果目标设备上存在压缩文件/data/local/bootanimation.zip,那么BootAnimation类的成员变量mZip就会指向它,否则的话,就会指向目标设备上的压缩文件/system/media/bootanimation.zip。无论BootAnimation类的成员变量mZip指向的是哪一个压缩文件,这个压缩文件都必须包含有一个名称为“desc.txt”的文件,用来描述用户自定义的开机动画是如何显示的。
文件desc.txt的内容格式如下面的例子所示:
p part1
p part2
第一行的三个数字分别表示开机动画在屏幕中的显示宽度、高度以及帧速(fps)。剩余的每一行都用来描述一个动画片断,这些行必须要以字符“p”来开头,后面紧跟着两个数字以及一个文件目录路径名称。第一个数字表示一个片断的循环显示次数,如果它的值等于0,那么就表示无限循环地显示该动画片断。第二个数字表示每一个片断在两次循环显示之间的时间间隔。这个时间间隔是以一个帧的时间为单位的。文件目录下面保存的是一系列png文件,这些png文件会被依次显示在屏幕中。
以上面这个desct.txt文件的内容为例,它描述了一个大小为600 x 480的开机动画,动画的显示速度为24帧每秒。这个开机动画包含有两个片断part1和part2。片断part1只显示一次,它对应的png图片保存在目录part1中。片断part2无限循环地显示,其中,每两次循环显示的时间间隔为10 x (1 / 24)秒,它对应的png图片保存在目录part2中。
上面的for循环语句分析完成desc.txt文件的内容后,就得到了开机动画的显示大小、速度以及片断信息。这些信息都保存在Animation对象animation中,其中,每一个动画片断都使用一个Animation::Part对象来描述,并且保存在Animation对象animation的成员变量parts所描述的一个片断列表中。
接下来,BootAnimation类的成员函数movie再断续将每一个片断所对应的png图片读取出来,如下所示:
// read all the data structures
const size_t pcount = animation.parts.size();
for (size_t i= ; i<numEntries ; i++) {
char name[];
ZipEntryRO entry = zip.findEntryByIndex(i);
if (zip.getEntryFileName(entry, name, ) == ) {
const String8 entryName(name);
const String8 path(entryName.getPathDir());
const String8 leaf(entryName.getPathLeaf());
if (leaf.size() > ) {
for (int j= ; j<pcount ; j++) {
if (path == animation.parts[j].path) {
int method;
// supports only stored png files
if (zip.getEntryInfo(entry, &method, , , , , )) {
if (method == ZipFileRO::kCompressStored) {
FileMap* map = zip.createEntryFileMap(entry);
if (map) {
Animation::Frame frame;
frame.name = leaf;
frame.map = map;
Animation::Part& part(animation.parts.editItemAt(j));
part.frames.add(frame);
}
}
}
}
}
}
}
}
每一个png图片都表示一个动画帧,使用一个Animation::Frame对象来描述,并且保存在对应的Animation::Part对象的成员变量frames所描述的一个帧列表中。
获得了开机动画的所有信息之后,接下来BootAnimation类的成员函数movie就准备开始显示开机动画了,如下所示:
// clear screen
glShadeModel(GL_FLAT);
glDisable(GL_DITHER);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glClear(GL_COLOR_BUFFER_BIT); eglSwapBuffers(mDisplay, mSurface); glBindTexture(GL_TEXTURE_2D, );
glEnable(GL_TEXTURE_2D);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); const int xc = (mWidth - animation.width) / ;
const int yc = ((mHeight - animation.height) / );
nsecs_t lastFrame = systemTime();
nsecs_t frameDuration = s2ns() / animation.fps; Region clearReg(Rect(mWidth, mHeight));
clearReg.subtractSelf(Rect(xc, yc, xc+animation.width, yc+animation.height));
前面的一系列gl函数首先用来清理屏幕,接下来的一系列gl函数用来设置OpenGL的纹理显示方式。
变量xc和yc的值用来描述开机动画的显示位置,即需要在屏幕中间显示开机动画,另外一个变量frameDuration的值用来描述每一帧的显示时间,它是以纳秒为单位的。
Region对象clearReg用来描述屏幕中除了开机动画之外的其它区域,它是用整个屏幕区域减去开机动画所点据的区域来得到的。
准备好开机动画的显示参数之后,最后就可以执行显示开机动画的操作了,如下所示:
for (int i= ; i<pcount && !exitPending() ; i++) {
const Animation::Part& part(animation.parts[i]);
const size_t fcount = part.frames.size();
glBindTexture(GL_TEXTURE_2D, ); for (int r= ; !part.count || r<part.count ; r++) {
for (int j= ; j<fcount && !exitPending(); j++) {
const Animation::Frame& frame(part.frames[j]); if (r > ) {
glBindTexture(GL_TEXTURE_2D, frame.tid);
} else {
if (part.count != ) {
glGenTextures(, &frame.tid);
glBindTexture(GL_TEXTURE_2D, frame.tid);
glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
initTexture(
frame.map->getDataPtr(),
frame.map->getDataLength());
} if (!clearReg.isEmpty()) {
Region::const_iterator head(clearReg.begin());
Region::const_iterator tail(clearReg.end());
glEnable(GL_SCISSOR_TEST);
while (head != tail) {
const Rect& r(*head++);
glScissor(r.left, mHeight - r.bottom,
r.width(), r.height());
glClear(GL_COLOR_BUFFER_BIT);
}
glDisable(GL_SCISSOR_TEST);
}
glDrawTexiOES(xc, yc, , animation.width, animation.height);
eglSwapBuffers(mDisplay, mSurface); nsecs_t now = systemTime();
nsecs_t delay = frameDuration - (now - lastFrame);
lastFrame = now;
long wait = ns2us(frameDuration);
if (wait > )
usleep(wait);
}
usleep(part.pause * ns2us(frameDuration));
} // free the textures for this part
if (part.count != ) {
for (int j= ; j<fcount ; j++) {
const Animation::Frame& frame(part.frames[j]);
glDeleteTextures(, &frame.tid);
}
}
} return false;
}
第一层for循环用来显示每一个动画片断,第二层的for循环用来循环显示每一个动画片断,第三层的for循环用来显示每一个动画片断所对应的png图片。这些png图片以纹理的方式来显示在屏幕中。
注意,如果一个动画片断的循环显示次数不等于1,那么就说明这个动画片断中的png图片需要重复地显示在屏幕中。由于每一个png图片都需要转换为一个纹理对象之后才能显示在屏幕中,因此,为了避免重复地为同一个png图片创建纹理对象,第三层的for循环在第一次显示一个png图片的时候,会调用函数glGenTextures来为这个png图片创建一个纹理对象,并且将这个纹理对象的名称保存在对应的Animation::Frame对象的成员变量tid中,这样,下次再显示相同的图片时,就可以使用前面已经创建好了的纹理对象,即调用函数glBindTexture来指定当前要操作的纹理对象。
如果Region对象clearReg所包含的区域不为空,那么在调用函数glDrawTexiOES和eglSwapBuffers来显示每一个png图片之前,首先要将它所包含的区域裁剪掉,避免开机动画可以显示在指定的位置以及大小中。
每当显示完成一个png图片之后,都要将变量frameDuration的值从纳秒转换为毫秒。如果转换后的值大小于,那么就需要调用函数usleep函数来让线程睡眠一下,以保证每一个png图片,即每一帧动画都按照预先指定好的速度来显示。注意,函数usleep指定的睡眠时间只能精确到毫秒,因此,如果预先指定的帧显示时间小于1毫秒,那么BootAnimation类的成员函数movie是无法精确地控制地每一帧的显示时间的。
还有另外一个地方需要注意的是,每当循环显示完成一个片断时,需要调用usleep函数来使得线程睡眠part.pause * ns2us(frameDuration)毫秒,以便可以按照预先设定的节奏来显示开机动画。
最后一个if语句判断一个动画片断是否是循环显示的,即循环次数不等于1。如果是的话,那么就说明前面为它所对应的每一个png图片都创建过一个纹理对象。现在既然这个片断的显示过程已经结束了,因此,就需要释放前面为它所创建的纹理对象。
至此,第三个开机画面的显示过程就分析完成了。
OpenGL—Android 开机动画源码分析二的更多相关文章
- OpenGL—Android 开机动画源码分析一
.1 Android开机动画实现方式目前实现Android开机动画的方式主要是逐帧动画和OpenGL动画. ?逐帧动画 逐帧动画是一种常见的动画形式(Frame By Frame),其原理是在“连续的 ...
- Android 开机动画源码分析
Android系统在启动SystemServer进程时,通过两个阶段来启动系统所有服务,在第一阶段启动本地服务,如SurfaceFlinger,SensorService等,在第二阶段则启动一系列的J ...
- Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题
4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...
- 框架-springmvc源码分析(二)
框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...
- Tomcat源码分析二:先看看Tomcat的整体架构
Tomcat源码分析二:先看看Tomcat的整体架构 Tomcat架构图 我们先来看一张比较经典的Tomcat架构图: 从这张图中,我们可以看出Tomcat中含有Server.Service.Conn ...
- 十、Spring之BeanFactory源码分析(二)
Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...
- Vue源码分析(二) : Vue实例挂载
Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...
- 多线程之美8一 AbstractQueuedSynchronizer源码分析<二>
目录 AQS的源码分析 该篇主要分析AQS的ConditionObject,是AQS的内部类,实现等待通知机制. 1.条件队列 条件队列与AQS中的同步队列有所不同,结构图如下: 两者区别: 1.链表 ...
- Android 属性动画 源码解析 深入了解其内部实现
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/42056859,本文出自:[张鸿洋的博客] 我参加了博客之星评选,如果你喜欢我的博 ...
随机推荐
- reshape2 数据操作 数据融合 (melt)
前面一篇讲了cast,想必已经见识到了reshape2的强大,当然在使用cast时配合上melt这种强大的揉数据能力才能表现的淋漓尽致. 下面我们来看下,melt这个函数以及它的特点. melt(da ...
- cmstop核心
控制器在什么时候实例化? ---每发送一次请求,都要从入口文件开始一步步往下走 index.php入口文件-->到第二个文件(核心文件cmstop.php)中实例化controller类核心文 ...
- flex安装debug
1.解决 不是最新版本的问题: cmd-->regedit-->HKEY_LOCAL_MATCHING-->SOFTWARE-->MACROMEDIA -->FLASHP ...
- delphi 集合的用法
http://blog.sina.com.cn/s/blog_9e2e8405010180jy.html delphi基础补充 1 开域语句 在面向对象的程序代码中,嵌套对象的现象十分 ...
- spring 集成mongo配置
spring继承Mongo使用的是spring-data. 如果需要加入两个mongo与spring集成的包,spring-data-commons-1.7.0.RELEASE.jar,spring- ...
- hudson
来源: hudson入门与实战 http://www.360doc.com/content/15/0304/22/12144668_452603921.shtml Hudson安装配置.部署应用及分析 ...
- 基于python的tagcloud
setp1: 安装jieba,pytagcloud pip install jieba apt-get install python-pygame pip install simplejson pip ...
- 十二、oracle 数据库(表)的逻辑备份与恢复
一.介绍逻辑备份是指使用工具export将数据对象的结构和数据导出到文件的过程.逻辑恢复是指当数据库对象被误操作而损坏后使用工具import利用备份的文件把数据对象导入到数据库的过程.物理备份即可在数 ...
- nodejs【伪】入门教程
声明: 本文适合白的不能再白的小白 不要被标题误导,本文不会讲nodejs基础,只是本人学习流程和资料的一个整合 如果想找一大堆教程自己看,没有电梯,自己拉到文章最下方吧 一.nodejs是什么 ...
- 使用node.js创建第一个应用
node.js应用组成部分 1.引入require模块:我们可以使用require指令来载入Node.js模块. 2.创建服务器:服务器可以监听客户端的请求,类似于Apache,Nninx等HTTP服 ...