哈喽小伙伴们大家好~欢迎继续学习探讨MIUI系统的安全防范知识!在上篇博客中:Android逆向工程:带你领略MIUI系统的账号安全防范机制:账号是从哪里获取的?我们了解到了MIUI系统通过对关键代码进行封装进系统内,对外采用统一调用接口的方式来防止关键代码被破解窥视,保护了系统应用的安全,同时我们发现了获取账号信息的准确接口,那么MIUI系统除此之外,还有什么值得称道的安全防护措施呢?接着上篇博客我们尚未解决的问题:为什么刷机都无法刷掉之前已经登陆的小米账号?我们发现的那个PassportFindDeviceImpl类它真正的作用是什么?下面就带着这些疑问,来开启我们今天的学习吧!

首先还是有请我们今天的教案对象:我的小米。在下面的学习中,我们主要围绕“我的小米”进行分析和探讨。在此声明,本次讲解内容不可用于不正当破坏行为,学习技术为主,搞破坏是不可以的!

在上篇博客中,我们发现了获取小米账号的系统方法: ExtraAccountManager.getXiaomiAccount(this);,同时发现如果此方法返回为null的话,那么就代表着不存在小米账号,既然如此,那么我们就来尝试一下,如果我们拦截到之后把它的返回值修改为null会出现什么样的情况呢?

下面就开始修改我们的拦截代码:

XposedHelpers.findAndHookMethod("miui.accounts.ExtraAccountManager", loadPackageParam.classLoader, "getXiaomiAccount", Context.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("小米账号获取:抓到方法ExtraAccountManager->getXiaomiAccount()");
 
                Class classAccount=XposedHelpers.findClass("android.accounts.Account",loadPackageParam.classLoader);
 
                Field []fs=classAccount.getDeclaredFields();
                for (Field field:fs){
                    field.setAccessible(true);
                    XposedBridge.log("小米账号获取:Account类 参数"+field.getName()+"值为:"+field.get(param.getResult()));
                }
 
                param.setResult(null);
            }
        });
在拦截方法内我们设置返回结果为null,下面就看一下出现了什么状况:

很直接,手机直接被锁了!为什么会这样?看到这个界面,已经关联的小米账号,包括解锁编号,喜欢刷机的小伙伴们估计会很熟悉,因为对之前已经登陆小米账号的手机进行刷机的时候,刷机完成就会出现这个界面,提示你此手机有关联的账号,让你输入账号密码进行解锁! 这就是那个刷机也刷不掉的小米账号!

很不错,看来MIUI系统确实是有两把刷子。按照常理,刷机就是更换了一整个系统,其中也包括了那些存放关键信息的系统文件和系统内部数据库,在文件和数据库已经被更换,数据也被清空的情况下,这个关联的账号信息又是从哪里获取到的?MIUI系统又是怎么知道这个设备(手机)之前有登录的小米账号呢?带着这些疑问,我们接着往下逆向分析!

首先这里我们对该锁定界面进行界面元素分析:目标是上面那条String资源:此手机已经关联到小米账号(xxxx):

很不错,是一条TextView,id值为:find_device_status。看到这个id值我们心里差不多明白了七八分,为了验证我们的猜测,那就去看看这条id被引用的代码:使用jadx对此id值进行全局字符串搜索:

找到了,在类:LockedAccountLoginByFindDeviceFragment中。这个类的名字挺长,但是可以大致的看出它的功能: 通过查找设备然后锁定账号进行登陆的页面!很不错,名字起的倒是很直白啊~找了这个页面,下面我们就需要知道,这个页面是在什么地方被调用的?接下来该怎么查找?直接全局搜索LockedAccountLoginByFindDeviceFragment,看看它在别的类中是否有引用,有引用的地方估计就是他被调用的地方!使用Jadax全局搜索 LockedAccountLoginByFindDeviceFragment:

搜索发现,在整个“我的小米”项目中, LockedAccountLoginByFindDeviceFragment类只在LoginBaseFragment类的checkFindDeviceStatusIfNecessary()方法下被引用了,那估计被调用就是这里没跑了,我们过去看一下checkFindDeviceStatusIfNecessary()方法的代码:

