Binder 框架及 Native 层

Binder机制使本地对象可以像操作当前对象一样调用远程对象,可以使不同的进程间互相通信。Binder 使用 Client/Server 架构,客户端通过服务端代理,经过 Binder 驱动与服务端交互。

Binder 机制实现进程间通信的奥秘在于 kernel 中的 Binder 驱动。

JNI 的代码位于 frameworks/base/core/jni 目录下,主要是 android_util_Binder.cpp 文件和头文件 android_util_Binder.h

Binder JNI 代码是 Binder Java 层操作到 Binder Native 层的接口封装,最后会被编译进 libandroid_runtime.so 系统库。

Binder 本地层的代码在 frameworks/native/libs/binder 目录下, 此目录在 Android 系统编译后会生成 libbinder.so 文件,供 JNI 调用。libbinder 封装了所有对 binder 驱动的操作,是上层应用与驱动交互的桥梁。头文件则在 frameworks/native/include/binder 目录下。

Binder Native 的入口

IInterface.cpp 是 Binder 本地层入口,与 java 层的 android.os.IInterface 对应,提供 asBinder() 的实现,返回 IBinder 对象。

在头文件中有两个类 BnInterface (Binder Native Interface) 和 BpInterface (Binder Proxy Interface), 对应于 java 层的 Stub和 Proxy

  1. sp<IBinder> IInterface::asBinder(const IInterface* iface)
  2. {
  3. if (iface == NULL) return NULL;
  4. return const_cast<IInterface*>(iface)->onAsBinder();
  5. }
  6. template<typename INTERFACE>
  7. class BnInterface : public INTERFACE, public BBinder
  8. {
  9. public:
  10. virtual sp<IInterface> queryLocalInterface(const String16& _descriptor);
  11. virtual const String16& getInterfaceDescriptor() const;
  12.  
  13. protected:
  14. virtual IBinder* onAsBinder();
  15. };
  16.  
  17. // ----------------------------------------------------------------------
  18.  
  19. template<typename INTERFACE>
  20. class BpInterface : public INTERFACE, public BpRefBase
  21. {
  22. public:
  23. BpInterface(const sp<IBinder>& remote);
  24.  
  25. protected:
  26. virtual IBinder* onAsBinder();
  27. };

其中 BnInterface 是实现Stub功能的模板,扩展BBinder的onTransact()方法实现Binder命令的解析和执行。BpInterface是实现Proxy功能的模板,BpRefBase里有个mRemote对象指向一个BpBinder对象。

Binder 本地层的整个函数/方法调用过程

1. Java 层 IRemoteService.Stub.Proxy 调用 android.os.IBinder (实现在 android.os.Binder.BinderProxy) 的 transact() 发送 Stub.TRANSACTION_addUser 命令。

2. 由 BinderProxy.transact() 进入 native 层。

3. 由 jni 转到 android_os_BinderProxy_transact() 函数。

4. 调用 IBinder->transact 函数。

  1. static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
  2. jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
  3. {
  4. IBinder* target = (IBinder*)
  5. env->GetLongField(obj, gBinderProxyOffsets.mObject);
  6. status_t err = target->transact(code, *data, reply, flags);
  7. }

而 gBinderProxyOffsets.mObject 则是在 java 层调用 IBinder.getContextObject() 时在 javaObjectForIBinder 函数中设置的

  1. static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
  2. {
  3. sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
  4. return javaObjectForIBinder(env, b);
  5. }
  6.  
  7. jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
  8. {
  9. ...
  10. LOGDEATH("objectForBinder %p: created new proxy %p !\n", val.get(), object);
  11. // The proxy holds a reference to the native object.
  12. env->SetLongField(object, gBinderProxyOffsets.mObject, (jlong)val.get());
  13. val->incStrong((void*)javaObjectForIBinder);
  14. ...
  15. }

经过 ProcessState::getContextObject() 和 ProcessState::getStrongProxyForHandle()

  1. sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
  2. {
  3. return getStrongProxyForHandle();
  4. }
  5.  
  6. sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
  7. {
  8. sp<IBinder> result;
  9. ...
  10. b = new BpBinder(handle);
  11. result = b;
  12. ...
  13. return result;
  14. }

