关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android aidl Binder框架浅析

    Android aidl Binder框架浅析

    2015-07-25 00:00:00 出处:风起云涌
    分享

    1、概述

    Binder能干什么?Binder可以提供系统中任何程序都可以访问的全局服务。这个功能当然是任何系统都应该提供的,下面我们简单看一下Android的Binder的框架

    Android Binder框架分为服务器接口、Binder驱动、以及客户端接口;简单想一下,需要提供一个全局服务,那么全局服务那端即是服务器接口,任何程序即客户端接口,它们之间通过一个Binder驱动访问。

    服务器端接口:实际上是Binder类的对象,该对象一旦创建,内部则会启动一个隐藏线程,会接收Binder驱动发送的消息,收到消息后,会执行Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务器端代码。

    Binder驱动:该对象也为Binder类的实例,客户端通过该对象访问远程服务。

    客户端接口:获得Binder驱动,调用其transact()发送消息至服务器

    假如大家对上述不了解,没关系,下面会通过例子来更好的说明,实践是检验真理的唯一标准嘛

    2、AIDL的使用

    假如对Android比较熟悉,那么一定使用过AIDL,假如你还不了解,那么也没关系,下面会使用一个例子展示AIDL的用法。

    我们使用AIDL实现一个跨进程的加减法调用

    1、服务端

    新建一个项目,创建一个包名:com.zhy.calc.aidl,在包内创建一个ICalcAIDL文件:

    package com.zhy.calc.aidl;  
    interface ICalcAIDL  
    {  
        int add(int x , int y);  
        int min(int x , int y );  
    }

    注意,文件名为ICalcAIDL.aidl

    然后在项目的gen目录下会生成一个ICalcAIDL.java文件,暂时不贴这个文件的代码了,后面会详细说明

    然后我们在项目中新建一个Service,代码如下:

    package com.example.zhy_binder;  
    
    import com.zhy.calc.aidl.ICalcAIDL;  
    
    import android.app.Service;  
    import android.content.Intent;  
    import android.os.IBinder;  
    import android.os.RemoteException;  
    import android.util.Log;  
    
    public class CalcService extends Service  
    {  
        private static final String TAG = "server";  
    
        public void onCreate()  
        {  
            Log.e(TAG, "onCreate");  
        }  
    
        public IBinder onBind(Intent t)  
        {  
            Log.e(TAG, "onBind");  
            return mBinder;  
        }  
    
        public void onDestroy()  
        {  
            Log.e(TAG, "onDestroy");  
            super.onDestroy();  
        }  
    
        public boolean onUnbind(Intent intent)  
        {  
            Log.e(TAG, "onUnbind");  
            return super.onUnbind(intent);  
        }  
    
        public void onRebind(Intent intent)  
        {  
            Log.e(TAG, "onRebind");  
            super.onRebind(intent);  
        }  
    
        private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()  
        {  
    
            @Override  
            public int add(int x, int y) throws RemoteException  
            {  
                return x + y;  
            }  
    
            @Override  
            public int min(int x, int y) throws RemoteException  
            {  
                return x - y;  
            }  
    
        };  
    
    }

    在此Service中,使用生成的ICalcAIDL创建了一个mBinder的对象,并在Service的onBind方法中返回

    最后记得在AndroidManifest中注册

    <service android:name="com.example.zhy_binder.CalcService" >  
               <intent-filter>  
                   <action android:name="com.zhy.aidl.calc" />  
    
                   <category android:name="android.intent.category.DEFAULT" />  
               </intent-filter>  
           </service>

    这里我们指定了一个name,因为我们一会会在别的应用程序中通过Intent来查找此Service;这个不需要Activity,所以我也就没写Activity,安装完成也看不到安装图标,悄悄在后台运行着。

    到此,服务端编写完毕。下面开始编写客户端

    2、客户端

    客户端的代码比较简单,创建一个布局,里面包含4个按钮,分别为绑定服务,解除绑定,调用加法,调用减法

    布局文件:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        xmlns:tools="http://schemas.android.com/tools"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:orientation="vertical" >  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="bindService"  
            android:text="BindService" />  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="unbindService"  
            android:text="UnbindService" />  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="addInvoked"  
            android:text="12+12" />  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="minInvoked"  
            android:text="50-12" />  
    
    </LinearLayout>

    主Activity

    package com.example.zhy_binder_client;  
    
    import android.app.Activity;  
    import android.content.ComponentName;  
    import android.content.Context;  
    import android.content.Intent;  
    import android.content.ServiceConnection;  
    import android.os.Bundle;  
    import android.os.IBinder;  
    import android.util.Log;  
    import android.view.View;  
    import android.widget.Toast;  
    
    import com.zhy.calc.aidl.ICalcAIDL;  
    
    public class MainActivity extends Activity  
    {  
        private ICalcAIDL mCalcAidl;  
    
        private ServiceConnection mServiceConn = new ServiceConnection()  
        {  
            @Override  
            public void onServiceDisconnected(ComponentName name)  
            {  
                Log.e("client", "onServiceDisconnected");  
                mCalcAidl = null;  
            }  
    
            @Override  
            public void onServiceConnected(ComponentName name, IBinder service)  
            {  
                Log.e("client", "onServiceConnected");  
                mCalcAidl = ICalcAIDL.Stub.asInterface(service);  
            }  
        };  
    
        @Override  
        protected void onCreate(Bundle savedInstanceState)  
        {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.activity_main);  
    
        }  
    
        /** 
         * 点击BindService按钮时调用 
         * @param view 
         */  
        public void bindService(View view)  
        {  
            Intent intent = new Intent();  
            intent.setAction("com.zhy.aidl.calc");  
            bindService(intent, mServiceConn, Context.BIND_AUTO_CREATE);  
        }  
        /** 
         * 点击unBindService按钮时调用 
         * @param view 
         */  
        public void unbindService(View view)  
        {  
            unbindService(mServiceConn);  
        }  
        /** 
         * 点击12+12按钮时调用 
         * @param view 
         */  
        public void addInvoked(View view) throws Exception  
        {  
    
            if (mCalcAidl != null)  
            {  
                int addRes = mCalcAidl.add(12, 12);  
                Toast.makeText(this, addRes + "", Toast.LENGTH_SHORT).show();  
            } else  
            {  
                Toast.makeText(this, "服务器被异常杀死,请重新绑定服务端", Toast.LENGTH_SHORT)  
                        .show();  
    
            }  
    
        }  
        /** 
         * 点击50-12按钮时调用 
         * @param view 
         */  
        public void minInvoked(View view) throws Exception  
        {  
    
            if (mCalcAidl != null)  
            {  
                int addRes = mCalcAidl.min(58, 12);  
                Toast.makeText(this, addRes + "", Toast.LENGTH_SHORT).show();  
            } else  
            {  
                Toast.makeText(this, "服务端未绑定或被异常杀死,请重新绑定服务端", Toast.LENGTH_SHORT)  
                        .show();  
    
            }  
    
        }  
    
    }

    很标准的绑定服务的代码。

    直接看运行结果:

    我们首先点击BindService按钮,查看log

    08-09 22:56:38.959: E/server(29692): onCreate  
    08-09 22:56:38.959: E/server(29692): onBind  
    08-09 22:56:38.959: E/client(29477): onServiceConnected

    可以看到,点击BindService之后,服务端执行了onCreate和onBind的方法,并且客户端执行了onServiceConnected方法,标明服务器与客户端已经联通

    然后点击12+12,50-12可以成功的调用服务端的代码并返回正确的结果

    下面我们再点击unBindService

    08-09 22:59:25.567: E/server(29692): onUnbind  
    08-09 22:59:25.567: E/server(29692): onDestroy

    由于我们当前只有一个客户端绑定了此Service,所以Service调用了onUnbind和onDestory

    然后我们继续点击12+12,50-12,通过上图可以看到,依然可以正确执行,也就是说即使onUnbind被调用,连接也是不会断开的,那么什么时候会端口呢?

    即当服务端被异常终止的时候,比如我们现在在手机的正在执行的程序中找到该服务:

    点击停止,此时查看log

    08-09 23:04:21.433: E/client(30146): onServiceDisconnected

    可以看到调用了onServiceDisconnected方法,此时连接被断开,现在点击12+12,50-12的按钮,则会弹出Toast服务端断开的提示。

    说了这么多,似乎和Binder框架没什么关系,下面我们来具体看一看AIDL为什么做了些什么。

    3、分析AIDL生成的代码

    1、服务端

    先看服务端的代码,可以看到我们服务端提供的服务是由

    private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()  
        {  
    
            @Override  
            public int add(int x, int y) throws RemoteException  
            {  
                return x + y;  
            }  
    
            @Override  
            public int min(int x, int y) throws RemoteException  
            {  
                return x - y;  
            }  
    
        };

    ICalcAILD.Stub来执行的,让我们来看看Stub这个类的声明:

    public static abstract class Stub extends android.os.Binder implements com.zhy.calc.aidl.ICalcAIDL

    清楚的看到这个类是Binder的子类,是不是符合我们文章开通所说的服务端其实是一个Binder类的实例

    接下来看它的onTransact()方法:

    @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_add:  
    {  
    data.enforceInterface(DESCRIPTOR);  
    int _arg0;  
    _arg0 = data.readInt();  
    int _arg1;  
    _arg1 = data.readInt();  
    int _result = this.add(_arg0, _arg1);  
    reply.writeNoException();  
    reply.writeInt(_result);  
    return true;  
    }  
    case TRANSACTION_min:  
    {  
    data.enforceInterface(DESCRIPTOR);  
    int _arg0;  
    _arg0 = data.readInt();  
    int _arg1;  
    _arg1 = data.readInt();  
    int _result = this.min(_arg0, _arg1);  
    reply.writeNoException();  
    reply.writeInt(_result);  
    return true;  
    }  
    }  
    return super.onTransact(code, data, reply, flags);  
    }

    文章开头也说到服务端的Binder实例会根据客户端依靠Binder驱动发来的消息,执行onTransact方法,然后由其参数决定执行服务端的代码。

    可以看到onTransact有四个参数

    code , data ,replay , flags

    code 是一个整形的唯一标识,用于区分执行哪个方法,客户端会传递此参数,告诉服务端执行哪个方法

    data客户端传递过来的参数

    replay服务器返回回去的值

    flags标明是否有返回值,0为有(双向),1为没有(单向)

    我们仔细看case TRANSACTION_min中的代码

    data.enforceInterface(DESCRIPTOR);与客户端的writeInterfaceToken对用,标识远程服务的名称

    int _arg0;
    _arg0 = data.readInt();
    int _arg1;
    _arg1 = data.readInt();

    接下来分别读取了客户端传入的两个参数

    int _result = this.min(_arg0, _arg1);
    reply.writeNoException();
    reply.writeInt(_result);

    然后执行this.min,即我们实现的min方法;返回result由reply写回。

    add同理,可以看到服务端通过AIDL生成Stub的类,封装了服务端本来需要写的代码。

    2、客户端

    客户端主要通过ServiceConnected与服务端连接

    private ServiceConnection mServiceConn = new ServiceConnection()  
        {  
            @Override  
            public void onServiceDisconnected(ComponentName name)  
            {  
                Log.e("client", "onServiceDisconnected");  
                mCalcAidl = null;  
            }  
    
            @Override  
            public void onServiceConnected(ComponentName name, IBinder service)  
            {  
                Log.e("client", "onServiceConnected");  
                mCalcAidl = ICalcAIDL.Stub.asInterface(service);  
            }  
        };

    假如你比较敏锐,应该会猜到这个onServiceConnected中的IBinder实例,其实就是我们文章开通所说的Binder驱动,也是一个Binder实例

    在ICalcAIDL.Stub.asInterface中最终调用了:

    return new com.zhy.calc.aidl.ICalcAIDL.Stub.Proxy(obj);

    这个Proxy实例传入了我们的Binder驱动,并且封装了我们调用服务端的代码,文章开头说,客户端会通过Binder驱动的transact()方法调用服务端代码

    直接看Proxy中的add方法

    @Override public int add(int x, int y) 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.writeInt(x);  
    _data.writeInt(y);  
    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);  
    _reply.readException();  
    _result = _reply.readInt();  
    }  
    finally {  
    _reply.recycle();  
    _data.recycle();  
    }  
    return _result;  
    }

    首先声明两个Parcel对象,一个用于传递数据,一个用户接收返回的数据

    _data.writeInterfaceToken(DESCRIPTOR);与服务器端的enforceInterfac对应

    _data.writeInt(x);
    _data.writeInt(y);写入需要传递的参数

    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);

    终于看到了我们的transact方法,第一个对应服务端的code,_data,_repay分别对应服务端的data,reply,0表示是双向的

    _reply.readException();
    _result = _reply.readInt();

    最后读出我们服务端返回的数据,然后return。可以看到和服务端的onTransact基本是一行一行对应的。

    到此,我们已经通过AIDL生成的代码解释了Android Binder框架的工作原理。Service的作用其实就是为我们创建Binder驱动,即服务端与客户端连接的桥梁。

    AIDL其实通过我们写的aidl文件,帮助我们生成了一个接口,一个Stub类用于服务端,一个Proxy类用于客户端调用。那么我们是否可以不通过写AIDL来实现远程的通信呢?下面向大家展示如何完全不依赖AIDL来实现客户端与服务端的通信。

    4、不依赖AIDL实现程序间通讯

    1、服务端代码

    我们新建一个CalcPlusService.java用于实现两个数的乘和除

    package com.example.zhy_binder;  
    
    import android.app.Service;  
    import android.content.Intent;  
    import android.os.Binder;  
    import android.os.IBinder;  
    import android.os.Parcel;  
    import android.os.RemoteException;  
    import android.util.Log;  
    
    public class CalcPlusService extends Service  
    {  
        private static final String DESCRIPTOR = "CalcPlusService";  
        private static final String TAG = "CalcPlusService";  
    
        public void onCreate()  
        {  
            Log.e(TAG, "onCreate");  
        }  
    
        @Override  
        public int onStartCommand(Intent intent, int flags, int startId)  
        {  
            Log.e(TAG, "onStartCommand");  
            return super.onStartCommand(intent, flags, startId);  
        }  
    
        public IBinder onBind(Intent t)  
        {  
            Log.e(TAG, "onBind");  
            return mBinder;  
        }  
    
        public void onDestroy()  
        {  
            Log.e(TAG, "onDestroy");  
            super.onDestroy();  
        }  
    
        public boolean onUnbind(Intent intent)  
        {  
            Log.e(TAG, "onUnbind");  
            return super.onUnbind(intent);  
        }  
    
        public void onRebind(Intent intent)  
        {  
            Log.e(TAG, "onRebind");  
            super.onRebind(intent);  
        }  
    
        private MyBinder mBinder = new MyBinder();  
    
        private class MyBinder extends Binder  
        {  
            @Override  
            protected boolean onTransact(int code, Parcel data, Parcel reply,  
                    int flags) throws RemoteException  
            {  
                switch (code)  
                {  
                case 0x110:  
                {  
                    data.enforceInterface(DESCRIPTOR);  
                    int _arg0;  
                    _arg0 = data.readInt();  
                    int _arg1;  
                    _arg1 = data.readInt();  
                    int _result = _arg0 * _arg1;  
                    reply.writeNoException();  
                    reply.writeInt(_result);  
                    return true;  
                }  
                case 0x111:  
                {  
                    data.enforceInterface(DESCRIPTOR);  
                    int _arg0;  
                    _arg0 = data.readInt();  
                    int _arg1;  
                    _arg1 = data.readInt();  
                    int _result = _arg0 / _arg1;  
                    reply.writeNoException();  
                    reply.writeInt(_result);  
                    return true;  
                }  
                }  
                return super.onTransact(code, data, reply, flags);  
            }  
    
        };  
    
    }

    我们自己实现服务端,所以我们自定义了一个Binder子类,然后复写了其onTransact方法,我们指定服务的标识为CalcPlusService,然后0×110为乘,0×111为除;

    记得在AndroidMenifest中注册

    <service android:name="com.example.zhy_binder.CalcPlusService" >  
               <intent-filter>  
                   <action android:name="com.zhy.aidl.calcplus" />  
                   <category android:name="android.intent.category.DEFAULT" />  
               </intent-filter>  
           </service>

    服务端代码结束。

    2、客户端代码

    单独新建了一个项目,代码和上例很类似

    首先布局文件:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        xmlns:tools="http://schemas.android.com/tools"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:orientation="vertical" >  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="bindService"  
            android:text="BindService" />  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="unbindService"  
            android:text="UnbindService" />  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="mulInvoked"  
            android:text="50*12" />  
    
        <Button  
            android:layout_width="fill_parent"  
            android:layout_height="wrap_content"  
            android:onClick="divInvoked"  
            android:text="36/12" />  
    
    </LinearLayout>

    可以看到加入了乘和除

    然后是Activity的代码

    package com.example.zhy_binder_client03;  
    
    import android.app.Activity;  
    import android.content.ComponentName;  
    import android.content.Context;  
    import android.content.Intent;  
    import android.content.ServiceConnection;  
    import android.os.Bundle;  
    import android.os.IBinder;  
    import android.os.RemoteException;  
    import android.util.Log;  
    import android.view.View;  
    import android.widget.Toast;  
    
    public class MainActivity extends Activity  
    {  
    
        private IBinder mPlusBinder;  
        private ServiceConnection mServiceConnPlus = new ServiceConnection()  
        {  
            @Override  
            public void onServiceDisconnected(ComponentName name)  
            {  
                Log.e("client", "mServiceConnPlus onServiceDisconnected");  
            }  
    
            @Override  
            public void onServiceConnected(ComponentName name, IBinder service)  
            {  
    
                Log.e("client", " mServiceConnPlus onServiceConnected");  
                mPlusBinder = service;  
            }  
        };  
    
        @Override  
        protected void onCreate(Bundle savedInstanceState)  
        {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.activity_main);  
    
        }  
    
        public void bindService(View view)  
        {  
            Intent intentPlus = new Intent();  
            intentPlus.setAction("com.zhy.aidl.calcplus");  
            boolean plus = bindService(intentPlus, mServiceConnPlus,  
                    Context.BIND_AUTO_CREATE);  
            Log.e("plus", plus + "");  
        }  
    
        public void unbindService(View view)  
        {  
            unbindService(mServiceConnPlus);  
        }  
    
        public void mulInvoked(View view)  
        {  
    
            if (mPlusBinder == null)  
            {  
                Toast.makeText(this, "未连接服务端或服务端被异常杀死", Toast.LENGTH_SHORT).show();  
            } else  
            {  
                android.os.Parcel _data = android.os.Parcel.obtain();  
                android.os.Parcel _reply = android.os.Parcel.obtain();  
                int _result;  
                try  
                {  
                    _data.writeInterfaceToken("CalcPlusService");  
                    _data.writeInt(50);  
                    _data.writeInt(12);  
                    mPlusBinder.transact(0x110, _data, _reply, 0);  
                    _reply.readException();  
                    _result = _reply.readInt();  
                    Toast.makeText(this, _result + "", Toast.LENGTH_SHORT).show();  
    
                } catch (RemoteException e)  
                {  
                    e.printStackTrace();  
                } finally  
                {  
                    _reply.recycle();  
                    _data.recycle();  
                }  
            }  
    
        }  
    
        public void divInvoked(View view)  
        {  
    
            if (mPlusBinder == null)  
            {  
                Toast.makeText(this, "未连接服务端或服务端被异常杀死", Toast.LENGTH_SHORT).show();  
            } else  
            {  
                android.os.Parcel _data = android.os.Parcel.obtain();  
                android.os.Parcel _reply = android.os.Parcel.obtain();  
                int _result;  
                try  
                {  
                    _data.writeInterfaceToken("CalcPlusService");  
                    _data.writeInt(36);  
                    _data.writeInt(12);  
                    mPlusBinder.transact(0x111, _data, _reply, 0);  
                    _reply.readException();  
                    _result = _reply.readInt();  
                    Toast.makeText(this, _result + "", Toast.LENGTH_SHORT).show();  
    
                } catch (RemoteException e)  
                {  
                    e.printStackTrace();  
                } finally  
                {  
                    _reply.recycle();  
                    _data.recycle();  
                }  
            }  
    
        }  
    }

    为了明了,我直接在mulInvoked里面写了代码,和服务端都没有抽象出一个接口。首先绑定服务时,通过onServiceConnected得到Binder驱动即mPlusBinder;

    然后准备数据,调用transact方法,通过code指定执行服务端哪个方法,代码和上面的分析一致。

    下面看运行结果:

    是不是很好的实现了我们两个应用程序间的通讯,并没有使用aidl文件,也从侧面分析了我们上述分析是正确的。

    好了,就到这里,相信大家看完这篇博文,对aidl和Binder的理解也会更加深刻。

    测试代码点击下载

    代码先安装server端的代码,然后再安装client端的。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    帝国CMS7.5编辑器上传图片取消宽高的三种方法

    帝国cms如何自动生成缩略图的实现方法

    Windows 12即将到来,将彻底改变人机交互

    帝国CMS 7.5忘记登陆账号密码怎么办?可以phpmyadmin中重置管理员密码

    帝国CMS 7.5 后台编辑器换行,修改回车键br换行为p标签

    Windows 11 版本与 Windows 10比较,新功能一览

    Windows 11激活产品密钥收集及专业版激活方法

    IT头条

    无线路由大厂 TP-Link突然大裁员:补偿N+3

    02:39

    Meta 千万美金招募AI高级人才

    00:22

    更容易爆炸?罗马仕充电宝被北京多所高校禁用,公司紧急回应

    17:19

    天衍”量子计算云平台,“超算+量算” 告别“算力孤岛时代”

    18:18

    华为Pura80系列新机预热,余承东力赞其复杂光线下的视频拍摄实力

    01:28

    技术热点

    MySQL基本调度策略浅析

    MySQL使用INSERT插入多条记录

    SQL Server高可用的常见问题

    3D立体图片展示幻灯片JS特效

    windows 7上网看视频出现绿屏的原因及解决方法

    windows 7 64位系统的HOSTS文件在哪里?想用它加快域名解析

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。