热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

AndroidBinder的原理与使用

这篇文章主要介绍了AndroidBinder的原理与使用,帮助大家更好的理解和学习使用Android开发,感兴趣的朋友可以了解下

前言

Binder是安卓中实现IPC(进程间通信的)常用手段,四大组件之间的跨进程通信也是利用Binder实现的,Binder是学习四大组件工作原理的的一个重要基础。 好多文章都会深入C代码去介绍Binder的工作流程,没点水平真的难以理解,本文不会太深入底层去剖析原理,尽可能较为简单的让大家了解Binder是怎么工作的。

Binder的使用

在介绍Binder原理之前,我们先来看看在安卓中怎么使用Binder来进程间通信。 在使用之前我们先来介绍Binder的几个方法:

public final boolean transact(int code, Parcel data, Parcel reply, int flags)
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)

这两个方法分别代表了客户端和服务端,transact用来发送消息,onTransact负责接收transact传过来的消息,这一点很容易理解。

  • code 方法标识符,在相同进程中,我们很容易的通过方法调用来执行我们的目标方法,但是在不同的进程间,方法调用的方式就不能再用了,所以我们使用code来表示远程调用函数的标识。这个标识必须介于FIRST_CALL_TRANSACTION(0x00000001)和LAST_CALL_TRANSACTION(0x00ffffff)之间。
  • data Parcel类型的数据包,要传给客户端的请求参数。
  • reply 如果客户端需要返回值,则reply就是服务端返回的数据。
  • flags 用来区分这个调用是普通调用还是单程调用,普通调用时,Client端线程会阻塞,直到从Server端接收到返回值,若flag==IBinder.FLAG_ONEWAY,则这次调用是单程调用,Client在传出数据后会立即执行下一段代码,此时两端异步执行,单程调用时函数返回值必须为void (也就是单程调用必须舍弃返回值,要返回值就必须阻塞等待)

利用这两个方法我们就可以实现Client和Server端的通信,接下来我们看看具体该怎么使用。 在Server接收到Client传来的消息(data)时,会对data进行验证data.enforceInterface(DESCRIPTOR),DESCRIPTOR是一个字符串类型的描述符,当data的描述符跟DESCRIPTOR相同时才能通过验证。

public class Stub extends Binder {
    //描述符
    public static final java.lang.String DESCRIPTOR = "MyTestBinder";
    //code 方法描述符
    public static final int TRANSACTION_test0 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    public static final int TRANSACTION_test1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            case TRANSACTION_test0:
                //验证描述符
                data.enforceInterface(DESCRIPTOR);
                //执行方法
                test0();
                return true;
            case TRANSACTION_test1:
                //验证描述符
                data.enforceInterface(DESCRIPTOR);
                //执行方法
                test1(data, reply);
                return true;
        }
        return super.onTransact(code, data, reply, flags);
    }

    //无返回值
    private void test0() {
        Log.d("MyBinderServer", "test0");
    }

    //有返回值
    private void test1(Parcel data, Parcel reply) {
        Log.d("MyBinderServer", "test1");
        reply.writeInt(data.readInt() + 1);
    }
}

我们知道,要想实现Client和Server端的通信连接,就必须让client知道server端的地址,就跟Http请求,我们要知道服务端的ip和端口。Binder通信其实也是一样的,那么我们怎么让Client拿到Server的地址呢? 一种是跟Http请求一样,我们知道Http请求要把域名转换成ip和端口,这就是DNS,我们也需要一个Binder的DNS。安卓中也为我们提供了Binder的“DNS”那就是ServiceManager,ServiceManager中注册了所有系统服务(如MediaServer等),我们可以使用ServiceManager拿到远程的Binder地址,这种方式叫做有名Binder查找(有名Binder,如MediaServer等这些系统服务被注册的时候都是有名字的,比如,我们通过WINDOW_SERVICE这个名字就能拿到WindowManager)。但是问题是向ServiceManager注册服务的过程是系统进程实现的,我们的应用进程不能注册自己的Binder。 另一种就是利用有名的Binder来辅助传递匿名的Binder,也就是说如果有某个有名Binder服务它提供了传递Binder的方法,那么我们就可以通过这个Binder服务来传递我们的匿名Binder,我们查找到这个有名的Binder是不是就能拿到我们的匿名Binder。正好AMS其实提供了这样的功能,它通过Service.onBind把匿名的Binder封装在了Service里面供我们调用。

 public class MyService extends Service {
    
    @Override
    public IBinder onBind(Intent intent) {
        return new Stub();
    }
}

我们使用binderService()来获取远程的Binder。

