ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

(更新,已反射hook到onActivityResult)如何避免使用onActivityResult,以提高代码可读性

2021-06-30 21:59:36  阅读:224  来源: 互联网

标签:可读性 callback hook requestCode activity onActivityResult public Callback


问题

Android中,通过startActivityForResult跳转页面获取数据应该不必多说,但是这种所有获取到的结果都需要到onActivityResult中处理的方式实在令人蛋疼。

试想一下,我们敲着代码唱着歌。突然,半路上跳出一群马匪,让我们到另一个页面获取一点数据,获取后还不让在当前代码位置处理逻辑,要去onActivityResult添加一个requestCode分支处理结果,处理完才让回来,等这一切都做完回来难免就会陷入这样的思考:我是谁,我在哪,我在干什么,我刚才写到哪了……

再想一下,你跟同事的代码,跟到一个startActivityForResult,于是不耐烦地ctrl+f找到onActivityResult,发现里面充斥着大量的requestCode分支,然后突然意识到刚才没记下requestCode是什么……

分析问题

问题的根源是所有处理结果的逻辑都要放到onActivityResult中,在里面根据requestCode作不同处理。而我们渴望的是能在发起startActivityForResult的时候捎带着把获取结果后处理的逻辑也传进去,并能在内部对requestCode判断好,不用我们再判断一遍。

解决问题

尝试一(不完美方式)

新建一个OnResultManager类,用来管理获取结果的回调,下面详细说。

分析问题时说了,我们希望在发起startActivityForResult的时候就指定好处理结果的逻辑,这个简单,在OnResultManager中创建一个Callback接口,里面定义一个OnActivityResult方法,参数和Activity的OnActivityResult方法参数完全一样,在发起start的时候除了intent和requestCode,再传一个callback进去。而OnresultManager负责控制在系统的onActivityResult触发时,调用对应callback的方法。

下面是OnResultManager的全部代码。

public class OnResultManager {
    private static final String TAG = "OnResultManager";
    //HashMap的key Integer为requestCode
    private static WeakHashMap<Activity,HashMap<Integer,Callback>> mCallbacks = new WeakHashMap<>();
    private WeakReference<Activity> mActivity;

    public OnResultManager(Activity activity) {
        mActivity = new WeakReference<Activity>(activity);
    }

    public void startForResult(Intent intent, int requestCode, Callback callback){
        Activity activity = getActivity();
        if(activity == null){
            return;
        }

        addCallback(activity,requestCode,callback);
        activity.startActivityForResult(intent,requestCode);
    }

    public void trigger(int requestCode, int resultCode, Intent data){
        Log.d(TAG,"----------- trigger");
        Activity activity = getActivity();
        if(activity == null){
            return;
        }

        Callback callback = findCallback(activity,requestCode);
        if(callback != null){
            callback.onActivityResult(requestCode,resultCode,data);
        }
    }

    //获取该activity、该requestCode对应的callback
    private Callback findCallback(Activity activity,int requestCode){
        HashMap<Integer,Callback> map = mCallbacks.get(activity);
        if(map != null){
            return map.remove(requestCode);
        }
        return null;
    }

    private void addCallback(Activity activity,int requestCode,Callback callback){
        HashMap<Integer,Callback> map = mCallbacks.get(activity);
        if(map == null){
            map = new HashMap<>();
            mCallbacks.put(activity,map);
        }
        map.put(requestCode,callback);
    }

    private Activity getActivity(){
        return mActivity.get();
    }

    public interface Callback{
        void onActivityResult(int requestCode, int resultCode, Intent data);
    }
} 

逻辑很简单,里面持有一个mActivity,使用弱引用以防止内存泄漏,在构造器中为其赋值。还有一个static的WeakHashMap<Activity,HashMap<Integer,Callback>> mCallbacks 用来存放所有的callback,先以activity分,在各个activity中又使用hashmap存储requestCode和callback的对应关系。

在startForResult时,最终还是调用的activity的startActivityForResult,只不过在跳转页面之前,把callback存入了mCallbacks中。

而trigger方法则是根据activity和requestCode从mCallbacks中取出对应的callback,调用方法。

现在callback的存和取都搞定了,那么问题来了,什么时候触发“取”的操作呢,即trigger方法怎么触发呢?答案是在onActivityResult中调用,嫌麻烦可以在BaseActivity中调用。