可见 android_os_BinderProxy_transact() 函数实际上调用的是 BpBinder::transact() 函数。

5. BpBinder::transact() 则又调用了 IPCThreadState::self()->transact() 函数。

  1. status_t IPCThreadState::transact(int32_t handle,
  2. uint32_t code, const Parcel& data,
  3. Parcel* reply, uint32_t flags)
  4. {
  5. status_t err = data.errorCheck();
  6.  
  7. flags |= TF_ACCEPT_FDS;
  8.  
  9. if (err == NO_ERROR) {
  10. LOG_ONEWAY(">>>> SEND from pid %d uid %d %s", getpid(), getuid(),
  11. (flags & TF_ONE_WAY) == ? "READ REPLY" : "ONE WAY");
  12. err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
  13. }
  14.  
  15. if ((flags & TF_ONE_WAY) == ) {
  16. if (reply) {
  17. err = waitForResponse(reply);
  18. } else {
  19. Parcel fakeReply;
  20. err = waitForResponse(&fakeReply);
  21. }
  22. } else {
  23. err = waitForResponse(NULL, NULL);
  24. }
  25.  
  26. return err;
  27. }
  28.  
  29. status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
  30. int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
  31. {
  32. binder_transaction_data tr;
  33.  
  34. tr.target.ptr = ; /* Don't pass uninitialized stack data to a remote process */
  35. tr.target.handle = handle;
  36. tr.code = code;
  37. ...
  38.  
  39. mOut.writeInt32(cmd);
  40. mOut.write(&tr, sizeof(tr));
  41.  
  42. return NO_ERROR;
  43. }

由函数内容可以看出, 数据再一次通过 writeTransactionData() 传递给 mOut 进行写入操作。 mOut 是一个 Parcel 对象, 声明在 IPCThreadState.h 文件中。之后则调用 waitForResponse() 函数。

6. IPCThreadState::waitForResponse() 在一个 while 循环里不断的调用 talkWithDriver() 并检查是否有数据返回。

  1. status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
  2. {
  3. uint32_t cmd;
  4. int32_t err;
  5.  
  6. while () {
  7. if ((err=talkWithDriver()) < NO_ERROR) break;
  8. ...
  9.  
  10. cmd = (uint32_t)mIn.readInt32();
  11.  
  12. switch (cmd) {
  13. case BR_TRANSACTION_COMPLETE:
  14. ...
  15.  
  16. case BR_REPLY:
  17. {
  18. binder_transaction_data tr;
  19. err = mIn.read(&tr, sizeof(tr));
  20. ALOG_ASSERT(err == NO_ERROR, "Not enough command data for brREPLY");
  21. if (err != NO_ERROR) goto finish;
  22.  
  23. if (reply) {
  24. if ((tr.flags & TF_STATUS_CODE) == ) {
  25. reply->ipcSetDataReference(
  26. reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
  27. tr.data_size,
  28. reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
  29. tr.offsets_size/sizeof(binder_size_t),
  30. freeBuffer, this);
  31. } else {
  32. err = *reinterpret_cast<const status_t*>(tr.data.ptr.buffer);
  33. freeBuffer(NULL,
  34. reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
  35. tr.data_size,
  36. reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
  37. tr.offsets_size/sizeof(binder_size_t), this);
  38. }
  39. } else {
  40. freeBuffer(NULL,
  41. reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
  42. tr.data_size,
  43. reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
  44. tr.offsets_size/sizeof(binder_size_t), this);
  45. continue;
  46. }
  47. }
  48. goto finish;
  49. }
  50.  
  51. default:
  52. err = executeCommand(cmd);
  53. if (err != NO_ERROR) goto finish;
  54. break;
  55. }
  56. }
  57. ...
  58. }

