作者 | 开开向前冲

地址 | http://www.jianshu.com/p/72bc48d59f0e

声明 | 本文是 开开向前冲 原创,已获授权发布,未经原作者允许请勿转载

前言

本文参考一个朋友兼同事ShadowN1ght的文章客户端到驱动通信流程;用一个简单的案例阐述了完整的Binder 通信过程;

Binder之于Android,犹如电话之于人类,都是用于传递信息;

写这篇文章前我酝酿了很久,不知道该何从下笔,文章写了又删,删了又写,因为始终感觉写的有点杂,一不注意就丢失了主线;现在通过跟踪一个完整的Binder调用来说明Binder IPC的过程——PowerManger调用isScreenOn();

分析

方案设计:Binder通信涉及APP层,framework层,Kernel层,虽然涉及的东西比较杂,但是都是代码实现的,既然如此,我们都可以通过增加调试Log信息来跟踪这个流程,本文的思路就是如此;

为了更好的理解 Binder 通信过程,你最好有一套完整的 Android 源码,再配上一个代码搜索神器 OpenGrok,如果你没有 android源码,不想自己搭建 OpenGrok 服务器,这里推荐一个公开的 Android Code In OpenGrok,只不过由于网速问题你可能得多花点时间等待;

1. APP——> framework
import com.example.bindservice.ProcessInfo;

public class MainActivity extends AppCompatActivity {    String TAG = "Bindertest MainActivity";

    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findViewById(R.id.mybtn).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                ProcessInfo processInfo = new ProcessInfo();                processInfo.nativeSelfCall();                PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);                Log.e(TAG,"App begin nativeCall");                boolean bool = powerManager.isScreenOn();                Log.e(TAG,"App end nativeCall");                Log.e(TAG,"" + bool);            }        });    }}

Binder过程很简单,就是调用了powerManager.isScreenOn();//Binder Call 代码;
processInfo.nativeSelfCall()是自己添加的JNI,目的是向Binder Kernel中传递cmd,然后在Binder Kernel中根据cmd获取到应用进程ID,过滤Log;这里暂时不说;

/frameworks/base/core/java/android/os/PowerManager.java

@Deprecatedpublic boolean isScreenOn() {    return isInteractive();}

public boolean isInteractive() {    try {        return mService.isInteractive();    } catch (RemoteException e) {        return false;    }}

PowerManger.java中的isScreenOn最终会调用 mService.isInteractive();这里的mService是什么呢???