使用示例:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        go.setOnClickListener {
            val intent = Intent(this,SecondActivity::class.java)
            onResultManager.startForResult(intent,REQUEST_CODE,{requestCode: Int, resultCode: Int, data: Intent? ->
                if (resultCode == Activity.RESULT_OK){
                    val text = data?.getStringExtra("text")
                    Toast.makeText(this,"result -> "+text,Toast.LENGTH_SHORT).show()
                }else{
                    Toast.makeText(this,"canceled",Toast.LENGTH_SHORT).show()
                }
            })
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        onResultManager.trigger(requestCode,resultCode,data)
    } 

可是这样好蠢啊,你是不是觉得要是不用手动触发,能自动触发多好。我也是这么想的,所以有整整一天我一直在找有什么办法能hook到onActivityResult方法,最后hook了Instrumentation,也hook了AMS,但是都对这个onActivityResult无能为力,看源码发现好像是在ActivityThread中传递的,但是很不幸的是这个ActivityThread没办法hook,至少通过简单的反射和代理没办法做到(如果谁有办法hook到,恳请您能分享出来,我真的特别想知道,我不甘心啊)

更新,强迫症福音,onActivityResult方法hook到了

这里感谢一下@world_hello的提醒,十分感谢。

之前看到ActivityThread的mH的时候总想着弄个代理继承它,然后重写handleMessage方法,来获取结果信息,可是却苦于继承不了,其实我一直忽视了Handler内部的一个Callback接口,其实完全不用代理。下面详细讲解一下。

先说一下Handler,我们一般使用的时候大多是继承自Handler,然后重写handleMessage方法,在里面处理接收消息。其实Handler还有个构造函数可以接收一个Handler.Callback(不要和我们自己定义的callback搞混了啊),在Handler.Callback的handleMessage方法中处理消息。

看一下Handler源码:

 /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    } 

可以看到,如果mCallback不为null,就会执行mCallback的handleMessage方法。如果这个handleMessage方法的返回值为true,就会直接return,如果为false,就会继续执行Handler本身的handleMessage方法。

下面打开Activity源码,startActivityForResult方法中有这么一段

if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            } 

ar是ActivityResult,如果不为null,就调用mMainThread的sendActivityResult方法,这个mMainThread就是ActivityThread类型的,所以继续打开ActivityThread源码,找到这个方法,顺着这个方法一直找一直找,这里中间的方法我就不贴了,最后会找到一个叫sendMessage的方法,这个方法最后一行是

mH.sendMessage(msg); 

这个mH是一个Handler的子类,所以很明显,activityresult在这个环节是通过handler传递的。

在H的handleMessage方法中有这样一个case分支

case SEND_RESULT:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult");
                    handleSendResult((ResultData)msg.obj);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break; 

不用再多说了吧,就是这里了。它调用了handleSendResult((ResultData)msg.obj),所以这个ResultData应该就是存放了我们需要的东西。只要能拦截到它,拿出我们需要的数据,那就算是hook到了onActivityResult了,那么怎么拿到呢?现在就要提到我们刚才提的Handler.Callback了,我们创建一个callback,并记得让handleMessage的方法返回false,以免影响mH本身的handleMessage方法的执行,然后通过反射把这个callback给mH set进去。下面需要您对反射有一点点了解。

public class HookUtil {
    public static void hookActivityThreadHandler() throws Exception {
        // 先获取到当前的ActivityThread对象
        final Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        currentActivityThreadField.setAccessible(true);
        final Object currentActivityThread = currentActivityThreadField.get(null);

        // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        Handler mH = (Handler) mHField.get(currentActivityThread);
        
        Handler.Callback mHCallback = new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if(msg.what == 108){
                    Log.d("hook-------","onActivityResult");

                    try{
                        Object resultData = msg.obj;

                        Field mActivitiesField = activityThreadClass.getDeclaredField("mActivities");
                        mActivitiesField.setAccessible(true);
                        ArrayMap mActivities = (ArrayMap) mActivitiesField.get(currentActivityThread);

                        Class<?> resultDataClass = Class.forName("android.app.ActivityThread$ResultData");
                        Field tokenField = resultDataClass.getDeclaredField("token");
                        tokenField.setAccessible(true);
                        IBinder token = (IBinder) tokenField.get(resultData);

                        //r是ActivityClientRecord类型的
                        Object r = mActivities.get(token);
                        Class<?> ActivityClientRecordClass = Class.forName("android.app.ActivityThread$ActivityClientRecord");
                        Field activityField = ActivityClientRecordClass.getDeclaredField("activity");
                        activityField.setAccessible(true);
                        Activity activity = (Activity) activityField.get(r); //至此,终于拿到activity了

                        Field resultsField = resultDataClass.getDeclaredField("results");
                        resultsField.setAccessible(true);
                        List results = (List) resultsField.get(resultData);

                        //ResultInfo类型
                        Object resultInfo = results.get(0);

                        Class<?> resultInfoClass = Class.forName("android.app.ResultInfo");
                        Field mRequestCodeField = resultInfoClass.getDeclaredField("mRequestCode");
                        mRequestCodeField.setAccessible(true);
                        int mRequestCode = (int) mRequestCodeField.get(resultInfo); //拿到requestCode

                        Field mResultCodeField = resultInfoClass.getDeclaredField("mResultCode");
                        mResultCodeField.setAccessible(true);
                        int mResultCode = (int) mResultCodeField.get(resultInfo); //拿到resultCode

                        Field mDataField = resultInfoClass.getDeclaredField("mData");
                        mDataField.setAccessible(true);
                        Intent mData = (Intent) mDataField.get(resultInfo); //拿到intent

                        new OnResultManager(activity).trigger(mRequestCode,mResultCode,mData);



                    }catch (Exception e){
                        e.printStackTrace();
                    }




                }

                return false;
            }
        };
        Field mCallBackField = Handler.class.getDeclaredField("mCallback");
        mCallBackField.setAccessible(true);

        mCallBackField.set(mH, mHCallback);

    }

} 