Intent serviceIntent = new Intent(this, MyService.class);
        bindService(serviceIntent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                //service可以理解为是远程Binder的地址,我们利用他跟远程通信,C++层会转换这个IBinder跟Binder进行通信
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, BIND_AUTO_CREATE);

获取到Binder之后我们补充好通信的代码:

        bindService(serviceIntent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Parcel data0 = Parcel.obtain();//请求参数
                Parcel reply0 = Parcel.obtain();//响应参数
                Parcel data1 = Parcel.obtain();
                Parcel reply1 = Parcel.obtain();

                //调用第一个方法
                try {
                    //添加描述符
                    data0.writeInterfaceToken(Stub.DESCRIPTOR);
                    /*
                     * 写入参数,要想传递多个int参数,顺序调用writeInt
                     * data0.writeInt(10);
                     * data0.writeInt(20);
                     * 获取
                     * int num10 = data0.readInt();
                     * int num20 = data0.readInt();
                     */
                    data0.writeInt(10);
                    //发起远程调用
                    service.transact(Stub.TRANSACTION_test0, data0, reply0, 0);
                    reply0.readException();
                } catch (RemoteException e) {
                    e.printStackTrace();
                } finally {
                    data0.recycle();
                    reply0.recycle();
                }

                //调用第二个方法
                try {
                    //添加描述符
                    data1.writeInterfaceToken(Stub.DESCRIPTOR);
                    data1.writeInt(10);
                    //发起远程调用
                    service.transact(Stub.TRANSACTION_test1, data1, reply1, 0);
                    reply1.readException();
                    //读取返回值
                    int num = reply1.readInt();
                    Log.d(TAG, "reply value: " + num);
                } catch (RemoteException e) {
                    e.printStackTrace();
                } finally {
                    data1.recycle();
                    reply1.recycle();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, BIND_AUTO_CREATE);

为了方便调用,我们写一个代理类来封装通信过程

public class Proxy {
    //描述符
    public static final java.lang.String DESCRIPTOR = "MyTestBinder";
    //code 方法描述符
    public static final int TRANSACTION_test0 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    public static final int TRANSACTION_test1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    private IBinder mRemote;

    public Proxy(IBinder remote) {
        this.mRemote = remote;
    }

    public void test1() {
        Parcel data0 = Parcel.obtain();//请求参数
        Parcel reply0 = Parcel.obtain();//响应参数
        //调用第一个方法
        try {
            //添加描述符
            data0.writeInterfaceToken(DESCRIPTOR);
            /*
             * 写入参数,要想传递多个int参数,顺序调用writeInt
             * data0.writeInt(10);
             * data0.writeInt(20);
             * 获取
             * int num10 = data0.readInt();
             * int num20 = data0.readInt();
             */
            data0.writeInt(10);
            //发起远程调用
            mRemote.transact(TRANSACTION_test0, data0, reply0, 0);
            reply0.readException();
        } catch (RemoteException e) {
            e.printStackTrace();
        } finally {
            data0.recycle();
            reply0.recycle();
        }
    }

    public int test2() {
        Parcel data1 = Parcel.obtain();
        Parcel reply1 = Parcel.obtain();
        //调用第二个方法
        int num = 0;
        try {
            //添加描述符
            data1.writeInterfaceToken(DESCRIPTOR);
            data1.writeInt(10);
            //发起远程调用
            mRemote.transact(TRANSACTION_test1, data1, reply1, 0);
            reply1.readException();
            //读取返回值
            num = reply1.readInt();
        } catch (RemoteException e) {
            e.printStackTrace();
        } finally {
            data1.recycle();
            reply1.recycle();
        }
        return num;
    }
}
 bindService(serviceIntent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Proxy proxy = new Proxy(service);
                proxy.test1();
                int i = proxy.test2();
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, BIND_AUTO_CREATE);

模糊进程间调用

前边就是Binder的使用方式,但是至此还遗留了一个问题,我们的Service只有指定了新的进程名之后才会是远程调用,如果通过bindService 传递过来的IBinder对象是同进程的,那我们就不需要使用IBinder.transact进行内核通信了。我们知道同进程之间利用方法调用方式就可以做到通信。 我们在onServiceConnected打印IBinder类型,如果发现是远程调用,传给我们的 iBinder 是 BinderProxy 类型,BinderProxy是Binder的内部类一样实现了IBinder接口,他在native层会对应一个C++的BpBinder,BpBinder 最终会通过Binder驱动跟Server端通信。如果是本地调用,打印出的类型为Stub,说明本地调用时,onServiceConnected传过来的就是我们在Service的onBinde方法返回的Stub对象本身。基于这个原理,我们可以设计一个转换方法。

首先我们要怎么判断当前是远程调用还是同进程调用呢? 我们使用queryLocalInterface(DESCRIPTOR)方法,Binder中queryLocalInterface不会返回空,而在BinderProxy的实现中,queryLocalInterface返回为null。 Binder:

public IInterface queryLocalInterface(String descriptor) {
        if (mDescriptor != null && mDescriptor.equals(descriptor)) {
            return mOwner;
        }
        return null;
    }

mOwner是attachInterface方法传进来的接口本身,后面还会出现这个方法。 BinderProxy:

public IInterface queryLocalInterface(String descriptor) {
        return null;
    }

当发现是远程调用时我们创建上边的Proxy来代理跨进程通信过程。要是本地调用我们直接返回本地Stub对象。

public static IMyInterface asInterface(IBinder iBinder){
        if ((iBinder == null)) {
            return null;
        }
        //获取本地interface
        IInterface iin = iBinder.queryLocalInterface(DESCRIPTOR);
        if (((iin != null) && (iin instanceof IMyInterface))) {
            //不为空,说明是本地调用,直接强转后返回。
            //IMyInterface封装了test0()、test1()两个方法,本地对象和Proxy都继承自接口
            return ((IMyInterface)iin );
        }
        //为null,远程调用,新建代理
        return new Proxy(iBinder);
    }

把上面相关代码完善之后

public interface IBinderTest extends android.os.IInterface {
    /**
     * 本地Stub对象
     */
    public static abstract class Stub extends android.os.Binder implements IBinderTest {
        private static final java.lang.String DESCRIPTOR = "com.XXX.XXXX.IBinderTest";

        public Stub() {
            //绑定DESCRIPTOR,并设置queryLocalInterface方法返回的mOwner
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * 本地远程转换
         */
        public static IBinderTest asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof IBinderTest))) {
                return ((IBinderTest) iin);
            }
            return new IBinderTest.Stub.Proxy(obj);
        }

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

        /**
         * 处理Client调用请求
         */
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_testVoidAidl: {
                    data.enforceInterface(descriptor);
                    this.testVoidAidl();
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_testStringAidl: {
                    data.enforceInterface(descriptor);
                    java.lang.String _result = this.testStringAidl();
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

        /**
         * 远程调用代理类
         */
        private static class Proxy implements IBinderTest {
            private android.os.IBinder mRemote;

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

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

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public void testVoidAidl() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_testVoidAidl, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public java.lang.String testStringAidl() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_testStringAidl, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        /**
         * 调用函数code
         */
        static final int TRANSACTION_testVoidAidl = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_testStringAidl = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    public void testVoidAidl() throws android.os.RemoteException;

    public java.lang.String testStringAidl() throws android.os.RemoteException;
}

如果你用过AIDL并且看过AIDL生成的代码,你就会发现上面代码就是AIDL生成的。 换掉Service的调用

public class MyService extends Service {

    private String TAG = "MyService";

    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    class MyBinder extends IBinderTest.Stub{

        @Override
        public void testVoidAidl() throws RemoteException {
            Log.d(TAG, "testVoidAidl");
        }

        @Override
        public String testStringAidl() throws RemoteException {
            Log.d(TAG, "testStringAidl");
            return "hello";
        }
    }
}

以上就是Binder的使用方法以及原理的简单介绍。

Binder原理

之前介绍了Binder的基本使用,接下来我们来看下Binder的底层原理。

(图片来源gityuan.com/2015/10/31/…),安卓的应用内存是隔离的,但是内核空间是共享的,我们要实现IPC就要靠共享的内核空间来交换数据。

Binder通信模型:

ioctl

Binder的通信原理:

由于用户空间的隔离机制(沙盒模式),所以我们要利用内核空间进行IPC操作,用户空间与内核空间的交互使用的是linux内核的ioctl函数,接下来简单了解一下ioctl的使用。 ioctl可以控制I/O设备 (驱动设备),提供了一种获得设备信息和向设备发送控制参数的手段。简单来说就是使用ioctl可以对驱动设备进行操作。由于我们IPC的过程中内核空间使用虚拟驱动设备/dev/binder控制通信过程,我们要跟这个Binder驱动设备交互就要使用ioctl命令。 简单介绍下,Linux要实现驱动设备的使用需要三个角色

  • 应用程序(调用方),使用ioctl来发送操作指令。
  • 驱动程序,用来处理ioctl传来的指令,并完成对驱动设备的操作。驱动程序被注册进内核之后,就会等待应用程序的调用。
  • 驱动设备,在Binder机制中,驱动设备是/dev/binder,这个文件被映射到每个系统Service的虚拟内存中(后边会讲到),驱动程序可以操作这个文件进行进程间数据交互。

下图是Linux中应用程序是怎么通过驱动来操作硬件设备的:

来个图来说一下应用层与驱动程序函数的ioctl之间的联系:

简单介绍一下函数:

int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);

参数:

  • inode和file:ioctl的操作有可能是要修改文件的属性,或者访问硬件。要修改文件属性的话,就要用到这两个结构体了,所以这里传来了它们的指针。
  • cmd:命令,接下来会讲
  • arg:参数,接下来会讲

返回值: 如果传入的非法命令,ioctl返回错误号-EINVAL。 内核中的驱动函数返回值都有一个默认的方法,只要是正数,内核就会傻乎乎的认为这是正确的返回,并把它传给应用层,如果是负值,内核就会认为它是错误了。

ioctl的cmd cmd就是一个数,如果应用层传来的数值在驱动中有对应的操作,那么就执行,就跟IBinder的transact方法中函数标识是一个道理. 要先定义个命令,就用一个简单的0,来个命令的头文件,驱动和应用函数都要包含这个头文件:

/*test_cmd.h*/
#ifndef _TEST_CMD_H
#define _TEST_CMD_H

#define TEST_CLEAR 0/*定义的cmd*/

#endif /*_TEST_CMD_H*/

驱动实现ioctl: 命令TEST_CLEAR的操作就是清空驱动中的kbuf。

int test_ioctl (struct inode *node, struct file *filp, unsigned int cmd, uns igned long arg)
{
    int ret = 0;
    struct _test_t *dev = filp->private_data;
    switch(cmd){
        case TEST_CLEAR:
        memset(dev->kbuf, 0, DEV_SIZE);
        dev->cur_size = 0;
        filp->f_pos = 0;
        ret = 0;
        break;
        default: /*命令错误时的处理*/
        P_DEBUG("error cmd!\n");
        ret = - EINVAL;
        break;
    }
    return ret;
}

然后在应用程序中调用ioctl(fd, TEST_CLEAR);就可以执行驱动程序中的清除kbuf的方法。

ioctl的arg ioctl命令还可以传递参数,应用层的ioctl(fd,cmd,...)后面的“...”是指可以传任意类型的一个参数,注意是一个不是任意多个,只是不检查类型。

binder初始化

我们了解ioctl之后就来看看Binder设备是怎么初始化的,这里介绍的是Binder设备,并不是Binder设备驱动程序,Binder驱动程序是misc设备驱动,要想了解Binder驱动程序的内容,请点击下面链接。

gityuan.com/2015/11/01/…

我们的系统服务创建的过程中,要创建打开Binder设备,下面是具体过程。 我们先来介绍下frameworks/native/libs/binder/ProcessState.cpp,ProcessState用来储存当前进程的各种信息,系统服务启动时会创建当前进程的ProcessState单例对象。

ProcessState::ProcessState()
    //打开binder 
    : mDriverFD(open_driver()) //
      //映射内存的起始地址
    , mVMStart(MAP_FAILED)
    , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
    , mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
    , mExecutingThreadsCount(0)
    , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
    , mStarvationStartTimeMs(0)
    , mManagesContexts(false)
    , mBinderContextCheckFunc(NULL)
    , mBinderContextUserData(NULL)
    , mThreadPoolStarted(false)
    , mThreadPoolSeq(1)
{
    if (mDriverFD >= 0) {
        //分配虚拟地址空间,完成数据wirte/read,内存的memcpy等操作就相当于write/read(mDriverFD)
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            close(mDriverFD);
            mDriverFD = -1;
        }
    }
}