mService
final IPowerManager mService;/** * {@hide} */public PowerManager(Context context, IPowerManager service, Handler handler) {    mContext = context;    mService = service;//mService初始化是在PowerManager构造方法中    mHandler = handler;}

mService 是IPowerManager 对象;这里根据名字我们就知道IPowerManager是通过AIDL生成的代码,可以在Android Studio中查找;找到:
out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/os/IPowerManager.java

------> IPowerManager.java——>Proxy      @Override      public boolean isInteractive() throws android.os.RemoteException {            android.os.Parcel _data = android.os.Parcel.obtain();            android.os.Parcel _reply = android.os.Parcel.obtain();            boolean _result;            try {                _data.writeInterfaceToken(DESCRIPTOR);                mRemote.transact(Stub.TRANSACTION_isInteractive, _data, _reply, 0);//核心核心                _reply.readException();                _result = (0 != _reply.readInt());            } finally {                _reply.recycle();                _data.recycle();            }            return _result;        }

_data用于包装客户端数据,_reply用于从服务端获取数据,mRemote是android.os.IBinder对象,DESCRIPTOR标识了IPowerManager ,DESCRIPTOR = "android.os.IPowerManager";TRANSACTION_isInteractive 是方法号,TRANSACTION_isInteractive = (android.os.IBinder.FIRST_CALL_TRANSACTION + 11);_result即我们应用层得到的值,这里是根据reply获取的值来赋值的;

这里我重点看看mRemote;

------> IPowerManager.java——>Proxy        ......        private static class Proxy implements android.os.IPowerManager {            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {                mRemote = remote;//赋值            }

            @Override            public android.os.IBinder asBinder() {                return mRemote;            }            ......        }

这里mRemote是在Proxy的构造方法中被调用,那Proxy是在什么地方调用呢?

------>IPowerManager.java——>Stub        /**         * Cast an IBinder object into an android.os.IPowerManager interface,         * generating a proxy if needed.         */        public static android.os.IPowerManager asInterface(android.os.IBinder obj) {            if ((obj == null)) {                return null;            }            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);            if (((iin != null) && (iin instanceof android.os.IPowerManager))) {                return ((android.os.IPowerManager) iin);            }            return new android.os.IPowerManager.Stub.Proxy(obj);//调用Proxy构造函数        }

这里IPowerManger.Stub asInterface在什么地方调用呢?asInterface返回的对象是IPowerManager对象;在Android Studio 查看该方法在那些地方被调用,你会发现有很对,但是我相信你会特别在ContextImpl的;
/frameworks/base/core/java/android/app/ContextImpl.java

------> ContextImpl.java        registerService(POWER_SERVICE, new ServiceFetcher() {                public Object createService(ContextImpl ctx) {                    IBinder b = ServiceManager.getService(POWER_SERVICE);                    IPowerManager service = IPowerManager.Stub.asInterface(b);                    //调用IPowerManger.stub.asInterface,传入的IBinder对象参数是从                     //ServiceManager.getService(POWER_SERVICE)获取的                    if (service == null) {                        Log.wtf(TAG, "Failed to get power manager service.");                    }                    return new PowerManager(ctx.getOuterContext(),                            service, ctx.mMainThread.getHandler());//返回PowerManager对象,                    //这里的service就是PowerManger类中的mService,//我们在应用层调用的  (PowerManager) getSystemService(Context.POWER_SERVICE)//得到的powerManger对象就是这里返回的new PowerManager,这部分代码跟着逻辑就能看到;             }});

看到registerService方法,我相信很多人都很熟悉,这里的service就是PowerManger类中的mService,
应用层调用的  (PowerManager) getSystemService(Context.POWER_SERVICE)得到的powerManger对象
就是这里返回的new PowerManager,顺着Activity的getSystemService方法的逻辑看就会清楚;
PowerManager中的mService就是这里的service,通过 IPowerManager.Stub.asInterface(b)获得;

IBinder b = ServiceManager.getService(POWER_SERVICE);
IPowerManager service = IPowerManager.Stub.asInterface(b);
这里真正开始接触IBiner,这里做个标记,等会可能会回头再来看

这里调用 IPowerManager.Stub.asInterface(b)的IBinder b参数是从ServiceManager.getService(POWER_SERVICE)获取的,那这个IBinder b值是什么呢???(我这里先透露一下,IBinder b是一个BinderProxy对象

/frameworks/base/core/java/android/os/ServiceManager.java

------> ServiceManager.java/**     * Returns a reference to a service with the given name.     *      * @param name the name of the service to get     * @return a reference to the service, or <code>null</code> if the service doesn't exist     */    public static IBinder getService(String name) {        try {            IBinder service = sCache.get(name);            if (service != null) {                return service;            } else {                return getIServiceManager().getService(name);//核心核心            }        } catch (RemoteException e) {            Log.e(TAG, "error in getService", e);        }        return null;    }

这里先从缓存中获取IBinder,缓存中没有则调用getIServiceManager().getService(name)获取;

------> ServiceManager.java    private static IServiceManager getIServiceManager() {        if (sServiceManager != null) {            return sServiceManager;        }        // Find the service manager        sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());//核心核心        return sServiceManager;    }

/frameworks/base/core/java/android/os/ServiceManagerNative.java
ServiceManagerNative.asInterface(BinderInternal.getContextObject())的参数为BinderInternal.getContextObject();

------> ServiceManagerNative.java  static public IServiceManager asInterface(IBinder obj)    {        if (obj == null) {            return null;        }        IServiceManager in =            (IServiceManager)obj.queryLocalInterface(descriptor);//如果没有真正的跨进程通信,则从这里返回        //Binder 类有实现,BinderProxy没有实现queryLocalInterface方法;        if (in != null) {            return in;//判断是否是真的跨进程,比如应用内部实现service,就没真正跨进程,从这里返回;        }        return new ServiceManagerProxy(obj);//核心核心核心,跨进程    }

使用OpenGrok搜索"implements IBinder",会发现Binder.java 文件中class Binder和
class BinderProxy两个类实现了IBinder
;此处我先不确定queryLocalInterface是否是调用的这里;我们先确定
static public IServiceManager asInterface(IBinder obj)的参数IBinder obj;obj是ServiceManager.java中调用ServiceManagerNative.asInterface(BinderInternal.getContextObject())传递过来的,所以
obj = BinderInternal.getContextObject();我们看看ServiceManagerProxy的构造方法,会发现ServiceManagerProxy中
public ServiceManagerProxy(IBinder remote) {
mRemote = remote;
},即BinderInternal.getContextObject()的返回值将赋值给mRemote


/frameworks/base/core/java/com/android/internal/os/BinderInternal.java

------> BinderInternal.java
public static final native IBinder getContextObject();

这里使用JNI来获取IBinder对象;根据Android JNI命名规则,我们知道getContextObject方法在android_util_Binder.cpp中实现;

------> android_util_Binder.cppstatic const JNINativeMethod gBinderInternalMethods[] = {     /* name, signature, funcPtr */    { "getContextObject", "()Landroid/os/IBinder;", (void*)android_os_BinderInternal_getContextObject },    { "joinThreadPool", "()V", (void*)android_os_BinderInternal_joinThreadPool },    { "disableBackgroundScheduling", "(Z)V", (void*)android_os_BinderInternal_disableBackgroundScheduling },    { "handleGc", "()V", (void*)android_os_BinderInternal_handleGc }};

static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz){    sp<IBinder> b = ProcessState::self()->getContextObject(NULL);//ProcessState采用单列,整个应用只有一个实例    return javaObjectForIBinder(env, b);//将native binder转换为Java Binder对象,这里返回BinderProxy对象,下面会说明;}

这里通过 ProcessState创建native IBinder对象;再调用javaObjectForIBinder将native Binder对象转换成Java层的Binder对象;

------> ProcessState.cppsp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)//caller 上面传递的值为null{    return getStrongProxyForHandle(0);//参数为0}

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle){    sp<IBinder> result;

    AutoMutex _l(mLock);

    handle_entry* e = lookupHandleLocked(handle);//此时handle=0

    if (e != NULL) {        // We need to create a new BpBinder if there isn'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 = e->binder;        if (b == NULL || !e->refs->attemptIncWeak(this)) {            if (handle == 0) {                // Special case for context manager...                // The context manager is the only object for which we create                // a BpBinder proxy without already holding a reference.                // Perform a dummy transaction to ensure the context manager                // is registered before we create the first local reference                // to it (which will occur when creating the BpBinder).                // If a local reference is created for the BpBinder when the                // context manager is not present, the driver will fail to                // provide a reference to the context manager, but the                // driver API does not return status.                //                // Note that this is not race-free if the context manager                // dies while this code runs.                //                // TODO: add a driver API to wait for context manager, or                // stop special casing handle 0 for context manager and add                // a driver API to get a handle to the context manager with                // proper reference counting.

                Parcel data;                status_t status = IPCThreadState::self()->transact(                        0, IBinder::PING_TRANSACTION, data, NULL, 0);                if (status == DEAD_OBJECT)                   return NULL;            }

            b = new BpBinder(handle); //创建BpBinder,即b = new BpBinder(0);            e->binder = b;            if (b) e->refs = b->getWeakRefs();            result = b;//用new BpBinder(0)给result赋值,最后返回result        } else {            // This little bit of nastyness is to allow us to add a primary            // reference to the remote proxy when this team doesn't have one            // but another team is sending the handle to us.            result.force_set(b);            e->refs->decWeak(this);        }    }

    return result;//返回new BpBinder(0)}

sp<IBinder> b = ProcessState::self()->getContextObject(NULL)返回的是new BpBinder(0);返回到android_os_BinderInternal_getContextObject方法中,接下来会调用javaObjectForIBinder方法;

------> javaObjectForIBinderjobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val){    if (val == NULL) return NULL;

    if (val->checkSubclass(&gBinderOffsets)) {        // One of our own!        jobject object = static_cast<JavaBBinder*>(val.get())->object();        LOGDEATH("objectForBinder %p: it's our own %p!\n", val.get(), object);        return object;    }

    // For the rest of the function we will hold this lock, to serialize    // looking/creation of Java proxies for native Binder proxies.    AutoMutex _l(mProxyLock);

    // Someone else's...  do we know about it?    jobject object = (jobject)val->findObject(&gBinderProxyOffsets);//gBinderProxyOffsets很重要,//gBinderProxyOffets在int_register_android_os_BinderProxy中初始化,指向Java层的BinderProxy(核心核心核心),//int_register_android_os_BinderProxy在register_android_os_Binder中调用,register_android_os_Binder//在开机过程中AndroidRuntime.startReg方法中被调用;    if (object != NULL) {        jobject res = jniGetReferent(env, object);        if (res != NULL) {            ALOGV("objectForBinder %p: found existing %p!\n", val.get(), res);            return res;        }        LOGDEATH("Proxy object %p of IBinder %p no longer in working set!!!", object, val.get());        android_atomic_dec(&gNumProxyRefs);        val->detachObject(&gBinderProxyOffsets);        env->DeleteGlobalRef(object);    }

    object = env->NewObject(gBinderProxyOffsets.mClass, gBinderProxyOffsets.mConstructor);    //gBinderProxyOffsets.mClass指向BinderProxy class,gBinderProxyOffsets.mConstructor指向BinderProxy构造方法;    if (object != NULL) {        LOGDEATH("objectForBinder %p: created new proxy %p !\n", val.get(), object);        // The proxy holds a reference to the native object.        env->SetLongField(object, gBinderProxyOffsets.mObject, (jlong)val.get());//val是BpBinder,        //这里利用JNI 调用java将读到的BpBinder 对象val存入BinderProxy的mObject变量中        val->incStrong((void*)javaObjectForIBinder);        // The native object needs to hold a weak reference back to the        // proxy, so we can retrieve the same proxy if it is still active.        jobject refObject = env->NewGlobalRef(                env->GetObjectField(object, gBinderProxyOffsets.mSelf));        val->attachObject(&gBinderProxyOffsets, refObject,                jnienv_to_javavm(env), proxy_cleanup);        // Also remember the death recipients registered on this proxy        sp<DeathRecipientList> drl = new DeathRecipientList;        drl->incStrong((void*)javaObjectForIBinder);        env->SetLongField(object, gBinderProxyOffsets.mOrgue, reinterpret_cast<jlong>(drl.get()));        // Note that a new object reference has been created.        android_atomic_inc(&gNumProxyRefs);        incRefsCreated(env);    }    return object;//返回Java BinderProxy对象}

所以前面BinderInternal.java 中 getContextObject()方法会返回一个BinderProxy对象;并将获取到的BpBinder对象存入Java层BinderProxy类的mObject变量中;

是不是有点蒙圈了,休息休息休息休息一下下,你还记得我们的这个BinderProxy返回到什么地方吗?哈哈,反正我是记得;因为我都记下来了,哈哈;BinderProxyBinderInternal.getContextObject()返回的,即这个BinderProxy将作为ServiceManagerNative.java 中static public IServiceManager asInterface(IBinder obj)方法的参数IBinder obj;如果Binder通信确实跨进程ServiceManagerNative.java的asInterface方法将返回
new ServiceManagerProxy(BinderProxy binderProxy)


到这里我想使用goto 语句了,调到我想去的地方,还记得我们是从什么时候开始分析这个IBinder对象的吗?

------> ContextImpl.java        registerService(POWER_SERVICE, new ServiceFetcher() {                public Object createService(ContextImpl ctx) {                    IBinder b = ServiceManager.getService(POWER_SERVICE);                    IPowerManager service = IPowerManager.Stub.asInterface(b);                    //调用IPowerManger.stub.asInterface,传入的IBinder对象参数是从                     //ServiceManager.getService(POWER_SERVICE)获取的                    if (service == null) {                        Log.wtf(TAG, "Failed to get power manager service.");                    }                    return new PowerManager(ctx.getOuterContext(),                            service, ctx.mMainThread.getHandler());//返回PowerManager对象,                    //这里的service就是PowerManger类中的mService,//我们在应用层调用的  (PowerManager) getSystemService(Context.POWER_SERVICE)//得到的powerManger对象就是这里返回的new PowerManager,这部分代码跟着逻辑就能看到;             }});

我们重新回到ContextImpl.java 中开始分析, IBinder b = ServiceManager.getService(POWER_SERVICE);其实就是ServiceManagerProxy的getService方法,ServiceManagerProxy类中mRemote的值就是BinderProxy,所以 IBinder b = ServiceManager.getService(POWER_SERVICE)最终会调用ServiceManagerProxy的方法;

------> ServiceMangerNative.java ——>ServiceManagerProxy    public IBinder getService(String name) throws RemoteException {        Parcel data = Parcel.obtain();        Parcel reply = Parcel.obtain();        data.writeInterfaceToken(IServiceManager.descriptor);        data.writeString(name);        mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);        IBinder binder = reply.readStrongBinder();//这里返回IBinder对象        reply.recycle();        data.recycle();        return binder;    }

IPowerManager service = IPowerManager.Stub.asInterface(b),这里asInterface方法的参数b就是ServiceManagerProxy类中getService返回的IBinder,这里是通过reply.readStrongBinder();reply是一个Parcel;

/frameworks/base/core/java/android/os/Parcel.java

------> Parcel.javapublic final IBinder readStrongBinder() {return nativeReadStrongBinder(mNativePtr);//JNI方法}

我们根据Android JNI命名规则,可以到android_os_Parcel.cpp中查看nativeReadStrongBinder方法:

------> android_os_Parcel.cppstatic jobject android_os_Parcel_readStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr){    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);    if (parcel != NULL) {        return javaObjectForIBinder(env, parcel->readStrongBinder());//这个方法前面有说过,                                            //根据parcel->readStrongBinder()的值返回,                                            //提前透露一下,parcel->readStrongBinder()返回的值是BpBinder对象    }    return NULL;}

parcel->readStrongBinder()

------> Parcel.cppsp<IBinder> Parcel::readStrongBinder() const{    sp<IBinder> val;    unflatten_binder(ProcessState::self(), *this, &val);//核心,解析Binder    return val;}------> Parcel.cppstatus_t unflatten_binder(const sp<ProcessState>& proc,    const Parcel& in, sp<IBinder>* out){    const flat_binder_object* flat = in.readObject(false);

    if (flat) {        switch (flat->type) {            case BINDER_TYPE_BINDER://Binder实体                *out = reinterpret_cast<IBinder*>(flat->cookie);                return finish_unflatten_binder(NULL, *flat, in);            case BINDER_TYPE_HANDLE://Binder 引用,我们这里是通过ServiceManager.getService获取的服务代理,即Binder引用                *out = proc->getStrongProxyForHandle(flat->handle);//核心核心,是不是很熟悉呢???                //该方法返回一个new BpBinder(flat->handle),这里的new BpBinder将会传递给readStringBinder方法的&val然后直接返回;                return finish_unflatten_binder(                    static_cast<BpBinder*>(out->get()), *flat, in);//类型转换        }    }    return BAD_TYPE;}

Parcel.cpp中的readStrongBinder方法将返回一个new BpBinder()对象,接着继续返回给android_os_Parcel.cpp的android_os_Parcel_readStrongBinder,在android_os_Parcel_readStrongBinder中调用javaObjectForIBinder方法将这个BpBinder对象转换为Java 层的BinderProxy对象返回,javaObjectForIBinder还会调用SetLongField将获取到的BpBinder对象保存到java层BinderProxy类的mObject变量中,所以呢所以呢???ServiceManagerProxy的getService方法将返回一个BinderProxy对象

再回到ContextImpl中,你将会明白所有参数:
IBinder b = ServiceManager.getService(POWER_SERVICE)    //IBinder b其实就是一个BinderProxy对象,是不是和前面透露的保持一致,其实我都怕透露出错,哈哈
IPowerManager service = IPowerManager.Stub.asInterface(b);//这里我们知道asInterface(b)最终会将参数b传递到IPowerManager.Stub.Proxy的构造函数,将BinderProxy对象赋值给IPowerManager.Stub.Proxy的mRemoteIPowerManager.Stub.asInterface(b)调用最终会返回IPowerManager.Stub.Proxy对象

有没有真相大白的感觉,困扰你的mRemote终于验明正身了;又没有很兴奋激动,反正我还是比较激动的。

小结:本小结主要阐述了Binder通信从APP层到framework层的通信过程,即
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE)
boolean bool = powerManager.isScreenOn()=========>mService.isInteractive(mService是IPowerManager对象)=========>mRemote.transact(Stub.TRANSACTION_isInteractive, _data, _reply, 0)(mRemote是一个BinderProxy对象)=========>BinderProxy.transact;下一篇文章将从BinderProxy开始分析

与之相关

1 三步掌握 Android 中的 AIDL

微信号:code-xiaosheng

公众号

「code小生」


Android Binder—APP-framework(mRemote的前世今生)相关推荐

  1. Android Binder通信一次拷贝你真的理解了吗?

        Android Binder通信一次拷贝你真的理解了吗? Android Binder框架实现目录: Android Binder框架实现之Binder的设计思想 Android Binder ...

  2. 理解Android Binder机制(3/3):Java层

    本文是Android Binder机制解析的第三篇,也是最后一篇文章.本文会讲解Binder Framework Java部分的逻辑. Binder机制分析的前面两篇文章,请移步这里: 理解Andro ...

  3. android binder - 客户端(c++层) 调用 服务端(java层),服务端回调客户端 例子

    学习了: android binder - 客户端(java层) 调用 服务端(c++层) 例子 http://blog.csdn.net/ganyue803/article/details/4131 ...

  4. Android Binder学习指南

    Binder是Android系统中最重要的特性之一,直观来说,Binder是Android中的一个类,它实现了IBinder接口.从Android Framework角度来说,Binder是Servi ...

  5. 不得不说的Android Binder机制与AIDL

    说起Android的进程间通信,想必大家都会不约而同的想起Android中的Binder机制.而提起Binder,想必也有不少同学会想起初学Android时被Binder和AIDL支配的恐惧感.但是作 ...

  6. Android Binder框架实现之bindService详解

        Android Binder框架实现之bindService详解 Android Binder框架实现目录: Android Binder框架实现之Binder的设计思想 Android Bi ...

  7. 【Android】Android Binder进程间通信AIDL示例与源码分析

    前言 众所周知,Android进程间通信采用的是Binder机制.Binder是Android系统独有的进程间通信方式,它是采用mmp函数将进程的用户空间与内核空间的一块内存区域进行映射,免去了一次数 ...

  8. Android Binder概述

    背景知识 为了更好的理解binder,我们要先澄清一下概念,因为Android 基于Linux内核,我们有必要了解相关知识. 进程隔离 进程隔离是为了保护操作系统进程之间互不干扰而设计的,这个技术是为 ...

  9. Android Binder的使用

     Binder是一个深入的话题,由于Binder太过于复杂,所以本文不涉及底层细节,要想要知道底层细节可以去阅读Android Bander设计与实现 - 设计篇.写给 Android 应用工程师的 ...

最新文章

  1. mysql经典书籍--MySQL 必知必会
  2. leetcode 16. 3Sum Closest | 16. 最接近的三数之和(双指针)
  3. mysql 乐观锁_使用Mysql乐观锁解决并发问题
  4. 95-38-050-Buffer-UnpooledHeapByteBuf
  5. oripa手机版_ORIPA - Origami Pattern Editor
  6. SQLite 时间格式化
  7. FasterRCNN理解
  8. UNI-APP实现扫描二维码
  9. 标签打印软件如何制作医疗废物标签
  10. 级数ex展开_泰勒级数的若干展开方法
  11. android:ems什么意思
  12. BGP双线IDC机房的接入方式
  13. 美团技术团队当家运营:美美正式出道啦(含福利)
  14. converting character set: invalid arguements
  15. 企业数据中心“云化”转型解决方案
  16. 计算机程序员求职信英语作文,英文程序员求职信
  17. psid mysql_DB2常用SQL的写法(持续更新中...)
  18. 申请支付宝当面付应用
  19. Python:安装 psycopg2
  20. strtok函数及其模拟

热门文章

  1. xp系统开机时出现unmountable boot volume错误和蓝屏代码STOP 0x000000ED
  2. window10下安装银河麒麟高级服务器操作系统(飞腾版)V10
  3. 人民日报80分申论范文:更好发挥文化的社会治理功能
  4. Struts2项目实例
  5. 全迹科技UWB高精度定位筑牢石化行业安全防线
  6. android+彩信+预览音频,android 信息(mms)开发(八)-- 彩信的解析
  7. python中wb是什么意思_python 中wb
  8. 使用栈计算前缀表达式
  9. 360推出儿童卫士2,硬件细节再更迭
  10. 智慧城市水质在线氨氮监测传感器