hookActivityThreadHandler方法中就是先获取到ActivityThread对象,再通过activityThread获取到mH,然后创建了一个Handler.Callback,最后用反射把这个callback set给mH。

相比之下,Handler.Callback中的handleMessage方法看起来更长一些,但其实并不复杂,只是反射使它变得面目全非了,它主要就是为了获取我们OnResultManager的trigger方法需要的数据,一个activity,以及requestCode、resultCode、data。

msg.what==108的判断,这里108就是SEND_RESULT,我图方便直接写死了,源码里看着为108,当然通过反射动态获取SEND_RESULT更规范一点,大家别在意这些细节。

先看activity怎么获取,主要看ActivityThread的handleSendResult方法,它的第一行

 private void handleSendResult(ResultData res) {
        ActivityClientRecord r = mActivities.get(res.token); 

这个res就是msg.obj,而获取到ActivityClientRecord之后,它有一个名为activity的field,获取的就是我们需要的activity对象了。本身很简单,只不过这短短的一行代码要用反射一点一点获取,就繁琐一些了。

另外三个数据都在一起,在ResultData里有个名为results的Field,它是一个List,里面就一个元素(可能有误,但我打断点看它一直都是一个),是ResultInfo类型,我们需要的requestCode、resultCode、data就都在这个resultInfo里面了,所以,继续反射。

都获取完了,调用

new OnResultManager(activity).trigger(mRequestCode,mResultCode,mData); 

就是把我们之前手动写在onActivityResult中的那句放到这里自动触发。

现在方法写完了,还要写个自定义Application,在onCreate中调用

HookUtil.hookActivityThreadHandler() 

大功告成啦!,赶紧测试一下吧,现在终于不用再在onActivityResult中手动触发啦!github上代码已经更新。

按理说我们该在OnResultManager中定义个init方法,然后在init方法中去调hookActivityThreadHandler貌似更规范一些,不过这只算个demo,就不考虑这么多了。

还有,这种通过反射hook的方法在稳定性和兼容性上都无法保证,所以这算是一种仅供娱乐的方式吧!领略一下hook的魅力。

OnResultManager项目地址

下面是world_hello实现的hook,虽然思路是一样的,但是编写的要比我的容易看懂得多,自愧不如,在此强烈推荐大家看一下。

world_hello实现的hook项目地址

第二种方式(参考RxPermissions的做法)

前段时间又来了个小项目,领导扔给我了,然后在这个项目里就把之前没用过(没错,之前总用H5开发……)的rxjava、kotlin都加进来了。有一天做动态权限处理,惊奇地发现RxPermissions居然摆脱了Activity的onRequestPermissionsResult方法!!!大家都知道,动态权限大体就是先调用requestPermissions方法,然后授权的结果要到onRequestPermissionsResult中处理,简直和startActivityForResult如出一辙。那RxPermissions是怎么做到的呢!!!

接着在前几天不太忙的时候看了下RxPermissions的源码,发现它内部持有一个Fragment,这个fragment没有视图,只负责请求权限和返回结果,相当于一个桥梁的作用,我们通过rxPermissions发起request的时候,其实并不是activity去request,而是通过这个fragment去请求,然后在fragment的onRequestPermissionsResult中把结果发送出来,如此来避开activity的onRequestPermissionsResult方法。

当时,没见过什么大场面的我差点就给跪了。

RxPermissions的源码就不贴了,网上的讲解应该也很多。

同样,Fragment也有startActivityForResult方法啊,那我们也可以采取类似的方法,为所欲为。

这次取名叫AvoidOnResult,主要就是AvoidOnResult和AvoidOnResultFragment两个类。先上代码:

public class AvoidOnResult {
    private static final String TAG = "AvoidOnResult";
    private AvoidOnResultFragment mAvoidOnResultFragment;

    public AvoidOnResult(Activity activity) {
        mAvoidOnResultFragment = getAvoidOnResultFragment(activity);
    }

    public AvoidOnResult(Fragment fragment){
        this(fragment.getActivity());
    }

    private AvoidOnResultFragment getAvoidOnResultFragment(Activity activity) {
        AvoidOnResultFragment avoidOnResultFragment = findAvoidOnResultFragment(activity);
        if (avoidOnResultFragment == null) {
            avoidOnResultFragment = new AvoidOnResultFragment();
            FragmentManager fragmentManager = activity.getFragmentManager();
            fragmentManager
                    .beginTransaction()
                    .add(avoidOnResultFragment, TAG)
                    .commitAllowingStateLoss();
            fragmentManager.executePendingTransactions();
        }
        return avoidOnResultFragment;
    }

    private AvoidOnResultFragment findAvoidOnResultFragment(Activity activity) {
        return (AvoidOnResultFragment) activity.getFragmentManager().findFragmentByTag(TAG);
    }

    public Observable<ActivityResultInfo> startForResult(Intent intent, int requestCode) {
        return mAvoidOnResultFragment.startForResult(intent, requestCode);
    }

    public Observable<ActivityResultInfo> startForResult(Class<?> clazz, int requestCode) {
        Intent intent = new Intent(mAvoidOnResultFragment.getActivity(), clazz);
        return startForResult(intent, requestCode);
    }

    public void startForResult(Intent intent, int requestCode, Callback callback) {
        mAvoidOnResultFragment.startForResult(intent, requestCode, callback);
    }

    public void startForResult(Class<?> clazz, int requestCode, Callback callback) {
        Intent intent = new Intent(mAvoidOnResultFragment.getActivity(), clazz);
        startForResult(intent, requestCode, callback);
    }

    public interface Callback {
        void onActivityResult(int requestCode, int resultCode, Intent data);
    }
} 

public class AvoidOnResultFragment extends Fragment {
private Map<Integer, PublishSubject> mSubjects = new HashMap<>();
private Map<Integer, AvoidOnResult.Callback> mCallbacks = new HashMap<>();

public AvoidOnResultFragment() {
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
}

public Observable<ActivityResultInfo> startForResult(final Intent intent, final int requestCode) {
    PublishSubject<ActivityResultInfo> subject = PublishSubject.create();
    mSubjects.put(requestCode, subject);
    return subject.doOnSubscribe(new Consumer<Disposable>() {
        @Override
        public void accept(Disposable disposable) throws Exception {
            startActivityForResult(intent, requestCode);
        }
    });
}

public void startForResult(Intent intent, int requestCode, AvoidOnResult.Callback callback) {
    mCallbacks.put(requestCode, callback);
    startActivityForResult(intent, requestCode);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    //rxjava方式的处理
    PublishSubject<ActivityResultInfo> subject = mSubjects.remove(requestCode);
    if (subject != null) {
        subject.onNext(new ActivityResultInfo(requestCode, resultCode, data));
        subject.onComplete();
    }

    //callback方式的处理
    AvoidOnResult.Callback callback = mCallbacks.remove(requestCode);
    if (callback != null) {
        callback.onActivityResult(requestCode, resultCode, data);
    }
}

}


### AvoidOnResult

先看AvoidOnResult,和RxPermissions一样,也持有一个无视图的fragment,在构造器中先去获取这个AvoidOnResultFragment,getAvoidOnResultFragment、findAvoidOnResultFragment这两个方法是从RxPermissions扒来的,大体就是先通过TAG获取fragment,如果是null就新建一个并add进去。

这个类内部也定义了一个Callback接口,不必多说。

继续,这个类有多个startForResult方法,主要看public void startForResult(Intent intent, int requestCode, Callback callback),它本身不干实事,只是调用fragment的同名方法,所有的逻辑都是fragment中处理,待会儿我们来看这个“同名方法”。

### AvoidOnResultFragment

再看这个fragment,它持有一个mCallbacks,存着requestCode和callback的对应关系。然后找到上边说的同名方法startForResult,只有两行代码,1、把callback存起来,2、调用fragment的startActivityForResult

继续看fragment的onActivityResult方法,主要看注释有“callback方式的处理”的代码,就是从mCallbacks中拿到requestCode对应的callback,调用callback的onActivityResult方法。总体的就是这样了,是不是很简单。

### 拓展

可以看到除了返回值为void的startForResult方法外,还有几个返回值为Observable的,原理一样,只不过fragment中不再是存callback,而是存subject,然后通过doOnSubscribe使它在subscribe的时候跳转页面,最后把得到的Observable返回。对应的,在onActivityResult中拿到对应的subject,通过onNext把数据发出去。

使用示例:

//callback方式
callback.setOnClickListener {
AvoidOnResult(this).startForResult(FetchDataActivity::class.java, REQUEST_CODE_CALLBACK, object : AvoidOnResult.Callback {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) =
if (resultCode == Activity.RESULT_OK) {
val text = data?.getStringExtra(“text”)
Toast.makeText(this@MainActivity, "callback -> " + text, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, “callback canceled”, Toast.LENGTH_SHORT).show()
}

})

}

