从零实现灵活且可高度定制的Android图片选择架构

https://www.jianshu.com/u/df76f81fe3ff

前言

这是我花费了数月闲暇时间从零开始写的一个库,在这期间,我学习到了很多,我想把自己的一些所得所感,以及这期间的一些思路,能够通过一篇文章的形式讲述出来,这就是本文的起源,项目地址:

https://github.com/qingmei2/RxImagePicker

动机

在展开本文之前,我希望能够占用一些篇幅先自我回答三个问题:

  1. 为什么要"重复"造轮子?

  2. 要实现一个怎么样的轮子?

  3. 和网上成熟的三方图片选择库相比,特点以及不足?

接下来我要花费 很大一部分的篇幅,根据上述的问题,阐述缘由,我坚信,相对于一个库而言,设计的初衷和思想更为重要 。

为什么“重复”造轮子

“Stop Trying to Reinvent the Wheel ( 不要重复造轮子 ) ”,可能是每个程序员入行被告知的第一条准则。

我是一个Github的(伪)开源爱好者,在公司的Android项目遇到APP图片选择的功能需求时,我花时间研究了Github上最受欢迎的那些图片选择库,这些库都是由行业各前辈花费很大心血一点点写出来的,也经过很多的项目和时间的检验,一点点迭代过来,从某种程度上讲,我认为这些库已经非常稳定并且成熟。

我很快意识到了一个很“严重”的问题,这些库对 图片选择功能 的实现,其原理基本都是基于以下的思路实现的:

  • 开发者先配置好自己的需求,比如,图片最大可选的数量,是否可选择视频,或者是图片缩略图的加载引擎(GlideEngine/PicassoEngine)等等;

  • 通过某个方法,定向打开某个固定的 Activity,这个 Activity 负责展示图片选择的UI;

  • 开发者在 Activity 的 onActivityResult() 方法中(或者其它的方式,比如回调方法),对选择结果的处理。

很合理的一种方式,但是对于我个人而言,还略显不够,我曾经在我的  这篇文章 中阐述了我的一个观点:

事实上,现在网络上越来越多出现别人封装好的 RecyclerViewAdapter 或其他工具,很多都不可避免出现了 过度封装 的情况:它也许能够涵括大多数的需求,但是这也恰恰是它致命的弊端,在涉及一些新的功能时,它也许会突然无能为力——过度的封装带来了严重的耦合,这种问题是架构级的。一个良好的设计需要更多的思考和尝试,更重要的也许是灵活,高度的可拓展性。

试想这样一个场景,我在项目中集成了目前非常流行的  Matisse ,这是知乎开源出来的一个图片选择库,拥有着非常 MaterialDesign 的 UI 设计(这也是我无比坚定拥护 Matisse 的原因),我们可以看一下它的 UI 效果:

非常完美,在大多数项目上,Matisse 是这样的,但是当我尝试深入它的原理时,我有了一个困惑,那就是,我无法实现 更私有化的UI定制, 比如 QQ 这样的设计:

不可否认,当我点击【相册】按钮时进入到的相册界面的UI,Matisse应该可以胜任(实际上,这也大概率需要修改Matisse的源码才能完全符合公司UI的设计),但是,在那之前,这个界面上的图片选择的UI需求,我需要自己去实现。

困惑就是,即使我实现了这个需求(简单分析来看,我需要自己实现一个Fragment来作为容器),这个界面的图片选择和打开相册界面的图片选择,完全是不相干的两个功能——它们都是图片选择,但一个是由我实现的,一个由借助三方的图片选择库实现,这是不合理的设计。

我更希望的是,能够有这样的一个架构,它能够将 图片选择的【业务功能】和【UI设计】分离开 ,在稳定成熟的基础上,让开发者根据需求的变更,灵活的实现图片选择的功能—— 当我需要对图片选择的需求进行修改或者增加时,我不需要替换框架,而是仅仅替换或者修改一个接口的实现。

遗憾的是,我并没有找到这样的轮子,于是我在18年年初给自己设定了一个目标,尝试设计出这样一个轮子。