7. IPCThreadState::talkWithDriver() 函数是真正与 binder 驱动交互的实现。ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) 就是使用系统调用函数 ioctl 向 binder 设备文件 /dev/binder 发送 BINDER_WRITE_READ命令。

  1. status_t IPCThreadState::talkWithDriver(bool doReceive)
  2. {
  3. if (mProcess->mDriverFD <= ) {
  4. return -EBADF;
  5. }
  6.  
  7. binder_write_read bwr;
  8.  
  9. // Is the read buffer empty?
  10. const bool needRead = mIn.dataPosition() >= mIn.dataSize();
  11.  
  12. // We don't want to write anything if we are still reading
  13. // from data left in the input buffer and the caller
  14. // has requested to read the next data.
  15. const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : ;
  16.  
  17. bwr.write_size = outAvail;
  18. bwr.write_buffer = (uintptr_t)mOut.data();
  19.  
  20. // This is what we'll read.
  21. if (doReceive && needRead) {
  22. bwr.read_size = mIn.dataCapacity();
  23. bwr.read_buffer = (uintptr_t)mIn.data();
  24. } else {
  25. bwr.read_size = ;
  26. bwr.read_buffer = ;
  27. }
  28.  
  29. // Return immediately if there is nothing to do.
  30. if ((bwr.write_size == ) && (bwr.read_size == )) return NO_ERROR;
  31.  
  32. bwr.write_consumed = ;
  33. bwr.read_consumed = ;
  34. status_t err;
  35.  
  36. #if defined(HAVE_ANDROID_OS)
  37. // 使用系统调用 ioctl 向 /dev/binder 发送 BINDER_WRITE_READ 命令
  38. if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= )
  39. err = NO_ERROR;
  40. else
  41. err = -errno;
  42. #else
  43. err = INVALID_OPERATION;
  44. #endif
  45.  
  46. do {
  47. if (mProcess->mDriverFD <= ) {
  48. err = -EBADF;
  49. }
  50. } while (err == -EINTR);
  51.  
  52. if (err >= NO_ERROR) {
  53. if (bwr.write_consumed > ) {
  54. if (bwr.write_consumed < mOut.dataSize())
  55. mOut.remove(, bwr.write_consumed);
  56. else
  57. mOut.setDataSize();
  58. }
  59. if (bwr.read_consumed > ) {
  60. mIn.setDataSize(bwr.read_consumed);
  61. mIn.setDataPosition();
  62. }
  63. return NO_ERROR;
  64. }
  65.  
  66. return err;
  67. }

经过 IPCThreadState::talkWithDriver() ,就将数据发送给了 Binder 驱动。

继续追踪 IPCThreadState::waitForResponse() ,可以从 第6步 发现 IPCThreadState 不断的循环读取 Binder 驱动返回,获取到返回命令后执行了 executeCommand(cmd) 函数。