protected void checkFindDeviceStatusIfNecessary() {
        if (PassportExternal.getPassportFindDeviceInterface() != null) {
            this.mCheckFindDeviceStatusTask = new CheckFindDeviceStatusTask.Builder(getActivity()).setCheckOperationFailedRunnable(new CheckOperationFailedRunnable() {
                public void run(String errorMessage) {
                    LoginBaseFragment.this.showCheckFindDeviceStatusFailedDialog(errorMessage);
                }
            }).setCheckOperationSuccessRunnable(new CheckOperationSuccessRunnable() {
                public void run(boolean isOpenFindDevice, String lockedUserId, String displayId) {
                    PassportStatHelper.statLoginCountEvent(StatConstants.CHECK_FIND_DEVICE_STATUS_SUCCESS, LoginBaseFragment.this.mOnSetupGuide);
                    if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
                }
            }).build();
            this.mCheckFindDeviceStatusTask.executeOnExecutor(XiaomiPassportExecutor.getSingleton(), new Void[0]);
        }
    }
checkFindDeviceStatusIfNecessary()方法从名字上就可以大致猜出它的功能:如果必要的话就检查查找设备状态!我们关键看下面那个if判断中的代码:

if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
这里判断一个isOpenFindDevice的布尔变量值,如果为true的话,那么就会创建一个LockedAccountLoginByFindDeviceFragment,接着就会开启那个锁屏页面!关键值在这个isOpenFindDevice变量,那这个isOpenFindDevice变量又是从哪来的呢?看这些代码:

.setCheckOperationSuccessRunnable(new CheckOperationSuccessRunnable() {
                public void run(boolean isOpenFindDevice, String lockedUserId, String displayId) {
                    PassportStatHelper.statLoginCountEvent(StatConstants.CHECK_FIND_DEVICE_STATUS_SUCCESS, LoginBaseFragment.this.mOnSetupGuide);
                    if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
                }
            }).build();
我们发现这是开启了一个线程,线程名字为:CheckOperationSuccessRunnable,在这个线程里run方法内,isOpenFindDevice变量被传入!看下该线程的名字大致可以猜到:检查操作成功后进行的操作,原来这个是检查执行结束之后调用的,这个isOpenFindDevice变量实质上是一个检查结果的标志,true代表发现了关联的账号信息,然后开启锁屏页面,false代表没有发现关联账号信息,就会正常登录!

既然不是检查实现的线程,那么我们接着把目光转移到上面:

this.mCheckFindDeviceStatusTask = new CheckFindDeviceStatusTask.Builder(getActivity()).setCheckOperationFailedRunnable(new CheckOperationFailedRunnable() {
                public void run(String errorMessage) {
                    LoginBaseFragment.this.showCheckFindDeviceStatusFailedDialog(errorMessage);
                }
            }).setCheckOperationSuccessRunnable(new CheckOperationSuccessRunnable() {
                public void run(boolean isOpenFindDevice, String lockedUserId, String displayId) {
                    PassportStatHelper.statLoginCountEvent(StatConstants.CHECK_FIND_DEVICE_STATUS_SUCCESS, LoginBaseFragment.this.mOnSetupGuide);
                    if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
                }
            }).build();
看第一行代码,我们发现一个名字为:CheckFindDeviceStatusTask的线程被调起,后面setCheckOperationFailedRunnable方法看名字就会知道,这是线程执行失败后执行的方法,会开启一个叫做CheckOperationFailedRunnable线程,正好与刚才我们分析那个setCheckOperationSuccessRunnable方法和CheckOperationSuccessRunnable线程是对应的!这里就差不多明白了,执行检查操作的正是CheckFindDeviceStatusTask线程,它分别设置了检查失败和检查成功两个回调方法,那下面我们就去看看这个CheckFindDeviceStatusTask实现代码:

public class CheckFindDeviceStatusTask extends AsyncTask<Void, Void, PassportCheckFindDeviceResult> {
    private static final String PROGRESS_DIALOG_TAG = "CheckFindDeviceStatusTaskProgressDialog";
    private final Activity mActivity;
    private final CheckOperationFailedRunnable mCheckOperationFailedRunnable;
    private final CheckOperationSuccessRunnable mCheckOperationSuccessRunnable;
    private SimpleDialogFragment mProgressDialogFragment;
 
    public static class Builder {
        private Activity mActivity;
        private CheckOperationFailedRunnable mCheckOperationFailedRunnable;
        private CheckOperationSuccessRunnable mCheckOperationSuccessRunnable;
 
        public Builder(Activity activity) {
            this.mActivity = activity;
        }
 
        public Builder setCheckOperationFailedRunnable(CheckOperationFailedRunnable runnable) {
            this.mCheckOperationFailedRunnable = runnable;
            return this;
        }
 