上文中,我以 Matisse 为例进行了说明,但这并不是选择性地针对它,事实上, Matisse 是我认为非常优秀的库,同样,在我设计的框架中,   Matisse 起到了至关重要的作用,这个后文会提到。

要实现一个怎么样的轮子

在我接触 RxJava 的这段时间里,这是一个需要一定学习成本的工具库,但我完全爱上了它,我希望我能实现的是一个 支持 RxJava 响应式流、灵活且可高度定制的 Android 图片选择器,因此我命名为 RxImagePicker。

  • 业务层

在业务层的设计上,它应该支持这些:

  • Android 拍照

  • Android 图片选择

  • 以响应式数据流的格式返回数据(支持Observable/Flowable/Single/Maybe,对,就是RxJava)

  • 动态配置响应式数据流的数据类型(就是返回值的类型,开发者不应该为它忧虑,而只需要调用一个接口):File,Bitmap,或是Uri

  • 控制UI展示的逻辑,比如是直接打开一个Activity,还是作为一个Fragment被放入一个指定Id的容器中进行展示(就像QQ那样)。

我喜欢 Retrofit 的设计 ,它将复杂的网络请求需求转换为一个接口进行配置,图片选择框架也许也可以这样做,比如这样:

public interface MyImagePicker {        @Gallery    //打开相册选择图片    @AsFile     //返回值为File类型    Observable<File> openGallery();        @Camera    //打开相机拍照    @AsBitmap  //返回值为Bitmap类型    Observable<Bitmap> openCamera();}

接下来,我想要拍照或者图片选择,只需要通过注解配置好一个自定义的接口,然后通过代理实现即可:

//打开系统默认的图片选择器private void onButtonClick() {        new RxImagePicker.Builder()                .with(this)                .build()                .create(MyImagePicker.class)                   .openGallery()                .subscribe(new Consumer<File>() {                    @Override                    public void accept(File file) throws Exception {                        //做你想做的                    }                });}
  • UI 层

同时,它的UI层的设计上,我希望至少提供这些支持:

  • 系统级别的拍照,相册选择图片功能

  • 知乎主题图片选择器UI  (可选的,包括日间和夜间的两种主题)

  • 微信主题图片选择器UI    (可选的)

  • 自定义的图片选择器UI    (可选的)

下图就是库的sample所展示的效果:

  • 系统图片选择
  • 知乎主题
  • 微信主题
  • 示到界面上
这其中的知乎主题的UI效果看起来和  Matisse  非常相似,事实上,UI层的架构就是基于    Matisse  进行的设计和修改——包括后面微信主题的图片选择UI,可以说,Matisse 库的源码,是 UI 层的核心。
和其他库相比,差异在哪?
  • 灵活

上文花了巨幅的笔墨,说明了我为什么要设计一个这样的框架,无论是UI层还是业务层,一旦和目前项目的需求有了冲突(修改或者添加),开发者考虑的不应该是【这个库实现不了,干脆换一个库吧】或者【不管这个库,我去再单独实现一个】,而是,基于同一个图片选择框架,修改或者添加对应配置的接口。

  • 可定制

对于一个单独的依赖库,我添加了它的依赖,意味着我需要依赖它的所有。

如果 RxImagePicker 只提供了一个依赖,依赖它开发者可以随便使用知乎/微信的主题,也可以自定义 UI,但是我无论选择哪一种实现方式,都意味着其他的主题没有用到,这些没有用到的类和文件资源占用了 apk 的体积,站在开发者的角度而言也许问题不大,但对用户有限的流量来说,这是毁灭性的灾难。

RxImagePicker 中, 类似于知乎/微信主题的 UI,对于开发者是可选的:

//下面的版本号是笔者写本文时最新的版本号,使用时,请以github上最新的版本作为参考

//【1】最基础的架构,仅提供了系统默认的图片选择器和拍照功能compile 'com.github.qingmei2:rximagepicker:0.2.0'

//【2】提供了自定义UI图片选择器的基本组件,自定义UI的需求需要添加该依赖compile 'com.github.qingmei2:rximagepicker_support:0.2.0'

//如果需要额外的UI支持,请选择依赖对应的UI拓展库compile 'com.github.qingmei2:rximagepicker_support_zhihu:0.2.0'     //【3】知乎图片选择器compile 'com.github.qingmei2:rximagepicker_support_wechat:0.2.0'    //【4】微信图片选择器

这样分层设计的原因是,开发者可以根据不同的需求选择添加不同的依赖:

  • 当仅仅需要集成系统默认的功能时,可以添加最基础的依赖 compile 'com.github.qingmei2:rximagepicker:0.2.0' ,让自己项目中的图片选择功能能够通过RxJava 的数据流观察到用户的行为。

  • 当需要实现仿微信/知乎的图片选择功能时,可以添加对应主题的依赖,并进行对应的UI 展示。

  • 当需要实现私有化的 UI 定制时,开发者可以选择依赖 compile 'com.github.qingmei2:rximagepicker_support:0.2.0', 其提供了最基础的 UI 组件(基于Matisse 源码上,进行了一些修改以适应架构),以方便开发者进行私有化的UI设计,只需要实现 RxImagePicker 提供的 ICustomPickerView 接口,然后一切都不需要再管,交给 RxImagePicker 就行了。——事实上,【知乎】/【微信】主题图片选择器也是基于这些UI组件进行了UI的调整和封装。

其依赖结构为:

【3】知乎 /【4】微信 → 【2】UI support  →  【1】基础组件

需要考虑的成本

RxImagePicker 是一个支持 RxJava2 响应式流、灵活可高度定制的 Android 图片选择器,因此,添加它的依赖,默认就会自动添加 RxJava 和 RxAndroid 的依赖,因此,对于项目中并未使用 RxJava 的项目来说,所需要的成本是需要去认真考量的。

看到这里,如果您对于我个人的浅见有所认同,或者对这个库有一些兴趣,请不妨继续慢慢看下去,并尝试去使用它。

在项目中使用它

本文不阐述如何使用 RxImagePicker,因为它更应该详细地展示在项目的 Github 页面上。

RxImagePicker 项目的 Github 地址,内含各种UI主题(系统默认/微信/知乎)的使用 Sample:

https://github.com/qingmei2/RxImagePicker

在阅读接下来的章节前,能够进入 Github 的项目主页,并花费一些时间下载尝试运行 RxImagePicker 的 Sample 也许是一个不错的选择。

我不能保证你会喜欢它,如果真的如此,花费时间阅读接下来的内容同样没有意义(虽然此时我还没有写接下来的内容,但我不认为它会比上面开头的内容少)。

从零实现 RxImagePicker

业务层架构设计

灵感来源于  Retrofit 和 RxCache 的设计 ,这两个库都通过将复杂的【网络请求】/【数据缓存】的需求转换为一个接口进行配置。

这种设计的优势很明显,开发者不需要真正了解底层的实现,通过 注解 和 参数的注入 对接口进行配置,同时通过动态代理完成对功能的实现。

于是在编码之前,我决定使用上述的这种方式实现 RxImagePicker 底部的搭建,开发者通过 @Camera 或者 @Gallery 注解来标记对应的行为(是相机拍照还是打开相册)。

同时,作为数据返回值的类型,我暂时提供了 File/Bitmap/Uri 三种可选项,其中Uri是默认的返回类型,开发者也不应该对返回值类型进行多余 的操作,最好是使用框架提供的接口,于是我添加了对应的注解  @AsFile , @AsBitmap 和 @AsUri 。

于是你能看到 RxImagePicker 中自定义接口标准的使用方式:

public interface MyImagePicker {

    @Gallery    //打开相册选择图片    @AsFile     //返回值为File类型    Observable<File> openGallery();

    @Camera    //打开相机拍照    @AsBitmap  //返回值为Bitmap类型    Observable<Bitmap> openCamera();}

同时,除了Observable,库的本身还提供了对 RxJava 其他响应式类型的支持,包括 Observable/Flowable/Single/Maybe。

实现业务层