8. IPCThreadState::executeCommand() 处理 Binder 驱动返回命令

  1. status_t IPCThreadState::executeCommand(int32_t cmd)
  2. {
  3. BBinder* obj;
  4. RefBase::weakref_type* refs;
  5. status_t result = NO_ERROR;
  6.  
  7. switch ((uint32_t)cmd) {
  8. ...
  9.  
  10. case BR_TRANSACTION:
  11. {
  12. binder_transaction_data tr;
  13. result = mIn.read(&tr, sizeof(tr));
  14. ...
  15. Parcel buffer;
  16. buffer.ipcSetDataReference(
  17. reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
  18. tr.data_size,
  19. reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
  20. tr.offsets_size/sizeof(binder_size_t), freeBuffer, this);
  21. ...
  22.  
  23. Parcel reply;
  24. status_t error;
  25. if (tr.target.ptr) {
  26. sp<BBinder> b((BBinder*)tr.cookie);
  27. error = b->transact(tr.code, buffer, &reply, tr.flags);
  28.  
  29. } else {
  30. error = the_context_object->transact(tr.code, buffer, &reply, tr.flags);
  31. }
  32. ...
  33. }
  34. break;
  35. ...
  36. }

9. 可以看出其调用了 BBinder::transact() 函数,将数据返回给上层。

  1. status_t BBinder::transact(
  2. uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
  3. {
  4. data.setDataPosition();
  5.  
  6. status_t err = NO_ERROR;
  7. switch (code) {
  8. case PING_TRANSACTION:
  9. reply->writeInt32(pingBinder());
  10. break;
  11. default:
  12. err = onTransact(code, data, reply, flags);
  13. break;
  14. }
  15.  
  16. if (reply != NULL) {
  17. reply->setDataPosition();
  18. }
  19.  
  20. return err;
  21. }

10. 而这里的 b->transact(tr.code, buffer, &reply, tr.flags) 中的 b (BBinder) 是 JavaBBinder 的实例,所以会调用 JavaBBinder::onTransact() 函数

  1. // frameworks/base/core/jni/android_util_Binder.cpp
  2. virtual status_t onTransact(
  3. uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = )
  4. {
  5. JNIEnv* env = javavm_to_jnienv(mVM);
  6. ...
  7. jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
  8. code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(reply), flags);
  9. }
  10.  
  11. static int int_register_android_os_Binder(JNIEnv* env)
  12. {
  13. ...
  14. gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
  15. ...
  16. }

11. 可见 JNI 通过 gBinderOffsets.mExecTransact 最后执行了 android.os.Binder 的 execTransact() 方法。

execTransact() 方法是 jni 回调的入口。

  1. // Entry point from android_util_Binder.cpp's onTransact
  2. private boolean execTransact(int code, long dataObj, long replyObj,
  3. int flags) {
  4. Parcel data = Parcel.obtain(dataObj);
  5. Parcel reply = Parcel.obtain(replyObj);
  6. ...
  7. try {
  8. res = onTransact(code, data, reply, flags);
  9. }
  10. ...
  11. }

12. 而我们则在服务端 IRemoteService.Stub 重载了 onTransact() 方法,所以数据最后会回到我们的服务端并执行服务端实现的 addUser() 方法。

  1. public static abstract class Stub extends android.os.Binder
  2. implements org.xdty.remoteservice.IRemoteService {
  3. ...
  4. @Override
  5. public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply,
  6. int flags) throws android.os.RemoteException {
  7. switch (code) {
  8. case INTERFACE_TRANSACTION: {
  9. reply.writeString(DESCRIPTOR);
  10. return true;
  11. }
  12. case TRANSACTION_basicTypes: {
  13. ...
  14. return true;
  15. }
  16. case TRANSACTION_addUser: {
  17. data.enforceInterface(DESCRIPTOR);
  18. org.xdty.remoteservice.User _arg0;
  19. if (( != data.readInt())) {
  20. _arg0 = org.xdty.remoteservice.User.CREATOR.createFromParcel(data);
  21. } else {
  22. _arg0 = null;
  23. }
  24. this.addUser(_arg0);
  25. reply.writeNoException();
  26. return true;
  27. }
  28. }
  29. return super.onTransact(code, data, reply, flags);
  30. }
  31. }

Binder 设备文件的打开和读写

我们看到 JNI 过程中调用了 ProcessState::getContextObject() 函数, 在 ProcessState 初始化时会打开 binder 设备

  1. // ProcessState.cpp
  2. ProcessState::ProcessState()
  3. : mDriverFD(open_driver())
  4. ...
  5. {
  6. ...
  7. }

open_driver() 函数内容如下

  1. // ProcessState.cpp
  2. static int open_driver()
  3. {
  4. // 打开设备文件
  5. int fd = open("/dev/binder", O_RDWR);
  6. if (fd >= ) {
  7. fcntl(fd, F_SETFD, FD_CLOEXEC);
  8. int vers = ;
  9. // 获取驱动版本
  10. status_t result = ioctl(fd, BINDER_VERSION, &vers);
  11. if (result == -) {
  12. ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
  13. close(fd);
  14. fd = -;
  15. }
  16. // 检查驱动版本是否一致
  17. if (result != || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
  18. ALOGE("Binder driver protocol does not match user space protocol!");
  19. close(fd);
  20. fd = -;
  21. }
  22. // 设置最多 15 个 binder 线程
  23. size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
  24. result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
  25. if (result == -) {
  26. ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
  27. }
  28. } else {
  29. ALOGW("Opening '/dev/binder' failed: %s\n", strerror(errno));
  30. }
  31. return fd;
  32. }

设备的读写

打开设备文件后,文件描述符被保存在 mDriverFD, 通过系统调用 ioctl 函数操作 mDriverFD 就可以实现和 binder 驱动的交互。

对 Binder 设备文件的所有读写及关闭操作则都在 IPCThreadState中,如上一小节提及到的 IPCThreadState::talkWithDriver函数