对于一个不懂C++的我,看起来其实挺难受的,但是这段代码很重要,还是要看懂的。 其实我们只需要关注这几行重要代码 open_driver() 下面会讲 mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0) 分配虚拟内存映射 我们先来看open_driver函数

static int open_driver()
{
    int fd = open("/dev/binder", O_RDWR | O_CLOEXEC); //打开 /dev/binder
    if (fd >= 0) {
        int vers = 0;
        //通过ioctl通知binder驱动binder版本
        status_t result = ioctl(fd, BINDER_VERSION, &vers);
        if (result == -1) {
            ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
            close(fd);
            fd = -1;
        }
        if (result != 0 || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
            ALOGE("Binder driver protocol does not match user space protocol!");
            close(fd);
            fd = -1;
        }
        //设置当前fd最多支持DEFAULT_MAX_BINDER_THREADS线程数量
        size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
        if (result == -1) {
            ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
        }
    } else {
        ALOGW("Opening '/dev/binder' failed: %s\n", strerror(errno));
    }
    return fd;
}

首先执行int fd = open("/dev/binder", O_RDWR | O_CLOEXEC); 获取到了驱动文件的文件描述符。 文件打开成功之后,使用ioctl查询了版本号,并设置了最大的连接线程数。 然后调用mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0)把/dev/binder文件映射到了进程虚拟内存空间,这里我们还要了解下linux的mmap函数。

