ICode9

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

远程升级(静默升级)

2021-09-13 14:34:32  阅读:139  来源: 互联网

标签:String void private public 升级 静默 new null 远程


需求:公司有个广告投放的屏幕,需要在屏幕上进行广告播放,当app升级新东西的时候,对广告屏幕进行远程升级,这个是有root权限的,当时网上找了好多资料,然后结合自己整出来的这个,前面是工具类,最后面有使用方法和清单文件配置

public class AutoInstaller extends Handler {
    private static final String TAG = AutoInstaller.class.getSimpleName();
    private static final int REQUEST_CODE_PERMISSION_STORAGE = 100;
    private static volatile AutoInstaller mAutoInstaller;
    private Context mContext;
    private String mTempPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Download";

    public enum MODE {
        ROOT_ONLY,
        AUTO_ONLY,
        BOTH
    }

    private MODE mMode = MODE.BOTH;

    private AutoInstaller(Context context) {
        mContext = context;
    }

    public static AutoInstaller getDefault(Context context) {
        if (mAutoInstaller == null) {
            synchronized (AutoInstaller.class) {
                if (mAutoInstaller == null) {
                    mAutoInstaller = new AutoInstaller(context);
                }
            }
        }
        return mAutoInstaller;
    }

    public interface OnStateChangedListener {
        void onStart();

        void onComplete();

        void onNeed2OpenService();

        void needPermission();
    }

    private OnStateChangedListener mOnStateChangedListener;

    public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) {
        mOnStateChangedListener = onStateChangedListener;
    }

    public void execLinuxCommand(){
        String cmd = "sleep 5; am start -n \"com.example.advertputproject/com.example.advertputproject.MainActivity\" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER";
        Runtime runtime = Runtime.getRuntime();
        OutputStream outputStream=null;
        try {
            Process su = runtime.exec("su");
            outputStream = su.getOutputStream();

            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeBytes(cmd);
            dataOutputStream.flush();
            Log.d("凉城aa", "设备准备重启");
        } catch (IOException pE) {
            pE.printStackTrace();
            Log.d("凉城aa", "failed"+pE.getMessage());
        }finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException pE) {
                    pE.printStackTrace();
                }
            }
        }
    }