        public Builder setCheckOperationSuccessRunnable(CheckOperationSuccessRunnable runnable) {
            this.mCheckOperationSuccessRunnable = runnable;
            return this;
        }
 
        public CheckFindDeviceStatusTask build() {
            return new CheckFindDeviceStatusTask(this.mActivity, this.mCheckOperationFailedRunnable, this.mCheckOperationSuccessRunnable);
        }
    }
 
    public interface CheckOperationFailedRunnable {
        void run(String str);
    }
 
    public interface CheckOperationSuccessRunnable {
        void run(boolean z, String str, String str2);
    }
 
    private CheckFindDeviceStatusTask(Activity activity, CheckOperationFailedRunnable checkOperationFailedRunnable, CheckOperationSuccessRunnable checkOperationSuccessRunnable) {
        this.mActivity = activity;
        this.mCheckOperationFailedRunnable = checkOperationFailedRunnable;
        this.mCheckOperationSuccessRunnable = checkOperationSuccessRunnable;
    }
 
    protected void onPreExecute() {
        this.mProgressDialogFragment = (SimpleDialogFragment) this.mActivity.getFragmentManager().findFragmentByTag(PROGRESS_DIALOG_TAG);
        if (this.mProgressDialogFragment == null) {
            this.mProgressDialogFragment = new AlertDialogFragmentBuilder(2).setMessage(this.mActivity.getString(R.string.passport_login_check_find_device)).create();
            this.mProgressDialogFragment.setCancelable(false);
            this.mProgressDialogFragment.show(this.mActivity.getFragmentManager(), PROGRESS_DIALOG_TAG);
        }
    }
 
    protected PassportCheckFindDeviceResult doInBackground(Void... params) {
        return PassportExternal.getPassportFindDeviceInterface().checkFindDeviceStatus(this.mActivity.getApplicationContext());
    }
 
    protected void onPostExecute(PassportCheckFindDeviceResult result) {
        if (!(this.mProgressDialogFragment == null || this.mProgressDialogFragment.getActivity() == null || this.mProgressDialogFragment.getActivity().isFinishing())) {
            this.mProgressDialogFragment.dismissAllowingStateLoss();
        }
        if (result != null && this.mActivity != null && !this.mActivity.isFinishing()) {
            if (result.checkOperationResult == CheckOperationResult.FAILED) {
                if (this.mCheckOperationFailedRunnable != null) {
                    this.mCheckOperationFailedRunnable.run(result.errorMessage);
                }
            } else if (result.checkOperationResult != CheckOperationResult.SUCCESS) {
                throw new IllegalStateException("Normally not reachable. ");
            } else if (this.mCheckOperationSuccessRunnable != null) {
                this.mCheckOperationSuccessRunnable.run(result.isOpen, result.sessionUserId, result.displayId);
            }
        }
    }
}
代码量有点多,不过没关系,我们首先看到CheckFindDeviceStatusTask继承的是AsyncTask,那就好办了,执行具体操作逻辑的方法是doInBackground(),我们直接看它的doInBackground()实现方法:

protected PassportCheckFindDeviceResult doInBackground(Void... params) {
        return PassportExternal.getPassportFindDeviceInterface().checkFindDeviceStatus(this.mActivity.getApplicationContext());
    }
只有一句代码,调用了这个checkFindDeviceStatus()方法!看到这里,小伙伴们有没有发现这个checkFindDeviceStatus()方法我们很眼熟啊,不就是那个PassportFindDeviceImpl类中重写实现的PassportFindDeviceInterface接口中的方法吗?!我们点击这个方法去查看它的来源:

果然是它!那这里就好办了,我们直接打开 PassportFindDeviceImpl类再次分析一下这个checkFindDeviceStatus()方法:

我们在昨天分析查找获取账号信息的时候,查到过这个方法,只不过那个时候调用的是它的onLoginSuccess()方法。我们可以看到在方法checkFindDeviceStatus()内,通过代码FindDeviceInfo info = findDeviceStatusManager.getFindDeviceInfoFromServer(); 获取到了一个FindDeviceInfo的实例,这个实例则用来给下面的PassportCheckFindDeviceResult实例赋值,最后返回这个PassportCheckFindDeviceResult实例。getFindDeviceInfoFromServer()方法和FindDeviceInfo类都是无法查看的,他们同样是封装在系统内的方法,不存在该项目内。包路径:

我们还可以看到赋值总共有四个值,分别为:isOpen,isLocked,sessionUserId,displayId。注意这个isOpen值,这个布尔值就是上面说的那个关键判断值:isOpenFindDevice!我们回去看下CheckFindDeviceStatusTask的收尾方法:onPostExecute:

protected void onPostExecute(PassportCheckFindDeviceResult result) {
        if (!(this.mProgressDialogFragment == null || this.mProgressDialogFragment.getActivity() == null || this.mProgressDialogFragment.getActivity().isFinishing())) {
            this.mProgressDialogFragment.dismissAllowingStateLoss();
        }
        if (result != null && this.mActivity != null && !this.mActivity.isFinishing()) {
            if (result.checkOperationResult == CheckOperationResult.FAILED) {
                if (this.mCheckOperationFailedRunnable != null) {
                    this.mCheckOperationFailedRunnable.run(result.errorMessage);
                }
            } else if (result.checkOperationResult != CheckOperationResult.SUCCESS) {
                throw new IllegalStateException("Normally not reachable. ");
            } else if (this.mCheckOperationSuccessRunnable != null) {
                this.mCheckOperationSuccessRunnable.run(result.isOpen, result.sessionUserId, result.displayId);
            }
        }
    }
在这里对返回的PassportCheckFindDeviceResult实例进行了处理,注意这句代码:

this.mCheckOperationSuccessRunnable.run(result.isOpen, result.sessionUserId, result.displayId);
调用了检查成功方法,传入的关键判断值正是isOpen变量!

分析到这里,我们差不多就明白了大致过程:在无法通过正常途径获得到账号信息的情况下,即方法ExtraAccountManager.getXiaomiAccount(this);返回的Account实例为空的时候(表示没有账号登录的时候),系统就会去启动CheckFindDeviceStatusTask这个线程去进行检查,这里的检查是检查是否存在关联账号,如果存在关联账号信息,那么就会展示锁定页面,提示你输入密码进行解锁,如果没有发现关联账号,那么就不会展示锁定页面!

关键还是这个读取到的FindDeviceInfo实例,我们虽然无法窥探它的代码,但是我们照样可以看到他的变量值,编写拦截代码,目标类是FindDeviceStatusManager,目标方法是:getFindDeviceInfoFromServer():

XposedHelpers.findAndHookMethod("miui.cloud.finddevice.FindDeviceStatusManager", loadPackageParam.classLoader, "getFindDeviceInfoFromServer", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("小米账号获取:抓到方法FindDeviceStatusManager->getFindDeviceInfoFromServer()");
                Class classFindDeviceInfo=XposedHelpers.findClass("miui.cloud.finddevice.FindDeviceInfo",loadPackageParam.classLoader);
             
                Field []fs=classFindDeviceInfo.getDeclaredFields();
                for (Field field:fs){
                    field.setAccessible(true);
                    XposedBridge.log("小米账号获取:FindDeviceInfo类:参数"+field.getName()+"值为:"+field.get(param.getResult()));
 
                
                }
 
              
            }
        });
这里我们拦截到方法后,然后通过反射机制访问FindDeviceInfo实例的变量值进行打印,下面就运行一下看看这个FindDeviceInfo实例的值都是什么:

这下终于明白了!displayId值原来为是那个解锁编号,sessionUserId值就是小米ID!原来检查查找设备竟然是这个鬼东西,就算刷机也能发现关联账号信息就是从这里面获取的!

下面我们对拦截代码进行修改,把displayId和sessionUserId值修改为null,isOpen值修改为false!这样看看它还会不会把我的界面给锁定了,修改拦截代码如下:

XposedHelpers.findAndHookMethod("miui.cloud.finddevice.FindDeviceStatusManager", loadPackageParam.classLoader, "getFindDeviceInfoFromServer", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("小米账号获取:抓到方法FindDeviceStatusManager->getFindDeviceInfoFromServer()");
                Class classFindDeviceInfo=XposedHelpers.findClass("miui.cloud.finddevice.FindDeviceInfo",loadPackageParam.classLoader);
                Object objectDeviceInfo=classFindDeviceInfo.newInstance();
                Field []fs=classFindDeviceInfo.getDeclaredFields();
                for (Field field:fs){
                    field.setAccessible(true);
                    XposedBridge.log("小米账号获取:FindDeviceInfo类:参数"+field.getName()+"值为:"+field.get(param.getResult()));
 
                    switch (field.getName()){
                        case "displayId":field.set(objectDeviceInfo,null);break;
                        case "sessionUserId":field.set(objectDeviceInfo,null);break;
                        case "isLocked":field.setBoolean(objectDeviceInfo,false);break;
                        case "isOpen":field.setBoolean(objectDeviceInfo,false);break;
                        default:field.set(objectDeviceInfo,field.get(param.getResult()));
                    }
                }
 
                param.setResult(objectDeviceInfo);
            }
        });