talkWithDriver() 函数封装了 BINDER_WRITE_READ 命令,会向 binder 驱动写入或从驱动读取封装在 binder_write_read 结构体中的本地或远程对象。

  1. // IPCThreadState.cpp
  2. status_t IPCThreadState::talkWithDriver(bool doReceive)
  3. {
  4. binder_write_read bwr;
  5. const bool needRead = mIn.dataPosition() >= mIn.dataSize();
  6. const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : ;
  7.  
  8. // 写入数据
  9. bwr.write_size = outAvail;
  10. bwr.write_buffer = (uintptr_t)mOut.data();
  11.  
  12. // 读取数据
  13. if (doReceive && needRead) {
  14. bwr.read_size = mIn.dataCapacity();
  15. bwr.read_buffer = (uintptr_t)mIn.data();
  16. } else {
  17. bwr.read_size = ;
  18. bwr.read_buffer = ;
  19. }
  20. ...
  21. // 使用 ioctl 系统调用发送 BINDER_WRITE_READ 命令到 biner 驱动
  22. if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= )
  23. err = NO_ERROR;
  24. ...
  25. }

BpBinder.cpp

BpBinder(Base proxy Binder) 对应于 Java 层的 Service Proxy,

先查看头文件 BpBinder.h 代码片断

  1. class BpBinder : public IBinder
  2. {
  3. public:
  4.  
  5. inline int32_t handle() const { return mHandle; }
  6.  
  7. virtual status_t transact( uint32_t code,
  8. const Parcel& data,
  9. Parcel* reply,
  10. uint32_t flags = );
  11.  
  12. virtual status_t linkToDeath(const sp<DeathRecipient>& recipient,
  13. void* cookie = NULL,
  14. uint32_t flags = );
  15. virtual status_t unlinkToDeath( const wp<DeathRecipient>& recipient,
  16. void* cookie = NULL,
  17. uint32_t flags = ,
  18. wp<DeathRecipient>* outRecipient = NULL);
  19. };