//    public void autoRestart(Long delay){
//        Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(mContext.getPackageName());
//        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT);
//        AlarmManager systemService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
//        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
//            systemService.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+delay, pendingIntent);
//        }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//            systemService.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+delay, pendingIntent);
//        }
//    }

    private boolean installUseRoot(String filePath) {
        if (TextUtils.isEmpty(filePath))
            throw new IllegalArgumentException("Please check apk file path!");
        boolean result = false;
        Process process = null;
        OutputStream outputStream = null;
        BufferedReader errorStream = null;
        try {
            process = Runtime.getRuntime().exec("su");
            outputStream = process.getOutputStream();

            String command = "pm install -r " + filePath + "\n";     // pm install -r  覆盖安装已存在Apk,并保持原有数据;
            Log.d("凉城", command.toString());
            outputStream.write(command.getBytes());
            outputStream.flush();
            outputStream.write("exit\n".getBytes());
            outputStream.flush();
            process.waitFor();
            errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            StringBuilder msg = new StringBuilder();
            String line;
            while ((line = errorStream.readLine()) != null) {
                msg.append(line);
            }
            Log.d(TAG, "install msg is " + msg);
            if (!msg.toString().contains("Failure")) {
                result = true;
            }
        } catch (Exception e) {
            Log.e(TAG, e.getMessage(), e);
            result = false;
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
                if (errorStream != null) {
                    errorStream.close();
                }
            } catch (IOException e) {
                outputStream = null;
                errorStream = null;
                process.destroy();
            }
        }
        return result;
    }

    private void installUseAS(String filePath) {
        // 存储空间
        if (permissionDenied()) {
            sendEmptyMessage(4);
            return;
        }

        // 允许安装应用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            boolean b = mContext.getPackageManager().canRequestPackageInstalls();
            if (!b) {
                sendEmptyMessage(4);
                return;
            }
        }

        File file = new File(filePath);
        if (!file.exists()) {
            Log.e(TAG, "apk file not exists, path: " + filePath);
            return;
        }
        Uri uri = Uri.fromFile(file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(mContext, "com.example.advertputproject.fileprovider", file);
            mContext.grantUriPermission(mContext.getPackageName(), contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(uri, "application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        mContext.startActivity(intent);
        if (!isAccessibilitySettingsOn(mContext)) {
            toAccessibilityService();
            sendEmptyMessage(3);
        }
    }

    private boolean permissionDenied() {
        if (Build.VERSION.SDK_INT >= 23) {
            String[] permissions = {
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
            };

            for (String str : permissions) {
                if (mContext.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
                    return true;
                }
            }
        }

        return false;
    }

    private void toAccessibilityService() {
        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
        mContext.startActivity(intent);
    }


    private boolean isAccessibilitySettingsOn(Context mContext) {
        int accessibilityEnabled = 0;
        final String service = mContext.getPackageName() + "/" + InstallAccessibilityService.class.getCanonicalName();
        try {
            accessibilityEnabled = Settings.Secure.getInt(
                    mContext.getApplicationContext().getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_ENABLED);
            Log.v(TAG, "accessibilityEnabled = " + accessibilityEnabled);
        } catch (Settings.SettingNotFoundException e) {
            Log.e(TAG, "Error finding setting, default accessibility to not found: "
                    + e.getMessage());
        }
        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');

        if (accessibilityEnabled == 1) {
            Log.v(TAG, "***ACCESSIBILITY IS ENABLED*** -----------------");
            String settingValue = Settings.Secure.getString(
                    mContext.getApplicationContext().getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                mStringColonSplitter.setString(settingValue);
                while (mStringColonSplitter.hasNext()) {
                    String accessibilityService = mStringColonSplitter.next();

                    Log.v(TAG, "-------------- > accessibilityService :: " + accessibilityService + " " + service);
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        Log.v(TAG, "We've found the correct setting - accessibility is switched on!");
                        return true;
                    }
                }
            }
        } else {
            Log.v(TAG, "***ACCESSIBILITY IS DISABLED***");
        }

        return false;
    }

    public void install(final String filePath) {
        if (TextUtils.isEmpty(filePath) || !filePath.endsWith(".apk"))
            throw new IllegalArgumentException("not a correct apk file path");
        new Thread(new Runnable() {
            @Override
            public void run() {
                sendEmptyMessage(1);
                switch (mMode) {
                    case BOTH:
                        if (!Utils.checkRooted() || !installUseRoot(filePath)) {
                            installUseAS(filePath);
                        }
                        break;
                    case ROOT_ONLY:
                        installUseRoot(filePath);
                        break;
                    case AUTO_ONLY:
                        installUseAS(filePath);
                }
                sendEmptyMessage(0);
            }
        }).start();
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 0:
                if (mOnStateChangedListener != null)
                    mOnStateChangedListener.onComplete();
                break;
            case 1:
                if (mOnStateChangedListener != null)
                    mOnStateChangedListener.onStart();
                break;

            case 3:
                if (mOnStateChangedListener != null)
                    mOnStateChangedListener.onNeed2OpenService();
                break;
            case 4:
                if (mOnStateChangedListener != null) {
                    mOnStateChangedListener.needPermission();
                }
                break;
        }
    }

    public void install(File file) {
        if (file == null)
            throw new IllegalArgumentException("file is null");
        install(file.getAbsolutePath());
    }

    public void installFromUrl(final String httpUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                sendEmptyMessage(1);
                File file = downLoadFile(httpUrl);
             //   execLinuxCommand();
                install(file);
            }
        }).start();
    }

    private File downLoadFile(String httpUrl) {
        if (TextUtils.isEmpty(httpUrl)) throw new IllegalArgumentException();
        String path = mContext.getFilesDir().getAbsolutePath();
        File file = new File(path);

        Log.d(TAG, "app路径:" + path);

        if (!file.exists()) file.mkdirs();
        file = new File(path + File.separator + "update.apk");
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        HttpURLConnection connection = null;
        try {
            URL url = new URL(httpUrl);
            connection = (HttpURLConnection) url.openConnection();
            if (connection instanceof HttpsURLConnection) {
                SSLContext sslContext = getSLLContext();
                if (sslContext != null) {
                    SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
                    ((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory);
                }
            }
            connection.setConnectTimeout(60 * 1000);
            connection.setReadTimeout(60 * 1000);
            connection.connect();
            inputStream = connection.getInputStream();
            outputStream = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null)
                    inputStream.close();
                if (outputStream != null)
                    outputStream.close();
                if (connection != null)
                    connection.disconnect();
            } catch (IOException e) {
                inputStream = null;
                outputStream = null;
            }
        }
        return file;
    }

    private SSLContext getSLLContext() {
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, new SecureRandom());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sslContext;
    }

