zoukankan      html  css  js  c++  java
  • Android : 跟我学Binder --- (6) JAVA实现


    目录:

    一、Java程序的编译和运行

     1.单步操作

     PC:Hello.java 编译成 Hello.class,运行于 jvm。

    javac Hello.java
    java Hello

      Android:Hello.java 编译成 Hello.class,通过dx指令转换成.dex格式,运行于 dalvikvm。

    javac Hello.java
    dx --dex --output=Hello.jar Hello.class
    dalvikvm -cp /mnt/android_fs/pack.jar Pack (启动方法1)
    CLASSPATH=/mnt/android_fs/pack.jar app_process /mnt/android_fs Pack(启动方法2)

     
    启动方式的差别:
    dalvikvm
    app_process :会创建2个binder线程: Binder_1, Binder_2 (实现读数据、解析数据、返回等操作)

    2.Android编译系统(Android.mk)

     把代码放到android源码中编译:
     添加Androd.mk,内容类似:  // 参考frameworks/base/cmds/am/Android.mk

    include $(CLEAR_VARS)
    LOCAL_SRC_FILES := $(call all-subdir-java-files)
    LOCAL_MODULE := pack
    include $(BUILD_JAVA_LIBRARY)

    3.使用java代码实现hello服务

     (1)IHelloService.aidl

        接口定义,编译生成IHelloService.java,里面有Stub  : onTransact, 它会分辨收到数据然后调用sayhello, sayhello_to;
      Proxy : 提供有sayhello, sayhello_to两个函数, 它们会构造数据然后发送给server。

    /** {@hide} */
    interface IHelloService
    {
        void sayhello();
        int sayhello_to(String name);
    }

     (2)IHelloService.java

    /*
     * This file is auto-generated.  DO NOT MODIFY.
     * Original file: frameworks/base/core/java/android/os/IHelloService.aidl
     */
    /** {@hide} */
    public interface IHelloService extends android.os.IInterface
    {
        /** Local-side IPC implementation stub class. */
        public static abstract class Stub extends android.os.Binder implements IHelloService
        {
            private static final java.lang.String DESCRIPTOR = "IHelloService";
            /** Construct the stub at attach it to the interface. */
            public Stub()
            {
                this.attachInterface(this, DESCRIPTOR);
            }
            /**
             * Cast an IBinder object into an IHelloService interface,
             * generating a proxy if needed.
             */
            public static IHelloService asInterface(android.os.IBinder obj)
            {
                if ((obj==null)) {
                return null;
                }
                android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
                if (((iin!=null)&&(iin instanceof IHelloService))) {
                return ((IHelloService)iin);
                }
                return new IHelloService.Stub.Proxy(obj);
            }
            
            @Override public android.os.IBinder asBinder()
            {
                return this;
            }
            @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
            {
                switch (code)
                {
                    case INTERFACE_TRANSACTION:
                    {
                        reply.writeString(DESCRIPTOR);
                        return true;
                    }
                    case TRANSACTION_sayhello:
                    {
                        data.enforceInterface(DESCRIPTOR);
                        this.sayhello();
                        reply.writeNoException();
                        return true;
                    }
                    case TRANSACTION_sayhello_to:
                    {
                        data.enforceInterface(DESCRIPTOR);
                        java.lang.String _arg0;
                        _arg0 = data.readString();
                        int _result = this.sayhello_to(_arg0);
                        reply.writeNoException();
                        reply.writeInt(_result);
                        return true;
                    }
                }
                return super.onTransact(code, data, reply, flags);
            }
    
            
            private static class Proxy implements IHelloService
            {
                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 sayhello() 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_sayhello, _data, _reply, 0);
                        _reply.readException();
                    }
                    finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
                @Override public int sayhello_to(java.lang.String name) throws android.os.RemoteException
                {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    int _result;
                    try {
                        _data.writeInterfaceToken(DESCRIPTOR);
                        _data.writeString(name);
                        mRemote.transact(Stub.TRANSACTION_sayhello_to, _data, _reply, 0);
                        _reply.readException();
                        _result = _reply.readInt();
                    }
                    finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                        return _result;
                }
            }
            
            static final int TRANSACTION_sayhello = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
            static final int TRANSACTION_sayhello_to = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
                
        }
        
        public void sayhello() throws android.os.RemoteException;
        public int sayhello_to(java.lang.String name) throws android.os.RemoteException;
    }

     (3)HelloService.java

      实现服务类: HelloService.java,在里面定义sayhello, sayhello_to;

    import android.util.Slog;
    
    /* 实现Hello服务的函数 */
    
    public class HelloService extends IHelloService.Stub {
        private static final String TAG = "HelloService";
        private int cnt1 = 0;
        private int cnt2 = 0;
    
        public void sayhello() throws android.os.RemoteException {
            cnt1++;
            Slog.i(TAG, "sayhello : cnt = "+cnt1);
        }
        
        public int sayhello_to(java.lang.String name) throws android.os.RemoteException {
            cnt2++;
            Slog.i(TAG, "sayhello_to "+name+" : cnt = "+cnt2);
            return cnt2;
        }
    }

     (4)IGoodbyeService.aidl (同HelloService)

    /** {@hide} */
    interface IGoodbyeService
    {
        void saygoodbye();
        int saygoodbye_to(String name);
    }

     (5)IGoodbyeService.java

    /*
     * This file is auto-generated.  DO NOT MODIFY.
     * Original file: frameworks/base/core/java/android/os/IGoodbyeService.aidl
     */
    /** {@hide} */
    public interface IGoodbyeService extends android.os.IInterface
    {
        /** Local-side IPC implementation stub class. */
        public static abstract class Stub extends android.os.Binder implements IGoodbyeService
        {
            private static final java.lang.String DESCRIPTOR = "IGoodbyeService";
            /** Construct the stub at attach it to the interface. */
            public Stub()
            {
                this.attachInterface(this, DESCRIPTOR);
            }
            /**
             * Cast an IBinder object into an IGoodbyeService interface,
             * generating a proxy if needed.
             */
            public static IGoodbyeService asInterface(android.os.IBinder obj)
            {
                if ((obj==null)) {
                    return null;
                }
                android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
                if (((iin!=null)&&(iin instanceof IGoodbyeService))) {
                    return ((IGoodbyeService)iin);
                }
                return new IGoodbyeService.Stub.Proxy(obj);
            }
            @Override public android.os.IBinder asBinder()
            {
                return this;
            }
            @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
            {
                switch (code)
                {
                    case INTERFACE_TRANSACTION:
                    {
                        reply.writeString(DESCRIPTOR);
                        return true;
                    }
                    case TRANSACTION_saygoodbye:
                    {
                        data.enforceInterface(DESCRIPTOR);
                        this.saygoodbye();
                        reply.writeNoException();
                        return true;
                    }
                    case TRANSACTION_saygoodbye_to:
                    {
                        data.enforceInterface(DESCRIPTOR);
                        java.lang.String _arg0;
                        _arg0 = data.readString();
                        int _result = this.saygoodbye_to(_arg0);
                        reply.writeNoException();
                        reply.writeInt(_result);
                        return true;
                    }
                }
                return super.onTransact(code, data, reply, flags);
            }
            private static class Proxy implements IGoodbyeService
            {
                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 saygoodbye() 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_saygoodbye, _data, _reply, 0);
                        _reply.readException();
                    }
                    finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
                @Override public int saygoodbye_to(java.lang.String name) throws android.os.RemoteException
                {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    int _result;
                    try {
                        _data.writeInterfaceToken(DESCRIPTOR);
                        _data.writeString(name);
                        mRemote.transact(Stub.TRANSACTION_saygoodbye_to, _data, _reply, 0);
                        _reply.readException();
                        _result = _reply.readInt();
                    }
                    finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                    return _result;
                }
            }
            static final int TRANSACTION_saygoodbye = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
            static final int TRANSACTION_saygoodbye_to = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
        }
        public void saygoodbye() throws android.os.RemoteException;
        public int saygoodbye_to(java.lang.String name) throws android.os.RemoteException;
    }

     (6)GoodbyeService.java

    import android.util.Slog;
    
    /* 实现Goodbye服务的函数 */
    
    public class GoodbyeService extends IGoodbyeService.Stub {
        private static final String TAG = "GoodbyeService";
        private int cnt1 = 0;
        private int cnt2 = 0;
    
        public void saygoodbye() throws android.os.RemoteException {
            cnt1++;
            Slog.i(TAG, "saygoodbye : cnt = "+cnt1);
        }
        
        public int saygoodbye_to(java.lang.String name) throws android.os.RemoteException {
            cnt2++;
            Slog.i(TAG, "saygoodbye_to "+name+" : cnt = "+cnt2);
            return cnt2;
        }
    }

     (7)TestServer.java (addService, 循环)

    import android.util.Slog;
    import android.os.ServiceManager;
    
    /* 1. addService
     * 2. while(true) { read data, parse data, call function, reply }
     */
    
    public class TestServer {
        private static final String TAG = "TestServer";
    
        public static void main(String args[])
        {
            /* add Service */
            Slog.i(TAG, "add hello service");
            ServiceManager.addService("hello", new HelloService());
    
            Slog.i(TAG, "add goodbye service");
            ServiceManager.addService("goodbye", new GoodbyeService());
    
            while (true)
            {
                try {
                    Thread.sleep(100);
                  } catch (Exception e){}
            }
            
        }
    }
    

     (8)TestClient.java (getService, 调用sayhello,sayhello_to(来自Proxy))

    import android.util.Slog;
    import android.os.ServiceManager;
    import android.os.IBinder;
    
    
    /* 1. getService
     * 2. 调用服务的sayhello,sayhello_to
     *
     */
    
    /* test_client <hello|goodbye> [name] */
    
    public class TestClient {
        private static final String TAG = "TestClient";
    
        public static void main(String args[])
        {
            if (args.length == 0)
            {
                System.out.println("Usage: need parameter: <hello|goodbye> [name]");
                return;
            }
    
            if (args[0].equals("hello"))
            {
                /* 1. getService */
                IBinder binder = ServiceManager.getService("hello");
                if (binder == null)
                {
                    System.out.println("can not get hello service");
                    Slog.i(TAG, "can not get hello service");
                    return;
                }
    
                IHelloService svr = IHelloService.Stub.asInterface(binder);
    
                if (args.length == 1)
                {
                        try {
                        svr.sayhello();
                        System.out.println("call sayhello");
                        Slog.i(TAG, "call sayhello");
                      } catch (Exception e) {}
                }
                else
                {
                        try {
                        int cnt = svr.sayhello_to(args[1]);
                        System.out.println("call sayhello_to "+args[1]+" : cnt = "+cnt);
                        Slog.i(TAG, "call sayhello_to "+args[1]+" : cnt = "+cnt);
                      } catch (Exception e) {
                            System.out.println("call sayhello_to , err :"+e);
                            Slog.i(TAG, "call sayhello_to , err : "+e);
                      }
                }
            }
            else if (args[0].equals("goodbye"))
            {
                /* 1. getService */
                IBinder binder = ServiceManager.getService("goodbye");
                if (binder == null)
                {
                    System.out.println("can not get goodbye service");
                    Slog.i(TAG, "can not get goodbye service");
                    return;
                }
    
                IGoodbyeService svr = IGoodbyeService.Stub.asInterface(binder);
    
                if (args.length == 1)
                {
                        try {
                        svr.saygoodbye();
                        System.out.println("call saygoodbye");
                        Slog.i(TAG, "call saygoodbye");
                      } catch (Exception e) {}
                }
                else
                {
                        try {
                        int cnt = svr.saygoodbye_to(args[1]);
                        System.out.println("call saygoodbye_to "+args[1]+" : cnt = "+cnt);
                        Slog.i(TAG, "call saygoodbye_to "+args[1]+" : cnt = "+cnt);
                      } catch (Exception e) {}
                }
            }
        }
    }

     

     (9)Android.mk

    # Copyright 2008 The Android Open Source Project
    #
    LOCAL_PATH:= $(call my-dir)
    
    include $(CLEAR_VARS)
    LOCAL_SRC_FILES := HelloService.java IHelloService.java GoodbyeService.java IGoodbyeService.java TestServer.java
    LOCAL_MODULE := TestServer
    include $(BUILD_JAVA_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_SRC_FILES := HelloService.java IHelloService.java GoodbyeService.java IGoodbyeService.java TestClient.java
    LOCAL_MODULE := TestClient
    include $(BUILD_JAVA_LIBRARY)

     编译&测试说明:

    (1) AIDL
    1. 把 IHelloService.aidl, IGoodbyeService.aidl 放入 frameworks/base/core/java/android/os
    2. 修改 frameworks/base/Android.mk  添加一行
             core/java/android/os/IVibratorService.aidl 
    +        core/java/android/os/IHelloService.aidl 
    +        core/java/android/os/IGoodbyeService.aidl 
    
    3. mmm frameworks/base
    
    4. 它会生成: 
    ./out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/os/IHelloService.java
    ./out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/os/IGoodbyeService.java
    
    (2) 编译:
    把程序放到 /work/android-5.0.2/frameworks/testing/APP_0005_Binder_JAVA_App
    执行:
    
    cd /work/android-5.0.2
    . setenv
    lunch // 选择单板
    
    mmm frameworks/testing/APP_0005_Binder_JAVA_App
    它会生成 TestServer.jar, TestClient.jar
    
    复制文件到NFS目录
    cp /work/android-5.0.2/out/target/product/tiny4412/system/framework/Test*.jar /work/nfs_root/android_fs/
    
    (3) 测试:
    logcat TestServer:* TestClient:* HelloService:* *:S &
    CLASSPATH=/mnt/android_fs/TestServer.jar app_process / TestServer &
    CLASSPATH=/mnt/android_fs/TestClient.jar app_process / TestClient hello
    CLASSPATH=/mnt/android_fs/TestClient.jar app_process / TestClient hello 100ask.taobao.com
    CLASSPATH=/mnt/android_fs/TestClient.jar app_process / TestClient goodbye
    CLASSPATH=/mnt/android_fs/TestClient.jar app_process / TestClient goodbye weidongshan

    、内部机制

    1.Client端
    1.1 ServiceManagerProxy中mRemote的构造 (用于addService/getService)
       使用0直接构造出一个java BinderProxy对象
    getIServiceManager().addService  / getIServiceManager().getService

    getIServiceManager()
            return ServiceManagerNative.asInterface(BinderInternal.getContextObject())

    a. BinderInternal.getContextObject()  // 得到了一个Java BinderProxy对象, 其中mObject指向new BpBinder(0);
       它是一个JNI调用,对应 android_os_BinderInternal_getContextObject // android_util_Binder.cpp
       
       android_os_BinderInternal_getContextObject
               sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
                                                                                                   getStrongProxyForHandle(0);
                                                                                                           b = new BpBinder(handle);  // mHandle = 0
                return javaObjectForIBinder(env, b);        // b = new BpBinder(0), mHandle = 0
                
                                        // 使用c代码调用NewObject来创建JAVA BinderProxy对象
                                        object = env->NewObject(gBinderProxyOffsets.mClass, gBinderProxyOffsets.mConstructor);
                                        
                                        // 设置该对象的mObject = val.get = b = new BpBinder(0)
                                        env->SetLongField(object, gBinderProxyOffsets.mObject, (jlong)val.get());
                                        
                                        return object;

    b. ServiceManagerNative.asInterface
                new ServiceManagerProxy(obj); // obj = BinderProxy对象
                                mRemote = obj = BinderProxy对象, 其中mObject指向new BpBinder(0);

    1.2 hello服务里的mRemote如何构造
    a. IBinder binder = ServiceManager.getService("hello");
     它的返回值就是一个java BinderProxy对象, 其中的mObject=new BpBinder(handle)
    new ServiceManagerProxy().getService("hello")
                ....
                IBinder binder = reply.readStrongBinder();
                                                                    nativeReadStrongBinder  // Parcel.java

    nativeReadStrongBinder是一个JNI调用, 对应的代码是 android_os_Parcel_readStrongBinder                                                                
    android_os_Parcel_readStrongBinder    
          // 把java Parce对象转换为c++ Parcel对象
          // client程序向sevice_manager发出getService请求,
          // 得到一个回复reply, 它里面含有flat_binder_object
          // 它被封装成一个c++ Parcel对象
            Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
            
            /* parcel->readStrongBinder()应该是一个 new BpBinder(handle)
             *             unflatten_binder(ProcessState::self(), *this, &val);
             *                  *out = proc->getStrongProxyForHandle(flat->handle);
             *                                  b = new BpBinder(handle);
             */
            
          // 它会创建一个java BinderProxy对象, 其中的mObject=new BpBinder(handle)对象
            return javaObjectForIBinder(env, parcel->readStrongBinder());

    b. IHelloService svr = IHelloService.Stub.asInterface(binder);
                                                            new IHelloService.Stub.Proxy(obj);  // obj = 步骤a得到的binder
                                                                            mRemote = remote;        

    1.3 现在知道了:mRemote就是一个java BinderProxy 对象
        看一下mRemote.transact()
                                        transactNative(code, data, reply, flags);
                                        它是一个JNI调用,对应android_os_BinderProxy_transact

      android_os_BinderProxy_transact
       // 从java BinderProxy对象中把mObject取出, 它就是一个BpBinder对象
        IBinder* target = (IBinder*)env->GetLongField(obj, gBinderProxyOffsets.mObject);                                    
        
        // 然后调用BpBinder的transact
        status_t err = target->transact(code, *data, reply, flags);

    2.Server端

    2.1 server如何读取数据
     使用app_process来启动server进程,

    /*测试指令*/
    logcat TestServer:* TestClient:* HelloService:* *:S & CLASSPATH=/mnt/android_fs/TestServer.jar app_process / TestServer & CLASSPATH=/mnt/android_fs/TestClient.jar app_process / TestClient hello CLASSPATH=/mnt/android_fs/TestClient.jar app_process / TestClient hello sheldon /*参考源码*/ // app_process: frameworksasecmdsapp_processapp_main.cpp

    它会先创建子线程(前面示例提到的两个binder线程:Binder_1, Binder_2):
    AppRuntime::onStarted()
            proc->startThreadPool();
                            spawnPooledThread(true);
                                    sp<Thread> t = new PoolThread(isMain);
                                    t->run(name.string());
                                            // 它会创建子线程, 并执行threadLoop
                                               IPCThreadState::self()->joinThreadPool(mIsMain);
                                               {
                                                        do {
                                                                        result = getAndExecuteCommand();
                                                                                                result = talkWithDriver();
                                                                                                result = executeCommand(cmd);
                                                                                                                        对于BR_TRANSACTION数据,
                                                                                                                        sp<BBinder> b((BBinder*)tr.cookie); //将.cookie转换成BBinder对象
                                                                                                                        error = b->transact(tr.code, buffer, &reply, tr.flags); //并调用其transact方法
                                                     } while(...)
                                               }


    2.2 server读到数据后怎么调用服务PRC层的onTransact函数
      在addService时设置.ptr/.cookie
      ServiceManager.addService("hello", new HelloService());

    结论:
    ① addService会通过JNI调用c++函数:
            创建一个BBinder派生类JavaBBinder对象,
                它的.mObject指向JAVA对象: new HelloService()
                它含有onTransact函数
            把这个对象存入.cookie(最终存入binder驱动中该服务对应的binder_node.cookie)

    ② server进程从驱动中读到数据,里面含有.cookie
        把它转换为BBinder对象,
        调用它的transact函数
        最终调用到派生类JavaBBinder中定义的onTransact函数

    ③ JavaBBinder中定义的onTransact函数(c++)
        它通过JNI调用java Binder的execTransact方法,
        然后调用Binder派生类IHelloService.Stub中定义的onTransact函数(JAVA)

    ④ IHelloService.Stub中定义的onTransact函数(JAVA):
            分析数据
            调用sayhello/sayhello_to

    源码阅读
    (1) ServiceManager.addService("hello", new HelloService());
        ServiceManagerProxy.addService:
                // Parcel.java
                data.writeStrongBinder(service);
                                nativeWriteStrongBinder(mNativePtr, val); // val = service = new HelloService()
                                它是一个JNI调用,对应android_os_Parcel_writeStrongBinder(c++)

    (2) android_os_Parcel_writeStrongBinder(c++)
        它会构造一个JavaBBinder对象(c++),.mObject=new HelloService() JAVA对象
        然后让.cookie=JavaBBinder对象(c++)
        
        // 把Java Parcel转换为c++ Parcel
        Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
        
        // .cookie = ibinderForJavaObject(env, object)得到一个JavaBBinder对象
        parcel->writeStrongBinder(ibinderForJavaObject(env, object))

    (3) ibinderForJavaObject(env, object) //object = new HelloService()
        把一个Java对象(new HelloService())转换为c++ IBinder对象
        
        JavaBBinderHolder* jbh = (JavaBBinderHolder*)env->GetLongField(obj, gBinderOffsets.mObject);
        return jbh != NULL ? jbh->get(env, obj) : NULL;
                                                         b = new JavaBBinder(env, obj); // obj = new HelloService()
                                                                     mObject = new HelloService()


    (4) 从驱动中得过了.cookie, 它是一个JavaBBinder对象
        调用它的transact函数,导致JavaBBinder对象的onTransact被调用
        
        JavaBBinder::onTransact (调用java里的某个函数)
        
                // mObject指向 HelloService对象
                // gBinderOffsets.mExecTransact指向: java Binder类中的execTransact方法
                // 调用HelloService(派生自Binder)对象中的execTransact方法
            jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
                code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(reply), flags);

    (5) java Binder execTransact:  
                    res = onTransact(code, data, reply, flags);
                                        调用HelloService中的onTransact方法(来自IHelloService.Stube)
                                            分辨数据
                                            调用sayhello/sayhello_to

     -end-

  • 相关阅读:
    信息化与信息系统4
    信息化与信息系统3
    信息化与信息系统2
    信息化与信息系统1
    ASP.NET Core教程:ASP.NET Core 程序部署到Windows系统
    C#几种单例模式
    SQL查询优化
    动态类型dynamic(ExpandoObject)
    PV(访问量)、UV(独立访客)、IP(独立IP)
    Spring Boot 2.配置
  • 原文地址:https://www.cnblogs.com/blogs-of-lxl/p/10522785.html
Copyright © 2011-2022 走看看