Android Framework中的线程Thread及它的threadLoop方法

在Framework中的Thread普遍的特点就是有一个 threadLoop方法。它到底是怎么循环起来的。

Android中java世界的Thread

先来看看java是怎么创建一个线程的。这个是最舒服的,也是我最熟悉的。

new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stub...}
}).start();

当然,你也可以在android中创建一个消息循环的HandlerThread

HandlerThread mThread = new HandlerThread("test");
mThread.start();
Handler mHandler = new Handler(mThread.getLooper()){@Overridepublic void handleMessage(Message msg) {// TODO Auto-generated method stubsuper.handleMessage(msg);}
};

上面中通过mHandler发送消息就可以在mThread中处理了,并且这个mThread不是UIThread,不会阻塞主线程。

Linux下c语言的Thread

java世界的Thread很方便,那么c呢?
Android基于linux所以,多线程编程也应该基于linux下的多线程。linux下的c语言用pthread。大家可以看这篇文章。
linux下C/C++,多线程pthread
我把里面的例子改良了一下
test.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *test(void *ptr)
{int i;for(i=0;i<8;i++){printf("the pthread running ,count: %d\n",i);sleep(1); }}int main(void)
{pthread_t pId;int i,ret;ret = pthread_create(&pId,NULL,test,NULL);if(ret != 0){printf("create pthread error!\n");exit(1);}for(i=0;i < 5;i++){printf("main thread running ,count : %d\n",i);sleep(1);}printf("main thread will exit when pthread is over\n");pthread_join(pId,NULL);printf("main thread  exit\n");return0;}

然后编译

gcc -o test test.c -lpthread./test

运行结果如下:

main thread running ,count : 0the pthread running ,count: 0
main thread running ,count : 1the pthread running ,count: 1
main thread running ,count : 2the pthread running ,count: 2
main thread running ,count : 3the pthread running ,count: 3
main thread running ,count : 4the pthread running ,count: 4
main thread will exit when pthread isoverthe pthread running ,count: 5the pthread running ,count: 6the pthread running ,count: 7
main thread  exit

例子比较简单,主要是创建一个线程,然后主线程等待子线程运行完毕再退出。

Android Framework中的Thread

下面焦点回到文章的主题当中,我们来看看Framework中常用的Thread是个何种形态。
先看看活生生的例子。
在源码中搜索threadLoop,当然也可以搜索thread,然后随便挑选一个Thread子类进行研究。这里挑了
/frameworks/av/services/audioflinger/AudioWatchdog.h

#ifndef AUDIO_WATCHDOG_H
#define AUDIO_WATCHDOG_H
#include <time.h>
#include <utils/Thread.h>namespace android {......class AudioWatchdog : public Thread {public:AudioWatchdog(unsigned periodMs = 50) : Thread(false/*canCallJava*/), mPaused(false),mPeriodNs(periodMs * 1000000), mMaxCycleNs(mPeriodNs * 2),// mOldTs// mLogTs initialized belowmOldTsValid(false), mUnderruns(0), mLogs(0), mDump(&mDummyDump){#define MIN_TIME_BETWEEN_LOGS_SEC 60// force an immediate log on first underrunmLogTs.tv_sec = MIN_TIME_BETWEEN_LOGS_SEC;mLogTs.tv_nsec = 0;}virtual         ~AudioWatchdog() { }// Do not call Thread::requestExitAndWait() without first calling requestExit().// Thread::requestExitAndWait() is not virtual, and the implementation doesn't do enough.virtualvoid        requestExit();// FIXME merge API and implementation with AudioTrackThreadvoid            pause();        // suspend thread from execution at next loop boundaryvoid            resume();       // allow thread to execute, if not requested to exit// Where to store the dump, or NULL to not updatevoid            setDump(AudioWatchdogDump* dump);private:virtual bool threadLoop();Mutex           mMyLock;        // Thread::mLock is privateCondition       mMyCond;        // Thread::mThreadExitedCondition is privatebool            mPaused;        // whether thread is currently paused......
};}   // namespace android#endif  // AUDIO_WATCHDOG_H