可以看到 BpBinder 中声明了 transact() linkToDeath() 等重要函数。再看具体实现

  1. status_t BpBinder::transact(
  2. uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
  3. {
  4. ...
  5. status_t status = IPCThreadState::self()->transact(
  6. mHandle, code, data, reply, flags);
  7. ...
  8.  
  9. return DEAD_OBJECT;
  10. }
  11.  
  12. status_t BpBinder::linkToDeath(
  13. const sp<DeathRecipient>& recipient, void* cookie, uint32_t flags)
  14. {
  15. ...
  16. IPCThreadState* self = IPCThreadState::self();
  17. self->requestDeathNotification(mHandle, this);
  18. self->flushCommands();
  19. ...
  20. return DEAD_OBJECT;
  21. }

可以看出 BPBinder 是最终是通过调用 IPCThreadState 的函数来完成数据传递操作。

IPCThreadState.cpp

AppOpsManager.cpp

APPOpsManager (APP Operation Manager) 是 应用操作管理者,实现对客户端操作的检查、启动、完成等

 

Binder Native 层(二)的更多相关文章

  1. Bn Bp Binder native层关系

    Servicemanager 源码在/frameworks/base/cmds/servicemanager/service_manager.c 编译成 systemmanager 可执行文件 sys ...

  2. Android Native层异步消息处理框架

     *本文系作者工作学习总结,尚有不完善及理解不恰当之处,欢迎批评指正* 一.前言 在NuPlayer中,可以发现许多类似于下面的代码: //============================== ...

  3. Android逆向之旅---Native层的Hook神器Cydia Substrate使用详解

    一.前言 在之前已经介绍过了Android中一款hook神器Xposed,那个框架使用非常简单,方法也就那几个,其实最主要的是我们如何找到一个想要hook的应用的那个突破点.需要逆向分析app即可.不 ...

  4. Android Framework 分析---2消息机制Native层

    在Android的消息机制中.不仅提供了供Application 开发使用的java的消息循环.事实上java的机制终于还是靠native来实现的.在native不仅提供一套消息传递和处理的机制,还提 ...

  5. Native层和so接口和Java层

    一.Java层加载so文件 Android在Java层加载so的接口是System.loadLibrary()逐级调用的过程: System.loadLibrary()系统源码: 987    pub ...

  6. {django模型层(二)多表操作}一 创建模型 二 添加表记录 三 基于对象的跨表查询 四 基于双下划线的跨表查询 五 聚合查询、分组查询、F查询和Q查询

    Django基础五之django模型层(二)多表操作 本节目录 一 创建模型 二 添加表记录 三 基于对象的跨表查询 四 基于双下划线的跨表查询 五 聚合查询.分组查询.F查询和Q查询 六 xxx 七 ...

  7. Android Java层,Native层,Lib层打印Log简介【转】

    本文转载自:https://blog.csdn.net/AndroidMage/article/details/52225068 说明: 这里我根据个人工作情况说明在各个层打印log.如有问题欢迎拍砖 ...

  8. 在Android Native层中创建Java虚拟机实例

    前言 Android应用中JNI代码,是作为本地方法运行的.而大部分情况下,这些JNI方法均需要传递Dalvik虚拟机实例作为第一个参数.例如,你需要用虚拟机实例来创建jstring和其他的Java对 ...

  9. SQLite数据库学习小结——native层实现

    1. SQlite概述 SQLite是一款轻量.快速.跨平台的嵌入式数据库,是遵守ACID(注:ACID指数据库事务正确执行的四个基本要素的缩写.包含:原子性(Atomicity).一致性(Consi ...

随机推荐

  1. 编写高质量代码改善C#程序的157个建议——建议137:委托和事件类型应添加上级后缀

    建议137:委托和事件类型应添加上级后缀 委托类型本身是一个类,考虑让派生类的名字以基类名字作为后缀.事件类型是一类特殊的委托,所以事件类型也遵循本建议. 委托和事件的正确的命名方式有: public ...

  2. 编写高质量代码改善C#程序的157个建议——建议118:使用SecureString保存密钥等机密字符串

    建议118:使用SecureString保存密钥等机密字符串 托管代码中的字符串是一类特殊的对象,它们不可用被改变.每次使用System.String类张的方法之一时,或者使用此类型进行运算时(如赋值 ...

  3. unittest测试框架详谈及实操(四)

    测试套件 应用unittest的Test Suite特性,可以将不同的测试组成一个逻辑组,然后设置统一的测试套来一起执行测试.通过TestSuite.TestLoader类来创建测试套件,最后用Tes ...

  4. VS2010下安装OpenCV2.4.3

    本文记录Windows 7 X86 SP1操作系统环境下,安装与配置OpenCV2.4.3的详细步骤.前置需求:已安装有VS2010. 下载并安装OpenCV 从http://www.opencv.o ...

  5. [LeetCode 题解]: palindromes

    Determine whether an integer is a palindrome. Do this without extra space. Some hints: Could negativ ...

  6. duilib入门简明教程 -- 界面设计器 DuiDesigner (10)

       上一个教程讲解了怎么布局最大化.最小化.关闭按钮,但是如果手动去计算这三个按钮的位置和大小的话,非常的不直观,也很不方便.     所以这一章准备介绍duilib的UI设计器,由于这个设计器很不 ...

  7. Linux下配置Apache为多端口

    1.打开Apache的配置文件httpd.conf,在Listen 80处另起一行输入Listen 8080(监听8080端口),要想再添加端口可依次添加 2.在httpd.conf文件最后一行添加: ...

  8. Qt学习(一)

    1. 简介 跨平台 GUI 通常有三种实现策略 API 映射 相当于将不同平台的 API 提取公共部分.界面库使用同一套 API,将其映射到不同的底层平台上面.相当于取交集 如wxWidgets. 优 ...

  9. pageadmin CMS Sql新建数据库和用户名教程

    用pageadmin网站制作如何Sql新建数据库和用户名 sql server软件安装完毕后,需要新建一个数据库用来作为网站的数据库. 1.打开sql管理界面,如图所示,找到数据库,右键单击数据库,选 ...

  10. Subway Pursuit (二分)(交互题)

    题目来源:codeforces1039B Subway Pursuit 题目大意: 在1到n里有一个运动的点,要求找到这个点,每次可以查询一个区间内有没有这个点,每次这个点往左或者往右移动1到k个位置 ...