Binder基本概念流程学习
一 Media Service进程启动
Init.rc中描述的service对应linux 的进程:
Media进程定义:
service media /system/bin/mediaserverclass mainuser mediagroup audio camera inet net_bt net_bt_admin net_bw_acct drmrpcioprio rt 4
servicemanager 进程定义:
service servicemanager /system/bin/servicemanagerclass coreuser systemgroup system
Media中有很多Native Service(AudioFlinger MediaPlayerService CameraService
AudioPolicyService等),整个Android(native或者framework)的Service都需要加入
servicemanager中进行统一管理。
那么Media Process 与ServiceManager Process是如何进行通信的呢——Binder.
通过Media Process中使用binder进行完成IPC过程,学习Binder的概念和使用方法。
\frameworks\av\media\mediaserver\ main_mediaserver.cpp:
int main(int argc, char** argv) {//创建ProcessState 当前进程属性sp<ProcessState> proc(ProcessState::self());//IServiceManager对象sp<IServiceManager> sm = defaultServiceManager();//初始化MediaPlayerService服务对象 MediaPlayerService::instantiate();……//启动进程的线程池ProcessState::self()->startThreadPool();//执行线程消息循环IPCThreadState::self()->joinThreadPool(); }
Sp:指针运算符和普通运算符的重载 StrongPointer。
二 Media Process执行过程
1 ProcessState对象创建
当前进程的状态属性,对象创建:
sp<ProcessState> ProcessState::self() {Mutex::Autolock _l(gProcessMutex);if (gProcess != NULL) {return gProcess;}gProcess = new ProcessState;return gProcess; }
ProcessState构造函数:
ProcessState::ProcessState(): mDriverFD(open_driver()) //打开binder驱动设备 , mVMStart(MAP_FAILED), mManagesContexts(false), mBinderContextCheckFunc(NULL), mBinderContextUserData(NULL), mThreadPoolStarted(false), mThreadPoolSeq(1) {if (mDriverFD >= 0) {//将binder的fd映射到当前进程虚拟空间地址中 与binder进行交互mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);} }
打开binder设备:
static int open_driver() {//打开binder设备驱动int fd = open("/dev/binder", O_RDWR);if (fd >= 0) {//bidner最大支持线程数size_t maxThreads = 15;result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);}return fd; }
ProcessState对象创建过程所做的事:
打开/dev/binder设备,得到binder设备的fd;
将bidner设备fd映射到当前进程虚拟地址空间建立交互的通道;
2 IServiceManager对象创建
sp<IServiceManager> sm = defaultServiceManager();
为什么需要一个IServiceManager对象呢,这个类是个抽象提供接口
class IServiceManager : public IInterface {virtual sp<IBinder> getService( const String16& name) const = 0;virtual status_t addService( const String16& name,const sp<IBinder>& service,bool allowIsolated = false) = 0;…… };
通过IServiceManager派生对象操作将Service加入到ServiceManager中.
defaultServiceManager()函数:
sp<IServiceManager> defaultServiceManager() {//单例对象if (gDefaultServiceManager != NULL) return gDefaultServiceManager;AutoMutex _l(gDefaultServiceManagerLock);if (gDefaultServiceManager == NULL) {//创建对象gDefaultServiceManager = interface_cast<IServiceManager>(ProcessState::self()->getContextObject(NULL));}return gDefaultServiceManager; }
ProcessState::self()->getContextObject(NULL):得到一个IBinder对象
ProcessState::self()刚才所创建的ProcessState对象。
sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& caller) {return getStrongProxyForHandle(0); }sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle) {sp<IBinder> result;AutoMutex _l(mLock);//从表中查询一个handle对应的handle_entry //若没有则创建一个 handle == 0handle_entry* e = lookupHandleLocked(handle);if (e != NULL) {IBinder* b = e->binder;if (b == NULL || !e->refs->attemptIncWeak(this)) {//handle_entry对象成员初始化 创建handle=0的BpBinder b = new BpBinder(handle); e->binder = b;if (b) e->refs = b->getWeakRefs();result = b;}}return result; }
handle_entry是什么呢?
struct handle_entry {IBinder* binder;RefBase::weakref_type* refs;};
也就是说ProcessState 有一个表Vector<handle_entry>mHandleToObject;
表里面的每一项存储了一个binder,每一个binder对应一个handle。
Handle = 0是什么,句柄? 代表谁的句柄——ServiceManager在binder中的资源。
从ProcessState::self()->getContextObject(NULL)得到一个 IBinder——BpBinder(0);
于是得到:
gDefaultServiceManager = interface_cast<IServiceManager>(BpBinder(0));
使用interface_cast将IBinder实例转化成IServiceManager实例。
3 interface_cast函数
\frameworks\native\include\binder\IInterface.h:
interface_cast是个内联模板函数:
template<typename INTERFACE> inline sp<INTERFACE> interface_cast(const sp<IBinder>& obj) {return INTERFACE::asInterface(obj); }
结合前面就是:
inline sp<IServiceManager> interface_cast(const sp<IBinder>& obj) {return IServiceManager::asInterface(obj); }
所以需要到IServiceManager里面去看看是如何实现的
4 IServiceManager类
\frameworks\native\include\binder\IServiceManager.h:
IServiceManager是一个抽象类:
class IServiceManager : public IInterface {public://宏声明 DECLARE_META_INTERFACE(ServiceManager);virtual sp<IBinder> getService( const String16& name) const = 0;virtual status_t addService( const String16& name,const sp<IBinder>& service,bool allowIsolated = false) = 0;…… }
DECLARE_META_INTERFACE声明:
#define DECLARE_META_INTERFACE(INTERFACE) \static const android::String16 descriptor; \static android::sp<I##INTERFACE> asInterface( \const android::sp<android::IBinder>& obj); \virtual const android::String16& getInterfaceDescriptor() const; \I##INTERFACE(); \virtual ~I##INTERFACE(); \
替换成IServiceManager:
//实现时传入:android.os.IServiceManager static const android::String16 descriptor; static android::sp<IServiceManager> asInterface( const android::sp<android::IBinder>& obj); virtual const android::String16& getInterfaceDescriptor() const; //构造析构函数 IServiceManager(); virtual ~IServiceManager();
实现\frameworks\native\include\binder\IServiceManager.cpp:
IMPLEMENT_META_INTERFACE(ServiceManager, "android.os.IServiceManager");
IMPLEMENT_META_INTERFACE实现:
看一下asInterface接口:
#define IMPLEMENT_META_INTERFACE(INTERFACE, NAME) \android::sp<I##INTERFACE> I##INTERFACE::asInterface( \const android::sp<android::IBinder>& obj) \{ \android::sp<I##INTERFACE> intr; \if (obj != NULL) { \intr = static_cast<I##INTERFACE*>( \obj->queryLocalInterface( \I##INTERFACE::descriptor).get()); \if (intr == NULL) { \intr = new Bp##INTERFACE(obj); \} \} \return intr; \ } \
替换成IServiceManager:
android::sp<IServiceManager> IServiceManager::asInterface( const android::sp<android::IBinder>& obj) { //obj BpBinder实例 android::sp<IServiceManager> intr; if (obj != NULL) {//返回NULL intr = static_cast<IServiceManager*>( obj->queryLocalInterface( IServiceManager::descriptor).get()); if (intr == NULL) { intr = new BpServiceManager(obj); } } return intr; }
这里得到IServiceManager 实例:
BpServiceManager:new BpServiceManager(new BpBinder(0));
5 BpServiceManager 和 BpInterface类
\frameworks\native\libs\binder\ IServiceManager.cpp:BpServiceManager
class BpServiceManager : public BpInterface<IServiceManager> { public://impl就是 new BpBinder(0)BpServiceManager(const sp<IBinder>& impl): BpInterface<IServiceManager>(impl){}virtual sp<IBinder> checkService(const String16& name) const{……remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);}virtual status_t addService(const String16& name, const sp<IBinder>& service,bool allowIsolated){……remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);} }
\frameworks\native\include\binder\ IInterface.h:模板类BpInterface
template<typename INTERFACE> class BpInterface : public INTERFACE, public BpRefBase // INTERFACE IServiceManager {public:BpInterface(const sp<IBinder>& remote);protected: virtual IBinder* onAsBinder(); };
BpInterface构造函数:
template<typename INTERFACE> inline BpInterface<INTERFACE>::BpInterface(const sp<IBinder>& remote) : BpRefBase(remote) { }
BpRefBase构造函数:
BpRefBase::BpRefBase(const sp<IBinder>& o) : mRemote(o.get()), mRefs(NULL), mState(0) {// IBinder mRemote 指向 o.get() :new BpBinder(0) }
gDefaultServiceManager = interface_cast<IServiceManager>(BpBinder(0));
实际为:
gDefaultServiceManager = new BpServiceManager(new BpBinder(0));
Bn代表Binder Native Bp代表Binder Proxy
BpServiceManager代理的BpBinder实例 BpBinder代理的handle(0)
这个关系有些复杂,看一下类继承结构图:
上面这个结构看起来感觉很熟悉——Bridge模式。将Binder数据交互和功能处理桥接起来。
在Media Process 的main函数中通过:
sp<IServiceManager> sm = defaultServiceManager();
我们得到了sm:是BpServiceManager对象。
三 MediaPlayerService加入到ServiceManager中
回到main函数中:
int main(int argc, char** argv) {//创建ProcessState 当前进程属性sp<ProcessState> proc(ProcessState::self());//IServiceManager对象sp<IServiceManager> sm = defaultServiceManager();//初始化MediaPlayerService服务对象MediaPlayerService::instantiate(); //执行到这里 ……//启动进程的线程池ProcessState::self()->startThreadPool();//执行线程消息循环IPCThreadState::self()->joinThreadPool(); }
1 MediaPlayerService初始化过程
void MediaPlayerService::instantiate() {// defaultServiceManager就是上面所述得到的BpServiceManager对象defaultServiceManager()->addService(String16("media.player"), new MediaPlayerService()); }
BpServiceManager添加Service:
virtual status_t addService(const String16& name, const sp<IBinder>& service,bool allowIsolated) {//生成数据包Parcel Parcel data, reply;// Write RPC headers 写入Interface名字 得到“android.os.IServiceManager” data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());//写入Service名字 “media.player” data.writeString16(name);//写入服务 data.writeStrongBinder(service);data.writeInt32(allowIsolated ? 1 : 0);// remote()返回BpBinder对象status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);return err == NO_ERROR ? reply.readExceptionCode() : err; }
remote()->transact 到BpBinder中:
status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { if (mAlive) {//到当前进程IPCThreadState中 mHandle=0status_t status = IPCThreadState::self()->transact(mHandle, code, data, reply, flags);if (status == DEAD_OBJECT) mAlive = 0;return status;}return DEAD_OBJECT; }
2 IPCThreadState中写入数据到Binder设备过程
IPCThreadState::self()->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;//将数据转化成binder_transaction_data 写入到Parcel实例mOut中err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);//写入数据err = waitForResponse(reply);return err; } status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult) {while (1) {//将数据写入到Binder设备中 talkWithDriver();……}return err; }status_t IPCThreadState::talkWithDriver(bool doReceive) {//将数据封装成binder_write_read结构 binder_write_read bwr;do {//将数据写入到所打开的Binder设备中ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)……} while (err == -EINTR);return NO_ERROR; }
将MediaPlayerService加入到ServiceManager中,
这里就通过BpServiceManager的AddService将数据写入到Binder设备传递给ServiceManager。
继续Media Process过程
四 Media Process消息循环
int main(int argc, char** argv) {//启动进程的线程池 ProcessState::self()->startThreadPool(); //走到了这里//执行线程消息循环IPCThreadState::self()->joinThreadPool(); }
1 创建工作者线程
startThreadPool:\frameworks\native\libs\binder\ ProcessState.cpp:
void ProcessState::startThreadPool() {spawnPooledThread(true); }void ProcessState::spawnPooledThread(bool isMain) {//创建PoolThread对象 并run ,非线程sp<Thread> t = new PoolThread(isMain);t->run(buf); }
PoolThread继承Thread
执行Thread的run函数:
status_t Thread::run(const char* name, int32_t priority, size_t stack) {//创建线程mThread _threadLoopbool res;res = createThreadEtc(_threadLoop,this, name, priority, stack, &mThread);return NO_ERROR; }
现在有两个线程:主线程和mThread线程
mThread线程执行:_threadLoop
int Thread::_threadLoop(void* user) {Thread* const self = static_cast<Thread*>(user);do {//调用子类的threadLoopresult = self->threadLoop();……} while(strong != 0);return 0; }class PoolThread : public Thread { protected:virtual bool threadLoop(){IPCThreadState::self()->joinThreadPool(mIsMain);return false;} };
2 进程间通信消息循环过程
消息循环:
void IPCThreadState::joinThreadPool(bool isMain) {mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);status_t result; //消息循环do {int32_t cmd;//从binder设备中读取命令result = talkWithDriver();if (result >= NO_ERROR) {cmd = mIn.readInt32();//执行命令result = executeCommand(cmd);}……} while (result != -ECONNREFUSED && result != -EBADF);mOut.writeInt32(BC_EXIT_LOOPER);talkWithDriver(false); }
命令执行:
status_t IPCThreadState::executeCommand(int32_t cmd) {BBinder* obj;RefBase::weakref_type* refs;switch (cmd) {case BR_DECREFS:break;case BR_ATTEMPT_ACQUIRE:break; case BR_TRANSACTION:binder_transaction_data tr;result = mIn.read(&tr, sizeof(tr));if (tr.target.ptr) {//将目标对象转化成BBindersp<BBinder> b((BBinder*)tr.cookie);//调用BBinder的transact 函数const status_t error = b->transact(tr.code, buffer, &reply, tr.flags);}break;……default:}return result; }
binder_transaction_data.cookie:target object cookie目标对象,这个target object是指那个呢?
在Media Process里面有几个Service:AudioFlinger、MediaPlayerService、CameraService等。
这个目标是这其中Service中的一个,假设目标对象为为MediaPlayerService,那为何要转化成BBinder呢?
3 Service对命令的处理
线程从binder接收到消息命令,将命令传递给Service处理。将目标对象转化成BBinder,然后调度此命令;
命令从远端传递到本地端进行处理,每个Service都对应BnXXX对象来处理远端BpXXX传来的命令。
sp<BBinder> b((BBinder*)tr.cookie);
const status_t error = b->transact(tr.code, buffer, &reply, tr.flags);
这里b代表某个Service:假设为MediaPlayerService;弄清楚执行过程,要弄清楚类继承关系。
本地端BnMediaPlayerService消息处理过程:真正的对象是MediaPlayerService实例。
从BBinder ->transact开始传递:
status_t BBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {//onTransact是个virtual函数 派生类BnMediaPlayerService重写err = onTransact(code, data, reply, flags);return err; }status_t BnMediaPlayerService::onTransact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {switch (code) {case CREATE: {pid_t pid = data.readInt32();sp<IMediaPlayerClient> client =interface_cast<IMediaPlayerClient>(data.readStrongBinder());//create是个virtual函数 派生类MediaPlayerService重写sp<IMediaPlayer> player = create(pid, client, audioSessionId);//创建player写入到数据包中 传回reply->writeStrongBinder(player->asBinder());return NO_ERROR;} break;……default:return BBinder::onTransact(code, data, reply, flags);} }
可以看看Client类继承关系结构图:
看到这个跟上面的MediaPlayerService继承关系非常的相似,
这个结构也非常的熟悉——Adapter模式;将binder消息交互和命令处理适配到一起。
五 Client端与Service端交互
Client对Service进行使用Binder通信,是得到一个Service BnXXX端对象的代理,在Client 为BpXXX代理,
然后使用此代理进行相关的操作. 前面在使用ServiceManager就是此种方式进行。
实际上通信的基础是Binder,Proxy不过是在Binder上进行了一层封装,封装了对binder驱动的底层操作,使具有面向对象的特性。
任意两个进程通过Binder进行通信,就是先得到另一个进程的binder标识,通过此binder进行数据交换。
1 新增加一个服务
看下面media Palyer类继承结构。
实现Bn端类继承结构:
实现Bp端类继承结构:
可以看到 :
- 需要定义服务公共接口IMediaPlayerService;
- 实现服务Bn端 派发消息BnMediaPlayerService;
- 实现服务的命令处理MediaPlayerService;
- 实现服务代理BpMediaPlayerService;
2 Client获取Service服务
Native Service以及framework Service都是加入到ServiceManger中,
不管native端还是Framework端得Service 其实都是要满足上面远程对象代理结构。
native端获取service:
//获取ServiceManager的代理对象 sp<IServiceManager> sm = defaultServiceManager(); //通过ServiceManager获取media Service binder binder = sm->getService(String16("media.player")); //将binder封装 构造media Service代理对象 BpMediaPlayerService sMediaPlayerService = interface_cast<IMediaPlayerService>(binder);
framework层的service,借助AIDL文件,实现跨进程的通信:
所有framework端service想要作为公共的服务,被别的应用程序调用,都要实现AIDL文件,服务要继承该AIDL文件内部类Stub。
其实AIDL文件是对Framework中service作为进程通信的框架的封装,系统自动生成中间需要的代码和步骤,统一结构:还是binder代理,服务代理。
下面看看PowerManagerService实现。
PowerManagerService服务:
\frameworks\base\services\java\com\android\server\PowerManagerService.java
public class PowerManagerService extends IPowerManager.Stub…… {public void goToSleep(long time){……} };AIDL文件:\frameworks\base\core\java\android\os\IPowerManager.aidl interface IPowerManager {void goToSleep(long time);…… }
AIDL对应的Java文件:
AIDL会自动生成一个对应的Java的interface文件,看看这个接口文件。
AIDL自动生成对应java文件位置:\out\target\common\obj\JAVA_LIBRARIES\framework_intermediates\src\core\java\android\os
public interface IPowerManager extends android.os.IInterface {//IPowerManager 内部类 Stubpublic static abstract class Stub extends android.os.Binder implementsandroid.os.IPowerManager {public static android.os.IPowerManager asInterface(android.os.IBinder obj){//生成一个Proxy对象return new android.os.IPowerManager.Stub.Proxy(obj);}public android.os.IBinder asBinder() {return this;}@Overridepublic boolean onTransact(int code, android.os.Parcel data,android.os.Parcel reply, int flags){switch (code) {case TRANSACTION_goToSleep:this.goToSleep(_arg0);return true;……}}//Stub内部类Proxy private static class Proxy implements android.os.IPowerManager {private android.os.IBinder mRemote;Proxy(android.os.IBinder remote) {mRemote = remote;}public void goToSleep(long time) {//将数据打包成Parcelandroid.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();_data.writeInterfaceToken(DESCRIPTOR);_data.writeLong(time);//传输数据到binder 到相关的Service 进程// IPCThreadState::transact方法中完成mRemote.transact(Stub.TRANSACTION_goToSleep, _data, _reply,0);_reply.readException();}}}//goToSleep接口对应的IDstatic final int TRANSACTION_goToSleep = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);//接口public void goToSleep(long time); }
看下其中类结构:
AIDL自动生成对应的java文件,将结构过程进行了统一,自动生成中间代码。
Application获取用Service:
IPowerManager m mPowerManagerService; //获取service的bingder IBinder binder = ServiceManager.getService("power"); //创建service代理对象 mPowerManagerService = IPowerManager.Stub.asInterface(binder); //调用接口 mPowerManagerService.goToSleep();
Native Service和 Framework Service结构方式基本一致的。
进程间通信Linux系统已经提供了很多方式,比如Socket,为什么Android非要另辟蹊径,设计了Binder呢?
可以阅读一下:http://www.cnblogs.com/bastard/archive/2012/10/17/2728155.html
Game Over !
参考文档:
http://www.cnblogs.com/innost/archive/2011/01/09/1931456.html
http://blog.csdn.net/maxleng/article/details/5490770
注:
“我们其实还一部分没有研究,就是同一个进程之间的对象传递与远程传递是区别的。同一个进程间专递服务地和对象,
就没有代理BpBinder产生,而只是对象的直接应用了。应用程序并不知道数据是在同一进程间传递还是不同进程间传递,
这个只有内核中的Binder知道,所以内核Binder驱动可以将Binder对象数据类型从BINDER_TYPE_BINDER修改为
BINDER_TYPE_HANDLE或者BINDER_TYPE_WEAK_HANDLE作为引用传递。” ——来自上述地址
这个可以看到在SystemServer运行的Service之间使用时,直接转化成了对应的对象,而不是通过代理。
Binder基本概念流程学习相关推荐
- Android Binder基本概念流程学习
一 Media Service进程启动 Init.rc中描述的service对应linux 的进程: Media进程定义: service media /system/bin/mediaserverc ...
- Android进程间通信(IPC)机制Binder简要介绍和学习计划
在Android系统中,每一个应用程序都是由一些Activity和Service组成的,这些Activity和Service有可能运行在同一个进程中,也有可能运行在不同的进程中.那么,不在同一个进程的 ...
- 软件开发流程--学习笔记
软件开发流程--学习笔记 背景 产生原因及作用 个人理解 软件开发流程 需求 需求调研 需求分析 需求确认 设计 概要设计 详细设计 编码 测试 交付 维护 知识拓展 软件开发生命周期(SDLC) 软 ...
- Blender 3.0机器人硬面建模材质渲染全流程学习课程
学习在Blender中建模硬表面机器人角色 你会学到什么 Blender 3.0建模工具 Blender 3.0硬面人物造型 机器人角色的UV展开 如何在Blender中渲染 MP4 |视频:h264 ...
- 【UE5教程】影棚拍摄于虚拟场景合成制作流程学习
用虚幻引擎预算虚拟生产5 你会学到什么 使用虚幻引擎5进行虚拟生产 使用虚幻引擎5的独立虚拟制作 用虚幻引擎预算虚拟生产5 用虚幻引擎5进行穷人虚拟生产 用虚幻引擎5进行自制虚拟制作 虚幻引擎5独立虚 ...
- 浅谈CMMI几个过程概念流程管理 (转)
浅谈CMMI几个过程概念流程管理 CMMI(Capability Maturity Model Integration)能力成熟度模型集成,正如它的名字一样,它是一个模型.个人觉得它更是一种概念.它带 ...
- Vue学习(增删改查、ES6模块化概念)-学习笔记
文章目录 Vue学习(增删改查.ES6模块化概念)-学习笔记 增删改查案例 ES6模块化概念 Vue学习(增删改查.ES6模块化概念)-学习笔记 增删改查案例 <!DOCTYPE html> ...
- webpack打包流程_了不起的 Webpack 构建流程学习指南
最近原创文章回顾: <了不起的 tsconfig.json 指南> <了不起的 Webpack HMR 学习指南(含源码分析)> <<你不知道的 Blob>番 ...
- Linux 启动流程学习
Linux 启动流程学习 开机自检(加载BIOS) 无论什么机器,在开机后都要进行通电自检(硬件),如果硬件有问题,则无法继续下去. (例如当内存条松动时或者损坏,就会发出嘀嘀嘀警报声).接着开始加载 ...
最新文章
- python语言if语句-Python if else语句详解
- VTK:vtkCursor2D用法实战
- 如何解决使用JSON.stringify时遇到的循环引用问题
- ICanPay 统一支付网关
- 关于使用在webforms里使用routing遇到的问题
- datagridview 绑定list 不能刷新界面_人人都可写代码-H5零基础编程-发布活动界面实操07...
- PCL中分割方法的介绍(2)
- dubbo学习(八)dubbo项目搭建--消费者(服务消费者)
- 记录.NET Core部署到Linux之.NET Core环境搭建(1)
- Ransac算法--直线拟合
- 素描头像的正确画法:掌握这三点就能把握绘画精髓~
- zzulioj1134: 字符串转换
- 计算机入门及操作技能训练,计算机入门及操作技能训练模拟试题.doc
- 网络安全(2) -- 关于一次XSS攻击-图片(img标签)的onerror事件
- Android7.0 8.0进程保活与拉活的实现方式
- 社交网络:有意义的不仅是邓巴数
- Highmaps上海地区地图数据JSON格式
- QQ给附近的人群发消息,需要验证的自动申请为好友 按键精灵源码
- OpenGL 半透明效果的实现(混合)、雾效果 和 圆滑(抗锯齿)
- redo日志的刷盘策略
热门文章
- 内核态文件操作【转】
- 项目部署,环境搭建(pip/ makefile)
- HTTP协议详解 (转)
- 12月第三周各国域名总量榜:中国729万居全球第二
- Rails secret token bug
- .NET 2.0泛型集合类与.NET 1.1集合类的区别(一)
- 系统设计和机器学习算法
- 【解决方案】kafka: client has run out of available brokers to talk to (Is your cluster reachable?)
- Vue学习笔记之02-Mustache语法以及一些指令
- 函数的梯度方向和切线方向_梯度是函数变化最快的方向