zoukankan      html  css  js  c++  java
  • 06、Android--Service服务

    Service

    Service声明

    Service的启动方式上,可以将Service分为Started Service和Bind Service。无论哪种具体的Service启动类型,都是通过继承Service基类自定义而来。在使用Service时,要想系统

    能够找到此自定义Service,无论哪种类型,都需要在AndroidManifest.xml中声明,语法格式如下:

    <service android:enabled=["true" | "false"]
         android:exported=["true" | "false"]
         android:icon="drawable resource"
         android:isolatedProcess=["true" | "false"]
         android:label="string resource"
         android:name="string"
         android:permission="string"
         android:process="string" >
    </service>
    

    上述使用到的属性说明在下面的表格:

    模式 描述
    enable Service是否能被系统初始化,默认为true。
    exported 其他应用能否访问该服务,如果不能,则只有本应用或有相同用户ID的应用能访问。
    icon 类似 label ,是图标,尽量用 drawable 资源的引用定义。
    isolatedProcess 设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。
    lable 可以显示给用户的服务名称。如果没设置,就用 <application>lable
    name 你所编写的服务类的类名,可填写完整名称,包名+类名
    permission 标识此Service的调用权限,如果没有设置,则默认使用的权限设置。
    process 服务运行所在的进程名。通常为默认为应用程序所在的进程,与包名同名。

    在清单文件进行注册之后,创建一个类继承Service并实现onBind()方法:

    public class CustomService extends Service {
        @Override
        public IBinder onBind(Intent intent) {
            throw new UnsupportedOperationException("Not yet implemented");
        }
    }
    

    Service基础

    开启Service有两种方式:start Service(标准启动) 和 bound Serive(绑定启动)。

    Start Service

    标准开启的Service会长期在后台运行,但是此时Activity是不能调用服务中的方法。

    Intent service = new Intent(MainActivity.this, CustomService.class);
    context.startService(service);
    

    如果想停止Service则调用stopService(intent)即可,当然Service也可以调用stopSelf()来停止自身。

    Bind Service

    绑定开启Service时,Activity可以调用服务中的方法,但是此时服务是不能长期在后台运行。

    Intent service = new Intent(MainActivity.this, CustomService.class);
    /**
     * service:         intent意图
     * ServiceConnection:   绑定开启的代理对象
     * BIND_AUTO_CREATE:    服务如果在绑定的时候不存在,会自动创建
     */
    bindService(service, new MyServiceConnection(), Context.BIND_AUTO_CREATE);
    

    绑定开启服务需要注意在Service中的onBind()方法必须有Bind的实现类的当做返回值,否则会报错。

    Mix Service

    mix Service(混合启动)来启动。它具备标准启动和绑定启动的共同优点,此时服务即可长期在后台运行,Activity也可以调用服务中的方法。

    1、首先通过标准模式启动服务,这样服务就长期在后台运行。
    2、如果需要调用服务中的方法,则再使用绑定模式绑定服务。
    3、如果需要解绑服务则调用unbindService()解绑服务。
    4、如果需要停止服务,则调用stopService()停止服务。

    注:有时候我们解绑服务后,发现还是可以调用服务中的方法,是因为垃圾回收器还没有回收调用该方法的对象。

    生命周期

    Service的生命周期根据不同的启动方式而不同,具体参看下图所示:

    ![img](file:///C:/Users/Legend/Documents/My Knowledge/temp/f7e21929-10e7-4a9f-a7c8-d88d9613bae6/128/index_files/641556de-37ab-4be9-8a6a-44074e20b0e7.jpg)

    Start Service的生命周期

    标准开启Service时,会执行onCreate(0 -> onStartCommand()方法,如果多次开启服务只会执行onStartCommand()方法。

    如果停止Service的话,会执行stopService() -> onDestory()方法。

    startService() -> onCreate() -> onStartCommand() -> running -> stopService()/stopSelf() -> onDestroy() -> stopped

    BInd Service的生命周期

    绑定开启Service时,会执行onCreate() -> onBind()方法,如果多次开启不会调用任何方法。如果停止Service的话会执行onUnbind() -> onDestory()方法。

    bindService() -> onCreate() -> onBind() -> running-> onUnbind() -> onDestroy() -> stopped

    绑定开启服务还有如下一些地方需要注意:

    1、如果oBind()方法返回值是null,onServerConnected方法不会被调用。
    2、绑定的服务在系统设置界面,正在运行条目是看不到的。
    3、绑定的服务和Activity不求同时生,但求同时死。
    4、解除绑定服务后,服务会立即停止,且服务只可以被解除绑定一次,多次解除绑定代码会抛出异常。

    Service的销毁

    Service进程在处理完任务后,要使用安卓Service的stopself方法或者onDestroy方法结束Service,所在进程的回收交给安卓的垃圾回收机制来做。

    其中,onStartCommand返回值有下面几种情况:建议不要修改onStartCommand方法的返回值,都交由系统处理比较好。

    START_STICKY:Service被异常kill掉,会被系统拉起,但Intent内容丢失。
    START_NOT_STICKY:Service被异常kill掉时,不会被系统拉起。
    START_REDELIVER_INTENT:Service被异常kill掉时,会被系统拉起,并且Intent内容保留。
    START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被终止后一定能重启。

    Service通信

    如果Activity需要和Service进行通信,我们必须用Bind Service的方法来开启服务。绑定方式开启的Service必须实现onBind()方法,该方法返回同一个定义

    了服务通信接口的IBinder对象,Activity或其他程序组件可以调用bindService()方法获取接口并且调用服务上的方法。

    创建绑定的服务,首先要定义客户端和服务通信方式的接口,这个接口必须是IBinder的实现类,并且必须通过onBind()方法返回,一旦客户端接收到IBinder,

    就可以通过这个接口进行交互。除此之外,多个客户端可以绑定一个服务,还可以通过unBindService()方法解除绑定,当没有组件绑定在该服务时,服务会自动销毁。

    // Activity
    public class MainActivity extends AppCompatActivity {
        private class MyServiceConnection implements ServiceConnection {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                // 调用Service中的方法
                MyBinder binder = (MyBinder) service;
                String message = binder.getMessage();
                System.out.println(message);
            }
            @Override
            public void onServiceDisconnected(ComponentName name) {
            }
        }
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findViewById(R.id.btn_nomal).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent service = new Intent(MainActivity.this, CustomService.class);
                    bindService(service, new MyServiceConnection(), Context.BIND_AUTO_CREATE);
                }
            });
        }
    }
    // Service
    public class CustomService extends Service {
        @Override
        public IBinder onBind(Intent intent) {
            return new MyBinder();
        }
        public class MyBinder extends Binder {
            /**
             * 该方法提供给Activity或其他组件进行调用
             */
            public String getMessage() {
                return "I am in Service";
            }
        }
    }
    

    IntentService

    IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,

    同时,当任务执行完后,IntentService会自动停止,另外,IntentService可以被启动多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent

    回调方法中执行,所有请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求。

    Service是运行在主线程的,所以它不能做耗时操作,如果要做耗时操作则可以开启一个线程,而IntentService就是解决该问题的,它的处理流程如下:

    创建worker线程传递给onStartCommand()的所有intent,不占用UI线程。
    创建一个工作队列,传递一个intent到你实现的onHandleIntent()方法,避免多线程。
    在所有启动请求被处理后,会自动关闭服务,不需要调用stopSelf()方法。
    默认提供onBind()的实现,并返回null。
    默认提供onStartCommand()的实现,实现发送intent到工作队列,再到onHandleIntent()方法实现。

    以上都是IntentService都已经实现的,我们需要做的就是实现构造方法和 onHandleIntent():

    public class MyIntentService extends IntentService {
        public MyIntentService() {
            super("MyIntentService");
        }
        @Override
        protected void onHandleIntent(Intent intent) {
            try {
                for (int i = 0; i < 20; i++) {
                    Log.w("MyIntentService:", String.valueOf(i));
                    Thread.sleep(1 * 1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        @Override
        public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
            return super.onStartCommand(intent, flags, startId);
        }
    }
    

    注意:如果需要重写其他回调方法一定要调用 super() 方法,保证 IntentService 正确处理 worker 线程,只有 onHandleIntent() 和 onBind() 不需要如此。

    前台服务

    Service按照运行来划分有两种:前台服务和后台服务。

    • 前台服务:前台服务可以一直保持运行状态,且不会在内存不足的情况下被回收。
    • 后台服务:后台服务也就是我们平时使用的普通服务,它的优先级比较低,会在内存不够的情况下可能被回收。
    public class ForeGroundService extends Service {
        @Override
        public void onCreate() {
            super.onCreate();
            // 构建通知栏
            Notification.Builder builder = new Notification.Builder(getApplicationContext());
            Intent intent = new Intent(this, MainActivity.class);
            builder.setContentIntent(PendingIntent.getActivity(this, 0, intent, 0))
            .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
            .setContentTitle("title")
            .setContentText("text")
            .setWhen(System.currentTimeMillis());
            Notification notification = builder.build();
            notification.defaults = Notification.DEFAULT_SOUND;
            // 设置服务为前台服务,参数一:唯一的通知标识;参数二:通知消息。
            startForeground(110, notification);
        }
        @Override
        public IBinder onBind(Intent intent) {
            // TODO: Return the communication channel to the service.
            throw new UnsupportedOperationException("Not yet implemented");
        }
        @Override
        public void onDestroy() {
            // 停止前台服务--参数:表示是否移除之前的通知
            stopForeground(true);
        }
    }
    

    远程服务

    aidl(Android Interface definition language),它是一种android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口IPC(interprocess communication)

    内部进程通信,满足两个进程之间接口数据的交换。

    使用远程服务有如下的优点和缺点:

    优点
    远程服务有自己的独立进程,不会受到其它进程的影响;
    可以被其它进程复用,提供公共服务;
    具有很高的灵活性。
    缺点
    相对普通服务,占用系统资源较多,使用AIDL进行IPC也相对麻烦。

    关于远程服务的通信示意图如下:

    远程服务创建

    定义AIDL接口 通过AIDL文件定义服务(Service)向客户端(Client)提供的接口,我们需要在对应的目录下添加一个后缀为.aidl的文件,IMyAidlInterface.aidl文件内容如下:

    interface IMyAidlInterface {
      String getMessage();
    }
    

    注:如果服务端与客户端不在同一App上,需要在客户端、服务端两侧都建立该aidl文件。

    创建远程Service

    在远程服务中,通过Service的onBind(),在客户端与服务端建立连接时,用来传递Stub(存根)对象。

    // 远程服务示例
    public class RemoteService extends Service {
      
      public RemoteService() {
      }
      
      @Override
      public IBinder onBind(Intent intent) {
        return stub;// 在客户端连接服务端时,Stub通过ServiceConnection传递到客户端
      }
      
      // 实现接口中暴露给客户端的Stub--Stub继承自Binder,它实现了IBinder接口
      private IMyAidlInterface.Stub stub = new IMyAidlInterface.Stub(){
      
        // 实现了AIDL文件中定义的方法
        @Override
        public String getMessage() throws RemoteException {
          // 在这里我们只是用来模拟调用效果,因此随便反馈值给客户端
          return "Remote Service方法调用成功";        
        }    
      };
    }
    

    同时,在AndroidManifest.xml中对Remote Service进行如下配置:

    <service
      android:name=".RemoteService"
      android:process="com.test.remote.msg">
      <intent-filter>
        <action android:name="com.legend.remoteservice.RemoteService"/>
      </intent-filter>
    </service>
    

    如果客户端与服务端在同个App中,AndroidManifest.xml中设置Remote Service的andorid:process属性时,有两种情况需要注意:

    设置的进程名以(:)开头,则新进程是私有的,每次被执行或者被需要的时候会在新进程中创建。
    设置的进程以小写字符开头,则服务运行在以这个名字命名的全局进程中,允许不同组件共享进程,从而减少资源浪费(需要相应权限)。

    客户端调用远程服务接口

    在客户端中建立与Remote Service的连接,获取Stub,然后调用Remote Service提供的方法来获取对应数据。

    public class MainActivity extends AppCompatActivity {
      
      private IMyAidlInterface iMyAidlInterface;// 定义接口变量
      private ServiceConnection connection;
      
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main);
        bindRemoteService();
      }
      
      private void bindRemoteService() {
        Intent intentService = new Intent();
        intentService.setClassName(this,"com.zihao.remoteservice.RemoteService");
      
        connection = new ServiceConnection() {
          @Override
          public void onServiceConnected(ComponentName componentName,IBinder iBinder) {
            // 从连接中获取Stub对象
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
            // 调用Remote Service提供的方法
            try {
              Log.d("MainActivity", "获取到消息:" + iMyAidlInterface.getMessage()); 
            } catch (RemoteException e) {
              e.printStackTrace();
            } 
          } 
      
          @Override
          public void onServiceDisconnected(ComponentName componentName) {
            // 断开连接
            iMyAidlInterface = null;
          }
        }; 
      
        bindService(intentService, connection, Context.BIND_AUTO_CREATE);
      } 
      
      @Override
      protected void onDestroy() {
        super.onDestroy(); 
        if (connection != null)
          unbindService(connection);// 解除绑定
      }
    }
    

    远程服务实例

    反射挂断电话

    1、找到上下文的mBase引用的类ContextImpl,通过查看getSystemService源码可以知道,所有的系统服务都在一个map集合中。

    public Object getSystemService(String name){
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }
    

    2、接下来去查找map集合SYSTEM_SERVERCE_MAP,发现它其实是一个hashMap,这里需要详细解说:

    registerService(POWER_SERVICE, new ServiceFetcher(){
        public Object createService(ContextImpl ctx){
            IBinder b = ServiceManager.getService(POWER_SERVICE);
            IPowerManager service = IPowerManager.Stub.asInterface(b);
            return new PowerManager(service, ctx.mMainThread.getHandler());
        }}
    );
    

    由于某些服务被认为不安全或侵犯用户隐私,所以谷歌在包装系统服务的时候,将某些服务进行了隐藏(@hide),比如挂断电话。我们需要先拿到ServiceManager对象, 但是谷歌不希望我们使用该对象,所以将该对象进行隐藏,所以参考下面的反射。

    IBinder iBinder = ServiceManager.getService(TELEPHONY_SERVICE);
    

    3、通过当前的service类的字节码来获取ServiceManager的字节码文件

    // IBinder iBinder = ServiceManager.getService(TELEPHONY_SERVICE);
    try{
        Class clazz = CallSmsSafeService.class.getClassLoader().loadClass("android.os.ServiceManager");
        Method method = clazz.getDeclaredMethod("getService", String.class);
        IBinder iBinder = (IBinder) method.invoke(null, TELEPHONY_SERVICE);
    }catch (Exception e){
        e.printStackTrace();
    }
    

    4、下一步则是将iBinder转成接口类型,需要两个aidl文件,其中一个是依赖另外一个存在的,注意保证包名一致
    1、android.telephony下的NeighboringCellInfo.aidl

    /* //device/java/android/android/content/Intent.aidl
    **
    ** Copyright 2007, The Android Open Source Project
    **
    ** Licensed under the Apache License, Version 2.0 (the "License");
    ** you may not use this file except in compliance with the License.
    ** You may obtain a copy of the License at
    **
    **     http://www.apache.org/licenses/LICENSE-2.0
    **
    ** Unless required by applicable law or agreed to in writing, software
    ** distributed under the License is distributed on an "AS IS" BASIS,
    ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    ** See the License for the specific language governing permissions and
    ** limitations under the License.
    */
    
    package android.telephony;
    
    parcelable NeighboringCellInfo;
    

    2、com.android.internal.telephony下的ITelephony.aidl

    /*
     * Copyright (C) 2007 The Android Open Source Project
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package com.android.internal.telephony;
    
    import android.os.Bundle;
    import java.util.List;
    import android.telephony.NeighboringCellInfo;
    
    /**
     * Interface used to interact with the phone.  Mostly this is used by the
     * TelephonyManager class.  A few places are still using this directly.
     * Please clean them up if possible and use TelephonyManager insteadl.
     *
     * {@hide}
     */
    interface ITelephony {
    
        /**
         * Dial a number. This doesn't place the call. It displays
         * the Dialer screen.
         * @param number the number to be dialed. If null, this
         * would display the Dialer screen with no number pre-filled.
         */
        void dial(String number);
    
        /**
         * Place a call to the specified number.
         * @param number the number to be called.
         */
        void call(String number);
    
        /**
         * If there is currently a call in progress, show the call screen.
         * The DTMF dialpad may or may not be visible initially, depending on
         * whether it was up when the user last exited the InCallScreen.
         *
         * @return true if the call screen was shown.
         */
        boolean showCallScreen();
    
        /**
         * Variation of showCallScreen() that also specifies whether the
         * DTMF dialpad should be initially visible when the InCallScreen
         * comes up.
         *
         * @param showDialpad if true, make the dialpad visible initially,
         *                    otherwise hide the dialpad initially.
         * @return true if the call screen was shown.
         *
         * @see showCallScreen
         */
        boolean showCallScreenWithDialpad(boolean showDialpad);
    
        /**
         * End call or go to the Home screen
         *
         * @return whether it hung up
         */
        boolean endCall();
    
        /**
         * Answer the currently-ringing call.
         *
         * If there's already a current active call, that call will be
         * automatically put on hold.  If both lines are currently in use, the
         * current active call will be ended.
         *
         * TODO: provide a flag to let the caller specify what policy to use
         * if both lines are in use.  (The current behavior is hardwired to
         * "answer incoming, end ongoing", which is how the CALL button
         * is specced to behave.)
         *
         * TODO: this should be a oneway call (especially since it's called
         * directly from the key queue thread).
         */
        void answerRingingCall();
    
        /**
         * Silence the ringer if an incoming call is currently ringing.
         * (If vibrating, stop the vibrator also.)
         *
         * It's safe to call this if the ringer has already been silenced, or
         * even if there's no incoming call.  (If so, this method will do nothing.)
         *
         * TODO: this should be a oneway call too (see above).
         *       (Actually *all* the methods here that return void can
         *       probably be oneway.)
         */
        void silenceRinger();
    
        /**
         * Check if we are in either an active or holding call
         * @return true if the phone state is OFFHOOK.
         */
        boolean isOffhook();
    
        /**
         * Check if an incoming phone call is ringing or call waiting.
         * @return true if the phone state is RINGING.
         */
        boolean isRinging();
    
        /**
         * Check if the phone is idle.
         * @return true if the phone state is IDLE.
         */
        boolean isIdle();
    
        /**
         * Check to see if the radio is on or not.
         * @return returns true if the radio is on.
         */
        boolean isRadioOn();
    
        /**
         * Check if the SIM pin lock is enabled.
         * @return true if the SIM pin lock is enabled.
         */
        boolean isSimPinEnabled();
    
        /**
         * Cancels the missed calls notification.
         */
        void cancelMissedCallsNotification();
    
        /**
         * Supply a pin to unlock the SIM.  Blocks until a result is determined.
         * @param pin The pin to check.
         * @return whether the operation was a success.
         */
        boolean supplyPin(String pin);
    
        /**
         * Handles PIN MMI commands (PIN/PIN2/PUK/PUK2), which are initiated
         * without SEND (so <code>dial</code> is not appropriate).
         *
         * @param dialString the MMI command to be executed.
         * @return true if MMI command is executed.
         */
        boolean handlePinMmi(String dialString);
    
        /**
         * Toggles the radio on or off.
         */
        void toggleRadioOnOff();
    
        /**
         * Set the radio to on or off
         */
        boolean setRadio(boolean turnOn);
    
        /**
         * Request to update location information in service state
         */
        void updateServiceLocation();
    
        /**
         * Enable location update notifications.
         */
        void enableLocationUpdates();
    
        /**
         * Disable location update notifications.
         */
        void disableLocationUpdates();
    
        /**
         * Enable a specific APN type.
         */
        int enableApnType(String type);
    
        /**
         * Disable a specific APN type.
         */
        int disableApnType(String type);
    
        /**
         * Allow mobile data connections.
         */
        boolean enableDataConnectivity();
    
        /**
         * Disallow mobile data connections.
         */
        boolean disableDataConnectivity();
    
        /**
         * Report whether data connectivity is possible.
         */
        boolean isDataConnectivityPossible();
    
        Bundle getCellLocation();
    
        /**
         * Returns the neighboring cell information of the device.
         */
        List<NeighboringCellInfo> getNeighboringCellInfo();
    
         int getCallState();
         int getDataActivity();
         int getDataState();
    
        /**
         * Returns the current active phone type as integer.
         * Returns TelephonyManager.PHONE_TYPE_CDMA if RILConstants.CDMA_PHONE
         * and TelephonyManager.PHONE_TYPE_GSM if RILConstants.GSM_PHONE
         */
        int getActivePhoneType();
    
        /**
         * Returns the CDMA ERI icon index to display
         */
        int getCdmaEriIconIndex();
    
        /**
         * Returns the CDMA ERI icon mode,
         * 0 - ON
         * 1 - FLASHING
         */
        int getCdmaEriIconMode();
    
        /**
         * Returns the CDMA ERI text,
         */
        String getCdmaEriText();
    
        /**
         * Returns true if CDMA provisioning needs to run.
         */
        boolean getCdmaNeedsProvisioning();
    
        /**
          * Returns the unread count of voicemails
          */
        int getVoiceMessageCount();
    
        /**
          * Returns the network type
          */
        int getNetworkType();
        
        /**
         * Return true if an ICC card is present
         */
        boolean hasIccCard();
    }
    

    这时系统会在gen目录的com.android.internal.telephony包下自动生成一个ITelephony.java的接口文件

    5、继续代码实现反射挂断电话的操作,这时会出现很多高级的api可以供我们使用了

    // IBinder iBinder = ServiceManager.getService(TELEPHONY_SERVICE)
    try {
        Class clazz = CallSmsSafeService.class.getClassLoader().loadClass("android.os.ServiceManager");
        Method method = clazz.getDeclaredMethod("getService", String.class);
        IBinder iBinder = (IBinder) method.invoke(null, TELEPHONY_SERVICE);
        ITelephony iTelephony = ITelephony.Stub.asInterface(iBinder);
        iTelephony.endCall();
    } catch (Exception e) {
        e.printStackTrace();
    }   
    

    要挂断电话还需要添加拨打电话的权限

    <uses-permission android:name="android.permission.CALL_PHONE"/>  
    

    此时如果是在API28以下的设备中可以正常拦截 但API28以上的设备会报如下错误:

    JAVA.LANG.NOSUCHMETHODERROR: NO INTERFACE METHOD ENDCALL()Z IN CLASS LCOM/ANDROID/INTERNAL/TELEPHONY/ITELEPHONY;

    出现这个异常的原因就是因为在API28以上的设备时,不再支持反射。而是同过TelecomManager 调用endCall()方法。

  • 相关阅读:
    软解析和硬解析
    oracle存储过程常用技巧
    jquery-1.11.1.js
    JavaScript遍历table
    JavaScript向select下拉框中添加和删除元素
    glog
    DDL引发的对象invalidation
    模拟cursor pin S wait on X
    rsync 排除目录
    JavaScript解决select下拉框中的内容太长显示不全的问题
  • 原文地址:https://www.cnblogs.com/pengjingya/p/5505332.html
Copyright © 2011-2022 走看看