//    public static class Builder {
//
//        private MODE mode = MODE.BOTH;
//        private Context context;
//        private OnStateChangedListener onStateChangedListener;
//        private String directory = Environment.getExternalStorageDirectory().getAbsolutePath();
//
//        public Builder(Context c) {
//            context = c;
//        }
//
//        public Builder setMode(MODE m) {
//            mode = m;
//            return this;
//        }
//
//        public Builder setOnStateChangedListener(OnStateChangedListener o) {
//            onStateChangedListener = o;
//            return this;
//        }
//
//        public Builder setCacheDirectory(String path) {
//            directory = path;
//            return this;
//        }
//
//        public AutoInstaller build() {
//            AutoInstaller autoInstaller = new AutoInstaller(context);
//            autoInstaller.mMode = mode;
//            autoInstaller.mOnStateChangedListener = onStateChangedListener;
//            autoInstaller.mTempPath = directory;
//            return autoInstaller;
//        }
//
//    }
}
public class InstallAccessibilityService extends AccessibilityService {

    private static final String TAG = InstallAccessibilityService.class.getSimpleName();

    private Map<Integer, Boolean> handledMap = new HashMap<>();

    private Handler mHandler = new Handler(Looper.getMainLooper());

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        Log.d(TAG, "onAccessibilityEvent: " + event.toString());

        if (!String.valueOf(event.getPackageName()).contains("packageinstaller")) {
            //不写完整包名,是因为某些手机(如小米)安装器包名是自定义的
            return;
        }