  • 使用动态代理

研究过 Retrofit 或者 RxCache 的源码的同学都知道,其底部原理都是基于Java 的动态代理,通过反射的方式解析接口方法中的配置(比如参数/返回值/注解),生成对应的 proxy对象。

RxImagePicker  也不例外,我仿照 Retrofit 和 RxCache,通过实现一个 InvocationHandler的实例,负责管理 proxy 对象的生成,并在 invoke()方法中对接口方法解析,并将返回值返回。

public final class ProxyProviders implements InvocationHandler {    //......    //省略其他代码    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        return Observable.defer(new Callable<ObservableSource<?>>() {            @Override            public ObservableSource<?> call() throws Exception {                //解析接口方法,获取配置的Bean对象                ImagePickerConfigProvider configProvider = proxyTranslator.processMethod(method, args);

               //实例化UI组件的Projector,display()方法意味着展示UI                proxyTranslator.instanceProjector(configProvider, fragmentActivity)                        .display(customPickConfigurations);

                //这个返回值就是用户选择结果的流                Observable<?> observable = rxImagePickerProcessor.process(configProvider);

                //根据返回值类型,进行数据转换                Class<?> methodType = method.getReturnType();

                if (methodType == Observable.class) return Observable.just(observable);

                if (methodType == Single.class)                    return Observable.just(Single.fromObservable(observable));

                if (methodType == Maybe.class)                    return Observable.just(Maybe.fromSingle(Single.fromObservable(observable)));

                if (methodType == Flowable.class)                    return Observable.just(observable.toFlowable(BackpressureStrategy.MISSING));

                throw new RuntimeException(method.getName() + " needs to return one of the next reactive types: observable, single, maybe or flowable");            }        }).blockingFirst();    }}

最终所有配置都会汇聚在一个实例化的 ImagePickerConfigProvider Bean 对象中,这个类包含所有的个性化配置:

/** * Entity class for user config. */public final class ImagePickerConfigProvider {

    private final String viewKey;    private final boolean singleActivity;  //是否作为一个activity打开    private final Class<? extends Activity> activityClass;

    private final SourcesFrom sourcesFrom;//是打开相册还是拍照    private final ObserverAs observerAs;  //返回值的类型,File,Uri还是Bitmap

    private final int containerViewId;  //若作为一个View(比如Fragment),承载它容器的Id    private final ICustomPickerView pickerView;  //View组件的实例(比如Fragment)

    public ImagePickerConfigProvider(boolean singleActivity,                                     String viewKey,                                     SourcesFrom sourcesFrom,                                     ObserverAs observerAs,                                     ICustomPickerView pickerView,                                     @IdRes int containerViewId,                                     Class<? extends Activity> activityClass    ) {        this.sourcesFrom = sourcesFrom;        this.observerAs = observerAs;        this.pickerView = pickerView;        this.containerViewId = containerViewId;        this.viewKey = viewKey;        this.singleActivity = singleActivity;        this.activityClass = activityClass;    }    //省略get()方法}

这样的好处显而易见,汇聚不同地方的配置,最终生成一个对象,它的职责是单一的,它保证了:在实现业务功能时,只需要持有一个对象,根据对应的属性进行对应的配置。

在这里,我们似乎并没有看到一些常见的配置,比如可选择图片的最大数量,UI 的主题等等。原因是,这个对象只是管理业务功能的配置,这些UI的配置应该交给对应的 UI 层去存储并管理。

  • 使用 Dagger2 依赖注入

我希望我能够对自己库所 额外添加的依赖,管理更加严苛一些,在最基础的组件中,我甚至没有添加 Glide 或者 picasso ,因为我认为它们更应该在UI层的拓展 library 中被添加或者管理,这样能够进一步减少基础业务组件的体积。

使用 Dagger2是一个艰难的决定,大家都知道,一个库的设计,依赖越少越好,这样就不会因为其他依赖库的升级,而导致库本身的 bug。

比如说,我的一个库底层依赖了 Glide,那么 Glide 在大版本升级时,API 的改变有可能影响到我自己库的使用,这是很难避免的。

同时,依赖的越多,意味着库本身的体积增加,这本身就是一种局限性。

但我还是选择了Dagger2,因为在我看来这样利大于弊,因此,在选择使用 RxImagePicker 的时候,默认除了  RxJava 和 RxAndroid 的依赖,还额外依赖了 dagger——这是 google 基于 square 的dagger上,fork 并自行拓展的依赖注入库。

dagger 的使用让我对架构中配置的管理游刃有余,这也要感谢  VictorAlbertos ,通过他的 RxCache ,让我知道了,依赖注入还可以这样用。

到这里,库本身的所有额外依赖都已经写清楚了:

当然,对于项目中使用到了dagger 和 RxJava 的同行来说,这种行为几乎没有影响。

  • 图片选择功能的实现

现在,通过动态代理和 Dagger,我已经获取到了接口方法对应的所有配置,接下来就是要将 UI 和业务进行绑定了。和 Retrofit 略微不同的是,前者并未涉及 UI,因此我还需要寻求其它库的一些帮助。

MLSDev:RxImagePicker,感谢这个库,让我看到了曙光。

这是和我名字相同的一个库(实际上我的基础组件的部分设计参考了它,给我带来了很大的帮助,衷心感谢!),它也是用来提供 Android 设备上图片选择的功能,但是有一个缺陷,就是它使用的是系统默认的拍照和相册功能,如果你想要自定义 UI,你需要拉他的源码进行修改,并自己实现 UI。

虽然有局限性,但是它的原理是,创建一个不可见的 Fragment,通过这个 Fragment,控制 Intent 打开系统的相册或者相机,在 onActiviyResult() 方法中将数据交给 Subject 发射,开发者就能在代码中通过 subscribe() 订阅获取对应的数据流了。

除此之外,我还研究参考了其它一些 RxJava 的拓展库,比如:

RxLifecycle: Lifecycle handling APIs for Android apps using RxJava

RxPermissions: Android runtime permissions powered by RxJava2

他们的原理都很相似,通过在生命周期组件中添加一个 Subject,用来接受并发射数据,然后将 Subject 返回给开发者,开发者订阅后,就能获取对应的数据。

我也实现了一个 Fragment:

//略微进行了调整,并省略大量代码,这里仅仅阐述自己的思路public final class SystemGalleryPickerView extends Fragment                   implements ICustomPickerView {

       //这个Subject发射的数据就是用户选择的图片       private PublishSubject<Uri> publishSubject;       //是否要终止本次订阅       private PublishSubject<Integer> canceledSubject;

       //当用户调用接口的方法打开相册或者拍照时,代理对象实际会调用该方法       //并将publishSubject强转成Obervable<T>返回给开发者       //同时规定,当canceledSubject发射数据时,publishSubject停止订阅       public Observable <Uri> pickImage() {          publishSubject = PublishSubject.create();          canceledSubject = PublishSubject.create();

          requestPickImage();          return publishSubject.takeUntil(canceledSubject);      }

        //实际上就是这个隐藏的Fragment打开相册,然后再onActivityResult()中监听        //当用户选择的图片返回,将图片的Uri从intent中取出并交给PublishSubject发射       @Override    public void onActivityResult(int requestCode, int resultCode, Intent data) {        if (resultCode == RESULT_OK) {            if (publishSubject != null) {            publishSubject.onNext(getActivityResultUri(data));            publishSubject.onComplete();            }        } else {            canceledSubject.onNext(requestCode);        }    }}

业务层和UI层的隔离尝试: ICustomPickerView

上文中,我的 Fragment 实现了 ICustomPickerView,这是一个接口,用于定义 UI 层的一些行为:

