热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AndroidIPC机制详解

oIBinder接口IBinder接口是对跨进程的对象的抽象。普通对象在当前进程可以访问,如果希望对象能被其它进程访问,那就必须实现IBinder接口

o IBinder接口

IBinder接口是对跨进程的对象的抽象。普通对象在当前进程可以访问,如果希望对象能被其它进程访问,那就必须实现IBinder接口。IBinder接口可以指向本地对象,也可以指向远程对象,调用者不需要关心指向的对象是本地的还是远程。

transact是IBinder接口中一个比较重要的函数,它的函数原型如下:

virtual status_t transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) = 0;

android中的IPC的基本模型是基于客户/服务器(C/S)架构的。


客户端

请求通过内核模块中转

服务端

如果IBinder指向的是一个客户端代理,那transact只是把请求发送给服务器。服务端的IBinder的transact则提供了实际的服务。

o 客户端

BpBinder是远程对象在当前进程的代理,它实现了IBinder接口。它的transact函数实现如下:

status_t BpBinder::transact(

uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)

{

// Once a binder has died, it will never come back to life.

if (mAlive) {

status_t status = IPCThreadState::self()->transact(

mHandle, code, data, reply, flags);

if (status == DEAD_OBJECT) mAlive = 0;

return status;

}

return DEAD_OBJECT;

}

参数说明:

· code 是请求的ID号。

· data 是请求的参数。

· reply 是返回的结果。

· flags 一些额外的标识,如FLAG_ONEWAY。通常为0。

transact只是简单的调用了IPCThreadState::self()的transact,在IPCThreadState::transact中:

status_t IPCThreadState::transact(int32_t handle,

uint32_t code, const Parcel& data,

Parcel* reply, uint32_t flags)