        AccessibilityNodeInfo nodeInfo = event.getSource();
        if (nodeInfo == null) {
            Log.i(TAG, "eventNode: null, 重新获取eventNode...");
            performGlobalAction(GLOBAL_ACTION_RECENTS); // 打开最近页面
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    performGlobalAction(GLOBAL_ACTION_BACK); // 返回安装页面
                }
            }, 320);
            return;
        }

        int eventType = event.getEventType();
        if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ||
                eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            if (handledMap.get(event.getWindowId()) == null) {
                boolean handled = iterateNodesAndHandle(nodeInfo);
                if (handled) {
                    handledMap.put(event.getWindowId(), true);
                }
            }
        }
    }

    private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            int childCount = nodeInfo.getChildCount();
            if ("android.widget.Button".equals(nodeInfo.getClassName())) {
                String nodeContent = nodeInfo.getText().toString();
                Log.d("TAG", "content is " + nodeContent);
                if (!TextUtils.isEmpty(nodeContent)
                        && ("安装".equals(nodeContent)
                        || "install".equals(nodeContent.toLowerCase())
                        || "done".equals(nodeContent.toLowerCase())
                        || "完成".equals(nodeContent)
                        || "确定".equals(nodeContent)
                )) {
                    nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return true;
                }
            } else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
            for (int i = 0; i < childCount; i++) {
                AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
                if (iterateNodesAndHandle(childNodeInfo)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onInterrupt() {

    }
}
public class Utils {

    public static final String TAG = "Utils";

    // 此方法工作有误
    @Deprecated
    public static boolean isRooted() {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec("su");
            OutputStream outputStream = process.getOutputStream();
            InputStream inputStream = process.getInputStream();
            outputStream.write("id\n".getBytes());
            outputStream.flush();
            outputStream.write("exit\n".getBytes());
            outputStream.flush();
            process.waitFor();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String s = bufferedReader.readLine();
            if (s.contains("uid=0")) return true;
        } catch (IOException e) {
            Log.e(TAG, "没有root权限");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (process != null)
                process.destroy();
        }
        return false;
    }

    public static boolean checkRooted() {
        boolean result = false;
        try {
            result = new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}

//应用

public class MainActivity extends AppCompatActivity {

    private Banner banner;
    public static final String APK_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Download" + File.separator + "app-release.apk";
    public static final String CACHE_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Download";
    public static final String APK_URL = "https://。。。。。/apk/app-release.apk";  //apk下载地址

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //沉浸式状态栏
        ImmersionModeUtil.setStatusBar(this, false);

        Log.d("凉城", "onCreate");

        if (Build.VERSION.SDK_INT >= 23) {
            int REQUEST_CODE_CONTACT = 101;
            String[] permissions = {
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE};
            //验证是否许可权限
            for (String str : permissions) {
                if (checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
                    //申请权限
                    requestPermissions(permissions, REQUEST_CODE_CONTACT);
                    initUpdate();
                    initView();
                    initData();
                    return;
                } else {
                    //这里就是权限打开之后自己要操作的逻辑
                    initUpdate();
                    initView();
                    initData();
                    return;
                }
            }
        }


    }

    private void initUpdate() {
        String versionName = getAppVersionName(this);
        Log.d("凉城versionName", versionName);

        String url = "/data/user/0/com.example.advertputproject/files/update.apk";  ///下载后apk存在的位置,

		//获取下载后apk的版本号
        String apkVersionName = "";
        PackageManager manager = getPackageManager();
        PackageInfo info = manager.getPackageArchiveInfo(url, PackageManager.GET_ACTIVITIES);
        if(info != null){
            ApplicationInfo applicationInfo = info.applicationInfo;
            applicationInfo.sourceDir = url;
            applicationInfo.publicSourceDir = url;
            String appName = manager.getApplicationLabel(applicationInfo).toString();// 得到应用名
            String packageName = applicationInfo.packageName;// 得到包名
            apkVersionName = info.versionName; // 得到版本信息

            Log.d("凉城", "apkInfo----appName "+appName);
            Log.d("凉城", "apkInfo----packageName "+packageName);
            Log.d("凉城", "apkInfo----apkVersionName "+apkVersionName);
        }

		//如果下载的版本号和app的版本号一样就不升级
        if(apkVersionName.equals(versionName)){
            return;
        }else {
            AutoInstaller installer = AutoInstaller.getDefault(MainActivity.this);
            installer.installFromUrl(APK_URL);
            installer.setOnStateChangedListener(new AutoInstaller.OnStateChangedListener() {
                @Override
                public void onStart() {
                }

                @Override
                public void onComplete() {
                }

                @Override
                public void onNeed2OpenService() {
                    Toast.makeText(MainActivity.this, "请打开辅助功能服务", Toast.LENGTH_SHORT).show();
                }

                @Override
                public void needPermission() {
                    Toast.makeText(MainActivity.this, "需要申请存储空间权限", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }

    private void initData() {
        Retrofit build = new Retrofit.Builder()
                .baseUrl(ApiService.baseUrl)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        ApiService apiService = build.create(ApiService.class);
        Observable<BannerBean> imageUrl = apiService.getImageUrl();
        imageUrl.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<BannerBean>() {
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    @Override
                    public void onNext(BannerBean pBannerBean) {
                        List<BannerBean.DataBean> data = pBannerBean.getData();
                        banner.setImages(data).setImageLoader(new ImageLoader() {
                            @Override
                            public void displayImage(Context context, Object path, ImageView imageView) {
                                BannerBean.DataBean bean = (BannerBean.DataBean) path;
                                Glide.with(MainActivity.this).load(bean.getImagePath()).into(imageView);
                            }
                        }).start();
                    }

                    @Override
                    public void one rror(Throwable e) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }

    private void initView() {
        banner = (Banner) findViewById(R.id.banner);
    }

升级完后发广播进行通知,重新启动app

public class AppInstallReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

        if (intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)) {
            Toast.makeText(context, "安装成功", Toast.LENGTH_LONG).show();
            AutoInstaller.getDefault(context).execLinuxCommand();
        }
        if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) {
            Toast.makeText(context, "卸载成功", Toast.LENGTH_LONG).show();
        }
        if (intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED)) {
            Toast.makeText(context, "替换成功", Toast.LENGTH_LONG).show();
            AutoInstaller.getDefault(context).execLinuxCommand();//升级完后重新启动app
        }

        Log.d("凉城升级", "onReceive");
    }
}

清单文件配置

<receiver
            android:name=".AppInstallReceiver"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_ADDED" />
                <action android:name="android.intent.action.PACKAGE_REPLACED" />
                <action android:name="android.intent.action.PACKAGE_REMOVED" />

                <data android:scheme="package" />
            </intent-filter>
        </receiver>
        <service
            android:name=".util.InstallAccessibilityService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>
        <!-- 文件访问 -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.advertputproject.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

//file_paths

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="download"
        path="Download" />

    <external-path
        name="ext_root"
        path="/" />

    <external-path
        name="external_storage_root"
        path="." />
    <external-path
        name="files_root"
        path="Android/data/com.example.advertputproject/" />
</paths>

//accessibility_service_config

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_service_description" />

标签:String,void,private,public,升级,静默,new,null,远程
来源: https://blog.csdn.net/qq_46237697/article/details/120265982

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

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

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

ICode9版权所有