我们通过反射创建一个FindDeviceInfo类对象,设置新对象的变量值,然后修改返回结果为新的对象。运行一下看看效果:

哈哈,成功了,没有把我的页面给锁定了!不过还是有细心的小伙伴会问,为什么输入框内还是出现了之前登录的小米账号呢?不急,我们这就看看,对页面进行分析:查看这个输入框的id为:et_account_name,然后老样子用jadx进行全局搜索,找到它的引用在类LoginBaseFragment中:

类中查找这个输入框设置文本的方法:

在onViewCreated()方法内发现了该输入框设置的文本,为字符串lastLoginUserId,lastLoginUserId又是方法getLastLoginAccountName()的返回值,我们去看一下这个getLastLoginAccountName()方法:

原来是从xml文件中拿到的,那这个xml字段又是在什么时候被放进去的呢?还记得我们上篇博客分析的那个addOrUpdateAccountManager()方法了吗?:

我们当时只重点看了那个onAddOrUpdateAccountManagerSuccess()方法,在这个方法的下面,是saveLastLoginAccountName()方法,我们去看看这个方法做了什么:

答案非常的显而易见,把最新登陆的账号保存在了xml文件中!这样做的目的并不涉及到安全策略,只是为了方便用户,直接输入密码就可以登录,不用输入账号了!

好了,至此我们成功的修改掉了锁定页面,我们再次把目光放到 FindDeviceInfo info = findDeviceStatusManager.getFindDeviceInfoFromServer();上。为什么刷机会刷不掉?这里很显然的是,MIUI系统把账号信息存放到了一个就硬件设备里,对外提供了一个获取方法 getFindDeviceInfoFromServer()来获取保存在硬件内的账号数据。这个硬件是什么?很大的可能是CPU,CPU内经常会被硬件厂商放入一些对用户来说极为重要的关键数据,放在CPU内比放在系统文件内可要安全可靠的多。比如华为,他把用户的指纹信息放在了CPU内,对外只提供一个匹配接口,拒不同意腾讯要求把指纹信息上传至腾讯云端,也因此到现在华为手机上使用微信支付还是不能使用指纹支付!

这里博主点评MIUI系统的安全策略就是:把用户的关键信息放置在了CPU内(这里姑且认为是CPU),对外提供了一个统一负责写入和读取的类:FindDeviceStatusManager。当我们在刷机的时候,把保存在系统文件和数据库中的账号信息删除掉了,通过那个统一获取账号信息接口ExtraAccountManager.getXiaomiAccount(Context);无法获取账号信息,这时候系统就会去调用FindDeviceStatusManager的获取接口,来查看CPU内是否保存的有用户信息,如果存在的话,那就说明该手机处于不安全的状态(比如手机丢失被人为刷机),就会把页面锁定,输入密码才能进行解锁,这样就会极大的保证了手机设备和账号的安全性!

原文链接:https://blog.csdn.net/qq_34149335/article/details/86621470

