深入了解mediaserver-2
来源:互联网 发布:飞天怎么样知乎 编辑:程序博客网 时间:2024/06/08 08:29
4.2 BnServiceManager<servicemanager进程>
上面说了,defaultServiceManager返回的是一个BpServiceManager,通过它可以把命令请求发送到binder设备,而且handle的值为0。那么,系统的另外一端肯定有个接收命令的,那又是谁呢?
很可惜啊,BnServiceManager不存在,但确实有一个程序完成了BnServiceManager的工作,那就是/system/bin/servicemanager进程.虽然service_manager没有从BnServiceManager中派生,但是它肯定完成了BnServiceManager的功能。
- //service_manager.c
- int main(int argc, char **argv)
- {
- struct binder_state *bs;
- void *svcmgr = BINDER_SERVICE_MANAGER;
- bs = binder_open(128*1024);
- if (binder_become_context_manager(bs)) {
- LOGE("cannot become context manager (%s)\n", strerror(errno));
- return -1;
- }
- svcmgr_handle = svcmgr;
- <span style="color:#ff0000;"> binder_loop(bs, svcmgr_handler);
- </span> return 0;
- }
- void binder_loop(struct binder_state *bs, binder_handler func)
- {
- int res;
- struct binder_write_read bwr;
- unsigned readbuf[32];
- bwr.write_size = 0;
- bwr.write_consumed = 0;
- bwr.write_buffer = 0;
- readbuf[0] = BC_ENTER_LOOPER;
- binder_write(bs, readbuf, sizeof(unsigned));
- for (;;) {
- bwr.read_size = sizeof(readbuf);
- bwr.read_consumed = 0;
- bwr.read_buffer = (unsigned) readbuf;
- <span style="color:#ff0000;">res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
- </span>
- if (res < 0) {
- LOGE("binder_loop: ioctl failed (%s)\n", strerror(errno));
- break;
- }
- <span style="color:#ff0000;">res = binder_parse(bs, 0, readbuf, bwr.read_consumed, func);
- </span> if (res == 0) {
- LOGE("binder_loop: unexpected reply?!\n");
- break;
- }
- if (res < 0) {
- LOGE("binder_loop: io error %d %s\n", res, strerror(errno));
- break;
- }
- }
- }
- int svcmgr_handler(struct binder_state *bs,
- struct binder_txn *txn,
- struct binder_io *msg,
- struct binder_io *reply)
- {
- struct svcinfo *si;
- uint16_t *s;
- unsigned len;
- void *ptr;
- // LOGI("target=%p code=%d pid=%d uid=%d\n",
- // txn->target, txn->code, txn->sender_pid, txn->sender_euid);
- if (txn->target != svcmgr_handle)
- return -1;
- s = bio_get_string16(msg, &len);
- if ((len != (sizeof(svcmgr_id) / 2)) ||
- memcmp(svcmgr_id, s, sizeof(svcmgr_id))) {
- fprintf(stderr,"invalid id %s\n", str8(s));
- return -1;
- }
- switch(txn->code) {
- case SVC_MGR_GET_SERVICE:
- case SVC_MGR_CHECK_SERVICE:
- s = bio_get_string16(msg, &len);
- ptr = do_find_service(bs, s, len);
- if (!ptr)
- break;
- bio_put_ref(reply, ptr);
- return 0;
- case SVC_MGR_ADD_SERVICE:
- s = bio_get_string16(msg, &len);
- ptr = bio_get_ref(msg);
- if (do_add_service(bs, s, len, ptr, txn->sender_euid)) <span style="color:#ff0000;">//add a service to svclist
- </span> return -1;
- break;
- case SVC_MGR_LIST_SERVICES: {
- unsigned n = bio_get_uint32(msg);
- si = svclist;
- while ((n-- > 0) && si)
- si = si->next;
- if (si) {
- bio_put_string16(reply, si->name);
- return 0;
- }
- return -1;
- }
- default:
- LOGE("unknown code %d\n", txn->code);
- return -1;
- }
- bio_put_uint32(reply, 0);
- return 0;
- }
5. MediaPlayerService等待请求
/system/bin/mediaserver在ProcessState::Self()中打开了binder,其looper又在哪儿呢?
- //main_mediaserver.cpp
- int main(int argc, char** argv)
- {
- sp<ProcessState> proc(ProcessState::self());
- sp<IServiceManager> sm = defaultServiceManager();
- LOGI("ServiceManager: %p", sm.get());
- AudioFlinger::instantiate();
- MediaPlayerService::instantiate();
- CameraService::instantiate();
- AudioPolicyService::instantiate();
- ProcessState::self()->startThreadPool();
- IPCThreadState::self()->joinThreadPool();
- }
5.1 startThreadPool
- void ProcessState::startThreadPool()
- {
- AutoMutex _l(mLock);
- if (!mThreadPoolStarted) {
- mThreadPoolStarted = true;
- spawnPooledThread(true);
- }
- }
- void ProcessState::spawnPooledThread(bool isMain)
- {
- if (mThreadPoolStarted) {
- int32_t s = android_atomic_add(1, &mThreadPoolSeq);
- char buf[32];
- sprintf(buf, "Binder Thread #%d", s);
- LOGV("Spawning new pooled thread, name=%s\n", buf);
- <span style="color:#ff0000;">//创建线程池,然后run起来,和java的Thread何其像也。
- </span> sp<Thread> t = new PoolThread(isMain);
- t->run(buf);
- }
- }
- class PoolThread : public Thread
- {
- public:
- PoolThread(bool isMain)
- : mIsMain(isMain)
- {
- }
- protected:
- virtual bool threadLoop()
- {
- IPCThreadState::self()->joinThreadPool(mIsMain);
- return false;
- }
- const bool mIsMain;
- };
- <span style="font-size:18px;color:#ff0000;">//还没有创建线程
- </span>status_t Thread::run(const char* name, int32_t priority, size_t stack)
- {
- Mutex::Autolock _l(mLock);
- if (mRunning) {
- // thread already started
- return INVALID_OPERATION;
- }
- // reset status and exitPending to their default value, so we can
- // try again after an error happened (either below, or in 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) {
- <span style="color:#ff0000;">//name为android:unnamed_thread
- </span> res = createThreadEtc<span style="color:#ff0000;">(_threadLoop</span>,
- this, name, priority, stack, &mThread);
- } else {
- res = androidCreateRawThreadEtc(_<span style="color:#ff0000;">threadLoop</span>,
- 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;
- }
- int androidCreateRawThreadEtc(android_thread_func_t entryFunction,
- void *userData,
- const char* threadName,
- int32_t threadPriority,
- size_t threadStackSize,
- android_thread_id_t *threadId)
- {
- pthread_attr_t attr;
- pthread_attr_init(&attr);
- pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
- #ifdef HAVE_ANDROID_OS /* valgrind is rejecting RT-priority create reqs */
- if (threadPriority != PRIORITY_DEFAULT || threadName != NULL) {
- // We could avoid the trampoline if there was a way to get to the
- // android_thread_id_t (pid) from pthread_t
- thread_data_t* t = new thread_data_t;
- t->priority = threadPriority;
- t->threadName = threadName ? strdup(threadName) : NULL;
- t->entryFunction = entryFunction;
- t->userData = userData;
- entryFunction = (android_thread_func_t)&thread_data_t::trampoline;
- userData = t;
- }
- #endif
- if (threadStackSize) {
- pthread_attr_setstacksize(&attr, threadStackSize);
- }
- errno = 0;
- pthread_t thread;
- <span style="color:#ff0000;">//终于看到了熟悉的线程创建,入口函数为:_threadLoop
- </span> int result = pthread_create(&thread, &attr,
- (android_pthread_entry)entryFunction, userData);
- if (result != 0) {
- LOGE("androidCreateRawThreadEtc failed (entry=%p, res=%d, errno=%d)\n"
- "(android threadPriority=%d)",
- entryFunction, result, errno, threadPriority);
- return 0;
- }
- if (threadId != NULL) {
- *threadId = (android_thread_id_t)thread; // XXX: this is not portable
- }
- return 1;
- }
新开的线程的入口函数为:_threadLoop
- <span style="color:#ff0000;">int Thread::_threadLoop(void* user)
- </span>{
- Thread* const self = static_cast<Thread*>(user);
- sp<Thread> strong(self->mHoldSelf);
- wp<Thread> weak(strong);
- self->mHoldSelf.clear();
- #if HAVE_ANDROID_OS
- // this is very useful for debugging with gdb
- self->mTid = gettid();
- #endif
- bool first = true;
- do {
- bool result;
- if (first) {
- first = false;
- self->mStatus = self->readyToRun();
- result = (self->mStatus == NO_ERROR);
- if (result && !self->mExitPending) {
- // 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(); <span style="color:#ff0000;">//调用自己的<span lang="EN-US">threadLoop</span>
- </span> }
- } else {
- result = self->threadLoop();
- }
- if (result == false || self->mExitPending) {
- self->mExitPending = true;
- self->mLock.lock();
- self->mRunning = false;
- self->mThreadExitedCondition.broadcast();
- self->mLock.unlock();
- 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 loop
- strong = weak.promote();
- } while(strong != 0);
- return 0;
- }
PoolThread::threadLoop
- class PoolThread : public Thread
- {
- public:
- PoolThread(bool isMain)
- : mIsMain(isMain)
- {
- }
- protected:
- <span style="color:#ff0000;">virtual bool threadLoop()</span>
- { ////mIsMain为true。 而且注意,这是一个新的线程,所以必然会创建一个新的IPCThreadState对象(记得线程本地存储吗?TLS)
- <span style="color:#ff0000;">IPCThreadState::self()->joinThreadPool(mIsMain);
- </span> return false;
- }
- const bool mIsMain;
- };
主线程和工作线程都调用了joinThreadPool
- void IPCThreadState::joinThreadPool(bool isMain)
- {
- LOG_THREADPOOL("**** THREAD %p (PID %d) IS JOINING THE THREAD POOL\n", (void*)pthread_self(), getpid());
- <span style="color:#ff0000;">mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
- </span>
- // This thread may have been spawned by a thread that was in the background
- // scheduling group, so first we will make sure it is in the default/foreground
- // one to avoid performing an initial transaction in the background.
- androidSetThreadSchedulingGroup(mMyThreadId, ANDROID_TGROUP_DEFAULT);
- status_t result;
- do {
- int32_t cmd;
- // When we've cleared the incoming command queue, process any pending derefs
- if (mIn.dataPosition() >= mIn.dataSize()) {
- size_t numPending = mPendingWeakDerefs.size();
- if (numPending > 0) {
- for (size_t i = 0; i < numPending; i++) {
- RefBase::weakref_type* refs = mPendingWeakDerefs[i];
- refs->decWeak(mProcess.get());
- }
- mPendingWeakDerefs.clear();
- }
- numPending = mPendingStrongDerefs.size();
- if (numPending > 0) {
- for (size_t i = 0; i < numPending; i++) {
- BBinder* obj = mPendingStrongDerefs[i];
- obj->decStrong(mProcess.get());
- }
- mPendingStrongDerefs.clear();
- }
- }
- // now get the next command to be processed, waiting if necessary
- <span style="color:#ff0000;">result = talkWithDriver();
- </span> if (result >= NO_ERROR) {
- size_t IN = mIn.dataAvail();
- if (IN < sizeof(int32_t)) continue;
- cmd = mIn.readInt32();
- IF_LOG_COMMANDS() {
- alog << "Processing top-level Command: "
- << getReturnString(cmd) << endl;
- }
- <span style="color:#ff0000;">result = executeCommand(cmd);
- </span> }
- // After executing the command, ensure that the thread is returned to the
- // default cgroup before rejoining the pool. The driver takes care of
- // restoring the priority, but doesn't do anything with cgroups so we
- // need to take care of that here in userspace. Note that we do make
- // sure to go in the foreground after executing a transaction, but
- // there are other callbacks into user code that could have changed
- // our group so we want to make absolutely sure it is put back.
- androidSetThreadSchedulingGroup(mMyThreadId, ANDROID_TGROUP_DEFAULT);
- // Let this thread exit the thread pool if it is no longer
- // needed and it is not the main process thread.
- if(result == TIMED_OUT && !isMain) {
- break;
- }
- } while (result != -ECONNREFUSED && result != -EBADF);
- LOG_THREADPOOL("**** THREAD %p (PID %d) IS LEAVING THE THREAD POOL err=%p\n",
- (void*)pthread_self(), getpid(), (void*)result);
- mOut.writeInt32(BC_EXIT_LOOPER);
- talkWithDriver(false);
- }
有loop了,但是有两个线程都执行了这个啊!这里有两个消息循环?
看看executeCommand
- status_t IPCThreadState::executeCommand(int32_t cmd)
- {
- BBinder* obj;
- RefBase::weakref_type* refs;
- status_t result = NO_ERROR;
- switch (cmd) {
- case BR_ERROR:
- result = mIn.readInt32();
- break;
- case BR_OK:
- break;
- case BR_ACQUIRE:
- refs = (RefBase::weakref_type*)mIn.readInt32();
- obj = (BBinder*)mIn.readInt32();
- LOG_ASSERT(refs->refBase() == obj,
- "BR_ACQUIRE: object %p does not match cookie %p (expected %p)",
- refs, obj, refs->refBase());
- obj->incStrong(mProcess.get());
- IF_LOG_REMOTEREFS() {
- LOG_REMOTEREFS("BR_ACQUIRE from driver on %p", obj);
- obj->printRefs();
- }
- mOut.writeInt32(BC_ACQUIRE_DONE);
- mOut.writeInt32((int32_t)refs);
- mOut.writeInt32((int32_t)obj);
- break;
- case BR_RELEASE:
- refs = (RefBase::weakref_type*)mIn.readInt32();
- obj = (BBinder*)mIn.readInt32();
- LOG_ASSERT(refs->refBase() == obj,
- "BR_RELEASE: object %p does not match cookie %p (expected %p)",
- refs, obj, refs->refBase());
- IF_LOG_REMOTEREFS() {
- LOG_REMOTEREFS("BR_RELEASE from driver on %p", obj);
- obj->printRefs();
- }
- mPendingStrongDerefs.push(obj);
- break;
- case BR_INCREFS:
- refs = (RefBase::weakref_type*)mIn.readInt32();
- obj = (BBinder*)mIn.readInt32();
- refs->incWeak(mProcess.get());
- mOut.writeInt32(BC_INCREFS_DONE);
- mOut.writeInt32((int32_t)refs);
- mOut.writeInt32((int32_t)obj);
- break;
- case BR_DECREFS:
- refs = (RefBase::weakref_type*)mIn.readInt32();
- obj = (BBinder*)mIn.readInt32();
- // NOTE: This assertion is not valid, because the object may no
- // longer exist (thus the (BBinder*)cast above resulting in a different
- // memory address).
- //LOG_ASSERT(refs->refBase() == obj,
- // "BR_DECREFS: object %p does not match cookie %p (expected %p)",
- // refs, obj, refs->refBase());
- mPendingWeakDerefs.push(refs);
- break;
- case BR_ATTEMPT_ACQUIRE:
- refs = (RefBase::weakref_type*)mIn.readInt32();
- obj = (BBinder*)mIn.readInt32();
- {
- const bool success = refs->attemptIncStrong(mProcess.get());
- LOG_ASSERT(success && refs->refBase() == obj,
- "BR_ATTEMPT_ACQUIRE: object %p does not match cookie %p (expected %p)",
- refs, obj, refs->refBase());
- mOut.writeInt32(BC_ACQUIRE_RESULT);
- mOut.writeInt32((int32_t)success);
- }
- break;
- <span style="color:#ff0000;">case BR_TRANSACTION:
- </span> {
- binder_transaction_data tr;
- result = mIn.read(&tr, sizeof(tr));
- LOG_ASSERT(result == NO_ERROR,
- "Not enough command data for brTRANSACTION");
- if (result != NO_ERROR) break;
- Parcel buffer;
- buffer.ipcSetDataReference(
- reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
- tr.data_size,
- reinterpret_cast<const size_t*>(tr.data.ptr.offsets),
- tr.offsets_size/sizeof(size_t), freeBuffer, this);
- const pid_t origPid = mCallingPid;
- const uid_t origUid = mCallingUid;
- mCallingPid = tr.sender_pid;
- mCallingUid = tr.sender_euid;
- int curPrio = getpriority(PRIO_PROCESS, mMyThreadId);
- if (gDisableBackgroundScheduling) {
- if (curPrio > ANDROID_PRIORITY_NORMAL) {
- // We have inherited a reduced priority from the caller, but do not
- // want to run in that state in this process. The driver set our
- // priority already (though not our scheduling class), so bounce
- // it back to the default before invoking the transaction.
- setpriority(PRIO_PROCESS, mMyThreadId, ANDROID_PRIORITY_NORMAL);
- }
- } else {
- if (curPrio >= ANDROID_PRIORITY_BACKGROUND) {
- // We want to use the inherited priority from the caller.
- // Ensure this thread is in the background scheduling class,
- // since the driver won't modify scheduling classes for us.
- // The scheduling group is reset to default by the caller
- // once this method returns after the transaction is complete.
- androidSetThreadSchedulingGroup(mMyThreadId,
- ANDROID_TGROUP_BG_NONINTERACT);
- }
- }
- //LOGI(">>>> TRANSACT from pid %d uid %d\n", mCallingPid, mCallingUid);
- Parcel reply;
- IF_LOG_TRANSACTIONS() {
- TextOutput::Bundle _b(alog);
- alog << "BR_TRANSACTION thr " << (void*)pthread_self()
- << " / obj " << tr.target.ptr << " / code "
- << TypeCode(tr.code) << ": " << indent << buffer
- << dedent << endl
- << "Data addr = "
- << reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer)
- << ", offsets addr="
- << reinterpret_cast<const size_t*>(tr.data.ptr.offsets) << endl;
- }
- <span style="color:#ff0000;"> if (tr.target.ptr) {
- sp<BBinder> b((BBinder*)tr.cookie); <span style="color:#3333ff;">//这里用的是<span lang="EN-US">BBinder</span>。
- </span> const status_t error = b->transact(tr.code, buffer, &reply, 0);
- if (error < NO_ERROR) reply.setError(error);
- } else {
- const status_t error = the_context_object->transact(tr.code, buffer, &reply, 0);
- if (error < NO_ERROR) reply.setError(error);
- }
- </span>
- //LOGI("<<<< TRANSACT from pid %d restore pid %d uid %d\n",
- // mCallingPid, origPid, origUid);
- if ((tr.flags & TF_ONE_WAY) == 0) {
- LOG_ONEWAY("Sending reply to %d!", mCallingPid);
- sendReply(reply, 0);
- } else {
- LOG_ONEWAY("NOT sending reply to %d!", mCallingPid);
- }
- mCallingPid = origPid;
- mCallingUid = origUid;
- IF_LOG_TRANSACTIONS() {
- TextOutput::Bundle _b(alog);
- alog << "BC_REPLY thr " << (void*)pthread_self() << " / obj "
- << tr.target.ptr << ": " << indent << reply << dedent << endl;
- }
- }
- break;
- case BR_DEAD_BINDER:
- {
- BpBinder *proxy = (BpBinder*)mIn.readInt32();
- proxy->sendObituary();
- mOut.writeInt32(BC_DEAD_BINDER_DONE);
- mOut.writeInt32((int32_t)proxy);
- } break;
- case BR_CLEAR_DEATH_NOTIFICATION_DONE:
- {
- BpBinder *proxy = (BpBinder*)mIn.readInt32();
- proxy->getWeakRefs()->decWeak(proxy);
- } break;
- case BR_FINISHED:
- result = TIMED_OUT;
- break;
- case BR_NOOP:
- break;
- case BR_SPAWN_LOOPER:
- mProcess->spawnPooledThread(false);
- break;
- default:
- printf("*** BAD COMMAND %d received from Binder driver\n", cmd);
- result = UNKNOWN_ERROR;
- break;
- }
- if (result != NO_ERROR) {
- mLastError = result;
- }
- return result;
- }
BBinder的transact函数如下:
- status_t BBinder::transact(
- uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
- {
- data.setDataPosition(0);
- status_t err = NO_ERROR;
- switch (code) {
- case PING_TRANSACTION:
- reply->writeInt32(pingBinder());
- break;
- default:
- err = onTransact(code, data, reply, flags); <span style="color:#ff0000;">//调用自己的onTransact,实际调用BnMediaPlayerService::onTransact
- </span> break;
- }
- if (reply != NULL) {
- reply->setDataPosition(0);
- }
- return err;
- }
BnMediaPlayerService从BBinder派生,所以会调用到它的onTransact函数,终于水落石出了,让我们看看BnMediaPlayerServcice的onTransact函数。
- status_t BnMediaPlayerService::onTransact(
- uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
- {
- <span style="color:#ff0000;"> // BnMediaPlayerService从BBinder和IMediaPlayerService派生
- // 所有IMediaPlayerService提供的函数都通过命令类型来区分
- </span>
- switch(code) {
- case CREATE_URL: {
- CHECK_INTERFACE(IMediaPlayerService, data, reply);
- pid_t pid = data.readInt32();
- sp<IMediaPlayerClient> client =
- interface_cast<IMediaPlayerClient>(data.readStrongBinder());
- const char* url = data.readCString();
- KeyedVector<String8, String8> headers;
- int32_t numHeaders = data.readInt32();
- for (int i = 0; i < numHeaders; ++i) {
- String8 key = data.readString8();
- String8 value = data.readString8();
- headers.add(key, value);
- }
- <span style="color:#ff0000;">//create是一个虚函数,由MediaPlayerService来实现,见MediaPlayerService::create
- </span> <span style="color:#ff0000;">//MediaPlayerService.cpp
- </span> sp<IMediaPlayer> player = create(
- pid, client, url, numHeaders > 0 ? &headers : NULL);
- reply->writeStrongBinder(player->asBinder());
- return NO_ERROR;
- } break;
- ...
- }
- }
其实,到这里,我们就明白了。BnXXX的onTransact函数收取命令,然后派发到派生类XXX的对应函数,由他们完成实际的工作。
说明:
这里有点特殊,startThreadPool和joinThreadPool完后确实有两个线程,主线程和工作线程,而且都在做消息循环。为什么要这么做呢?他们参数isMain都是true。不知道google搞什么。难道是怕一个线程工作量太多,所以搞两个线程来工作?这种解释应该也是合理的。
网上有人测试过把最后一句屏蔽掉,也能正常工作。但是难道主线程退出了,程序还能不退出吗?这个...管它的,反正知道有两个线程在那处理就行了。
- 深入了解mediaserver-2
- 深入了解mediaserver-2
- 深入了解MediaServer-1
- 深入了解mediaserver-3
- 深入了解MediaServer-1
- 深入了解MediaServer-1
- 深入了解mediaserver-3
- 深入了解MediaServer-1
- mediaserver
- 深入了解GCD:Part 2/2
- 深入了解php4(2)--重访过去
- 深入了解:液晶显示器的工作原理 2
- 深入了解GCD:Part 1/2
- rpc学习2-深入了解原理
- 深入了解AngularJs-Ui-router(2)
- 深入了解计算机端口
- 深入了解C语言
- 深入了解C语言
- 日本公共廁所不爲人知的新功能
- seo做个出色的地图导航
- Android数据的四种存储方式SharedPreferences、SQLite、Content Provider和File (一) —— 总览
- struts+hibernate+spring jar包官网下载地址【转】
- 【论坛好帖收藏】之程序员恶性循环
- 深入了解mediaserver-2
- 如何才能算一个合格的SEO
- 初学Android,增加手势到手势库(五十)
- 京市政府机关与市属事业单位的信息管理系统严重依赖XP
- 汉诺塔计数 实现输出64个圆盘移动多少次 java代码
- STM32F407之DMA
- Android Studio升级到了5.1的问题
- 中国本土禁片知多少
- 报表上加字段