//rxjava方式
rxjava.setOnClickListener {
AvoidOnResult(this)
.startForResult(FetchDataActivity::class.java, REQUEST_CODE_RXJAVA)
//下面可自由变换
.filter { it.resultCode == Activity.RESULT_OK }
.flatMap {
val text = it.data.getStringExtra(“text”)
Observable.fromIterable(text.asIterable())
}
.subscribe({
Log.d("-------> ", it.toString())
}, {
Toast.makeText(this, “error”, Toast.LENGTH_SHORT).show()
}, {
Toast.makeText(this, “complete”, Toast.LENGTH_SHORT).show()
})
}


[AvoidOnResult项目地址](https://github.com/AnotherJack/AvoidOnResult)

所有的工具类都是用java写的,避免使用kotlin编写,出现java无法调用kotlin的情况。测试代码用的kotlin,不过没用太多kotlin的特性,所以即便没接触过kotlin的应该也很好看懂吧!

最后祝大家新年快乐!要是能赏几个star就更好啦!

![](https://www.icode9.com/i/ll/?i=img_convert/0bcb171d8a62277705da04904e5ac384.png)

  

最后

按照国际惯例,给大家分享一套十分好用的Android进阶资料:《全网最全Android开发笔记》。

整个笔记一共8大模块、729个知识点,3382页,66万字,可以说覆盖了当下Android开发最前沿的技术点,和阿里、腾讯、字节等等大厂面试看重的技术。

图片

图片

因为所包含的内容足够多,所以,这份笔记不仅仅可以用来当学习资料,还可以当工具书用。

如果你需要了解某个知识点,不管是Shift+F 搜索,还是按目录进行检索,都能用最快的速度找到你要的内容。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照整个知识体系编排的。

详细文档可以点我下载,记得点赞哦~

(一)架构师必备Java基础

1、深入理解Java泛型

2、注解深入浅出

3、并发编程

4、数据传输与序列化

5、Java虚拟机原理

6、高效IO

……

图片

(二)设计思想解读开源框架

1、热修复设计

2、插件化框架设计

3、组件化框架设计

4、图片加载框架

5、网络访问框架设计

6、RXJava响应式编程框架设计

……

图片

(三)360°全方位性能优化

1、设计思想与代码质量优化

2、程序性能优化

  • 启动速度与执行效率优化
  • 布局检测与优化
  • 内存优化
  • 耗电优化
  • 网络传输与数据储存优化
  • APK大小优化

3、开发效率优化

  • 分布式版本控制系统Git
  • 自动化构建系统Gradle

……

图片

(四)Android框架体系架构

1、高级UI晋升

2、Android内核组件

3、大型项目必备IPC

4、数据持久与序列化

5、Framework内核解析

……

图片

(五)NDK模块开发

1、NDK开发之C/C++入门

2、JNI模块开发

3、Linux编程

4、底层图片处理

5、音视频开发

6、机器学习

……

图片

(六)Flutter学习进阶

1、Flutter跨平台开发概述

2、Windows中Flutter开发环境搭建

3、编写你的第一个Flutter APP

4、Flutter Dart语言系统入门

……

图片

(七)微信小程序开发

1、小程序概述及入门

2、小程序UI开发

3、API操作

4、购物商场项目实战

……

图片

(八)kotlin从入门到精通

1、准备开始

2、基础

3、类和对象

4、函数和lambda表达式

5、其他

……

图片

好啦,这份资料就给大家介绍到这了,*有需要详细文档的小伙伴可以点我下载~~~~*

标签:可读性,callback,hook,requestCode,activity,onActivityResult,public,Callback
来源: https://blog.csdn.net/Aerfa789/article/details/118370240

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有