Android逆向工程:MIUI系统大揭秘:去不掉的小米账号!相关推荐

  1. Android开发 调用系统相机相册图片功能,解决小米手机拍照或者图片横竖相反问题,及小米手机相册图片路径问题

    Android开发 调用系统相机相册图片功能,解决小米手机拍照或者图片横竖相反问题,及小米手机相册图片路径问题 1.调用相机,兼容7.0 AndroidManifest配置 <providera ...

  2. Qihoo口碑营销系统大揭秘系列一

    首先,我不得不承认,我有点标题党.真正的标题应该是"Qihoo口碑营销系统大猜测系列",但如果我的猜测不幸命中,那我就很高兴了. 最近,Qihoo召开了社区大会,揭开了口碑营销的序 ...

  3. 小米八android耗电比例很大,手机技巧 篇一:小米新系统推送,耗电严重待解决!教你5招实现三天一充...

    手机技巧 篇一:小米新系统推送,耗电严重待解决!教你5招实现三天一充 2019-04-29 18:36:26 0点赞 1收藏 0评论 MIUI新系统推送不知道有没有接受的到呢?表示楼主已经接收到更新推 ...

  4. 小米10的Android安全更新,MIUI 12首批更新名单被曝光,小米10系列优先上Android 11...

    喜欢小米无论外界如何风云变幻都是喜欢,不喜欢小米,无论小米如何脱胎换骨还是不喜欢,我是喜欢小米那一拨.我用华为,华为坚决不能更新安卓11.鸿蒙上红米note4x默默看着,有也不刷.体验太差.上次推送更 ...

  5. Android逆向工程:带你领略MIUI系统的账号安全防范机制:账号是从哪里获取的?

    小伙伴们大家好!今天已经是1.23号了,春节正在逐渐临近,不知道你是不是现在已经回到了家里帮爸妈置办年货,还是像博主这样继续奋战在工作一线呢?不管如何,在这里博主首先献上新春的节日问候,祝大家春节快乐 ...

  6. MIUI系统获取短信权限问题

    问题描述 2019年开发安卓软件的过程中(API level 28),在测试时发现在小米手机上并未弹出"获取发送短信权限"窗口,经查是小米手机的MIUI系统的封闭限制了发送短信权限 ...

  7. 小米6更新系统显示无网络连接到服务器,小米6刷上统信 UOS 系统,操作流畅但安装需谨慎...

    原标题:小米6刷上统信 UOS 系统,操作流畅但安装需谨慎 统信UOS是国内多家操作系统企业共同打造的国产操作系统,目前已得到国内主要CPU厂商.重点整机厂商.主流应用厂商的支持.上月,统信软件宣布, ...

  8. h2os android版本,EMUI、MIUI、H2OS,国产系统大对比,谁更胜一筹?

    目前市场上,智能手机系统无非就是ios与安卓两种,而安卓系统因为手机厂家不同,又要分成几大类.今天小编就来和大家讲讲,作为国产的手机系统,EMUI.MIUI.H2OS到底谁强谁弱呢? 系统一:EMUI ...

  9. 小米八android耗电比例很大,小米手机耗电太快?MIUI系统最全的省电方法,解决手机耗电问题...

    原标题:小米手机耗电太快?MIUI系统最全的省电方法,解决手机耗电问题 MIUI10系统作为小米8周年发布会上的一个重磅产品,一发布就有不少小米完成升级.随着近期的MIUI10稳定版的推出,并且支持多 ...

  10. android系统华为彩蛋,【小智】打开安卓“隐藏关卡”—— Android系统彩蛋大揭秘...

    本帖最后由 丶浩南丶 于 2015-5-6 13:46 编辑 1354428994073.jpg (9.06 KB, 下载次数: 1) 2015-5-6 11:56 上传   "自从Andr ...

最新文章

  1. 把梯度下降算法变成酷炫游戏,这有一份深度学习通俗讲义
  2. 「土行孙」机器人登上Science子刊封面,用气流在地下穿梭自如,速度达每秒4.8米...
  3. 简析 Google Gadget 的数据丢失原因
  4. c++编程例子_如何开始厉害的C语言编程?大神都是这样开始的!
  5. 一起用C#做个五子棋的小游戏 增加了程序对战功能
  6. 计算机中的进制和编码
  7. python中pickle简介
  8. C++的深拷贝与浅拷贝
  9. ssl1257-产生数【图论,最短路】
  10. Android学习总结(3)——Handler深入详解
  11. 印尼Widya Robotics携手华为云,让建筑工地安全看得见
  12. 【产品经理】 产品经理进阶之路(十一):怎么看微信的公众号和百度的直达号,哪个更有优势
  13. android listview仿ios 3dTouch效果
  14. java根据物流单号查询物流详细
  15. WPS之Excel表格如何设置下拉选项
  16. python 爬虫 ip池维护思路
  17. clearWState(WState_Polished)编译出错
  18. python接收http请求_python通过get,post方式发送http请求和接收http响应
  19. bootstrap图片叠加_详解Bootstrap四种图片样式
  20. 2020年需要学习的十大按需编程语言

热门文章

  1. 厉害了!「00后缩写黑话翻译器」登上GitHub热榜,中年网民终于能看懂年轻人的awsl...
  2. 机器学习中非平衡数据的处理 —— smote算法
  3. 计算机学院吴琴,06年浙江大学计算机与软件学院拒绝报道生名单
  4. 前端程序员专用的在线工具箱
  5. JS生成UUID唯一标识方法
  6. 事实表 的指标 维度表_事实表和维度表 | Power BI星球
  7. RS485通讯协议的应用
  8. java模拟器.apk_java游戏模拟器安卓版下载-java模拟器apk下载 v2.2.0 安卓版-IT猫扑网...
  9. 当时明月在,曾照彩云归。
  10. ceph最低配置和硬件推荐