mmap参考自:blog.itpub.net/7728585/vie…

在LINUX中我们可以使用mmap用来在进程虚拟地址空间中分配创建一片虚拟内存地址映射

我们可以在当前进程的虚拟内存中获得一块映射区域,我们直接操作映射区域就可以间接操作内核中的文件。 我们使用mmap的目的是创建共享文件映射

进程都有一份文件映射,并且都指向同一个文件,这样就实现了共享内存,Binder就是利用这种共享内存方式去进行数据的交互。每个进程都会保留一份dev/binder设备的映射区域,这样我们利用Binder,数据经过一次拷贝就可以实现跨进程,Linux的管道机制则需要四次拷贝

总结

1、介绍了Binder在Android中的使用方式 2、Binder机制原理,用户进程隔离,借助内核空间进行IPC 3、使用ioctl系统调用函数,调用Binder设备驱动程序,完成IPC调用 4、dev/binder是Binder机制中的虚拟设备,利用Binder驱动可以操作该设备(数据交互) 5、mmap指令可以创建进程虚拟内存映射空间,映射dev/binder文件,实现共享内存,Binder一次拷贝原理

以上就是Android Binder的原理与使用的详细内容,更多关于Android Binder的资料请关注其它相关文章!


推荐阅读
  • 本文详细介绍了 Java 网站开发的相关资源和步骤,包括常用网站、开发环境和框架选择。 ... [详细]
  • 本文整理了一份基础的嵌入式Linux工程师笔试题,涵盖填空题、编程题和简答题,旨在帮助考生更好地准备考试。 ... [详细]
  • 本文详细解析了 Git diff 命令的输出结果,帮助开发者更好地理解和使用这一重要工具。 ... [详细]
  • 本文将详细介绍如何在Android Studio中导入和编译OSChina Android 2.4版本的源码。包括所需软件、下载地址以及一些注意事项。 ... [详细]
  • 使用 Git Rebase -i 合并多个提交
    在开发过程中,频繁的小改动往往会生成多个提交记录。为了保持代码仓库的整洁,我们可以使用 git rebase -i 命令将多个提交合并成一个。 ... [详细]
  • 本文详细介绍了 com.apollographql.apollo.api.internal.Optional 类中的 orNull() 方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。 ... [详细]
  • malloc 是 C 语言中的一个标准库函数,全称为 memory allocation,即动态内存分配。它用于在程序运行时申请一块指定大小的连续内存区域,并返回该区域的起始地址。当无法预先确定内存的具体位置时,可以通过 malloc 动态分配内存。 ... [详细]
  • 华为捐赠欧拉操作系统,承诺不推商用版
    华为近日宣布将欧拉开源操作系统捐赠给开放原子开源基金会,并承诺不会推出欧拉的商用发行版。此举旨在推动欧拉和鸿蒙操作系统的全场景融合与生态发展。 ... [详细]
  • Python多线程详解与示例
    本文介绍了Python中的多线程编程,包括僵尸进程和孤儿进程的概念,并提供了具体的代码示例。同时,详细解释了0号进程和1号进程在系统中的作用。 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • 本文详细介绍了Linux系统中用于管理IPC(Inter-Process Communication)资源的两个重要命令:ipcs和ipcrm。通过这些命令,用户可以查看和删除系统中的消息队列、共享内存和信号量。 ... [详细]
  • 本文详细介绍如何在忘记MySQL服务器密码的情况下进行密码重置,包括具体的步骤和注意事项。 ... [详细]
  • 在OpenShift上部署基于MongoDB和Node.js的多层应用程序
    本文档详细介绍了如何在OpenShift 4.x环境中部署一个包含MongoDB数据库和Node.js后端及前端的多层应用程序。通过逐步指导,读者可以轻松完成整个部署过程。 ... [详细]
  • 专业人士如何做自媒体 ... [详细]
  • 本文介绍了一种通过检测 DAC 左右声道能量来解决串音问题的方法。当某一声道无信号时,将其设置为高阻态,以减少干扰。文章详细描述了检测和处理的具体实现。 ... [详细]
author-avatar
刻骨铭心2502914183_610
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有