{

status_t err = data.errorCheck();

flags |= TF_ACCEPT_FDS;

IF_LOG_TRANSACTIONS() {

TextOutput::Bundle _b(alog);

alog <<"BC_TRANSACTION thr " <<(void*)pthread_self() <<" / hand "

<

<

}

if (err &#61;&#61; NO_ERROR) {

LOG_ONEWAY(">>>> SEND from pid %d uid %d %s", getpid(), getuid(),

(flags & TF_ONE_WAY) &#61;&#61; 0 ? "READ REPLY" : "ONE WAY");

err &#61; writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);

}

if (err !&#61; NO_ERROR) {

if (reply) reply->setError(err);

return (mLastError &#61; err);

}

if ((flags & TF_ONE_WAY) &#61;&#61; 0) {

if (reply) {

err &#61; waitForResponse(reply);

} else {

Parcel fakeReply;

err &#61; waitForResponse(&fakeReply);

}

IF_LOG_TRANSACTIONS() {

TextOutput::Bundle _b(alog);

alog <<"BR_REPLY thr " <<(void*)pthread_self() <<" / hand "

<

if (reply) alog <

else alog <<"(none requested)" <

}

} else {

err &#61; waitForResponse(NULL, NULL);

}

return err;

}

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)

{

int32_t cmd;

int32_t err;

while (1) {

if ((err&#61;talkWithDriver()) break;

err &#61; mIn.errorCheck();

if (err break;

if (mIn.dataAvail() &#61;&#61; 0) continue;

cmd &#61; mIn.readInt32();

IF_LOG_COMMANDS() {

alog <<"Processing waitForResponse Command: "

<

}

switch (cmd) {

case BR_TRANSACTION_COMPLETE:

if (!reply && !acquireResult) goto finish;

break;

case BR_DEAD_REPLY:

err &#61; DEAD_OBJECT;

goto finish;

case BR_FAILED_REPLY:

err &#61; FAILED_TRANSACTION;

goto finish;

case BR_ACQUIRE_RESULT:

{

LOG_ASSERT(acquireResult !&#61; NULL, "Unexpected brACQUIRE_RESULT");

const int32_t result &#61; mIn.readInt32();

if (!acquireResult) continue;

*acquireResult &#61; result ? NO_ERROR : INVALID_OPERATION;

}

goto finish;

case BR_REPLY:

{

binder_transaction_data tr;

err &#61; mIn.read(&tr, sizeof(tr));

LOG_ASSERT(err &#61;&#61; NO_ERROR, "Not enough command data for brREPLY");

if (err !&#61; NO_ERROR) goto finish;

if (reply) {

if ((tr.flags & TF_STATUS_CODE) &#61;&#61; 0) {

reply->ipcSetDataReference(

reinterpret_cast(tr.data.ptr.buffer),

tr.data_size,

reinterpret_cast(tr.data.ptr.offsets),

tr.offsets_size/sizeof(size_t),

freeBuffer, this);

} else {

err &#61; *static_cast(tr.data.ptr.buffer);

freeBuffer(NULL,

reinterpret_cast(tr.data.ptr.buffer),

tr.data_size,

reinterpret_cast(tr.data.ptr.offsets),

tr.offsets_size/sizeof(size_t), this);

}

} else {

freeBuffer(NULL,

reinterpret_cast(tr.data.ptr.buffer),

tr.data_size,

reinterpret_cast(tr.data.ptr.offsets),

tr.offsets_size/sizeof(size_t), this);

continue;

}

}

goto finish;

default:

err &#61; executeCommand(cmd);

if (err !&#61; NO_ERROR) goto finish;

break;

}

}

finish:

if (err !&#61; NO_ERROR) {

if (acquireResult) *acquireResult &#61; err;

if (reply) reply->setError(err);

mLastError &#61; err;

}

return err;

}

这里transact把请求经内核模块发送了给服务端&#xff0c;服务端处理完请求之后&#xff0c;沿原路返回结果给调用者。这里也可以看出请求是同步操作&#xff0c;它会等待直到结果返回为止。

在BpBinder之上进行简单包装&#xff0c;我们可以得到与服务对象相同的接口&#xff0c;调用者无需要关心调用的对象是远程的还是本地的。拿ServiceManager来说&#xff1a;
(frameworks/base/libs/utils/IServiceManager.cpp)

class BpServiceManager : public BpInterface

{

public:

BpServiceManager(const sp& impl)

: BpInterface(impl)

{

}

...

virtual status_t addService(const String16& name, const sp& service)

{

Parcel data, reply;

data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());

data.writeString16(name);

data.writeStrongBinder(service);

status_t err &#61; remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);

return err &#61;&#61; NO_ERROR ? reply.readInt32() : err;

}

...

};

BpServiceManager实现了 IServiceManager和IBinder两个接口&#xff0c;调用者可以把BpServiceManager的对象看作是一个IServiceManager对象或者IBinder对象。当调用者把BpServiceManager对象当作IServiceManager对象使用时&#xff0c;所有的请求只是对BpBinder::transact的封装。这样的封装使得调用者不需要关心IServiceManager对象是本地的还是远程的了。

客户通过defaultServiceManager函数来创建BpServiceManager对象&#xff1a;
(frameworks/base/libs/utils/IServiceManager.cpp)

sp defaultServiceManager()

{

if (gDefaultServiceManager !&#61; NULL) return gDefaultServiceManager;

{

AutoMutex _l(gDefaultServiceManagerLock);

if (gDefaultServiceManager &#61;&#61; NULL) {

gDefaultServiceManager &#61; interface_cast(

ProcessState::self()->getContextObject(NULL));

}

}

return gDefaultServiceManager;

}

先通过ProcessState::self()->getContextObject(NULL)创建一个Binder对象&#xff0c;然后通过interface_cast和IMPLEMENT_META_INTERFACE(ServiceManager, “android.os.IServiceManager”)把Binder对象包装成 IServiceManager对象。原理上等同于创建了一个BpServiceManager对象。

ProcessState::self()->getContextObject调用ProcessState::getStrongProxyForHandle创建代理对象&#xff1a;

sp ProcessState::getStrongProxyForHandle(int32_t handle)

{

sp result;

AutoMutex _l(mLock);

handle_entry* e &#61; lookupHandleLocked(handle);

if (e !&#61; NULL) {

// We need to create a new BpBinder if there isn&#39;t currently one, OR we

// are unable to acquire a weak reference on this current one. See comment

// in getWeakProxyForHandle() for more info about this.

IBinder* b &#61; e->binder;

if (b &#61;&#61; NULL || !e->refs->attemptIncWeak(this)) {

b &#61; new BpBinder(handle);

e->binder &#61; b;

if (b) e->refs &#61; b->getWeakRefs();

result &#61; b;

} else {

// This little bit of nastyness is to allow us to add a primary

// reference to the remote proxy when this team doesn&#39;t have one

// but another team is sending the handle to us.

result.force_set(b);

e->refs->decWeak(this);

}

}

return result;

}

如果handle为空&#xff0c;默认为context_manager对象&#xff0c;context_manager实际上就是ServiceManager。
o 服务端
服务端也要实现IBinder接口&#xff0c;BBinder类对IBinder接口提供了部分默认实现&#xff0c;其中transact的实现如下&#xff1a;

status_t BBinder::transact(

uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)

{

data.setDataPosition(0);

status_t err &#61; NO_ERROR;

switch (code) {

case PING_TRANSACTION:

reply->writeInt32(pingBinder());

break;

default:

err &#61; onTransact(code, data, reply, flags);

break;

}

if (reply !&#61; NULL) {

reply->setDataPosition(0);

}

return err;

}

PING_TRANSACTION请求用来检查对象是否还存在&#xff0c;这里简单的把 pingBinder的返回值返回给调用者。其它的请求交给onTransact处理。onTransact是BBinder里声明的一个protected类型的虚函数&#xff0c;这个要求它的子类去实现。比如CameraService里的实现如下&#xff1a;

status_t CameraService::onTransact(

uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)

{

// permission checks...

switch (code) {

case BnCameraService::CONNECT:

IPCThreadState* ipc &#61; IPCThreadState::self();

const int pid &#61; ipc->getCallingPid();

const int self_pid &#61; getpid();

if (pid !&#61; self_pid) {

// we&#39;re called from a different process, do the real check

if (!checkCallingPermission(

String16("android.permission.CAMERA")))

{

const int uid &#61; ipc->getCallingUid();

LOGE("Permission Denial: "

"can&#39;t use the camera pid&#61;%d, uid&#61;%d", pid, uid);

return PERMISSION_DENIED;

}

}

break;

}

status_t err &#61; BnCameraService::onTransact(code, data, reply, flags);

LOGD("&#43;&#43;&#43; onTransact err %d code %d", err, code);

if (err &#61;&#61; UNKNOWN_TRANSACTION || err &#61;&#61; PERMISSION_DENIED) {

// the &#39;service&#39; command interrogates this binder for its name, and then supplies it

// even for the debugging commands. that means we need to check for it here, using

// ISurfaceComposer (since we delegated the INTERFACE_TRANSACTION handling to

// BnSurfaceComposer before falling through to this code).

LOGD("&#43;&#43;&#43; onTransact code %d", code);

CHECK_INTERFACE(ICameraService, data, reply);

switch(code) {

case 1000:

{

if (gWeakHeap !&#61; 0) {

sp h &#61; gWeakHeap.promote();

IMemoryHeap *p &#61; gWeakHeap.unsafe_get();

LOGD("CHECKING WEAK REFERENCE %p (%p)", h.get(), p);

if (h !&#61; 0)

h->printRefs();

bool attempt_to_delete &#61; data.readInt32() &#61;&#61; 1;

if (attempt_to_delete) {

// NOT SAFE!

LOGD("DELETING WEAK REFERENCE %p (%p)", h.get(), p);

if (p) delete p;

}

return NO_ERROR;

}

}

break;

default:

break;

}

}

return err;

}

由此可见&#xff0c;服务端的onTransact是一个请求分发函数&#xff0c;它根据请求码(code)做相应的处理。

o 消息循环

服务端(任何进程都可以作为服务端)有一个线程监听来自客户端的请求&#xff0c;并循环处理这些请求。

如果在主线程中处理请求&#xff0c;可以直接调用下面的函数&#xff1a;

IPCThreadState::self()->joinThreadPool(mIsMain);

如果想在非主线程中处理请求&#xff0c;可以按下列方式&#xff1a;

sp

proc &#61; ProcessState::self();

if (proc->supportsProcesses()) {

LOGV("App process: starting thread pool./n");

proc->startThreadPool();

}

startThreadPool的实现原理&#xff1a;

void ProcessState::startThreadPool()

{

AutoMutex _l(mLock);

if (!mThreadPoolStarted) {

mThreadPoolStarted &#61; true;

spawnPooledThread(true);

}

}

void ProcessState::spawnPooledThread(bool isMain)

{

if (mThreadPoolStarted) {

int32_t s &#61; android_atomic_add(1, &mThreadPoolSeq);

char buf[32];

sprintf(buf, "Binder Thread #%d", s);

LOGV("Spawning new pooled thread, name&#61;%s/n", buf);

sp

t &#61; new PoolThread(isMain);

t->run(buf);

}

}

这里创建了PoolThread的对象&#xff0c;实现上就是创建了一个线程。所有的线程类都要实现threadLoop虚函数。PoolThread的threadLoop的实现如下&#xff1a;

virtual bool threadLoop()

{

IPCThreadState::self()->joinThreadPool(mIsMain);

return false;

}

上述代码&#xff0c;简而言之就是创建了一个线程&#xff0c;然后在线程里调用 IPCThreadState::self()->joinThreadPool函数。

下面再看joinThreadPool的实现&#xff1a;

do

{

...

result &#61; talkWithDriver();

if (result >&#61; NO_ERROR) {

size_t IN &#61; mIn.dataAvail();

if (IN

cmd &#61; mIn.readInt32();

IF_LOG_COMMANDS() {

alog <<"Processing top-level Command: "

<

}

result &#61; executeCommand(cmd);

}

...

while(...);

这个函数在循环中重复执行下列动作&#xff1a;


  1. talkWithDriver 通过ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)读取请求和写回结果。
  2. executeCommand 执行相应的请求

在IPCThreadState::executeCommand(int32_t cmd)函数中&#xff1a;


  1. 对于控制对象生命周期的请求&#xff0c;像BR_ACQUIRE/BR_RELEASE直接做了处理。
  2. 对于BR_TRANSACTION请求&#xff0c;它调用被请求对象的transact函数。

按下列方式调用实际的对象&#xff1a;

if (tr.target.ptr) {

sp b((BBinder*)tr.COOKIE);

const status_t error &#61; b->transact(tr.code, buffer, &reply, 0);

if (error

} else {

const status_t error &#61; the_context_object->transact(tr.code, buffer, &reply, 0);

if (error

}

如果tr.target.ptr不为空&#xff0c;就把tr.COOKIE转换成一个Binder对象&#xff0c;并调用它的transact函数。如果没有目标对象&#xff0c;就调用 the_context_object对象的transact函数。奇怪的是&#xff0c;根本没有谁对the_context_object进行初始化&#xff0c;the_context_object是空指针。原因是context_mgr的请求发给了ServiceManager&#xff0c;所以根本不会走到else语句里来。

o 内核模块

android使用了一个内核模块binder来中转各个进程之间的消息。模块源代码放在binder.c里&#xff0c;它是一个字符驱动程序&#xff0c;主要通过binder_ioctl与用户空间的进程交换数据。其中BINDER_WRITE_READ用来读写数据&#xff0c;数据包中有一个cmd域用于区分不同的请求&#xff1a;


  1. binder_thread_write用于发送请求或返回结果。
  2. binder_thread_read用于读取结果。

从binder_thread_write中调用binder_transaction中转请求和返回结果&#xff0c;binder_transaction的实现如下&#xff1a;

对请求的处理&#xff1a;


  1. 通过对象的handle找到对象所在的进程&#xff0c;如果handle为空就认为对象是context_mgr&#xff0c;把请求发给context_mgr所在的进程。
  2. 把请求中所有的binder对象全部放到一个RB树中。
  3. 把请求放到目标进程的队列中&#xff0c;等待目标进程读取。

如何成为context_mgr呢&#xff1f;内核模块提供了BINDER_SET_CONTEXT_MGR调用:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)

{

...

case BINDER_SET_CONTEXT_MGR:

if (binder_context_mgr_node !&#61; NULL) {

printk(KERN_ERR "binder: BINDER_SET_CONTEXT_MGR already set/n");

ret &#61; -EBUSY;

goto err;

}

if (binder_context_mgr_uid !&#61; -1) {

if (binder_context_mgr_uid !&#61; current->euid) {

printk(KERN_ERR "binder: BINDER_SET_"

"CONTEXT_MGR bad uid %d !&#61; %d/n",

current->euid,

binder_context_mgr_uid);

ret &#61; -EPERM;

goto err;

}

} else

binder_context_mgr_uid &#61; current->euid;

binder_context_mgr_node &#61; binder_new_node(proc, NULL, NULL);

if (binder_context_mgr_node &#61;&#61; NULL) {

ret &#61; -ENOMEM;

goto err;

}

binder_context_mgr_node->local_weak_refs&#43;&#43;;

binder_context_mgr_node->local_strong_refs&#43;&#43;;

binder_context_mgr_node->has_strong_ref &#61; 1;

binder_context_mgr_node->has_weak_ref &#61; 1;

break;

ServiceManager&#xff08;frameworks/base/cmds/servicemanager)通过下列方式成为了context_mgr进程&#xff1a;

int binder_become_context_manager(struct binder_state *bs)

{

return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);

}

int main(int argc, char **argv)

{

struct binder_state *bs;

void *svcmgr &#61; BINDER_SERVICE_MANAGER;

bs &#61; binder_open(128*1024);

if (binder_become_context_manager(bs)) {

LOGE("cannot become context manager (%s)/n", strerror(errno));

return -1;

}

svcmgr_handle &#61; svcmgr;

binder_loop(bs, svcmgr_handler);

return 0;

}

o 如何得到服务对象的handle


  1. 服务提供者通过defaultServiceManager得到ServiceManager对象&#xff0c;然后调用addService向服务管理器注册。
  2. 服务使用者通过defaultServiceManager得到ServiceManager对象&#xff0c;然后调用getService通过服务名称查找到服务对象的handle。

o 如何通过服务对象的handle找到服务所在的进程

0表示服务管理器的handle&#xff0c;getService可以查找到系统服务的handle。这个handle只是代表了服务对象&#xff0c;内核模块是如何通过handle找到服务所在的进程的呢&#xff1f;


  1. 对于ServiceManager: ServiceManager调用了binder_become_context_manager使用自己成为context_mgr&#xff0c;所有handle为0的请求都会被转发给ServiceManager。
  2. 对于系统服务和应用程序的Listener&#xff0c;在第一次请求内核模块时(比如调用addService)&#xff0c;内核模块在一个RB树中建立了服务对象和进程的对应关系。

3. off_end &#61; (void *)offp &#43; tr->offsets_size;

4. for (; offp

5. struct flat_binder_object *fp;

6. if (*offp > t->buffer->data_size - sizeof(*fp)) {

7. binder_user_error("binder: %d:%d got transaction with "

8. "invalid offset, %d/n",

9. proc->pid, thread->pid, *offp);

10. return_error &#61; BR_FAILED_REPLY;

11. goto err_bad_offset;

12. }

13. fp &#61; (struct flat_binder_object *)(t->buffer->data &#43; *offp);

14. switch (fp->type) {

15. case BINDER_TYPE_BINDER:

16. case BINDER_TYPE_WEAK_BINDER: {

17. struct binder_ref *ref;

18. struct binder_node *node &#61; binder_get_node(proc, fp->binder);

19. if (node &#61;&#61; NULL) {

20. node &#61; binder_new_node(proc, fp->binder, fp->COOKIE);

21. if (node &#61;&#61; NULL) {

22. return_error &#61; BR_FAILED_REPLY;

23. goto err_binder_new_node_failed;

24. }

25. node->min_priority &#61; fp->flags & FLAT_BINDER_FLAG_PRIORITY_MASK;

26. node->accept_fds &#61; !!(fp->flags & FLAT_BINDER_FLAG_ACCEPTS_FDS);

27. }

28. if (fp->COOKIE !&#61; node->COOKIE) {

29. binder_user_error("binder: %d:%d sending u%p "

30. "node %d, COOKIE mismatch %p !&#61; %p/n",

31. proc->pid, thread->pid,

32. fp->binder, node->debug_id,

33. fp->COOKIE, node->COOKIE);

34. goto err_binder_get_ref_for_node_failed;

35. }

36. ref &#61; binder_get_ref_for_node(target_proc, node);

37. if (ref &#61;&#61; NULL) {

38. return_error &#61; BR_FAILED_REPLY;

39. goto err_binder_get_ref_for_node_failed;

40. }

41. if (fp->type &#61;&#61; BINDER_TYPE_BINDER)

42. fp->type &#61; BINDER_TYPE_HANDLE;

43. else

44. fp->type &#61; BINDER_TYPE_WEAK_HANDLE;

45. fp->handle &#61; ref->desc;

46. binder_inc_ref(ref, fp->type &#61;&#61; BINDER_TYPE_HANDLE, &thread->todo);

47. if (binder_debug_mask & BINDER_DEBUG_TRANSACTION)

48. printk(KERN_INFO " node %d u%p -> ref %d desc %d/n",

49. node->debug_id, node->ptr, ref->debug_id, ref->desc);

} break;


  1. 请求服务时&#xff0c;内核先通过handle找到对应的进程&#xff0c;然后把请求放到服务进程的队列中。

o C调用JAVA

前面我们分析的是C代码的处理。对于JAVA代码&#xff0c;JAVA调用C的函数通过JNI调用即可。从内核时读取请求是在C代码(executeCommand)里进行了&#xff0c;那如何在C代码中调用那些用JAVA实现的服务呢&#xff1f;

android_os_Binder_init里的JavaBBinder对Java里的Binder对象进行包装。

JavaBBinder::onTransact调用Java里的execTransact函数&#xff1a;

jboolean res &#61; env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,

code, (int32_t)&data, (int32_t)reply, flags);

jthrowable excep &#61; env->ExceptionOccurred();

if (excep) {

report_exception(env, excep,

"*** Uncaught remote exception! "

"(Exceptions are not yet supported across processes.)");

res &#61; JNI_FALSE;

/* clean up JNI local ref -- we don&#39;t return to Java code */

env->DeleteLocalRef(excep);

}

o 广播消息

binder不提供广播消息&#xff0c;不过可以ActivityManagerService服务来实现广播。
(frameworks/base/core/java/android/app/ActivityManagerNative.java)

接收广播消息需要实现接口BroadcastReceiver&#xff0c;然后调用ActivityManagerProxy::registerReceiver注册。

触发广播调用ActivityManagerProxy::broadcastIntent。(应用程序并不直接调用它&#xff0c;而是调用Context对它的包装)

Category: Android

You can follow any responses to this entry through the RSS 2.0 feed. You can skip to the end and leave a response. Pinging is currently not allowed.

3 Responses


  1. clip_image001

1

Dig 
Thursday, 14. May 2009

太强大&#xff0c;太具体了&#xff0c;膜拜下


  1. clip_image003

2

peimichael 
Friday, 17. July 2009

写的挺详细的。
我还有一个问题想问一下&#xff0c;那个wirteStrongBinder究竟是怎么回事&#xff1f;看代码没看明白。我原来的理解是&#xff0c;通过某种手段把一个IBinder对象传给了对端&#xff0c;对端再根据传过来的数据在本地重新生成一个IBinder对象&#xff0c;但是这样理解又好像有问题。
比如在BpCameraClient类中有一段代码
void jpegCallback(sp &picture)
{
Parcel data,reply;
data.writeInterfaceToken(XXXXX);
data.writeStrongBinder(picture->asBinder());
remote->transact(XXXXX);
}
这个picture里面有一块内存&#xff0c;用来保存图片数据&#xff0c;那么这个binder是怎么把这个图片传给另外一个进程的呢&#xff1f;这个asBinder是干什么的&#xff1f;binder会将图片的内存一起复制给对端还是在内核里面建立一个映射让对端进程共享这一块内存呢&#xff1f;
期待您的解答&#xff0c;谢谢!


  1. clip_image003[1]

3

peimichael 
Friday, 17. July 2009

不好意思&#xff0c;上面代码有个笔误
void jpegCallback(sp& picture)
少了个


推荐阅读
  • STM32代码编写STM32端不需要写关于连接MQTT服务器的代码,连接的工作交给ESP8266来做,STM32只需要通过串口接收和发送数据,间接的与服务器交互。串口三配置串口一已 ... [详细]
  • UVa 11683: 激光雕刻技术解析
    自1958年发明以来,激光技术已在众多领域得到广泛应用,包括电子设备、医疗手术工具、武器等。本文将探讨如何使用激光技术进行材料雕刻,并通过编程解决一个具体的激光雕刻问题。 ... [详细]
  • 页面预渲染适用于主要包含静态内容的页面。对于依赖大量API调用的动态页面,建议采用SSR(服务器端渲染),如Nuxt等框架。更多优化策略可参见:https://github.com/HaoChuan9421/vue-cli3-optimization ... [详细]
  • 本文详细介绍了 Node.js 中 OS 模块的 arch 方法,包括其功能、语法、参数以及返回值,并提供了具体的使用示例。 ... [详细]
  • 本文基于Java官方文档进行了适当修改,旨在介绍如何实现一个能够同时处理多个客户端请求的服务端程序。在前文中,我们探讨了单客户端访问的服务端实现,而本篇将深入讲解多客户端环境下的服务端设计与实现。 ... [详细]
  • 本文详细介绍了Socket在Linux内核中的实现机制,包括基本的Socket结构、协议操作集以及不同协议下的具体实现。通过这些内容,读者可以更好地理解Socket的工作原理。 ... [详细]
  • 个人博客:打开链接依赖倒置原则定义依赖倒置原则(DependenceInversionPrinciple,DIP)定义如下:Highlevelmo ... [详细]
  • 本文详细探讨了 Android Service 组件中 onStartCommand 方法的四种不同返回值及其应用场景。Service 可以在后台执行长时间的操作,无需提供用户界面,支持通过启动和绑定两种方式创建。 ... [详细]
  • Canopy环境安装与使用指南
    《利用Python进行数据分析》一书推荐使用EPDFree版本的环境,然而随着技术的发展,目前更多人倾向于使用Canopy。本文将详细介绍Canopy的安装及使用方法。 ... [详细]
  • 本文分享了作者在使用LaTeX过程中的几点心得,涵盖了从文档编辑、代码高亮、图形绘制到3D模型展示等多个方面的内容。适合希望深入了解LaTeX高级功能的用户。 ... [详细]
  • 本文详细介绍如何在SSM(Spring + Spring MVC + MyBatis)框架中实现分页功能。包括分页的基本概念、数据准备、前端分页栏的设计与实现、后端分页逻辑的编写以及最终的测试步骤。 ... [详细]
  • 本文探讨了使用Python实现监控信息收集的方法,涵盖从基础的日志记录到复杂的系统运维解决方案,旨在帮助开发者和运维人员提升工作效率。 ... [详细]
  • Gradle 是 Android Studio 中默认的构建工具,了解其基本配置对于开发效率的提升至关重要。本文将详细介绍如何在 Gradle 中定义和使用共享变量,以确保项目的一致性和可维护性。 ... [详细]
  • 深入理解iOS中的链式编程:以Masonry为例
    本文通过介绍Masonry这一轻量级布局框架,探讨链式编程在iOS开发中的应用。Masonry不仅简化了Auto Layout的使用,还提高了代码的可读性和维护性。 ... [详细]
  • 本文介绍了如何在C# WinForms应用程序中通过自定义绘制实现圆形按钮的方法,适合初学者参考。 ... [详细]
author-avatar
花神
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有