我们可以看到AudioWatchDog确实是Thread的子类,那好,下面看实现。
/frameworks/av/services/audioflinger/AudioWatchdog.cpp

#define LOG_TAG "AudioWatchdog" //#define LOG_NDEBUG 0
#include <utils/Log.h>
#include "AudioWatchdog.h"
namespace android {void AudioWatchdogDump::dump(int fd)
{char buf[32];if (mMostRecent != 0) {// includes NUL terminatorctime_r(&mMostRecent, buf);} else {strcpy(buf, "N/A\n");}fdprintf(fd, "Watchdog: underruns=%u, logs=%u, most recent underrun log at %s",mUnderruns, mLogs, buf);
}bool AudioWatchdog::threadLoop()
{{AutoMutex _l(mMyLock);if (mPaused) {mMyCond.wait(mMyLock);// ignore previous timestamp after resume()mOldTsValid = false;// force an immediate log on first underrun after resume()mLogTs.tv_sec = MIN_TIME_BETWEEN_LOGS_SEC;mLogTs.tv_nsec = 0;// caller will check for exitPending()returntrue;}}struct timespec newTs;int rc = clock_gettime(CLOCK_MONOTONIC, &newTs);if (rc != 0) {pause();returnfalse;}if (!mOldTsValid) {mOldTs = newTs;mOldTsValid = true;returntrue;}time_t sec = newTs.tv_sec - mOldTs.tv_sec;long nsec = newTs.tv_nsec - mOldTs.tv_nsec;if (nsec < 0) {--sec;nsec += 1000000000;}mOldTs = newTs;// cycleNs is same as sec*1e9 + nsec, but limited to about 4 secondsuint32_t cycleNs = nsec;if (sec > 0) {if (sec < 4) {cycleNs += sec * 1000000000;} else {cycleNs = 4000000000u;}}mLogTs.tv_sec += sec;if ((mLogTs.tv_nsec += nsec) >= 1000000000) {mLogTs.tv_sec++;mLogTs.tv_nsec -= 1000000000;}if (cycleNs > mMaxCycleNs) {mDump->mUnderruns = ++mUnderruns;if (mLogTs.tv_sec >= MIN_TIME_BETWEEN_LOGS_SEC) {mDump->mLogs = ++mLogs;mDump->mMostRecent = time(NULL);ALOGW("Insufficient CPU for load: expected=%.1f actual=%.1f ms; underruns=%u logs=%u",mPeriodNs * 1e-6, cycleNs * 1e-6, mUnderruns, mLogs);mLogTs.tv_sec = 0;mLogTs.tv_nsec = 0;}}struct timespec req;req.tv_sec = 0;req.tv_nsec = mPeriodNs;rc = nanosleep(&req, NULL);if (!((rc == 0) || (rc == -1 && errno == EINTR))) {pause();returnfalse;}returntrue;
}void AudioWatchdog::requestExit()
{// must be in this order to avoid a race conditionThread::requestExit();resume();
}void AudioWatchdog::pause()
{AutoMutex _l(mMyLock);mPaused = true;
}void AudioWatchdog::resume()
{AutoMutex _l(mMyLock);if (mPaused) {mPaused = false;mMyCond.signal();}
}void AudioWatchdog::setDump(AudioWatchdogDump *dump)
{mDump = dump != NULL ? dump : &mDummyDump;
}}   // namespace android

很明显,它的核心方法就是threadLoop(),但是它是怎么启动的呢?又是怎么循环运行的呢?带着疑问我又在源码中搜索关键字AudioWatchdog
结果发现有两个地方引用了。

/frameworks/av/services/audioflinger/AudioFlinger.h
/frameworks/av/services/audioflinger/AudioFlinger.cpp

在AudioFlinger.h中MixerThread中有个AudioWatchdog的sp对象

 class MixerThread : public PlaybackThread {public:MixerThread (const sp<AudioFlinger>& audioFlinger,AudioStreamOut* output,audio_io_handle_t id,audio_devices_t device,type_t type = MIXER);virtual             ~MixerThread();protected:AudioMixer* mAudioMixer;    // normal mixerprivate:sp<AudioWatchdog> mAudioWatchdog; // non-0 if there is an audio watchdog thread};

我们再看代码
/frameworks/av/services/audioflinger/AudioFlinger.cpp

AudioFlinger::MixerThread::MixerThread(const sp<AudioFlinger>& audioFlinger, AudioStreamOut* output,audio_io_handle_t id, audio_devices_t device, type_t type):PlaybackThread(audioFlinger, output, id, device, type),// mAudioMixer below// mFastMixer belowmFastMixerFutex(0)// mOutputSink below// mPipeSink below// mNormalSink below
{......
#ifdef AUDIO_WATCHDOG// create and start the watchdogmAudioWatchdog =new AudioWatchdog();mAudioWatchdog->setDump(&mAudioWatchdogDump);mAudioWatchdog->run("AudioWatchdog", PRIORITY_URGENT_AUDIO);tid = mAudioWatchdog->getTid();err = requestPriority(getpid_cached, tid, kPriorityFastMixer);if (err !=0) {ALOGW("Policy SCHED_FIFO priority %d is unavailable for pid %d tid %d; error %d",kPriorityFastMixer, getpid_cached, tid, err);}
#endif......
}

删掉不相关代码,我们看到AudioWatchdog对象确实创建了,并且调用了它的run方法。在java中Thread的run方法就是启动, 这个也应该如此。但是如之前的源码所示AudioWatchdog.cpp中并没有实现run方法,怎么办呢?别紧张,它还有父类Thread.

/frameworks/native/include/utils/Thread.h

#ifndef _LIBS_UTILS_THREAD_H
#define _LIBS_UTILS_THREAD_H
#include <stdint.h>
#include <sys/types.h>
#include <time.h>
#if defined(HAVE_PTHREADS)
# include <pthread.h>
#endif
#include <utils/Condition.h>
#include <utils/Errors.h>
#include <utils/Mutex.h>
#include <utils/RefBase.h>
#include <utils/Timers.h>
#include <utils/ThreadDefs.h>// ---------------------------------------------------------------------------
namespace android {
// ---------------------------------------------------------------------------class Thread : virtual public RefBase
{
public:// Create a Thread object, but doesn't create or start the associated// thread. See the run() method.Thread(bool canCallJava = true);virtual             ~Thread();// Start the thread in threadLoop() which needs to be implemented.virtual status_t  run(    constchar* name = 0, int32_t priority = PRIORITY_DEFAULT, size_t stack = 0);// Ask this object's thread to exit. This function is asynchronous, when the// function returns the thread might still be running. Of course, this// function can be called from a different thread.virtualvoid        requestExit();// Good place to do one-time initializationsvirtual status_t    readyToRun();// Call requestExit() and wait until this object's thread exits.// BE VERY CAREFUL of deadlocks. In particular, it would be silly to call// this function from this object's thread. Will return WOULD_BLOCK in// that case.status_t    requestExitAndWait();// Wait until this object's thread exits. Returns immediately if not yet running.// Do not call from this object's thread; will return WOULD_BLOCK in that case.status_t    join();#ifdef HAVE_ANDROID_OS// Return the thread's kernel ID, same as the thread itself calling gettid() or// androidGetTid(), or -1 if the thread is not running.pid_t       getTid() const;
#endifprotected:// exitPending() returns trueifrequestExit() has been called.boolexitPending() const;private:// Derived class must implement threadLoop(). The thread starts its life// here. There are two ways of using the Thread object:// 1) loop: if threadLoop() returns true, it will be called again if//          requestExit() wasn't called.// 2) once: if threadLoop() returns false, the thread will exit upon return.virtual bool threadLoop() = 0;private:Thread& operator=(const Thread&);staticint             _threadLoop(void* user);constbool            mCanCallJava;// always hold mLock when reading or writingthread_id_t     mThread;mutable Mutex           mLock;Condition       mThreadExitedCondition;status_t        mStatus;// note that all accesses of mExitPending and mRunning need to hold mLockvolatilebool           mExitPending;volatilebool           mRunning;sp<Thread>      mHoldSelf;
#ifdef HAVE_ANDROID_OS// legacy for debugging, not used by getTid() as it is set by the child thread// and so is not initialized until the child reaches that pointpid_t           mTid;
#endif
};}; // namespace android// ---------------------------------------------------------------------------#endif // _LIBS_UTILS_THREAD_H// 

可以看到确实有run方法。
status_t Thread::run(const char* name, int32_t priority, size_t stack)
{
Mutex::Autolock _l(mLock);

if (mRunning) {// thread already startedreturn INVALID_OPERATION;
}// reset status and exitPending to their default value, so we can
//try again after an error happened (either below, orin readyToRun())
mStatus = NO_ERROR;
mExitPending = false;
mThread = thread_id_t(-1);// hold a strong reference on ourself
mHoldSelf = this;mRunning = true;bool res;
if (mCanCallJava) {res = createThreadEtc(_threadLoop,this, name, priority, stack, &mThread);
} else {res = androidCreateRawThreadEtc(_threadLoop,this, name, priority, stack, &mThread);
}if (res == false) {mStatus = UNKNOWN_ERROR;   // something happened!mRunning = false;mThread = thread_id_t(-1);mHoldSelf.clear();  //"this" may have gone away after this.return UNKNOWN_ERROR;
}// Do not refer to mStatus here: The thread is already running (may, in fact
// already have exited with a valid mStatus result). The NO_ERROR indication
// here merely indicates successfully starting the thread and does not// imply successful termination/execution.
return NO_ERROR;// Exiting scope of mLock is a memory barrier and allows new thread to run

}

run()方法中有这么一段

if (mCanCallJava) {res = createThreadEtc(_threadLoop,this, name, priority, stack, &mThread);} else {res = androidCreateRawThreadEtc(_threadLoop,this, name, priority, stack, &mThread);}

mCanCallJava的意思是能不能被JNI层调用,然后根据值去创建Thread,这里有两个分支,我们就选择createThreadEtc()
最终代码会走到这里

int androidCreateRawThreadEtc(android_thread_func_t entryFunction,void *userData,const char* threadName,int32_t threadPriority,size_t threadStackSize,android_thread_id_t *threadId)
{......entryFunction = (android_thread_func_t)&thread_data_t::trampoline;userData = t;}
#endifif (threadStackSize) {pthread_attr_setstacksize(&attr, threadStackSize);}errno = 0;pthread_t thread;int result = pthread_create(&thread, &attr,(android_pthread_entry)entryFunction, userData);pthread_attr_destroy(&attr);if (result != 0) {ALOGE("androidCreateRawThreadEtc failed (entry=%p, res=%d, errno=%d)\n""(android threadPriority=%d)",entryFunction, result, errno, threadPriority);return0;}......return1;
}

删除了不相关代码,大家看看是不是很熟悉啊。我在文章开始的部分就写出了linux下c语言pthread创建线程的例子,大家可以回头看看。也就 是pthread_create()。这里面传进来的entryFunction是Thread中的_threadLoop()

int Thread::_threadLoop(void* user)
{Thread* const self= static_cast<Thread*>(user);sp<Thread> strong(self->mHoldSelf);wp<Thread> weak(strong);self->mHoldSelf.clear();#ifdef HAVE_ANDROID_OS// this is very useful for debugging with gdbself->mTid = gettid();
#endifbool first =true;do {bool result;if (first) {first =false;self->mStatus =self->readyToRun();result = (self->mStatus == NO_ERROR);if (result &&!self->exitPending()) {// Binder threads (and maybe others) rely on threadLoop// running at least once after a successful ::readyToRun()// (unless, of course, the thread has already been asked to exit// at that point).// This is because threads are essentially used like this://   (new ThreadSubclass())->run();// The caller therefore does not retain a strong reference to// the thread and the thread would simply disappear after the// successful ::readyToRun() call instead of entering the// threadLoop at least once.result =self->threadLoop();}} else {result =self->threadLoop();}// establish a scope for mLock{Mutex::Autolock _l(self->mLock);if (result ==false||self->mExitPending) {self->mExitPending =true;self->mRunning =false;// clear thread ID so that requestExitAndWait() does not exit if// called by a new thread using the same thread ID as this one.self->mThread = thread_id_t(-1);// note that interested observers blocked in requestExitAndWait are// awoken by broadcast, but blocked on mLock until break exits scopeself->mThreadExitedCondition.broadcast();break;}}// Release our strong reference, to let a chance to the thread// to die a peaceful death.strong.clear();// And immediately, re-acquire a strong reference for the next loopstrong = weak.promote();} while(strong !=0);return0;
}

_threadLoop()这个方法就是Thread的最大秘密,它是一个while循环。
1、创建线程时,会sp和wp一次线程本身。
2、如果是第一次执行会运行线程的readyToRun()方法,再执行threadLoop(),否则,直接运行threadLoop()。
3、threadLoop()方法有返回值,如果threadLoop()返回false的时候,线程会做清理工作,然后退出while循环,结束运行。

所以在这里,我开始时的疑问—为什么线程Thread中的threadLoop()能够循环处理数据就到此做了说明。Thread被创 建,Thread中的run被调用,__threadLoop()被调用,readyToRun()被调用,然后循环调用threadLoop()。并且 在threadLoop()返回false时,可以退出循环。

特殊情况

有的时候Android Framework中Thread的run()方法很难发现在哪里被调用。如SurfaceFlinger它也是一个Thread子类。在源码中搜索可以发现它的创建位置

class SurfaceFlinger : public BinderService<SurfaceFlinger>,public BnSurfaceComposer,private IBinder::DeathRecipient,private Thread,private HWComposer::EventHandler
{
public:staticcharconst* getServiceName() {return"SurfaceFlinger";}SurfaceFlinger();/* ------------------------------------------------------------------------* Thread interface*/virtualbool threadLoop();virtual status_t readyToRun();virtualvoid onFirstRef();};// ---------------------------------------------------------------------------
}; // namespace android#endif // ANDROID_SURFACE_FLINGER_H

去找它创建的地方
/frameworks/base/cmds/system_server/library/system_init.cpp

extern"C" status_t system_init()
{ALOGI("Entered system_init()");sp<ProcessState> proc(ProcessState::self());sp<IServiceManager> sm = defaultServiceManager();ALOGI("ServiceManager: %p\n", sm.get());char propBuf[PROPERTY_VALUE_MAX];property_get("system_init.startsurfaceflinger", propBuf, "1");if (strcmp(propBuf, "1") == 0) {// Start the SurfaceFlingerSurfaceFlinger::instantiate();}// And now start the Android runtime.  We have to do this bit// of nastiness because the Android runtime initialization requires// some of the core system services to already be started.// All other servers should just start the Android runtime at// the beginning of their processes's main(), before calling// the init function.ALOGI("System server: starting Android runtime.\n");AndroidRuntime* runtime = AndroidRuntime::getRuntime();ALOGI("System server: starting Android services.\n");JNIEnv* env = runtime->getJNIEnv();if (env == NULL) {return UNKNOWN_ERROR;}jclass clazz = env->FindClass("com/android/server/SystemServer");if (clazz == NULL) {return UNKNOWN_ERROR;}jmethodID methodId = env->GetStaticMethodID(clazz, "init2", "()V");if (methodId == NULL) {return UNKNOWN_ERROR;}env->CallStaticVoidMethod(clazz, methodId);ALOGI("System server: entering thread pool.\n");ProcessState::self()->startThreadPool();IPCThreadState::self()->joinThreadPool();ALOGI("System server: exiting thread pool.\n");return NO_ERROR;
}

我们可以看到

SurfaceFlinger::instantiate();

但它本身并没有实现instantiate()方法,那找它的父类。
/frameworks/native/include/binder/BinderService.h

namespaceandroid {template<typename SERVICE>
classBinderService
{public:static status_t publish(bool allowIsolated = false) {sp<IServiceManager> sm(defaultServiceManager());return sm->addService(String16(SERVICE::getServiceName()), new SERVICE(), allowIsolated);}static void publishAndJoinThreadPool(bool allowIsolated = false) {sp<IServiceManager> sm(defaultServiceManager());sm->addService(String16(SERVICE::getServiceName()), new SERVICE(), allowIsolated);ProcessState::self()->startThreadPool();IPCThreadState::self()->joinThreadPool();}staticvoid instantiate() { publish(); }static status_t shutdown() {return NO_ERROR;}
};}; // namespace android// ---------------------------------------------------------------------------#endif // ANDROID_BINDER_SERVICE_H

会调用publish()方法。
而SERVICE在这里是一个模板类。在这里SERVICE自然对应SurfaceFlinger
所以publish()会向ServiceManager添加一个Service这个Service就是Surfaceflinger。
然后我们看SurfaceFlinger的构造函数
/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

SurfaceFlinger::SurfaceFlinger():   BnSurfaceComposer(), Thread(false),mTransactionFlags(0),mTransactionPending(false),mAnimTransactionPending(false),mLayersRemoved(false),mRepaintEverything(0),mBootTime(systemTime()),mVisibleRegionsDirty(false),mHwWorkListDirty(false),mDebugRegion(0),mDebugDDMS(0),mDebugDisableHWC(0),mDebugDisableTransformHint(0),mDebugInSwapBuffers(0),mLastSwapBufferTime(0),mDebugInTransaction(0),mLastTransactionTime(0),mBootFinished(false)
{ALOGI("SurfaceFlinger is starting");// debugging stuff...charvalue[PROPERTY_VALUE_MAX];property_get("debug.sf.showupdates", value, "0");mDebugRegion = atoi(value);property_get("debug.sf.ddms", value, "0");mDebugDDMS = atoi(value);if (mDebugDDMS) {if (!startDdmConnection()) {// start failed, and DDMS debugging not enabledmDebugDDMS = 0;}}ALOGI_IF(mDebugRegion, "showupdates enabled");ALOGI_IF(mDebugDDMS, "DDMS debugging enabled");
}

可还是没有发现run()方法的影踪,没有办法只得去父类构造方法看
结果发现也没有!!!
没有办法,继续在源码中搜索SurfaceFlinger,结果发现与之相关的信息大多是sp
就看看sp吧。
sp是Android在c++中搞得类似java中弱引用、强引用的一套指针概念,那应该是方便回收吧。
而Android Framework中的c++世界,RefBase这个类有点像java中的Object.
而sp是一个模板类。

总之调用sp时会调用SurfaceFlinger的onFirstRef()方法。
那好,看代码吧

void SurfaceFlinger::onFirstRef()
{mEventQueue.init(this);run("SurfaceFlinger", PRIORITY_URGENT_DISPLAY);// Wait for the main thread to be done with its initializationmReadyToRunBarrier.wait();
}

看见没有?run()方法在这里调用了。

所以,在Framework中如果你找不到一个Thread在何处被启动,那么去它的onFirstRef()方法中去看看吧

Android Thread之threadLoop方法相关推荐

  1. Android Framework中的线程Thread及它的threadLoop方法

    当初跟踪Camera的代码中的时候一直追到了HAL层,而在Framework中的代码看见了许许多多的Thread.它们普遍的特点就是有一个threadLoop方法.按照字面的意思应该是这个线程能够循环 ...

  2. android 接口实现方法,Android应用开发之Android 请求网络接口实现方法

    本文将带你了解Android应用开发之Android 请求网络接口实现方法,希望本文对大家学Android有所帮助. public   class Fragment01 extends Fragmen ...

  3. Android调用JNI本地方法跟踪目标代码

    正如Android调用JNI本地方法经过有点改变章所说跟踪代码是可行的,但是跟踪某些代码会出现anr,点击取消,还是不好运,有提高办法吗?回答是有(gdb还没试过,本文只讨论ida). 下面是我使用  ...

  4. android monitor的使用方法,Android Device Monitor使用方法及常见问题解决方案

    Android Device Monitor使用方法及常见问题解决方案 发布时间:2018-07-17 18:18, 浏览次数:1184 , 标签: Android Device Monitor DD ...

  5. android源码下载方法 批量下载 基于windows os

    安装win版的Gitbash, 在这里 http://msysgit.googlecode.com/files/Git-1.6.0.2-preview20080923.exe. 选择默认安装路径(否则 ...

  6. android屏幕密度高度,Android获取常用辅助方法(获取屏幕高度、宽度、密度、通知栏高度、截图)...

    我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现.下面就分享一下Android中常用的一些辅助方法: 获取屏幕高度: /** * 获得屏幕高度 * @para ...

  7. android studio 如何提示方法的用法

    方法/步骤1在 Eclipse中鼠标放上去就可以提示方法的用法,实际上Android Studio也可以设置的.如图 Preferences > Editor >Generan> S ...

  8. Android滑动冲突解决方法

    Android滑动冲突解决方法 滑动冲突 首先讲解一下什么是滑动冲突.当你需要在一个ScrollView中嵌套使用ListView或者RecyclerView的时候你会发现只有ScrollView能够 ...

  9. 【Groovy】Groovy 扩展方法 ( 实例扩展方法配置 | 扩展方法示例 | 编译实例扩展类 | 打包实例扩展类字节码到 jar 包中 | 测试使用 Thread 实例扩展方法 )

    文章目录 一.扩展方法示例 二.实例扩展方法配置 三.编译实例扩展类 四.打包静态扩展类字节码到 jar 包中 五.测试使用 Thread 实例扩展方法 一.扩展方法示例 为 Thread 扩展 he ...

最新文章

  1. 如何评估一项技术是否值得长期投入?
  2. 【数理知识】《积分变换与场论》王振老师-第5章-场论
  3. 第四代计算机软件系统,第四代计算机是什么计算机
  4. 【算法笔记】重刷PAT 题解合集
  5. C++描述杭电OJ 2010.水仙花数 ||
  6. 微服务实战(六):落地微服务架构到直销系统(事件存储)
  7. js 对象和构造函数
  8. 谷歌pay服务端文档_Google pay 服务器验证
  9. oracle命令行登录sy,oracle里常用命令
  10. 【转】SQL Server联机丛书:存储过程及其创建
  11. 图书管理系统E-R图转关系图
  12. 基于simhash的文本去重原理
  13. java利用poi导出word文档
  14. 地图对接汇总(百度地图)
  15. 家的范围 Home on the Range【USACO 3.3 】
  16. NAS如何进行磁盘碎片整理?
  17. 齐博模板直接写mysql_齐博CMS使用教程系统安装.ppt
  18. 教你如何在宝塔上快速部署优客365网址导航
  19. 不用甘特图,怎么能做好项目管理?
  20. 9.28前端培训评价 + 前端作业1的问题与解决

热门文章

  1. PictureSelector框架的简单实现(图片选择添加,不包括视频)
  2. Windows(32bit.64bit) OpenSSL生成数字证书pfx、cer。拒绝收费,(不需要编译openssl源代码,快速安装)
  3. STM32F767--LTC4015--SMBUS通信
  4. CAD软件默认模板如何设置
  5. webug4.0通关笔记---(第一天:布尔注入)
  6. libjpeg-turbo使用教程
  7. aix修改日志服务器,配置aix将audit日志发送syslog服务器
  8. 隧道工具——socat
  9. 原创课程:WordPress快速建站培训课程
  10. Quartz.Net+Microsoft.Extensions.Hosting创建服务