public interface ICustomPickerView {    //展示UI界面    void display(FragmentActivity fragmentActivity,                 @IdRes int viewContainer,                 String tag,                 ICustomPickerConfiguration configuration);    //要返回的数据    Observable<Uri> pickImage();}

它的意义是,将用户的图片选择或者拍照行为,所需要展示的界面,抽象成一个接口,供RxImagePicker 调用。

很好理解,我依赖了知乎主题的图片选择器,其中的 ZhihuImagePickerFragment 类负责展示知乎主题的 UI:

compile 'com.github.qingmei2:rximagepicker_support_zhihu:0.2.0' 

实际上,因为依赖关系是 RxImagePicker 被依赖于知乎主题的UI库,底层的 RxImagePicker 基础组件无法获取上层的 ZhihuImagePickerFragment 类的引用,因此我设计了一个接口,就是 ICustomPickerView。

这之后,再让 ZhihuImagePickerFragment 实现 ICustomPickerView 接口:

public class ZhihuImagePickerFragment extends Fragment implements        IGalleryCustomPickerView {        //...}

这样,底层的组件只需要负责调用两个方法,什么时候展示UI,以及什么时候返回数据,至于是如何实现的,底层组件并不关心。

UI 层的设计和实现

上文中,我已经定义好了UI层接口,对于一些拓展的UI选择器,我只需要按照UI层接口的规范,实现对应的逻辑即可。

UI层的设计我借鉴了    Matisse  ,它的设计已经足够好,稳定性也一定比我自己实现好得多,拓展性功能的API也已经涵括了大多数的需求。

我花了一些时间基于  Matisse  的源码进行了部分的修改。

我把  Matisse  的代码分成了两层:

//提供了自定义UI图片选择器的基本组件,自定义UI的需求需要添加该依赖compile 'com.github.qingmei2:rximagepicker_support:0.2.0'//知乎图片选择器compile 'com.github.qingmei2:rximagepicker_support_zhihu:0.2.0'     

其中我将公共的放入了 rximagepicker_support 包中,作为UI层的基本组件,而详细的 UI实现界面,我放入了rximagepicker_support_zhihu 包中:

这之后就是长时间的 UI 界面代码的修改和编写,相比之前热情饱满的业务设计,UI 层代码写起来真的有点枯燥(其间我加深了关于自定义控件的理解,收获颇多,但是我还是要说,很枯燥...)。

最终,除了 zhihu 主题的图片选择器,我还基于 Matisse 修改后的 UI 组件,设计出了Wechat 主题的图片选择器以供参考——就像文章开篇时的 sample 展示效果一样。

总结

经过几个月的学习和尝试,我最终实现出来了 RxImagePicker  ,坦白来讲,其中的设计并非如我预期的那般完美,但是它依然是我目前比较满意的作品。它达到了我想要的样子:灵活 且 可高度定制 ,并且支持 RxJava 。

这不是终点,因为一个工具是依靠不断的尝试和迭代才能慢慢完善的,我已经在公司的项目中尝试使用了它,至少目前为止,它正在经历 Production 级别的检验。

这几个月的经历,RxImagePicker 并不是一个很重的库,它仅仅是一个工具,便让我更加体会到了开源的艰难,而这才仅仅开始。

在此要特别感谢一位前辈, JessYan ,有幸和他在数次交流中,让我深刻理解了开源的意义,开源重要的不仅仅是 方便快速实现 和 便于装 X ,更重要的是 思想的交流和责任感。

我会坚持维护下去,也希望有朋友能够尝试使用它,并在 issues 中提供反馈,我将第一时间进行回复。最后留下 RxImagePicker 的 Github 地址:

https://github.com/qingmei2/RxImagePicker

Android系统(74)--- 从零实现灵活且可高度定制的Android图片选择架构相关推荐

  1. 深入浅出 - Android系统移植与平台开发(五)- 定制手机模拟器ROM

    作者:唐老师,华清远见嵌入式学院讲师. 一. 修改化定制Android4.0系统 Android系统启动时,先加载Linux内核,在Linux的framebuffer驱动里可以定制开机界面,Linux ...

  2. android系统应用开发_利用ADB工具免root停用Android系统应用

    最近迷上了安卓的开发,写篇冻结app的教程吧. 主要是实现,免root冻结系统中的某些应用,你懂得哈哈 利用ADB工具免root停用Android系统应用 - Hanada​hanada.info 上 ...

  3. android系统手势app,8种iOS手势规定和14种android手势规定详解

    不知道大家对ios系统和android系统的规定的原生手势有哪些吗?看到这样的标题,你能够回答出几个呢? 其实,APP设计师和h5开发工程师对移动设备的手势的了解和理解是非常有必要的.只有掌握了这些平 ...

  4. 安卓8.0 android系统,谷歌安卓8.0重磅来袭!国产定制系统有必要马上适配吗?

    8月末,万众期待的Android 8.0终于正式发布了,而广大用户最关心的问题莫过于自己的手机能否吃上奥利奥了. 其实就目前来说,国外的Android和国内的安卓可以视作是两个不同的生态圈,大部分国产 ...

  5. 氢os android系统耗电,安卓用久了会卡是定制系统惹的祸?氢OS:这锅我不背!

    原标题:安卓用久了会卡是定制系统惹的祸?氢OS:这锅我不背! 可能大家的脑海里都有这种印象:相对于苹果手机能用三四年还依旧流畅不同,安卓手机普遍用个一两年就会出现各种卡顿.为什么会出现这种情况呢?众所 ...

  6. android系统版本卸掉,使用内置软件卸载最新版本的Android

    Power Software Park提供了最新版本的内置软件卸载Android. 一个实用且功能强大的手机软件,它可以帮助您卸载手机中无用的系统程序,使您的手机更加简单方便,并且不会被太多无用的混乱 ...

  7. miui7+android系统版本,用户升级完MIUI 7之后发现居然是Android 4.4,然后就疯了

    原标题:用户升级完MIUI 7之后发现居然是Android 4.4,然后就疯了 火星文:MIUI 7近日开放下载,很多米粉都升了级.但这次升级也惹怒了部分粉丝,原因是他们发现升级后系统版本仍是Andr ...

  8. Android 系统(213)---如何内置多张静态壁纸(图片)到系统中

    如何内置多张静态壁纸(图片)到系统中 系统默认只有一张内置的静态壁纸,如如何修改可以内置多张静态壁纸? Note:静态壁纸的宽.高必须是:宽 = 屏幕分辨率的宽*2,高 = 屏幕分辨率的高 N/M/L ...

  9. Android acra 日志上报,基于开源项目acra实现的定制化Android crash上报库及后台系统...

    出发点: 开源的acra crash上报库( http://code.google.com/p/acra/)的缺点有: 1. crash上报到google doc里的话,由于被墙了,所以看不到数据. ...

最新文章

  1. CSS sprites
  2. AngularDart4.0 指南- 显示数据
  3. 小米路由器dns辅服务器未响应,小米路由器频繁掉线的原因与解决办法
  4. 【C语言】05-printf和scanf函数
  5. 湘潭大学计算机网络安全学院,省委网信办与湘潭大学共建网络空间安全学院签约暨揭牌仪式举行...
  6. 中移物联网模块M8321P调试记录
  7. 高通about.html 文件,高通case提交指南2015Oct(4)(1)
  8. rstudio的数据集怎么建立和保存_在R Studio中保存
  9. kafka报错:The Cluster ID doesn‘t match stored clusterId Some in meta.properties
  10. 解决浏览器驱动和浏览器版本不匹配的报错:This version of ChromeDriver only supports Chrome version 97
  11. 送书 | 教你下载B站指定视频
  12. 第一次阅读与准备作业
  13. PDF文档翻译中文的方法
  14. 终于读完了《Essential C++》
  15. Arduino+Proteus 模拟摩尔斯码求救信号
  16. weightbias新手教程
  17. 利用winHex对文件进行修复
  18. 比较不错的互联网盈利模式分析
  19. 海康摄像头视频调用出错,Jni Error(app bug): accessed stale local reference解决办法
  20. 2022G3锅炉水处理考试练习题及答案

热门文章

  1. I2C总线学习—查缺补漏—S3C2440的I2C控制器
  2. jsp springmvc 视图解析器_SpringMVC工作原理
  3. java复习系列[2] - Java多线程
  4. java函数的参数传递
  5. 配置win2003 server IIS的总结,为什么IIs的工作进程会在空闲时间释放的问题。同时学会了throw的真正含义,throw的真正含义就是导致程序停止,崩溃,很简单,网摘也有记录。...
  6. Visualforce简介
  7. Excel TargetRange.Validation为空的
  8. 算法原理与分析第二次作业
  9. 4/2上海DevDays2004
  10. Error response from daemon:###unable to delete ### (must be forced) - image is being used by stopped