From 85c3805f35882e9093be9e4bbedb70fad2627a06 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 3 Feb 2016 19:38:43 +0800 Subject: [PATCH 001/373] Update --- ...05\345\255\230\345\210\206\346\236\220.md" | 5 + ...05\345\255\230\346\263\204\346\274\217.md" | 92 ++++++++++++++++++- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git "a/Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" "b/Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" index f4e91422..1d9e42c3 100644 --- "a/Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" +++ "b/Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" @@ -18,6 +18,11 @@ MAT内存分析 看一下上图中`data object`这一栏中的`Total Size`的大小是保持稳定还是有明显的变大趋势,如果有明显的变大趋势就说明这个页面存在内存泄露的问题,需要进行具体分析。 +很长时间之前学习的,一直想记录出来,总是忙,到现在才开始整理,但是现在已经很少用`MAT`了,因为它太费劲了。 +自从良心企业发布了`leakCanary`后,都已经转投到大神门下。 + + + --- diff --git "a/Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" "b/Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" index 32f46a39..01063c87 100644 --- "a/Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" +++ "b/Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" @@ -1,9 +1,6 @@ 内存泄露 === -`Java`和`C++`一个很大的区别就是`Java`有垃圾回收`GC(Garbage Collection)`自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。 -`Java`中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。 -`GC`会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么`GC`就认为这块内存不再使用了。 ##定义 内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。 @@ -16,6 +13,30 @@ 只有一个,那就是虚拟机占用内存过高,导致`OOM`(内存溢出)。 对于`Android`应用来说,就是你的用户打开一个`Activity`,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制就会出现`FC`。 + +先来说一下`Java`程序运行时的内存分配策略: + +- 静态内存:存放静态数据,这块内存是在编译时就已经分配好的,在程序整个运行期间都存在。 +- 栈内存:程序执行时,局部变量的创建存储区,执行结束后将自动释放。(当时学习java时花的内存分配图,现在都生疏了- -!) +- 堆内存:存储一些new出来的内存,也叫动态内存分配,这部分内存在不使用时将会有`Java`垃圾回收器来负责回收。 + +举一个典型的内存泄漏的例子: +```java +Vector v = new Vector(100); +for (int i = 1; i < 100,; i++) { + Object o = new Object(); + v.add(o); + o = null; +} +``` +在这个例子中,我们循环申请了`Object`对象,并将所申请的对象放入一个集合中,如果我们仅仅释放引用本身,那么`Vector`仍然引用 +该对象,所以这个对象对`GC`来说是不可回收的。因此,如果对象假如`Vector`后,还必须从`Vector`中删除,最简单的方法就是将 +`Vector`对象设置为`null`. + +`Java`和`C++`一个很大的区别就是`Java`有垃圾回收`GC(Garbage Collection)`自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。 +`Java`中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。 +`GC`会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么`GC`就认为这块内存不再使用了。 + `Android`手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成`OutOfMemoryError` `OutOfMemoryError`主要由以下几种情况造成: @@ -120,6 +141,25 @@ 事实上由于我们的线程是`Activity`的内部类,所以`MyThread`中保存了`Activity`的一个引用,当`MyThread`的`run`函数没有结束时,`MyThread`是不会被销毁的, 因此它所引用的`Activity`也不会被销毁,因此就出现了内存泄露的问题。 +11. 单例造成的内存泄漏 + 由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏,比如: + ```java + public class AppManager { + private static AppManager instance; + private Context context; + private AppManager(Context context) { + this.context = context; + } + public static AppManager getInstance(Context context) { + if (instance != null) { + instance = new AppManager(context); + } + return instance; + } + } + ``` + 这里如果传入的是`Activity`的`Context`,当该`Context`的`Activity`退出后,由于其被单例对象引用,所以会导致`Activity`无法被回收,就造成内存泄漏。 + 8. 尽量使用`ApplicationContext` **`Context`引用的生命周期超过它本身的生命周期,也会导致`Context`泄漏**。 所以如果打算保存一个长时间的对象时尽量使用`Application`这种`Context`类型。 @@ -164,8 +204,50 @@ mHandler.removeCallbacks(mRunnable); } ``` - -总结一下避免`Contex`t泄漏应该注意的问题: +10. 由上面的`Handler`可以引伸出来的匿名内部类、非静态内部类和异步现成导致的内存泄漏。 + 下面看一个非静态内部类创建静态实例导致的内存泄漏 + ```java + public class MainActivity extends AppCompatActivity { + private static TestResource mResource = null; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if(mManager == null){ + mManager = new TestResource(); + } + //... + } + class TestResource { + //... + } + } + ``` + 因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态实例,该实例的声明周期与应用的一样长, + 这就导致了静态实例一直会持有`Activity`的引用而造成内存泄漏。 + 下面再看一个匿名内部类和异步现成的现象: + ```java + public class MainActivity extends Activity { + ... + Runnable ref1 = new MyRunable(); + Runnable ref2 = new Runnable() { + @Override + public void run() { + + } + }; + ... + } + ``` + 上面的离职中`ref1`对象是没问题的,但是`ref2`这个匿名类的实现对象中有外部类的引用,如果此时线程的生命周期与`Activity`的不一致时就会造成了泄漏。 + +10. 集合类泄漏 + 集合类中如果只有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性),那么没有 + 响应的删除机制,很可能导致集合所占用的内存只增不减。 + + +总结一下避免`Contex`t泄漏应该注意的问题: + - 使用`getApplicationContext()`类型。 - 注意对`Context`的引用不要超过它本身的生命周期。 - 慎重的使用`static`关键字。 From b011115385f7d920cbcefb1c8fe9e5b5fd0565bd Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 24 Feb 2016 18:19:19 +0800 Subject: [PATCH 002/373] =?UTF-8?q?Add=20Android=E5=8A=A0=E5=BC=BA/Android?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=A8=A1=E5=BC=8F=E8=AF=A6=E8=A7=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...41\345\274\217\350\257\246\350\247\243.md" | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" new file mode 100644 index 00000000..4b2e9aa4 --- /dev/null +++ "b/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -0,0 +1,45 @@ +Android启动模式详解 +=== + +- standard + 默认模式。在该模式下,`Activity`可以拥有多个实例,并且这些实例既可以位于同一个task,也可以位于不同的task。每次都会新创建。 +- singleTop + 该模式下,在同一个`task`中,如果存在该`Activity`的实例,并且该`Activity`实例位于栈顶则不会创建该`Activity`的示例,而仅仅只是调用`Activity`的`onNewIntent()`。否则的话,则新建该`Activity`的实例,并将其置于栈顶。 +- singleTask + 顾名思义,只容许有一个包含该`Activity`实例的`task`存在! + 在`android`浏览器`browser`中,`BrowserActivity`的`launcherMode="singleTask"`,因为`browser`不断地启动自己,所以要求这个栈中保持只能有一个自己的实例,`browser`上网的时候, + 遇到播放视频的链接,就会通过隐式`intent`方式跳转找`Gallery3D`中的`MovieView`这个类来播放视频,这时候如果你点击`home`键,再点击`browser`,你会发现`MovieView`这个类已经销毁不存在了, + 而不会像保存这个`MovieView`的类对象,给客户带来的用户体验特别的不好。就像别人总结的`singleTask`模式的`Activity`不管是位于栈顶还是栈底,再次运行这个`Activity`时,都会`destory`掉它上面的`Activity`来保证整个栈中只有一个自己。 + 下面是官方文档中的介绍: + `The system creates a new task and instantiates the activity at the root of the new task. However, if an instance of the activity already exists in a separate task, the system routes the intent to the existing + instance through a call to its onNewIntent() method, rather than creating a new instance. Only one instance of the activity can exist at a time.` + 以`singleTask`方式启动的`Activity`,全局只有唯一个实例存在,因此,当我们第一次启动这个`Activity`时,系统便会创建一个新的任务栈,并且初始化一个`Activity`实例,放在新任务栈的底部,如果下次再启动这个`Activity`时, + 系统发现已经存在这样的`Activity`实例,就会调用这个`Activity`实例的`onNewIntent`方法,从而把它激活起来。从这句话就可以推断出,以`singleTask`方式启动的`Activity`总是属于一个任务栈的根`Activity`。 + 下面我们看一下示例图:  + ![image](https://github.com/CharonChui/Pictures/blob/master/singletask.gif?raw=true) + 坑爹啊!有木有!前面刚说`singleTask`会在新的任务中运行,并且位于任务堆栈的底部,这里在`Task B`中,一个赤裸裸的带着`singleTask`标签的箭头无情地指向`Task B`堆栈顶端的`Activity Y`,什么鬼? +这其实是和`taskAffinity`有关,在将要启动时,系统会根据要启动的`Activity`的`taskAffinity`属性值在系统中查找这样的一个`Task`:`Task`的`affinity`属性值与即将要启动的`Activity`的`taskAffinity`属性值一致。如果存在, +就返回这个`Task`堆栈顶端的`Activity`回去,不重新创建任务栈了,再去启动另外一个`singletask`的`activity`时就会在跟它有相同`taskAffinity`的任务中启动,并且位于这个任务的堆栈顶端,于是,前面那个图中, +就会出现一个带着`singleTask`标签的箭头指向一个任务堆栈顶端的`Activity Y`了。在上面的`AndroidManifest.xml`文件中,没有配置`MainActivity`和`SubActivity`的`taskAffinity`属性, +于是它们的`taskAffinity`属性值就默认为父标签`application`的`taskAffinity`属性值,这里,标签`application`的`taskAffinity`也没有配置,于是它们就默认为包名。 +总的来说:`singleTask`的结论与`android:taskAffinity`相关:   + - 设置了`singleTask`启动模式的`Activity`,它在启动的时候,会先在系统中查找属性值`affinity`等于它的属性值`taskAffinity`的任务栈的存在;如果存在这样的任务栈,它就会在这个任务栈中启动,否则就会在新任务栈中启动。 + 因此,如果我们想要设置了`singleTask`启动模式的`Activity`在新的任务栈中启动,就要为它设置一个独立的`taskAffinity`属性值。以`A`启动`B`来说当`A`和`B`的`taskAffinity`相同时:第一次创建`B`的实例时,并不会启动新的`task`, + 而是直接将`B`添加到`A`所在的`task`;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。 + - 如果设置了`singleTask`启动模式的`Activity`不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的`Activity`实例,如果存在,就会把位于这个`Activity`实例上面的`Activity`全部结束掉, + 即最终这个Activity实例会位于任务的堆栈顶端中。以`A`启动`B`来说,当`A`和`B`的`taskAffinity`不同时:第一次创建`B`的实例时,会启动新的`task`,然后将`B`添加到新建的`task`中;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。 +- singleInstance +顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例! +它与`singleTask`有相同之处,也有不同之处。 +相同之处:任意时刻,最多只允许存在一个实例。 +不同之处: + - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 + - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 + - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, + 不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From bbdf066b78860654d7a5b7ec7e8c34a0c7ab1e08 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 4 Mar 2016 20:48:27 +0800 Subject: [PATCH 003/373] Add more notes. --- ...57\345\212\250\350\277\207\347\250\213.md" | 385 ++++++++++++++++++ ...37\347\220\206\345\210\206\346\236\220.md" | 127 ++++++ ...36\346\224\266\346\234\272\345\210\266.md" | 9 + .../MVC\344\270\216MVP\345\217\212MVVM.md" | 77 ++++ .../hashCode\344\270\216equals.md" | 25 ++ ...14Synchronized\345\214\272\345\210\253.md" | 27 ++ ...12\346\234\211\345\272\217\346\200\247.md" | 73 ++++ ...01\350\231\232\345\274\225\347\224\250.md" | 24 +- ...40\347\232\204\345\216\237\347\220\206.md" | 48 +++ ...05\345\256\271\346\200\273\347\273\223.md" | 25 +- 10 files changed, 804 insertions(+), 16 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" create mode 100644 "Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" create mode 100644 "Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" create mode 100644 "Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" create mode 100644 "Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" create mode 100644 "Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" create mode 100644 "Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" create mode 100644 "Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" diff --git "a/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" new file mode 100644 index 00000000..cca9b41c --- /dev/null +++ "b/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" @@ -0,0 +1,385 @@ +Activity启动过程 +=== + +前两天面试了天猫的开发,被问到了`Activity`启动过程,不懂啊.... +今天就来分析一下,我们开启`Activity`主要有两种方式: + +- 通过桌面图标启动,桌面就是`Launcher`其实他也是一个应用程序,他也是继承`Activity`。 +- 在程序内部调用`startActivity()`开启。 + +而`Launcher`点击图标其实也是调用了`Activity`的`startActivity()`方法,所以我们就从`startActivity()`方法入手了。 +首先看一下`Activity`类中的`startActivity()`方法: +```java +@Override +public void startActivity(Intent intent) { + this.startActivity(intent, null); +} +``` +继续看`startActivity(intent, null)`: +```java +/** + * Launch a new activity. You will not receive any information about when + * the activity exits. This implementation overrides the base version, + * providing information about + * the activity performing the launch. Because of this additional + * information, the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag is not + * required; if not specified, the new activity will be added to the + * task of the caller. + * + *

This method throws {@link android.content.ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param intent The intent to start. + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + * Context.startActivity(Intent, Bundle)} for more details. + * + * @throws android.content.ActivityNotFoundException + * + * @see {@link #startActivity(Intent)} + * @see #startActivityForResult + */ +@Override +public void startActivity(Intent intent, @Nullable Bundle options) { + if (options != null) { + startActivityForResult(intent, -1, options); + } else { + // Note we want to go through this call for compatibility with + // applications that may have overridden the method. + startActivityForResult(intent, -1); + } +} +``` +接下来会调用`startActivityForResult()`方法(注释也很重要啊,里面也说明了singleTask启动模式时该方法不会走到回调中): +```java +/** + * Launch an activity for which you would like a result when it finished. + * When this activity exits, your + * onActivityResult() method will be called with the given requestCode. + * Using a negative requestCode is the same as calling + * {@link #startActivity} (the activity is not launched as a sub-activity). + * + *

Note that this method should only be used with Intent protocols + * that are defined to return a result. In other protocols (such as + * {@link Intent#ACTION_MAIN} or {@link Intent#ACTION_VIEW}), you may + * not get the result when you expect. For example, if the activity you + * are launching uses the singleTask launch mode, it will not run in your + * task and thus you will immediately receive a cancel result. + * + *

As a special case, if you call startActivityForResult() with a requestCode + * >= 0 during the initial onCreate(Bundle savedInstanceState)/onResume() of your + * activity, then your window will not be displayed until a result is + * returned back from the started activity. This is to avoid visible + * flickering when redirecting to another activity. + * + *

This method throws {@link android.content.ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param intent The intent to start. + * @param requestCode If >= 0, this code will be returned in + * onActivityResult() when the activity exits. + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + * Context.startActivity(Intent, Bundle)} for more details. + * + * @throws android.content.ActivityNotFoundException + * + * @see #startActivity + */ +public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + // mParent也是Activity类,通过名字就能看明白了。 + if (mParent == null) { + // 开始了啊 + Instrumentation.ActivityResult ar = + mInstrumentation.execStartActivity( + this, mMainThread.getApplicationThread(), mToken, this, + intent, requestCode, options); + if (ar != null) { + mMainThread.sendActivityResult( + mToken, mEmbeddedID, requestCode, ar.getResultCode(), + ar.getResultData()); + } + if (requestCode >= 0) { + // If this start is requesting a result, we can avoid making + // the activity visible until the result is received. Setting + // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the + // activity hidden during this time, to avoid flickering. + // This can only be done when a result is requested because + // that guarantees we will get information back when the + // activity is finished, no matter what happens to it. + mStartedActivity = true; + } + + cancelInputsAndStartExitTransition(options); + // TODO Consider clearing/flushing other event sources and events for child windows. + } else { + if (options != null) { + mParent.startActivityFromChild(this, intent, requestCode, options); + } else { + // Note we want to go through this method for compatibility with + // existing applications that may have overridden it. + mParent.startActivityFromChild(this, intent, requestCode); + } + } +} +``` +里面调用了`mInstrumentation.execStartActivity`这是啥玩意,先看一下`mInstrumentation`属性,他是`Instrumentation`类,我们看下文档 +```java +/** + * Base class for implementing application instrumentation code. When running + * with instrumentation turned on, this class will be instantiated for you + * before any of the application code, allowing you to monitor all of the + * interaction the system has with the application. An Instrumentation + * implementation is described to the system through an AndroidManifest.xml's + * <instrumentation> tag. + */ +public class Instrumentation { + ... +} +``` +放狗查了下`Instrumentation`的意思是仪器、工具、装置的意思。我就大体翻一下(英语不好- -~,可能不准确)该类是实现应用程序代码的基类,当该类在 +启动的状态下运行时,该类会在其他任何应用程序运行前进行初始化,允许你件事所有应用程序与系统的交互。一个`Instrumentation`实例会通过`Manifest`文件 +中的`This method returns an {@link ActivityResult} object, which you can + * use when intercepting application calls to avoid performing the start + * activity action but still return the result the application is + * expecting. To do this, override this method to catch the call to start + * activity so that it returns a new ActivityResult containing the results + * you would like the application to see, and don't call up to the super + * class. Note that an application is only expecting a result if + * requestCode is >= 0. + * + *

This method throws {@link android.content.ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param who The Context from which the activity is being started. + * @param contextThread The main thread of the Context from which the activity + * is being started. + * @param token Internal token identifying to the system who is starting + * the activity; may be null. + * @param target Which activity is performing the start (and thus receiving + * any result); may be null if this call is not being made + * from an activity. + * @param intent The actual Intent to start. + * @param requestCode Identifier for this request's result; less than zero + * if the caller is not expecting a result. + * @param options Addition options. + * + * @return To force the return of a particular result, return an + * ActivityResult object containing the desired data; otherwise + * return null. The default implementation always returns null. + * + * @throws android.content.ActivityNotFoundException + * + * @see Activity#startActivity(Intent) + * @see Activity#startActivityForResult(Intent, int) + * @see Activity#startActivityFromChild + * + * {@hide} + */ +public ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Activity target, + Intent intent, int requestCode, Bundle options) { + IApplicationThread whoThread = (IApplicationThread) contextThread; + Uri referrer = target != null ? target.onProvideReferrer() : null; + if (referrer != null) { + intent.putExtra(Intent.EXTRA_REFERRER, referrer); + } + if (mActivityMonitors != null) { + synchronized (mSync) { + final int N = mActivityMonitors.size(); + for (int i=0; i= 0 ? am.getResult() : null; + } + break; + } + } + } + } + try { + intent.migrateExtraStreamToClipData(); + intent.prepareToLeaveProcess(); + // 通过注释然后再结合代码一看我们就知道这应该就是分发到系统activity manager的过程 + int result = ActivityManagerNative.getDefault() + .startActivity(whoThread, who.getBasePackageName(), intent, + intent.resolveTypeIfNeeded(who.getContentResolver()), + token, target != null ? target.mEmbeddedID : null, + requestCode, 0, null, options); + checkStartActivityResult(result, intent); + } catch (RemoteException e) { + throw new RuntimeException("Failure from system", e); + } + return null; +} +``` +那就直接看下`ActivityManagerNative.getDefault().startActivity()`方法,看之前我们先看看`ActivityManagerNative.getDefault()`是什么鬼: +```java +/** + * Retrieve the system's default/global activity manager. + */ +static public IActivityManager getDefault() { + return gDefault.get(); +} +``` +注释说的明白的就是拿到系统默认/全局的`activity manager`,通过名字也能看出来`IActivityManager`是个接口,那就继续看`IActivityManager.startActivity()`方法吧: +```java +public int startActivity(IApplicationThread caller, String callingPackage, Intent intent, + String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, + ProfilerInfo profilerInfo, Bundle options) throws RemoteException; +``` +这里就顺便看一下`IActivityManager`接口是神马鬼: +``` +/** + * System private API for talking with the activity manager service. This + * provides calls from the application back to the activity manager. + * + * {@hide} + */ +public interface IActivityManager extends IInterface { + ... +} +``` +但是看到这里我不知道怎么继续往下分析了啊,既然是接口我们要找到实现类啊,又是通过`ActivityManagerNative.getDefault()`得到的`IActivityManager`实例, +所以这里我们再看下`ActivityManagerNative`类。 +```java +/** {@hide} */ +public abstract class ActivityManagerNative extends Binder implements IActivityManager { + ... +} +``` +啊! 隐藏的,连个注释也没有,我拖动了下这个类一看`5992`行,不知道从哪下手了,还能从哪下手啊,当然是从`ActivityManagerNative.getDefault()`方法啊 +```java +public abstract class ActivityManagerNative extends Binder implements IActivityManager { + + // 继承Binder接口,而且asInterFace方法,我好想明白了点什么 + /** + * Cast a Binder object into an activity manager interface, generating + * a proxy if needed. + */ + static public IActivityManager asInterface(IBinder obj) { + if (obj == null) { + return null; + } + IActivityManager in = + (IActivityManager)obj.queryLocalInterface(descriptor); + if (in != null) { + return in; + } + + return new ActivityManagerProxy(obj); + } + + /** + * Retrieve the system's default/global activity manager. + */ + static public IActivityManager getDefault() { + // 使用了getDefaule的get方法 + return gDefault.get(); + } + .... +} +``` +继续看:   +```java +private static final Singleton gDefault = new Singleton() { + protected IActivityManager create() { + // 进程间通信了 + IBinder b = ServiceManager.getService("activity"); + if (false) { + Log.v("ActivityManager", "default service binder = " + b); + } + // 看到了吗?用到了刚才我们说的asInterface方法 + IActivityManager am = asInterface(b); + if (false) { + Log.v("ActivityManager", "default service = " + am); + } + return am; + } +}; +``` +好了,我们再看下`asInterface()`方法:     +```java +static public IActivityManager asInterface(IBinder obj) { + if (obj == null) { + return null; + } + IActivityManager in = + (IActivityManager)obj.queryLocalInterface(descriptor); + if (in != null) { + return in; + } + // 在这里 + return new ActivityManagerProxy(obj); +} +``` +终于找到了其实就是`ActivityManagerProxy`类,刚才我们找到`IActivityManager`接口的`startActivity()` 方法,现在终于找到了在这里使用的是`IActivityManager`接口实现类的 +`ActivityManagerProxy`类,我们来看一下该类实现的`startActivity()`方法: +```java +class ActivityManagerProxy implements IActivityManager +{ + public ActivityManagerProxy(IBinder remote) + { + mRemote = remote; + } + + public IBinder asBinder() + { + return mRemote; + } + public int startActivity(IApplicationThread caller, String callingPackage, Intent intent, + String resolvedType, IBinder resultTo, String resultWho, int requestCode, + int startFlags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(caller != null ? caller.asBinder() : null); + data.writeString(callingPackage); + intent.writeToParcel(data, 0); + data.writeString(resolvedType); + data.writeStrongBinder(resultTo); + data.writeString(resultWho); + data.writeInt(requestCode); + data.writeInt(startFlags); + if (profilerInfo != null) { + data.writeInt(1); + profilerInfo.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + if (options != null) { + data.writeInt(1); + options.writeToParcel(data, 0); + } else { + data.writeInt(0); + } + mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0); + reply.readException(); + int result = reply.readInt(); + reply.recycle(); + data.recycle(); + return result; + } + ... +} +``` +再继续我就不知道走到哪里了.... + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" new file mode 100644 index 00000000..d1808e42 --- /dev/null +++ "b/Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -0,0 +1,127 @@ +HashMap实现原理分析 +=== + +`HashMap`主要是用数组来存储数据的,我们都知道它会对`key`进行哈希运算,哈系运算会有重复的哈希值,对于哈希值的冲突,`HashMap`采用链表来解决的。 +`在HashMap`里有这样的一句属性声明: +```java +transient Entry[] table; +``` +可以看到`Map`是通过数组的方式来储存`Entry`那`Entry`是神马呢?就是`HashMap`存储数据所用的类,它拥有的属性如下: +```java +static class Entry implements Map.Entry { + final K key; + V value; + Entry next; + final int hash; + ...//More code goes here +} +``` +看到`next`了吗?`next`就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好吧, +将新加的元素也放到第10个位置,将第10个位置的原有`Entry`赋值给当前新加的`Entry`的`next`属性。数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 + +好了,总结一下: + +- `HashMap`中有一个叫`table`的`Entry`数组。 +- 这个数组存储了`Entry`类的对象。`HashMap`类有一个叫做`Entry`的内部类。这个`Entry`类包含了`key-value`作为实例变量。 +- 每当往`Hashmap`里面存放`key-value`对的时候,都会为它们实例化一个`Entry`对象,这个`Entry`对象就会存储在前面提到的`Entry`数组`table`中。 +现在你一定很想知道,上面创建的`Entry`对象将会存放在具体哪个位置(在`table`中的精确位置)。答案就是,根据`key`的`hashcode()`方法计算出来的`hash`值来决定。 +`hash`值用来计算`key`在`Entry`数组的索引。 +- 我们往`hashmap`放了4个`key-value`对,但是有时候看上去好像只有2个元素!!!这是因为,如果两个元素有相同的`hashcode`,它们会被放在同一个索引上。 +问题出现了,该怎么放呢?原来它是以链表`(LinkedList)`的形式来存储的。 + +接下来看一下`put`方法: +```java +/** +* Associates the specified value with the specified key in this map. If the +* map previously contained a mapping for the key, the old value is +* replaced. +* +* @param key +* key with which the specified value is to be associated +* @param value +* value to be associated with the specified key +* @return the previous value associated with key, or null +* if there was no mapping for key. (A null return +* can also indicate that the map previously associated +* null with key.) +*/ +public V put(K key, V value) { + // 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。 + if (key == null) + return putForNullKey(value); + // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 + int i = indexFor(hash, table.length); + + for (Entry e = table[i]; e != null; e = e.next) { + // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 + Object k; + // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), + // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 + modCount++; + addEntry(hash, key, value, i); + return null; +} +``` +再看一下`get`方法:     +```java +/** + * Returns the value to which the specified key is mapped, or {@code null} + * if this map contains no mapping for the key. + * + *

+ * More formally, if this map contains a mapping from a key {@code k} to a + * value {@code v} such that {@code (key==null ? k==null : + * key.equals(k))}, then this method returns {@code v}; otherwise it returns + * {@code null}. (There can be at most one such mapping.) + * + *

+ * A return value of {@code null} does not necessarily indicate that + * the map contains no mapping for the key; it's also possible that the map + * explicitly maps the key to {@code null}. The {@link #containsKey + * containsKey} operation may be used to distinguish these two cases. + * + * @see #put(Object, Object) + */ +public V get(Object key) { + // 如果key是null,table[0]这个位置的元素将被返回。 + if (key == null) + return getForNullKey(); + // 计算hash值 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 + for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { + Object k; + // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) + return e.value; + } + return null; +} +``` + +总结: + +- `HashMap`有一个叫做`Entry`的内部类,它用来存储`key-value`对。 +- 上面的`Entry`对象是存储在一个叫做`table`的`Entry`数组中。 +- `table`的索引在逻辑上叫做“桶”`(bucket)`,它存储了链表的第一个元素。 +- `key`的`hashcode()`方法用来找到`Entry`对象所在的桶。 +- 如果两个`key`有相同的`hash`值,他们会被放在`table`数组的同一个桶里面。 +- `key`的`equals()`方法用来确保`key`的唯一性。 +- `value`对象的`equals()`和`hashcode()`方法根本一点用也没有。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" "b/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" new file mode 100644 index 00000000..4d70690e --- /dev/null +++ "b/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" @@ -0,0 +1,9 @@ +JVM垃圾回收机制 +=== + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" "b/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" new file mode 100644 index 00000000..d08971cf --- /dev/null +++ "b/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" @@ -0,0 +1,77 @@ +MVC与MVP及MVVM +=== + +MVC +--- + +`MVC`是一种使用Model View Controller模型-视图-控制器设计创建`Web`应用程序的模式: + +- `Model`(模型)表示应用程序核心(比如数据库记录列表)是应用程序中用于处理应用程序数据逻辑的部分。 +- `View`(视图)显示数据(数据库记录)是应用程序中处理数据显示的部分。 +- `Controller`(控制器)处理输入(写入数据库记录)应用程序中处理用户交互的部分。 + +MVC 分层有助于管理复杂的应用程序,因为您可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。 +MVC 分层同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。 + +- 优点 + - 耦合性低 + - 重用性高 + - 可维护性高 + - 有利软件工程化管理 + +- 缺点 + - 没有明确的定义 + - 视图与控制器间的过于紧密的连接 + - 增加系统结构和实现的复杂性 + +MVP +--- + +`MVP`是从经典的模式`MVC`演变而来,它们的基本思想有相通的地方:`Controller/Presenter`负责逻辑的处理,`Model`提供数据,`View`负责显示。 +作为一种新的模式,`MVP`与`MVC`有着一个重大的区别:在`MVP`中`View`并不直接使用`Model`,它们之间的通信是通过`Presenter`(`MVC`中的`Controller`)来进行的, +所有的交互都发生在`Presenter`内部,而在`MVC`中`View`会直接从`Model`中读取数据而不是通过`Controller`。 +在`MVC`里,`View`是可以直接访问`Model`的!从而,`View`里会包含`Model`信息,不可避免的还要包括一些业务逻辑。 +在`MVC`模型里,更关注的`Model`的不变,而同时有多个对`Model`的不同显示及`View`。所以,在`MVC`模型里,`Model`不依赖于`View`,但是`View`是依赖于`Model`的。 +不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 + +![image](https://github.com/CharonChui/Pictures/blob/master/MVP.jpg?raw=true) +![image](https://github.com/CharonChui/Pictures/blob/master/MVC.jpg?raw=true) + +在`MVP`里,`Presenter`完全把`Model`和`View`进行了分离,主要的程序逻辑在`Presenter`里实现。而且`Presenter`与具体的`View`是没有直接关联的, +而是通过定义好的接口进行交互,从而使得在变更`View`时候可以保持`Presenter`的不变,即重用! +在MVP里,应用程序的逻辑主要在`Presenter`来实现,其中的`View`是很薄的一层。因此就有人提出了`Presenter First`的设计模式, +就是根据`User Story`来首先设计和开发`Presenter`。在这个过程中,`View`是很简单的,能够把信息显示清楚就可以了。 +在后面,根据需要再随便更改`View`,而对`Presenter`没有任何的影响了。 如果要实现的`UI`比较复杂,而且相关的显示逻辑还跟`Model`有关系, +就可以在`View`和`Presenter`之间放置一个`Adapter`。由这个`Adapter`来访问`Model`和`View`,避免两者之间的关联。 +而同时,因为`Adapter`实现了`View`的接口,从而可以保证与`Presenter`之间接口的不变。这样就可以保证`View`和`Presenter`之间接口的简洁, +又不失去`UI`的灵活性。在`MVP`模式里,`View`只应该有简单的`Set/Get`的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容, +绝不容许直接访问`Model`这就是与`MVC`很大的不同之处。 + +- 优点 + - 模型与视图完全分离,我们可以修改视图而不影响模型 + - 可以更高效地使用模型,因为所有的交互都发生在一个地方——`Presenter`内部 + - 我们可以将一个`Presenter`用于多个视图,而不需要改变`Presenter`的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。 + - 如果我们把逻辑放在`Presenter`中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试) + +- 缺点 + 由于对视图的渲染放在了`Presenter`中,所以视图和`Presenter`的交互会过于频繁。还有一点需要明白,如果`Presenter`过多地渲染了视图, + 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 + 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 + +MVVM +--- + +MVVM是Model-View-ViewModel的简写。 + +- 优点 + `MVVM`模式和`MVC`模式一样,主要目的是分离视图`View`和模型`Model` +- 低耦合。 +- 可重用性。 +- 独立开发。 +- 可测试。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" "b/Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" new file mode 100644 index 00000000..75bf9067 --- /dev/null +++ "b/Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" @@ -0,0 +1,25 @@ +hashCode与equals +=== + +`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时环境是如何判断`HashSet` +中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? +在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范:在`Java`中任何一个对象都具备`equals(Object obj)` +和`hashcode()`这两个方法,因为他们是在`Object`类中定义的。`equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 +`hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 + +接下来有两个个关于这两个方法的重要规范: +- 若重写`equals(Object obj)`方法,有必要重写`hashcode()`方法,确保通过`equals(Object obj)`方法判断结果为`true`的两个对象具备相等的`hashcode()`返回值。 + 说得简单点就是: 如果两个对象相同,那么他们的hashcode应该相等。不过请注意:这个只是规范,如果你非要写一个类让`equals(Object obj)`返回`true` + 而`hashcode()`返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了`Java`规范,程序也就埋下了`BUG`。 +- `如果equals(Object obj)`返回`false`,即两个对象“不相同”,并不要求对这两个对象调用`hashcode()`方法得到两个不相同的数。 + 说的简单点就是:“如果两个对象不相同,他们的`hashcode`可能相同”。 +- 如果两个对象相同,那么它们的`hashCode`值一定要相同; +- 如果两个对象的`hashCode`相同,它们并不一定相同 +上面说的对象相同指的是用`eqauls`方法比较。 +你当然可以不按要求去做了,但你会发现,相同的对象可以出现在`Set`集合中。同时,增加新元素的效率会大大下降。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" new file mode 100644 index 00000000..33b473ea --- /dev/null +++ "b/Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" @@ -0,0 +1,27 @@ +volatile和Synchronized区别 +=== + +- volatile + `Java`语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量 +的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而`volatile`关键字就是提示`JVM`:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 +使用建议:在两个或者更多的线程访问的成员变量上使用`volatile`。当要访问的变量已在`synchronized`代码块中,或者为常量时,不必使用。 +由于使用`volatile`屏蔽掉了`JVM`中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟`C`中的一样 禁止编译器进行优化. + + 注意:如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对`long`和`double`的简单操作之外,`volatile`并不能提供原子性。 +所以,就算你将一个变量修饰为`volatile`,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生! + +- synchronized + `synchronized`为一段操作或内存进行加锁,它具有互斥性。当线程要操作被`synchronized`修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。 + 它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 + - 当两个并发线程访问同一个对象中的这个`synchronized(this)`同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 + - 然而,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,另一个线程仍然可以访问该`object`中的非`synchronized(this)`同步代码块。 + - 尤其关键的是,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,其他线程对`object`中所有其它`synchronized(this)`同步代码块的访问将被阻塞。 + +- 区别: + - `volatile`是变量修饰符,而`synchronized`则作用于一段代码或方法。 + - `volatile`只是在线程内存和“主”内存间同步某个变量的值;而`synchronized`通过锁定和解锁某个监视器同步所有变量的值。显然`synchronized`要比`volatile`消耗更多资源。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" new file mode 100644 index 00000000..c7348eba --- /dev/null +++ "b/Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" @@ -0,0 +1,73 @@ +原子性、可见性以及有序性 +=== + +- 原子性: + 众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 + 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 + 最简单的一个例子就是银行转账问题,赋值或者`return`。比如`a = 1;`和 `return a;`这样的操作都具有原子性 + 原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作! + 加锁可以保证复合语句的原子性,`sychronized`可以保证多条语句在`synchronized`块中语意上是原子的。 + +- 可见性: + 在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时, + 多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。 + (这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在! + 这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作覆盖掉了, + 这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 + +- 非原子性操作 + 类似`a += b`这样的操作不具有原子性,在某些`JVM`中`a += b`可能要经过这样三个步骤: + - 取出`a`和`b` + - 计算`a+b` + - 将计算结果写入内存 + 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, + 相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 + 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 + +- 有序性 + 有序性:即程序执行的顺序按照代码的先后顺序执行。 + ```java + int i = 0; + boolean flag = false; + i = 1; //语句1 + flag = true; //语句2 + ``` + 上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? + 不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 +  下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 +  比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。 +  但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? + 再看下面一个例子: + ```java + int a = 10; //语句1 + int r = 2; //语句2 + a = a + 3; //语句3 + r = a*a; //语句4 + ``` + 这段代码有4个语句,那么可能的一个执行顺序是: + 语句2->语句1->语句3->语句4 + 那么可能不可能是这个执行顺序呢?语句2->语句1->语句4->语句3,这是不可能的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性, + 如果一个指令`Instruction 2`必须用到`Instruction 1`的结果,那么处理器会保证`Instruction 1`会在`Instruction 2`之前执行。 + + 虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: + ```java + //线程1: + context = loadContext(); //语句1 + inited = true; //语句2 + + //线程2: + while(!inited ){ + sleep() + } + doSomethingwithconfig(context); + ``` + 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成, + 那么就会跳出`while`循环,去执行`doSomethingwithconfig(context)`方法,而此时`context`并没有被初始化,就会导致程序出错。 + 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 +  也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" index ad030e5b..f0a495ab 100644 --- "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -6,22 +6,22 @@ 垃圾回收器绝不会回收它。当内存空 间不足,`Java`虚拟机宁愿抛出`OutOfMemoryError`错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 - 软引用(SoftReference) - 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 - 只要垃圾回收器没有回收它,该对象就可以被程序使用。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, - 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 + 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 + 只要垃圾回收器没有回收它,该对象就可以被程序使用。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, + 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - 弱引用(WeakReference) - 如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:**只具有弱引用的对象拥有更短暂的生命周期**。 - 在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 - 不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 - 弱引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果弱引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 + 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 + 弱引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果弱引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - 虚引用(PhantomReference) - "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用, - 那么它就和没有任何引用一样,在 任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于: - 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前, - 把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 - 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用, + 那么它就和没有任何引用一样,在 任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于: + 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前, + 把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 + 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 + 虚引用主要用于检测对象是否已经从内存中删除。 有关弱引用以及软引用再分析一下: diff --git "a/Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" "b/Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" new file mode 100644 index 00000000..7206fb29 --- /dev/null +++ "b/Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" @@ -0,0 +1,48 @@ +线程池的原理 +=== + +- 在什么情况下使用线程池? + - 单个任务处理的时间比较短 + - 将需处理的任务的数量大 + +- 使用线程池的好处: + - 减少在创建和销毁线程上所花的时间以及系统资源的开销 + - 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 + - 提交相应速度 + +- 工作流程 + 线程池的任务就在于负责这些线程的创建,销毁和任务处理参数传递、唤醒和等待。 + - 创建若干线程,置入线程池 + - 任务达到时,从线程池取空闲线程 + - 取得了空闲线程,立即进行任务处理 + - 否则新建一个线程,并置入线程池,执行3 + - 如果创建失败或者线程池已满,根据设计策略选择返回错误或将任务置入处理队列,等待处理 + - 销毁线程池 + +- 风险 + 虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。 + 用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁, + 它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。 + +- 资源不足 + 线程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。 + 线程消耗包括内存和其它系统资源在内的大量资源。除了`Thread`对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。 + 除此以外`JVM`可能会为每个`Java`线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后虽然线程之间切换的调度开销很小, + 但如果有很多线程,环境切换也可能严重地影响程序的性能。 + 如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间, + 而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。 + 除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如`JDBC`连接、套接字或文件。 + 这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配`JDBC`连接。 + +- 线程池的关闭 + 我们可以通过调用线程池的`shutdown`或`shutdownNow`方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的`interrupt`方法来中断线程,所以无法响应中断的任务可能永远无法终止。 + 但是它们存在一定的区别,`shutdownNow`首先将线程池的状态设置成`STOP`,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表, + 而`shutdown`只是将线程池的状态设置成`SHUTDOWN`状态,然后中断所有没有正在执行任务的线程。 + 只要调用了这两个关闭方法的其中一个,`isShutdown`方法就会返回`true`。当所有的任务都已关闭后,才表示线程池关闭成功, + 这时调用`isTerminaed`方法会返回`true`。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用`shutdown`来关闭线程池,如果任务不一定要执行完,则可以调用`shutdownNow`。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" "b/Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" index ea7967d2..e52cfe12 100644 --- "a/Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" +++ "b/Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" @@ -42,9 +42,16 @@ 必须建立连接,效率会稍低 三次握手: - - 第一次:我问你:在么? - - 第二次:你回答:在。 - - 第三次:我反馈:哦,我知道你在。 + - 第一次:我问你:在么? 建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。 + - 第二次:你回答:在。服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。 + - 第三次:我反馈:哦,我知道你在了。客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。 + + 四次挥手: + - 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。 + - 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。 + - 服务器B关闭与客户端A的连接,发送一个FIN给客户端A。 + - 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。 + - Socket: - Socket是对TCP/IP协议的封装和应用。Socket本身并不是协议,而是一个调用接口。通过Socket,我们才能使用TCP/IP协议.在设计模式中Socket其实就是一个门面模式。 他将复杂的TCP/IP协议归隐到Socket接口后面,对于使用来说,一组简单的接口就是全部,让Socket去组织符合制定协议的数据。 @@ -191,7 +198,17 @@ - 响应正文: - 第一个空行之后的全部都是响应正文,浏览器显示的就是正文中的内容 - +- HTTPS + HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 + 它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版。 + +- HTTPS和HTTP的区别: + - https协议需要到ca申请证书,一般免费证书很少,需要交费。 + - http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议 + - http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。 + - http的连接很简单,是无状态的 + - HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全 + ---- - 邮箱 :charon.chui@gmail.com - Good Luck! From 66dda0f95a78ad613c097222d4c9da54ca93f435 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 4 Mar 2016 20:49:20 +0800 Subject: [PATCH 004/373] Add note. --- .../\345\215\225\351\223\276\350\241\250.md" | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 "Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" diff --git "a/Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" "b/Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" new file mode 100644 index 00000000..8108244a --- /dev/null +++ "b/Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" @@ -0,0 +1,28 @@ +单链表 +=== + +链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 +链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 +每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,链表比较方便插入和删除操作。 + +用一组地址任意的存储单元存放线性表中的数据元素。以元素(数据元素的映象) + 指针(指示后继元素存储位置) = 结点。 +以“结点的序列”表示线性表,称作线性链表(单链表)。单链表是一种顺序存取的结构,为找第 i 个数据元素,必须先找到第 i-1 个数据元素。 +链表的结点结构:  + ┌──┬──┐──┐ + │data│next│ + └──┴──┘──┘ + data域:存放结点值的数据域    + next域:存放结点的直接后继的地址(位置)的指针域(链域)。 + 注意:①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。    + ②每个结点只有一个链域的链表称为单链表(Single Linked List)。 +所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。 + +和数组相比,链表的优势在于长度不受限制,并且在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组想同,实际效率上还是比数组要高很多 + +劣势在于随机访问,无法像数组那样直接通过下标找到特定的数据项 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From d08bb5ce3ce0a4a9fbf06d10bf53f045a5ad89a0 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 7 Mar 2016 16:31:46 +0800 Subject: [PATCH 005/373] Add some notes. --- .../ART\344\270\216Dalvik.md" | 41 + ...57\345\212\250\350\277\207\347\250\213.md" | 1116 +++++++++++++++++ .../Parcelable\345\217\212Serializable.md" | 21 + ...36\346\224\266\346\234\272\345\210\266.md" | 106 ++ ...01\350\231\232\345\274\225\347\224\250.md" | 3 + 5 files changed, 1287 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" create mode 100644 "Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" diff --git "a/Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" "b/Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" new file mode 100644 index 00000000..4158d194 --- /dev/null +++ "b/Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" @@ -0,0 +1,41 @@ +ART与Dalvik +=== + +ART +--- + +`Android 4.4`提供了一种与`Dalvik`截然不同的运行环境`ART`支持,`ART`源于`google`收购的`Flexycore`的公司。 +`ART`模式与`Dalvik`模式最大的不同在于,启用`ART`模式后,系统在安装应用的时候会进行一次预编译,将字节码转换为机器语言存储在本地, +这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。 +`ART`代表`Android Runtime`,其处理应用程序执行的方式完全不同于`Dalvik`,`Dalvik`是依靠一个`Just-In-Time` (`JIT`)编译器去解释字节码。 +开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。 +`ART`则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫`Ahead-Of-Time`(`AOT`)编译。 +在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。 + +Dalvik +--- + +`Dalvik`是`Google`公司自己设计用于`Android`平台的`Java`虚拟机。它可以支持已转换为`.dex`(即`Dalvik Executable`)格式的`Java`应用程序的运行, +`.dex`格式是专为`Dalvik`设计的一种压缩格式,适合内存和处理器速度有限的系统。`Dalvik`经过优化,允许在有限的内存中同时运行多个虚拟机的实例, +并且每一个`Dalvik`应用作为一个独立的`Linux`进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 + +`AOT`的编译器分两种模式: +- 在开发机上编译预装应用; +- 在设备上编译新安装的应用,在应用安装时将dex字节码翻译成本地机器码。 + +ART优点: +- 系统性能的显著提升。 +- 应用启动更快、运行更快、体验更流畅、触感反馈更及时。 +- 更长的电池续航能力。 +- 支持更低的硬件。 + +ART缺点: +- 更大的存储空间占用,可能会增加10%-20%。 +- 更长的应用安装时间。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" index cca9b41c..43ba62de 100644 --- "a/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" +++ "b/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" @@ -377,6 +377,1122 @@ class ActivityManagerProxy implements IActivityManager } ``` 再继续我就不知道走到哪里了.... +这一块其实是用了`Binder`机制,上面的`IActivityManager.getDefault()`方法返回的是`ActivityManagerService`的远程接口,所以接下来 +我们应该看一下`ActivityManagerService.startActivity()`类。(具体`Binder`机制我们后续会专门写一篇文章,这里就不说了,不然就说不完了) +```java +@Override +public final int startActivity(IApplicationThread caller, String callingPackage, + Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, + int startFlags, ProfilerInfo profilerInfo, Bundle options) { + return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo, + resultWho, requestCode, startFlags, profilerInfo, options, + UserHandle.getCallingUserId()); +} +``` +继续看下`startActivityAsUser()`方法: +```java +@Override +public final int startActivityAsUser(IApplicationThread caller, String callingPackage, + Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, + int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) { + enforceNotIsolatedCaller("startActivity"); + userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, + false, ALLOW_FULL_ONLY, "startActivity", null); + // TODO: Switch to user app stacks here. + return mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent, + resolvedType, null, null, resultTo, resultWho, requestCode, startFlags, + profilerInfo, null, null, options, false, userId, null, null); +} +``` +继续看一下`mStackSupervisor.startActivityMayWait()`方法,这里`mStackSupervisor`是`ActivityStackSupervisor`类: +```java +final int startActivityMayWait(IApplicationThread caller, int callingUid, + String callingPackage, Intent intent, String resolvedType, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + IBinder resultTo, String resultWho, int requestCode, int startFlags, + ProfilerInfo profilerInfo, WaitResult outResult, Configuration config, + Bundle options, boolean ignoreTargetSecurity, int userId, + IActivityContainer iContainer, TaskRecord inTask) { + // Refuse possible leaked file descriptors + if (intent != null && intent.hasFileDescriptors()) { + throw new IllegalArgumentException("File descriptors passed in Intent"); + } + boolean componentSpecified = intent.getComponent() != null; + + // Don't modify the client's object! + intent = new Intent(intent); + + // 解析出要开启的Activity的信息,包名、类名、参数等。 + // Collect information about the target of the Intent. + ActivityInfo aInfo = + resolveActivity(intent, resolvedType, startFlags, profilerInfo, userId); + + ActivityContainer container = (ActivityContainer)iContainer; + synchronized (mService) { + if (container != null && container.mParentActivity != null && + container.mParentActivity.state != RESUMED) { + // Cannot start a child activity if the parent is not resumed. + return ActivityManager.START_CANCELED; + } + final int realCallingPid = Binder.getCallingPid(); + final int realCallingUid = Binder.getCallingUid(); + int callingPid; + if (callingUid >= 0) { + callingPid = -1; + } else if (caller == null) { + callingPid = realCallingPid; + callingUid = realCallingUid; + } else { + callingPid = callingUid = -1; + } + + final ActivityStack stack; + if (container == null || container.mStack.isOnHomeDisplay()) { + stack = mFocusedStack; + } else { + stack = container.mStack; + } + stack.mConfigWillChange = config != null && mService.mConfiguration.diff(config) != 0; + if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, + "Starting activity when config will change = " + stack.mConfigWillChange); + + final long origId = Binder.clearCallingIdentity(); + + if (aInfo != null && + (aInfo.applicationInfo.privateFlags + &ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) { + // This may be a heavy-weight process! Check to see if we already + // have another, different heavy-weight process running. + if (aInfo.processName.equals(aInfo.applicationInfo.packageName)) { + if (mService.mHeavyWeightProcess != null && + (mService.mHeavyWeightProcess.info.uid != aInfo.applicationInfo.uid || + !mService.mHeavyWeightProcess.processName.equals(aInfo.processName))) { + int appCallingUid = callingUid; + if (caller != null) { + ProcessRecord callerApp = mService.getRecordForAppLocked(caller); + if (callerApp != null) { + appCallingUid = callerApp.info.uid; + } else { + Slog.w(TAG, "Unable to find app for caller " + caller + + " (pid=" + callingPid + ") when starting: " + + intent.toString()); + ActivityOptions.abort(options); + return ActivityManager.START_PERMISSION_DENIED; + } + } + + IIntentSender target = mService.getIntentSenderLocked( + ActivityManager.INTENT_SENDER_ACTIVITY, "android", + appCallingUid, userId, null, null, 0, new Intent[] { intent }, + new String[] { resolvedType }, PendingIntent.FLAG_CANCEL_CURRENT + | PendingIntent.FLAG_ONE_SHOT, null); + + Intent newIntent = new Intent(); + if (requestCode >= 0) { + // Caller is requesting a result. + newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_HAS_RESULT, true); + } + newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_INTENT, + new IntentSender(target)); + if (mService.mHeavyWeightProcess.activities.size() > 0) { + ActivityRecord hist = mService.mHeavyWeightProcess.activities.get(0); + newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_CUR_APP, + hist.packageName); + newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_CUR_TASK, + hist.task.taskId); + } + newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_NEW_APP, + aInfo.packageName); + newIntent.setFlags(intent.getFlags()); + newIntent.setClassName("android", + HeavyWeightSwitcherActivity.class.getName()); + intent = newIntent; + resolvedType = null; + caller = null; + callingUid = Binder.getCallingUid(); + callingPid = Binder.getCallingPid(); + componentSpecified = true; + try { + ResolveInfo rInfo = + AppGlobals.getPackageManager().resolveIntent( + intent, null, + PackageManager.MATCH_DEFAULT_ONLY + | ActivityManagerService.STOCK_PM_FLAGS, userId); + aInfo = rInfo != null ? rInfo.activityInfo : null; + aInfo = mService.getActivityInfoForUser(aInfo, userId); + } catch (RemoteException e) { + aInfo = null; + } + } + } + } + // 根据上面解析的内容去开启了。。。 + int res = startActivityLocked(caller, intent, resolvedType, aInfo, + voiceSession, voiceInteractor, resultTo, resultWho, + requestCode, callingPid, callingUid, callingPackage, + realCallingPid, realCallingUid, startFlags, options, ignoreTargetSecurity, + componentSpecified, null, container, inTask); + + Binder.restoreCallingIdentity(origId); + + if (stack.mConfigWillChange) { + // If the caller also wants to switch to a new configuration, + // do so now. This allows a clean switch, as we are waiting + // for the current activity to pause (so we will not destroy + // it), and have not yet started the next activity. + mService.enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION, + "updateConfiguration()"); + stack.mConfigWillChange = false; + if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, + "Updating to new configuration after starting activity."); + mService.updateConfigurationLocked(config, null, false, false); + } + + if (outResult != null) { + outResult.result = res; + if (res == ActivityManager.START_SUCCESS) { + mWaitingActivityLaunched.add(outResult); + do { + try { + mService.wait(); + } catch (InterruptedException e) { + } + } while (!outResult.timeout && outResult.who == null); + } else if (res == ActivityManager.START_TASK_TO_FRONT) { + ActivityRecord r = stack.topRunningActivityLocked(null); + if (r.nowVisible && r.state == RESUMED) { + outResult.timeout = false; + outResult.who = new ComponentName(r.info.packageName, r.info.name); + outResult.totalTime = 0; + outResult.thisTime = 0; + } else { + outResult.thisTime = SystemClock.uptimeMillis(); + mWaitingActivityVisible.add(outResult); + do { + try { + mService.wait(); + } catch (InterruptedException e) { + } + } while (!outResult.timeout && outResult.who == null); + } + } + } + + return res; + } +} +``` +那就继续看一下`startActivityLocked()`方法: +```java +final int startActivityLocked(IApplicationThread caller, + Intent intent, String resolvedType, ActivityInfo aInfo, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, + IBinder resultTo, String resultWho, int requestCode, + int callingPid, int callingUid, String callingPackage, + int realCallingPid, int realCallingUid, int startFlags, Bundle options, + boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, + ActivityContainer container, TaskRecord inTask) { + int err = ActivityManager.START_SUCCESS; + + ProcessRecord callerApp = null; + if (caller != null) { + // mService就是ActivityManagerService类 + callerApp = mService.getRecordForAppLocked(caller); + if (callerApp != null) { + callingPid = callerApp.pid; + callingUid = callerApp.info.uid; + } else { + Slog.w(TAG, "Unable to find app for caller " + caller + + " (pid=" + callingPid + ") when starting: " + + intent.toString()); + err = ActivityManager.START_PERMISSION_DENIED; + } + } + + final int userId = aInfo != null ? UserHandle.getUserId(aInfo.applicationInfo.uid) : 0; + + if (err == ActivityManager.START_SUCCESS) { + Slog.i(TAG, "START u" + userId + " {" + intent.toShortString(true, true, true, false) + + "} from uid " + callingUid + + " on display " + (container == null ? (mFocusedStack == null ? + Display.DEFAULT_DISPLAY : mFocusedStack.mDisplayId) : + (container.mActivityDisplay == null ? Display.DEFAULT_DISPLAY : + container.mActivityDisplay.mDisplayId))); + } + + ActivityRecord sourceRecord = null; + ActivityRecord resultRecord = null; + if (resultTo != null) { + sourceRecord = isInAnyStackLocked(resultTo); + if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, + "Will send result to " + resultTo + " " + sourceRecord); + if (sourceRecord != null) { + if (requestCode >= 0 && !sourceRecord.finishing) { + resultRecord = sourceRecord; + } + } + } + + final int launchFlags = intent.getFlags(); + + if ((launchFlags & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0 && sourceRecord != null) { + // Transfer the result target from the source activity to the new + // one being started, including any failures. + if (requestCode >= 0) { + ActivityOptions.abort(options); + return ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT; + } + resultRecord = sourceRecord.resultTo; + if (resultRecord != null && !resultRecord.isInStackLocked()) { + resultRecord = null; + } + resultWho = sourceRecord.resultWho; + requestCode = sourceRecord.requestCode; + sourceRecord.resultTo = null; + if (resultRecord != null) { + resultRecord.removeResultsLocked(sourceRecord, resultWho, requestCode); + } + if (sourceRecord.launchedFromUid == callingUid) { + // The new activity is being launched from the same uid as the previous + // activity in the flow, and asking to forward its result back to the + // previous. In this case the activity is serving as a trampoline between + // the two, so we also want to update its launchedFromPackage to be the + // same as the previous activity. Note that this is safe, since we know + // these two packages come from the same uid; the caller could just as + // well have supplied that same package name itself. This specifially + // deals with the case of an intent picker/chooser being launched in the app + // flow to redirect to an activity picked by the user, where we want the final + // activity to consider it to have been launched by the previous app activity. + callingPackage = sourceRecord.launchedFromPackage; + } + } + + if (err == ActivityManager.START_SUCCESS && intent.getComponent() == null) { + // We couldn't find a class that can handle the given Intent. + // That's the end of that! + err = ActivityManager.START_INTENT_NOT_RESOLVED; + } + + if (err == ActivityManager.START_SUCCESS && aInfo == null) { + // We couldn't find the specific class specified in the Intent. + // Also the end of the line. + err = ActivityManager.START_CLASS_NOT_FOUND; + } + + if (err == ActivityManager.START_SUCCESS + && !isCurrentProfileLocked(userId) + && (aInfo.flags & FLAG_SHOW_FOR_ALL_USERS) == 0) { + // Trying to launch a background activity that doesn't show for all users. + err = ActivityManager.START_NOT_CURRENT_USER_ACTIVITY; + } + + if (err == ActivityManager.START_SUCCESS && sourceRecord != null + && sourceRecord.task.voiceSession != null) { + // If this activity is being launched as part of a voice session, we need + // to ensure that it is safe to do so. If the upcoming activity will also + // be part of the voice session, we can only launch it if it has explicitly + // said it supports the VOICE category, or it is a part of the calling app. + if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) == 0 + && sourceRecord.info.applicationInfo.uid != aInfo.applicationInfo.uid) { + try { + intent.addCategory(Intent.CATEGORY_VOICE); + if (!AppGlobals.getPackageManager().activitySupportsIntent( + intent.getComponent(), intent, resolvedType)) { + Slog.w(TAG, + "Activity being started in current voice task does not support voice: " + + intent); + err = ActivityManager.START_NOT_VOICE_COMPATIBLE; + } + } catch (RemoteException e) { + Slog.w(TAG, "Failure checking voice capabilities", e); + err = ActivityManager.START_NOT_VOICE_COMPATIBLE; + } + } + } + + if (err == ActivityManager.START_SUCCESS && voiceSession != null) { + // If the caller is starting a new voice session, just make sure the target + // is actually allowing it to run this way. + try { + if (!AppGlobals.getPackageManager().activitySupportsIntent(intent.getComponent(), + intent, resolvedType)) { + Slog.w(TAG, + "Activity being started in new voice task does not support: " + + intent); + err = ActivityManager.START_NOT_VOICE_COMPATIBLE; + } + } catch (RemoteException e) { + Slog.w(TAG, "Failure checking voice capabilities", e); + err = ActivityManager.START_NOT_VOICE_COMPATIBLE; + } + } + // Activity栈 + final ActivityStack resultStack = resultRecord == null ? null : resultRecord.task.stack; + + if (err != ActivityManager.START_SUCCESS) { + if (resultRecord != null) { + resultStack.sendActivityResultLocked(-1, + resultRecord, resultWho, requestCode, + Activity.RESULT_CANCELED, null); + } + ActivityOptions.abort(options); + return err; + } + + boolean abort = false; + + final int startAnyPerm = mService.checkPermission( + START_ANY_ACTIVITY, callingPid, callingUid); + + if (startAnyPerm != PERMISSION_GRANTED) { + final int componentRestriction = getComponentRestrictionForCallingPackage( + aInfo, callingPackage, callingPid, callingUid, ignoreTargetSecurity); + final int actionRestriction = getActionRestrictionForCallingPackage( + intent.getAction(), callingPackage, callingPid, callingUid); + + if (componentRestriction == ACTIVITY_RESTRICTION_PERMISSION + || actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) { + if (resultRecord != null) { + resultStack.sendActivityResultLocked(-1, + resultRecord, resultWho, requestCode, + Activity.RESULT_CANCELED, null); + } + String msg; + if (actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) { + msg = "Permission Denial: starting " + intent.toString() + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ")" + " with revoked permission " + + ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction()); + } else if (!aInfo.exported) { + msg = "Permission Denial: starting " + intent.toString() + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ")" + + " not exported from uid " + aInfo.applicationInfo.uid; + } else { + msg = "Permission Denial: starting " + intent.toString() + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ")" + + " requires " + aInfo.permission; + } + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + + if (actionRestriction == ACTIVITY_RESTRICTION_APPOP) { + String message = "Appop Denial: starting " + intent.toString() + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ")" + + " requires " + AppOpsManager.permissionToOp( + ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction())); + Slog.w(TAG, message); + abort = true; + } else if (componentRestriction == ACTIVITY_RESTRICTION_APPOP) { + String message = "Appop Denial: starting " + intent.toString() + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ")" + + " requires appop " + AppOpsManager.permissionToOp(aInfo.permission); + Slog.w(TAG, message); + abort = true; + } + } + + abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid, + callingPid, resolvedType, aInfo.applicationInfo); + + if (mService.mController != null) { + try { + // The Intent we give to the watcher has the extra data + // stripped off, since it can contain private information. + Intent watchIntent = intent.cloneFilter(); + abort |= !mService.mController.activityStarting(watchIntent, + aInfo.applicationInfo.packageName); + } catch (RemoteException e) { + mService.mController = null; + } + } + + if (abort) { + if (resultRecord != null) { + resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode, + Activity.RESULT_CANCELED, null); + } + // We pretend to the caller that it was really started, but + // they will just get a cancel result. + ActivityOptions.abort(options); + return ActivityManager.START_SUCCESS; + } + // ActivityRecord: An entry in the history stack, representing an activity. + ActivityRecord r = new ActivityRecord(mService, callerApp, callingUid, callingPackage, + intent, resolvedType, aInfo, mService.mConfiguration, resultRecord, resultWho, + requestCode, componentSpecified, voiceSession != null, this, container, options); + if (outActivity != null) { + outActivity[0] = r; + } + + if (r.appTimeTracker == null && sourceRecord != null) { + // If the caller didn't specify an explicit time tracker, we want to continue + // tracking under any it has. + r.appTimeTracker = sourceRecord.appTimeTracker; + } + + final ActivityStack stack = mFocusedStack; + if (voiceSession == null && (stack.mResumedActivity == null + || stack.mResumedActivity.info.applicationInfo.uid != callingUid)) { + if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid, + realCallingPid, realCallingUid, "Activity start")) { + PendingActivityLaunch pal = + new PendingActivityLaunch(r, sourceRecord, startFlags, stack); + mPendingActivityLaunches.add(pal); + ActivityOptions.abort(options); + return ActivityManager.START_SWITCHES_CANCELED; + } + } + + if (mService.mDidAppSwitch) { + // This is the second allowed switch since we stopped switches, + // so now just generally allow switches. Use case: user presses + // home (switches disabled, switch to home, mDidAppSwitch now true); + // user taps a home icon (coming from home so allowed, we hit here + // and now allow anyone to switch again). + mService.mAppSwitchesAllowedTime = 0; + } else { + mService.mDidAppSwitch = true; + } + + doPendingActivityLaunchesLocked(false); + // 继续调用方法 + err = startActivityUncheckedLocked(r, sourceRecord, voiceSession, voiceInteractor, + startFlags, true, options, inTask); + + if (err < 0) { + // If someone asked to have the keyguard dismissed on the next + // activity start, but we are not actually doing an activity + // switch... just dismiss the keyguard now, because we + // probably want to see whatever is behind it. + notifyActivityDrawnForKeyguard(); + } + return err; +} +``` +再继续看`startActivityUncheckedLocked`方法:     +```java +final int startActivityUncheckedLocked(final ActivityRecord r, ActivityRecord sourceRecord, + IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags, + boolean doResume, Bundle options, TaskRecord inTask) { + final Intent intent = r.intent; + final int callingUid = r.launchedFromUid; + + // In some flows in to this function, we retrieve the task record and hold on to it + // without a lock before calling back in to here... so the task at this point may + // not actually be in recents. Check for that, and if it isn't in recents just + // consider it invalid. + if (inTask != null && !inTask.inRecents) { + Slog.w(TAG, "Starting activity in task not in recents: " + inTask); + inTask = null; + } + + // 判断不同的启动模式 + final boolean launchSingleTop = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP; + final boolean launchSingleInstance = r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE; + final boolean launchSingleTask = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK; + + ..... + + // We may want to try to place the new activity in to an existing task. We always + // do this if the target activity is singleTask or singleInstance; we will also do + // this if NEW_TASK has been requested, and there is not an additional qualifier telling + // us to still place it in a new task: multi task, always doc mode, or being asked to + // launch this as a new task behind the current one. + if (((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0 && + (launchFlags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0) + || launchSingleInstance || launchSingleTask) { + // If bring to front is requested, and no result is requested and we have not + // been given an explicit task to launch in to, and + // we can find a task that was started with this same + // component, then instead of launching bring that one to the front. + if (inTask == null && r.resultTo == null) { + // See if there is a task to bring to the front. If this is + // a SINGLE_INSTANCE activity, there can be one and only one + // instance of it in the history, and it is always in its own + // unique task, so we do a special search. + ActivityRecord intentActivity = !launchSingleInstance ? + findTaskLocked(r) : findActivityLocked(intent, r.info); + if (intentActivity != null) { + // When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused + // but still needs to be a lock task mode violation since the task gets + // cleared out and the device would otherwise leave the locked task. + if (isLockTaskModeViolation(intentActivity.task, + (launchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK)) + == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))) { + showLockTaskToast(); + Slog.e(TAG, "startActivityUnchecked: Attempt to violate Lock Task Mode"); + return ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION; + } + if (r.task == null) { + r.task = intentActivity.task; + } + if (intentActivity.task.intent == null) { + // This task was started because of movement of + // the activity based on affinity... now that we + // are actually launching it, we can assign the + // base intent. + intentActivity.task.setIntent(r); + } + targetStack = intentActivity.task.stack; + targetStack.mLastPausedActivity = null; + // If the target task is not in the front, then we need + // to bring it to the front... except... well, with + // SINGLE_TASK_LAUNCH it's not entirely clear. We'd like + // to have the same behavior as if a new instance was + // being started, which means not bringing it to the front + // if the caller is not itself in the front. + final ActivityStack focusStack = getFocusedStack(); + ActivityRecord curTop = (focusStack == null) + ? null : focusStack.topRunningNonDelayedActivityLocked(notTop); + boolean movedToFront = false; + if (curTop != null && (curTop.task != intentActivity.task || + curTop.task != focusStack.topTask())) { + r.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); + if (sourceRecord == null || (sourceStack.topActivity() != null && + sourceStack.topActivity().task == sourceRecord.task)) { + // We really do want to push this one into the user's face, right now. + if (launchTaskBehind && sourceRecord != null) { + intentActivity.setTaskToAffiliateWith(sourceRecord.task); + } + movedHome = true; + targetStack.moveTaskToFrontLocked(intentActivity.task, noAnimation, + options, r.appTimeTracker, "bringingFoundTaskToFront"); + movedToFront = true; + if ((launchFlags & + (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME)) + == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME)) { + // Caller wants to appear on home activity. + intentActivity.task.setTaskToReturnTo(HOME_ACTIVITY_TYPE); + } + options = null; + } + } + if (!movedToFront) { + if (DEBUG_TASKS) Slog.d(TAG_TASKS, "Bring to front target: " + targetStack + + " from " + intentActivity); + targetStack.moveToFront("intentActivityFound"); + } + + // If the caller has requested that the target task be + // reset, then do so. + if ((launchFlags&Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) { + intentActivity = targetStack.resetTaskIfNeededLocked(intentActivity, r); + } + if ((startFlags & ActivityManager.START_FLAG_ONLY_IF_NEEDED) != 0) { + // We don't need to start a new activity, and + // the client said not to do anything if that + // is the case, so this is it! And for paranoia, make + // sure we have correctly resumed the top activity. + if (doResume) { + resumeTopActivitiesLocked(targetStack, null, options); + + // Make sure to notify Keyguard as well if we are not running an app + // transition later. + if (!movedToFront) { + notifyActivityDrawnForKeyguard(); + } + } else { + ActivityOptions.abort(options); + } + return ActivityManager.START_RETURN_INTENT_TO_CALLER; + } + if ((launchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK)) + == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK)) { + // The caller has requested to completely replace any + // existing task with its new activity. Well that should + // not be too hard... + reuseTask = intentActivity.task; + reuseTask.performClearTaskLocked(); + reuseTask.setIntent(r); + } else if ((launchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0 + || launchSingleInstance || launchSingleTask) { + // In this situation we want to remove all activities + // from the task up to the one being started. In most + // cases this means we are resetting the task to its + // initial state. + ActivityRecord top = + intentActivity.task.performClearTaskLocked(r, launchFlags); + if (top != null) { + if (top.frontOfTask) { + // Activity aliases may mean we use different + // intents for the top activity, so make sure + // the task now has the identity of the new + // intent. + top.task.setIntent(r); + } + ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, + r, top.task); + top.deliverNewIntentLocked(callingUid, r.intent, r.launchedFromPackage); + } else { + // A special case: we need to start the activity because it is not + // currently running, and the caller has asked to clear the current + // task to have this activity at the top. + addingToTask = true; + // Now pretend like this activity is being started by the top of its + // task, so it is put in the right place. + sourceRecord = intentActivity; + TaskRecord task = sourceRecord.task; + if (task != null && task.stack == null) { + // Target stack got cleared when we all activities were removed + // above. Go ahead and reset it. + targetStack = computeStackFocus(sourceRecord, false /* newTask */); + targetStack.addTask( + task, !launchTaskBehind /* toTop */, false /* moving */); + } + + } + } else if (r.realActivity.equals(intentActivity.task.realActivity)) { + // In this case the top activity on the task is the + // same as the one being launched, so we take that + // as a request to bring the task to the foreground. + // If the top activity in the task is the root + // activity, deliver this new intent to it if it + // desires. + if (((launchFlags&Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0 || launchSingleTop) + && intentActivity.realActivity.equals(r.realActivity)) { + ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, r, + intentActivity.task); + if (intentActivity.frontOfTask) { + intentActivity.task.setIntent(r); + } + intentActivity.deliverNewIntentLocked(callingUid, r.intent, + r.launchedFromPackage); + } else if (!r.intent.filterEquals(intentActivity.task.intent)) { + // In this case we are launching the root activity + // of the task, but with a different intent. We + // should start a new instance on top. + addingToTask = true; + sourceRecord = intentActivity; + } + } else if ((launchFlags&Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) == 0) { + // In this case an activity is being launched in to an + // existing task, without resetting that task. This + // is typically the situation of launching an activity + // from a notification or shortcut. We want to place + // the new activity on top of the current task. + addingToTask = true; + sourceRecord = intentActivity; + } else if (!intentActivity.task.rootWasReset) { + // In this case we are launching in to an existing task + // that has not yet been started from its front door. + // The current task has been brought to the front. + // Ideally, we'd probably like to place this new task + // at the bottom of its stack, but that's a little hard + // to do with the current organization of the code so + // for now we'll just drop it. + intentActivity.task.setIntent(r); + } + if (!addingToTask && reuseTask == null) { + // We didn't do anything... but it was needed (a.k.a., client + // don't use that intent!) And for paranoia, make + // sure we have correctly resumed the top activity. + if (doResume) { + targetStack.resumeTopActivityLocked(null, options); + if (!movedToFront) { + // Make sure to notify Keyguard as well if we are not running an app + // transition later. + notifyActivityDrawnForKeyguard(); + } + } else { + ActivityOptions.abort(options); + } + return ActivityManager.START_TASK_TO_FRONT; + } + } + } + } + + //String uri = r.intent.toURI(); + //Intent intent2 = new Intent(uri); + //Slog.i(TAG, "Given intent: " + r.intent); + //Slog.i(TAG, "URI is: " + uri); + //Slog.i(TAG, "To intent: " + intent2); + + if (r.packageName != null) { + // If the activity being launched is the same as the one currently + // at the top, then we need to check if it should only be launched + // once. + ActivityStack topStack = mFocusedStack; + ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(notTop); + if (top != null && r.resultTo == null) { + if (top.realActivity.equals(r.realActivity) && top.userId == r.userId) { + if (top.app != null && top.app.thread != null) { + if ((launchFlags & Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0 + || launchSingleTop || launchSingleTask) { + ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, top, + top.task); + // For paranoia, make sure we have correctly + // resumed the top activity. + topStack.mLastPausedActivity = null; + if (doResume) { + resumeTopActivitiesLocked(); + } + ActivityOptions.abort(options); + if ((startFlags&ActivityManager.START_FLAG_ONLY_IF_NEEDED) != 0) { + // We don't need to start a new activity, and + // the client said not to do anything if that + // is the case, so this is it! + return ActivityManager.START_RETURN_INTENT_TO_CALLER; + } + top.deliverNewIntentLocked(callingUid, r.intent, r.launchedFromPackage); + return ActivityManager.START_DELIVERED_TO_TOP; + } + } + } + } + + } else { + if (r.resultTo != null && r.resultTo.task.stack != null) { + r.resultTo.task.stack.sendActivityResultLocked(-1, r.resultTo, r.resultWho, + r.requestCode, Activity.RESULT_CANCELED, null); + } + ActivityOptions.abort(options); + return ActivityManager.START_CLASS_NOT_FOUND; + } + + boolean newTask = false; + boolean keepCurTransition = false; + + TaskRecord taskToAffiliate = launchTaskBehind && sourceRecord != null ? + sourceRecord.task : null; + + // Should this be considered a new task? + if (r.resultTo == null && inTask == null && !addingToTask + && (launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { + newTask = true; + targetStack = computeStackFocus(r, newTask); + targetStack.moveToFront("startingNewTask"); + + if (reuseTask == null) { + r.setTask(targetStack.createTaskRecord(getNextTaskId(), + newTaskInfo != null ? newTaskInfo : r.info, + newTaskIntent != null ? newTaskIntent : intent, + voiceSession, voiceInteractor, !launchTaskBehind /* toTop */), + taskToAffiliate); + if (DEBUG_TASKS) Slog.v(TAG_TASKS, + "Starting new activity " + r + " in new task " + r.task); + } else { + r.setTask(reuseTask, taskToAffiliate); + } + if (isLockTaskModeViolation(r.task)) { + Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r); + return ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION; + } + if (!movedHome) { + if ((launchFlags & + (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME)) + == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME)) { + // Caller wants to appear on home activity, so before starting + // their own activity we will bring home to the front. + r.task.setTaskToReturnTo(HOME_ACTIVITY_TYPE); + } + } + } else if (sourceRecord != null) { + final TaskRecord sourceTask = sourceRecord.task; + if (isLockTaskModeViolation(sourceTask)) { + Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r); + return ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION; + } + targetStack = sourceTask.stack; + targetStack.moveToFront("sourceStackToFront"); + final TaskRecord topTask = targetStack.topTask(); + if (topTask != sourceTask) { + targetStack.moveTaskToFrontLocked(sourceTask, noAnimation, options, + r.appTimeTracker, "sourceTaskToFront"); + } + if (!addingToTask && (launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) { + // In this case, we are adding the activity to an existing + // task, but the caller has asked to clear that task if the + // activity is already running. + ActivityRecord top = sourceTask.performClearTaskLocked(r, launchFlags); + keepCurTransition = true; + if (top != null) { + ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, r, top.task); + top.deliverNewIntentLocked(callingUid, r.intent, r.launchedFromPackage); + // For paranoia, make sure we have correctly + // resumed the top activity. + targetStack.mLastPausedActivity = null; + if (doResume) { + targetStack.resumeTopActivityLocked(null); + } + ActivityOptions.abort(options); + return ActivityManager.START_DELIVERED_TO_TOP; + } + } else if (!addingToTask && + (launchFlags&Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) != 0) { + // In this case, we are launching an activity in our own task + // that may already be running somewhere in the history, and + // we want to shuffle it to the front of the stack if so. + final ActivityRecord top = sourceTask.findActivityInHistoryLocked(r); + if (top != null) { + final TaskRecord task = top.task; + task.moveActivityToFrontLocked(top); + ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, r, task); + top.updateOptionsLocked(options); + top.deliverNewIntentLocked(callingUid, r.intent, r.launchedFromPackage); + targetStack.mLastPausedActivity = null; + if (doResume) { + targetStack.resumeTopActivityLocked(null); + } + return ActivityManager.START_DELIVERED_TO_TOP; + } + } + // An existing activity is starting this new activity, so we want + // to keep the new one in the same task as the one that is starting + // it. + r.setTask(sourceTask, null); + if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + r + + " in existing task " + r.task + " from source " + sourceRecord); + + } else if (inTask != null) { + // The caller is asking that the new activity be started in an explicit + // task it has provided to us. + if (isLockTaskModeViolation(inTask)) { + Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r); + return ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION; + } + targetStack = inTask.stack; + targetStack.moveTaskToFrontLocked(inTask, noAnimation, options, r.appTimeTracker, + "inTaskToFront"); + + // Check whether we should actually launch the new activity in to the task, + // or just reuse the current activity on top. + ActivityRecord top = inTask.getTopActivity(); + if (top != null && top.realActivity.equals(r.realActivity) && top.userId == r.userId) { + if ((launchFlags & Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0 + || launchSingleTop || launchSingleTask) { + ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, top, top.task); + if ((startFlags&ActivityManager.START_FLAG_ONLY_IF_NEEDED) != 0) { + // We don't need to start a new activity, and + // the client said not to do anything if that + // is the case, so this is it! + return ActivityManager.START_RETURN_INTENT_TO_CALLER; + } + top.deliverNewIntentLocked(callingUid, r.intent, r.launchedFromPackage); + return ActivityManager.START_DELIVERED_TO_TOP; + } + } + + if (!addingToTask) { + // We don't actually want to have this activity added to the task, so just + // stop here but still tell the caller that we consumed the intent. + ActivityOptions.abort(options); + return ActivityManager.START_TASK_TO_FRONT; + } + + r.setTask(inTask, null); + if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + r + + " in explicit task " + r.task); + + } else { + // This not being started from an existing activity, and not part + // of a new task... just put it in the top task, though these days + // this case should never happen. + targetStack = computeStackFocus(r, newTask); + targetStack.moveToFront("addingToTopTask"); + ActivityRecord prev = targetStack.topActivity(); + r.setTask(prev != null ? prev.task : targetStack.createTaskRecord(getNextTaskId(), + r.info, intent, null, null, true), null); + mWindowManager.moveTaskToTop(r.task.taskId); + if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + r + + " in new guessed " + r.task); + } + + mService.grantUriPermissionFromIntentLocked(callingUid, r.packageName, + intent, r.getUriPermissionsLocked(), r.userId); + + if (sourceRecord != null && sourceRecord.isRecentsActivity()) { + r.task.setTaskToReturnTo(RECENTS_ACTIVITY_TYPE); + } + if (newTask) { + EventLog.writeEvent(EventLogTags.AM_CREATE_TASK, r.userId, r.task.taskId); + } + ActivityStack.logStartActivity(EventLogTags.AM_CREATE_ACTIVITY, r, r.task); + targetStack.mLastPausedActivity = null; + + // 上面这部分代码很多,各种启动模式、栈的处理啊,我们就不继续看了,接着看下面的启动。 + targetStack.startActivityLocked(r, newTask, doResume, keepCurTransition, options); + if (!launchTaskBehind) { + // Don't set focus on an activity that's going to the back. + mService.setFocusedActivityLocked(r, "startedActivity"); + } + return ActivityManager.START_SUCCESS; +} +``` +继续看`targetStack.startActivityLocked()`方法,这里`targetStack`就是`ActivityStack`类: +```java +final void startActivityLocked(ActivityRecord r, boolean newTask, + boolean doResume, boolean keepCurTransition, Bundle options) { + TaskRecord rTask = r.task; + final int taskId = rTask.taskId; + // mLaunchTaskBehind tasks get placed at the back of the task stack. + if (!r.mLaunchTaskBehind && (taskForIdLocked(taskId) == null || newTask)) { + // Last activity in task had been removed or ActivityManagerService is reusing task. + // Insert or replace. + // Might not even be in. + insertTaskAtTop(rTask, r); + mWindowManager.moveTaskToTop(taskId); + } + TaskRecord task = null; + if (!newTask) { + // If starting in an existing task, find where that is... + boolean startIt = true; + for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) { + task = mTaskHistory.get(taskNdx); + if (task.getTopActivity() == null) { + // All activities in task are finishing. + continue; + } + if (task == r.task) { + // Here it is! Now, if this is not yet visible to the + // user, then just add it without starting; it will + // get started when the user navigates back to it. + if (!startIt) { + if (DEBUG_ADD_REMOVE) Slog.i(TAG, "Adding activity " + r + " to task " + + task, new RuntimeException("here").fillInStackTrace()); + task.addActivityToTop(r); + r.putInHistory(); + mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken, + r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen, + (r.info.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0, + r.userId, r.info.configChanges, task.voiceSession != null, + r.mLaunchTaskBehind); + if (VALIDATE_TOKENS) { + validateAppTokensLocked(); + } + ActivityOptions.abort(options); + return; + } + break; + } else if (task.numFullscreen > 0) { + startIt = false; + } + } + } + + // Place a new activity at top of stack, so it is next to interact + // with the user. + + // If we are not placing the new activity frontmost, we do not want + // to deliver the onUserLeaving callback to the actual frontmost + // activity + if (task == r.task && mTaskHistory.indexOf(task) != (mTaskHistory.size() - 1)) { + mStackSupervisor.mUserLeaving = false; + if (DEBUG_USER_LEAVING) Slog.v(TAG_USER_LEAVING, + "startActivity() behind front, mUserLeaving=false"); + } + + task = r.task; + + // Slot the activity into the history stack and proceed + if (DEBUG_ADD_REMOVE) Slog.i(TAG, "Adding activity " + r + " to stack to task " + task, + new RuntimeException("here").fillInStackTrace()); + task.addActivityToTop(r); + task.setFrontOfTask(); + + r.putInHistory(); + if (!isHomeStack() || numActivities() > 0) { + // We want to show the starting preview window if we are + // switching to a new task, or the next activity's process is + // not currently running. + boolean showStartingIcon = newTask; + ProcessRecord proc = r.app; + if (proc == null) { + proc = mService.mProcessNames.get(r.processName, r.info.applicationInfo.uid); + } + if (proc == null || proc.thread == null) { + showStartingIcon = true; + } + if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION, + "Prepare open transition: starting " + r); + if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_ANIMATION) != 0) { + mWindowManager.prepareAppTransition(AppTransition.TRANSIT_NONE, keepCurTransition); + mNoAnimActivities.add(r); + } else { + mWindowManager.prepareAppTransition(newTask + ? r.mLaunchTaskBehind + ? AppTransition.TRANSIT_TASK_OPEN_BEHIND + : AppTransition.TRANSIT_TASK_OPEN + : AppTransition.TRANSIT_ACTIVITY_OPEN, keepCurTransition); + mNoAnimActivities.remove(r); + } + mWindowManager.addAppToken(task.mActivities.indexOf(r), + r.appToken, r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen, + (r.info.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0, r.userId, + r.info.configChanges, task.voiceSession != null, r.mLaunchTaskBehind); + boolean doShow = true; + if (newTask) { + // Even though this activity is starting fresh, we still need + // to reset it to make sure we apply affinities to move any + // existing activities from other tasks in to it. + // If the caller has requested that the target task be + // reset, then do so. + if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) { + resetTaskIfNeededLocked(r, r); + doShow = topRunningNonDelayedActivityLocked(null) == r; + } + } else if (options != null && new ActivityOptions(options).getAnimationType() + == ActivityOptions.ANIM_SCENE_TRANSITION) { + doShow = false; + } + if (r.mLaunchTaskBehind) { + // Don't do a starting window for mLaunchTaskBehind. More importantly make sure we + // tell WindowManager that r is visible even though it is at the back of the stack. + mWindowManager.setAppVisibility(r.appToken, true); + ensureActivitiesVisibleLocked(null, 0); + } else if (SHOW_APP_STARTING_PREVIEW && doShow) { + // Figure out if we are transitioning from another activity that is + // "has the same starting icon" as the next one. This allows the + // window manager to keep the previous window it had previously + // created, if it still had one. + ActivityRecord prev = mResumedActivity; + if (prev != null) { + // We don't want to reuse the previous starting preview if: + // (1) The current activity is in a different task. + if (prev.task != r.task) { + prev = null; + } + // (2) The current activity is already displayed. + else if (prev.nowVisible) { + prev = null; + } + } + mWindowManager.setAppStartingWindow( + r.appToken, r.packageName, r.theme, + mService.compatibilityInfoForPackageLocked( + r.info.applicationInfo), r.nonLocalizedLabel, + r.labelRes, r.icon, r.logo, r.windowFlags, + prev != null ? prev.appToken : null, showStartingIcon); + r.mStartingWindowShown = true; + } + } else { + // If this is the first activity, don't do any fancy animations, + // because there is nothing for it to animate on top of. + mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken, + r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen, + (r.info.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0, r.userId, + r.info.configChanges, task.voiceSession != null, r.mLaunchTaskBehind); + ActivityOptions.abort(options); + options = null; + } + if (VALIDATE_TOKENS) { + validateAppTokensLocked(); + } + + if (doResume) { + mStackSupervisor.resumeTopActivitiesLocked(this, r, options); + } +} +``` + + 在Android应用程序框架层中,是由ActivityManagerService组件负责为Android应用程序创建新的进程的,它本来也是运行在一个独立的进程之中,不过这个进程是在系统启动的过程中创建的。 + ActivityManagerService组件一般会在什么情况下会为应用程序创建一个新的进程呢?当系统决定要在一个新的进程中启动一个Activity或者Service时,它就会创建一个新的进程了, + 然后在这个新的进程中启动这个Activity或者Service --- diff --git "a/Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" "b/Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" new file mode 100644 index 00000000..1a77bc21 --- /dev/null +++ "b/Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" @@ -0,0 +1,21 @@ +Parcelable及Serializable +=== + +`Serializable`的作用是为了保存对象的属性到本地文件、数据库、网络流、`rmi`以方便数据传输, +当然这种传输可以是程序内的也可以是两个程序间的。而`Parcelable`的设计初衷是因为`Serializable`效率过慢, +为了在程序内不同组件间以及不同`Android`程序间(`AIDL`)高效的传输数据而设计,这些数据仅在内存中存在,`Parcelable`是通过`IBinder`通信的消息的载体。 + +`Parcelable`的性能比`Serializable`好,在内存开销方面较小,所以在内存间数据传输时推荐使用`Parcelable`, +如`activity`间传输数据,而`Serializable`可将数据持久化方便保存,所以在需要保存或网络传输数据时选择 +`Serializable`,因为`android`不同版本`Parcelable`可能不同,所以不推荐使用`Parcelable`进行数据持久化。 + +区别: +- Parcelable is faster than serializable interface +- Parcelable interface takes more time for implemetation compared to serializable interface +- serializable interface is easier to implement +- serializable interface create a lot of temporary objects and cause quite a bit of garbage collection +- Parcelable array can be pass via Intent in android. + +---- +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" "b/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" index 4d70690e..03e7f1cd 100644 --- "a/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" +++ "b/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" @@ -1,8 +1,114 @@ JVM垃圾回收机制 === +引用计数算法 +--- + +在`JDK1.2`之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区, +堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象, +同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1, +而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候, +标志着这个对象已经没有引用了,可以回收了! +这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题,那就是互相引用的问题: +```java +ObjA.obj = ObjB +ObjB.obj - ObjA +``` +这样的代码会产生如下引用情形`objA`指向`objB`,而`objB`又指向`objA`,这样当其他所有的引用都消失了之后, +`objA`和`objB`还有一个相互的引用,也就是说两个对象的引用计数器各为1, +而实际上这两个对象都已经没有额外的引用,已经是垃圾了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/yinyongjishu.jpg) + +根搜索算法 +--- + +根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, +从一个节点`GC ROOT`开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点, +当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/genshousuo.jpg) + +目前java中可作为GC Root的对象有: + +- 虚拟机栈中引用的对象(本地变量表) +- 方法区中静态属性引用的对象 +- 方法区中常量引用的对象 +- 本地方法栈中引用的对象(Native对象) + +垃圾回收算法 +--- + +而手机后的垃圾是通过什么算法来回收的呢: + +- 标记-清除算法 +- 复制算法 +- 标记整理算法 + +那我们就继续分析下这三种算法: + +- 标记-清除算法(Mark-Sweep) + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_qingchu.jpg) + 标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记 的对象,进行回收,如上图所示。 + 标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片! + +- 复制算法(Copying) + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.jpg) + + 复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。 + +- 标记-整理算法(Mark-Compact) + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.jpg) + + 整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。 + +- 分代收集算法(Generational Collection) + 分代收集算法是目前大部分`JVM`的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。 + 一般情况下将堆区划分为老年代(`Tenured Generation`)和新生代(`Young Generation`),老年代的特点是每次垃圾收集时只有少量对象需要被回收, + 而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 +  目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少, + 但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的`Eden`空间和两块较小的`Survivor`空间, + 每次使用`Eden`空间和其中的一块`Survivor`空间,当进行回收时,将`Eden`和`Survivor`中还存活的对象复制到另一块`Survivor`空间中, + 然后清理掉`Eden`和刚才使用过的`Survivor`空间。 + +  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法。 +  注意,在堆区之外还有一个代就是永久代(`Permanet Generation`),它用来存储`class`类、常量、方法描述等。对永久代的回收主要回收两部分内容: + 废弃常量和无用的类。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xinshengdai.jpg) + 对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的`Eden Space`和`From Space`, + 少数情况下会直接分配在老年代。如果新生代的`Eden Space`和`From Space`的空间不足,则会发起一次`GC`,如果进行了`GC`之后,`Eden Space`和`From Space` + 能够容纳该对象就放在`Eden Space`和`From Space`。在`GC`的过程中,会将`Eden Space`和`From Space`中的存活对象移动到`To Space`, + 然后将`Eden Space`和`From Space`进行清理。如果在清理的过程中,`To Space`无法足够来存储某个对象,就会将该对象移动到老年代中。 + 在进行了`GC之`后,使用的便是`Eden space`和`To Space`了,下次`GC`时会将存活对象复制到`From Space`,如此反复循环。 + 当对象在`Survivor`区躲过一次`GC`的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。 + +垃圾收集器 +--- +垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。 +下面介绍一下`HotSpot(JDK 7)`虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。 +- Serial/Serial Old + `Serial/Serial Old`收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时, + 必须暂停所有用户线程。`Serial`收集器是针对新生代的收集器,采用的是`Copying`算法,`Serial Old`收集器是针对老年代的收集器, + 采用的是`Mark-Compact`算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。 + +- ParNew + `ParNew`收集器是`Serial`收集器的多线程版本,使用多个线程进行垃圾收集。 + +- Parallel Scavenge + `Parallel Scavenge`收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是`Copying`算法, + 该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。 +- Parallel Old + `Parallel Old`是`Parallel Scavenge`收集器的老年代版本(并行收集器),使用多线程和`Mark-Compact`算法。 + +- CMS + `CMS(Current Mark Sweep)`收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是`Mark-Sweep`算法。 + +- G1 + `G1`收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多`CPU`、多核环境。因此它是一款并行与并发收集器, + 并且它能建立可预测的停顿时间模型。 + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" index f0a495ab..a8d8ccfc 100644 --- "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -14,6 +14,8 @@ 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 弱引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果弱引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 + 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 - 虚引用(PhantomReference) "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用, @@ -21,6 +23,7 @@ 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前, 把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 + 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 虚引用主要用于检测对象是否已经从内存中删除。 From 1c1da23ac0b5f488f9b7c871111062a53947ce9a Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 7 Mar 2016 19:50:37 +0800 Subject: [PATCH 006/373] =?UTF-8?q?Add=20=E5=89=91=E6=8C=87offer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\345\211\221\346\214\207Offer.md" | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" new file mode 100644 index 00000000..321a04fe --- /dev/null +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" @@ -0,0 +1,305 @@ +剑指Offer +=== + +最近面试,遇到一些笔试题,写不上来,内心是崩溃的,该好好复习下了,所以决定仔细做一遍,随便也整理下,方便大家学习。 + +1. 我没找到第一题是什么- -!,谁知道的给补充下吧 + +2. 实现单例模式 + 单例的实现分为好几种: + - 饿汉式 + - 懒汉式 + - 枚举 + + - 饿汉式 + ```java + public class Singleton { + private Singleton() { + } + + private static final Singleton SINGLETON = new Singleton(); + + public static Singleton getInstance() { + return SINGLETON; + } + } + ``` + - 懒汉式 + ```java + public class Singleton { + private Singleton() { + } + + private static Singleton singleton = null; + + public static Singleton getInstance() { + // 同步会导致效率低,这里采用双重判断的方式来提高效率 + if (singleton == null) { + synchronized (Singleton.class) { + if (singleton == null) { + singleton = new Singleton(); + } + } + } + return singleton; + } + } + ``` + - 枚举 + ```java + public enum Singleton { + INSTANCE; + private Singleton() { + + } + } + ``` + + 我这里写一种自我感觉是单例最完美的实现方式:   + ```java + public class Singleton { + // Private constructor prevents instantiation from other classes + private Singleton() { } + + /** + * SingletonHolder is loaded on the first execution of Singleton.getInstance() + * or the first access to SingletonHolder.INSTANCE, not before. + */ + private static class SingletonHolder { + public static final Singleton INSTANCE = new Singleton(); + } + + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` + +3. 二维数组中的查找 + 题目描述:一个二维数组,每一行从左到右递增,每一列从上到下递增.输入一个二维数组和一个整数,判断数组中是否含有整数。 + + 分析: + 1 6 11 + 5 9 15 + 7 13 20 + + 假设我们要找7,那怎么找呢? + 我们先从第一行找,从后往前找,因为他是递增的,先是11,这里11>7所以肯定不是第三列的。这时候我们就找第二列, + 这个值是6,6 < 7,所以我们可以从第二列往下找,这个数可能会再第二列或者第一列。把行数加1,来到第二行第二列的9 + 这时候一判断9 > 7,所以不可能是第二列了,这时候把列数再前移,来到第一列,刚才是第二行,所以我们取第一列第二行 + 的数,也就是5,5 < 7,所以还要继续往后找,就是把行数加1,就来到了第三行第一列,也就是7,一判断就是他了。 + 整体思路就是从右上角开始,逐渐前移列数或者增加行数。 + ```java + public static boolean find(int[][] array, int number) { + if (array == null) { + return false; + } + + // 从第一行最后一列的数开始 + int column = array[0].length - 1; + int row = 0; + while (row < array.length && column >= 0) { + if (array[row][column] == number) { + return true; + } + + if (array[row][column] > number) { + // 如果这个数比要找的数大,那肯定不是这一列了,只能是前一列 + column--; + } else { + // 小的话,那肯定在该数的下面,就要增大行数 + row++; + } + } + return false; + } + ``` + +4. 替换空格 + 请实现一个函数,把字符串中的每个空格替换成`%20`。 + 思路: 很简单,就是判断每个字符是否为空着,但也要注意使用`StringBuilder`会比`StringBuffer`效率稍高。 + ```java + public String replaceBlank(String input) { + if (input == null) { + return null; + } + StringBuilder sb = new StringBuilder (); + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) == ' ') { + sb.append("%"); + sb.append("2"); + sb.append("0"); + } else { + sb.append(String.valueOf(input.charAt(i))); + } + } + return new String(sb); + } + ``` + +5. 从尾到头打印链表 + 输入一个链表的头结点,从尾到头反过来打印出每个节点的值。 + 思路: 我们可以从头开始遍历,但是要让先遍历的最后打印,这就是一个吃进去、吐出来的方式,最适合的就是栈. + - 遍历的方式 + ```java + public class ListNodeTest { + public static void main(String args[]) { + ListNode node1 = new ListNode(); + ListNode node2 = new ListNode(); + ListNode node3 = new ListNode(); + node1.data = 1; + node2.data = 2; + node3.data = 3; + node1.next = node2; + node2.next = node3; + + ListNodeTest.reversePrint(node1); + } + + public static void reversePrint(ListNode headNode) { + Stack stack = new Stack(); + while (headNode != null) { + // 遍历,然后用栈来保存 + stack.push(headNode); + headNode = headNode.next; + } + while (!stack.isEmpty()) { + System.out.println(stack.pop().data); + } + } + } + + class ListNode { + public ListNode() { + + } + + ListNode next; + int data; + } + ``` + - 递归 + ```java + public static void printListReverse(ListNode headNode) { + if (headNode != null) { + if (headNode.next != null) { + printListReverse(headNode.next); + } + } + System.out.println(headNode.data); + } + ``` + +6. 重建二叉树 + 输入二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设前 + 序遍历和中序遍历结果中都不包含重复的数字,例如输入的前序遍历序列 + {1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}重建出如图所示的二叉树。 + + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binary_offer.png) + 思路: 前序遍历序列中,第一个数字总是树的根节点的值。 + 中序遍历根节点在序列的中间,左边是左子树,右边是右子树。 + 所以我们可以根据前序遍历的第一个值去中旬遍历数组中查找,就能找出来中序的分割点是谁。 + 这样在他左边的就都是左子树,右边是右子树。这样也就能知道左子树的个数,再用这个数量去前子树中去前几个数。 + 就这样再递归下去排列二级左子树的根节点。 + ```java + import java.util.Arrays; + + public class BinaryTree { + public static void main(String[] args) throws Exception { + int[] preorder = { 1, 2, 4, 7, 3, 5, 6, 8 }; + int[] inorder = { 4, 7, 2, 1, 5, 3, 8, 6 }; + BinaryTreeNode root = constructCore(preorder, inorder); + } + + public static BinaryTreeNode constructCore(int[] preorder, int[] inorder) + throws Exception { + if (preorder == null || inorder == null) { + return null; + } + if (preorder.length != inorder.length) { + throw new IllegalArgumentException("error params"); + } + BinaryTreeNode root = new BinaryTreeNode(); + for (int i = 0; i < inorder.length; i++) { + if (inorder[i] == preorder[0]) { + root.value = inorder[i]; + // 递归让再去构建左子树的下一个根节点 + root.leftNode = constructCore( + // 前序遍历从第二个开始后的i个都是左子树的 + Arrays.copyOfRange(preorder, 1, i + 1), + // 中序遍历最边边这i个也是左子树 + Arrays.copyOfRange(inorder, 0, i)); + root.rightNode = constructCore( + Arrays.copyOfRange(preorder, i + 1, preorder.length), + Arrays.copyOfRange(inorder, i + 1, inorder.length)); + } + // 就这样循环递归下去,就OK了。 + } + return root; + } + } + + class BinaryTreeNode { + public static int value; + public BinaryTreeNode leftNode; + public BinaryTreeNode rightNode; + } + ``` + +7. 用两个栈实现队列 + 用两个栈实现一个队列,实现对了的两个函数`appendTail`和 + `deleteHead`,分别完成在队列尾插入结点和在队列头部删除结点的功能。 + 思路: 栈是啥?栈是先进后出,因为栈是一个出口啊,先进入的被压在最下面了,出要从上面开始出,也就是吃了吐出来。 + 队列是啥?两头的,就想管道一样,先进先出。不雅的说,就是吃了拉出来。 + 队列尾插入节点好说啊,就是在栈中往里放。 + 那队列头部删除怎么弄?因为他在栈的最底部啊,你没法直接删他啊,不要忘了,我们是用两个栈来实现。所以自然想到 + 就是把这个栈中的数据都取出放入到第二个栈中,然后删除第二个栈的最上面的元素就可以了。 + ```java + public class StackListTest { + private Stack stack1 = new Stack(); + private Stack stack2 = new Stack(); + + public void appendTail(T t) { + // 往栈1中存 + stack1.push(t); + } + + public T deleteHead() throws Exception { + if (stack2.isEmpty()) { + // 把栈1的数都放到栈2中 + while (!stack1.isEmpty()) { + stack2.push(stack1.pop()); + } + } + // 转移到栈2后,就相当于把栈1倒序了。 + if (stack2.isEmpty()) { + throw new Exception("队列为空,不能删除"); + } + // 直接取栈2中最上层的就可以了。 + return stack2.pop(); + } + + public static void main(String args[]) throws Exception { + StackListTest p7 = new StackListTest(); + p7.appendTail("1"); + p7.appendTail("2"); + p7.appendTail("3"); + p7.deleteHead(); + } + } + ``` + +8. 旋转数组的最小数字 + 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的 + 旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数 + 组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为 1. + + 思路: 开始看到这道题,我感觉很简单,就是循环比较下找出最下的就完了,我感觉什么旋转数组都是面试官 + 放的烟雾弹。后来我发现我错了。旋转数组是有用的。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 2e18fbfe44f6ab75cf8de800f425e5389c1f1b05 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 8 Mar 2016 09:35:50 +0800 Subject: [PATCH 007/373] update --- .../\345\211\221\346\214\207Offer.md" | 276 +++++++++++++++++- 1 file changed, 275 insertions(+), 1 deletion(-) diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" index 321a04fe..38ce89f9 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" @@ -296,10 +296,284 @@ 思路: 开始看到这道题,我感觉很简单,就是循环比较下找出最下的就完了,我感觉什么旋转数组都是面试官 放的烟雾弹。后来我发现我错了。旋转数组是有用的。 + ```java + public class FindTest { + public static void main(String[] args) { + // int[] array={1,1,1,2,0}; + // int[] array={3,4,5,1,2}; + int[] array = { 1, 0, 1, 1, 1 }; // 这是0,1,1,1,1的旋转 + System.out.println(findMinNum(array)); + } + + public static Integer findMinNum(int[] array) { + if (array == null) { + return null; + } + int leftIndex = 0; + int rightIndex = array.length - 1; + int mid = 0; + // 最小的数就在这中间 + while (array[leftIndex] >= array[rightIndex]) { + if (rightIndex - leftIndex <= 1) { + // 这就是最小的了 + mid = rightIndex; + break; + } + // 去中间的值,类似二分法查找 + mid = (leftIndex + rightIndex) / 2; + // 前、中、后三个值都相等的情况,主要就是为了区分0,1,1,1,1这种数值相同的情况 + if (array[leftIndex] == array[rightIndex] && array[leftIndex] == array[mid]) { + // 把指针在移动一下,不相等就继续变mid的值 + if (array[leftIndex + 1] != array[rightIndex - 1]) { + mid = array[leftIndex + 1] < array[rightIndex - 1] ? (leftIndex + 1) : (rightIndex - 1); + break; + } else { + leftIndex++; + rightIndex--; + } + } else { + if (array[mid] >= array[leftIndex]) + leftIndex = mid; + else { + if (array[mid] <= array[rightIndex]) + rightIndex = mid; + } + } + return array[mid]; + } + return null; + } + } + ``` +9. 斐波那契数列 + 什么是斐波那契数列呢? 就是f(0)=0;f(1)=1;f(n)=f(n-1)+f(n-2); + 写一个函数,输入n,求斐波那契数列的第n项。 + 思路:标准的一个递归。 + ```java + public long fibonacci1(int n) { + if (n == 0) { + return 0; + } + + if (n == 1) { + return 1; + } + + return fibonacci1(n-1) + fibonacci1(n-2); + } + ``` + 貌似很合理,但其实也是有问题的,这样会导致重复计算,例如我们在算f(10),需要先求出f(9)和f(8),而算f(9)又要求出f(8)和f(7),很显然重复了。显然面试官不会满意的。那该怎么做呢? 那就是累加。 + ```java + public class Fibonacci { + public static long fibonacci(int n) { + long result = 0; + long preOne = 0; + long preTwo = 1; + if (n == 0) { + return preOne; + } + if (n == 1) { + return preTwo; + } + for (int i = 2; i <= n; i++) { + result = preOne + preTwo; + preOne = preTwo; + preTwo = result; + } + return result; + } + } + ``` +10. 2进制中1的个数 + 请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如 把 9 表示成二进制是 1001;有 2 位是 1,因此如果输入 9,函数输出 2. + 思路: 把一个整数减去1,再和原整数做与运算,会把该整数最右边的一个1变成0.那么一个整数的二进制表示中有多少个1,就可以进行多少次运算。 + ```java + public int numberOf1(int n) { + int count = 0; + while (n != 0) { + count++; + n = (n - 1) & n; + } + return count; + } + ``` + +11. 数值的整数次方 + 实现函数double Power(double base,int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。 + 思路:就是不断的累计去乘. + ```java + public double powerWithExponent(double base, int exponent) { + double result = 1.0; + for (int i = 1; i <= exponent; i++) { + result = result * base; + } + return result; + } + ``` + 本来想着挺简单,其实已经写错了。因为exponent如果是0或者负数呢? + 思路:当指数为负数的时候,可以先对指数求绝对值,然后算出次方的结果之后再取倒数。既然有求倒数,我们很自然的就要想到有没有可能对0求倒数,如果对0求倒数怎么办?当底数base是零且指数是负数的时候,我们不做特殊的处理,就会发现对0求倒数从而导致程序运行出错。怎么告诉函数的调用者出现了这种错误?在Java中可以抛出异常来解决。 + ```java + public static double power(double base, int exponent) throws Exception { + double result = 0.0; + // 如果是求0的负数次幂 + if (equal(base, 0.0) && exponent < 0) { + throw new Exception("0的负数次幂没有意义"); + } + if (exponent < 0) { + // 负数次幂,先取绝对值算出次方后再求倒数 + result = 1.0 / powerWithExpoment(base, -exponent); + } else { + result = powerWithExpoment(base, exponent); + } + return result; + } + + private static double powerWithExpoment(double base, int exponent) { + if (exponent == 0) { + return 1; + } + if (exponent == 1) { + return base; + } + double result = 1.0; + for (int i = 1; i <= exponent; i++) { + result = result * base; + } + return result; + } + + /** + * 判断两个double数据是否相等 + * @param num1 + * @param num2 + * @return + */ + private static boolean equal(double num1, double num2) { + if ((num1 - num2 > -0.0000001) && num1 - num2 < 0.0000001) { + return true; + } else { + return false; + } + + } + ``` +12. 打印 1 到最大的 n 位数 + 输入数字n,按顺序打印出从1最大的的n位数十进制数。比如输入3,则打印出1,2,3一直到最大的3位数即999. + 思路: 1位数就是10-1,两位数就是10*10-1三位数就是10*10*10-1 + + ```java + public void print1ToMaxOfNDigits(int n) { + int number = 1; + int i = 0; + while (i++ < n) { + number *= 10; + } + for (int j = 1; j < number; ++j) + System.out.println(j); + } + ``` + 感觉挺简单,其实已经错了。因为没有规定n的值,如果很大的话,显然会超过int型的最大值。我们很自然的想到解决这个问题需要一个大数。最常用的也是最容易的用字符串或者数组表达大数。接下来我们用数组来解决大数问题。 + 思路:每一位数都是0到9,这样弄一个数组,数组的长度就是n,每一位都是0-9,这样,循环去打印数组就可以了 + ```java + public static void main(String[] args) { + printToMaxOfNDigits(2); + } + public static void printToMaxOfNDigits(int n) { + int[] array = new int[n]; + if (n <= 0) + return; + printArray(array, 0); + } + + private static void printArray(int[] array, int n) { + // 每一位都是0-9 + for (int i = 0; i < 10; i++) { + if (n != array.length) { + array[n] = i; + // 递归弄另一位 + printArray(array, n + 1); + } else { + boolean isFirstNo0 = false; + for (int j = 0; j < array.length; j++) { + if (array[j] != 0) { + System.out.print(array[j]); + if (!isFirstNo0) { + isFirstNo0 = true; + } + } else { + if (isFirstNo0) { + // 10 20 这种后位是0的 + System.out.print(array[j]); + } + } + } + System.out.println(); + // 打印完就return + return; + } + } + } +``` + +13. 在O(1)时间删除链表节点 + 给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。 + 思路:在单向链表中删除一个节点,最常规的方法无疑是从链表的头结点开始,顺序遍历查找要删除的节点,并在链表中删除该节点。删除就是将这个要被删除的节点的前一节点设置成该要被删除节点的下一节点。- -! + ```java + public class DeleteListNodeTest { + public static void main(String[] args) { + ListNode head = new ListNode(); + ListNode second = new ListNode(); + ListNode third = new ListNode(); + head.nextNode = second; + second.nextNode = third; + head.data = 1; + second.data = 2; + third.data = 3; + deleteNode(head, second); + System.out.println(head.nextNode.data); + } + /** + * + * @param head + * 头结点 + * @param deListNode + * 将被删除的节点 + */ + public static void deleteNode(ListNode head, ListNode deListNode) { + if (deListNode == null || head == null) { + return; + } + + if (head == deListNode) { + // 要删除的这个节点正好是头节点 + head = null; + } else if (deListNode.nextNode == null) { + // 要删除的这个节点正好是最后一个节点 + ListNode pointListNode = head; + while (pointListNode.nextNode.nextNode != null) { + pointListNode = pointListNode.nextNode; + } + pointListNode.nextNode = null; + } else { + // 要删除的节点是中间的节点,直接把该节点的值和next指向下一个节点就可以。 + deListNode.data = deListNode.nextNode.data; + deListNode.nextNode = deListNode.nextNode.nextNode; + } + } + } + + class ListNode { + int data; + ListNode nextNode; + } + ``` +14. 调整数组顺序使奇数位于偶数前面 + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! + From 9cf1037ef371d9701bac5d28bab392593155eb14 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 8 Mar 2016 20:20:39 +0800 Subject: [PATCH 008/373] Update --- .../\345\211\221\346\214\207Offer.md" | 288 ++++++++++++++---- 1 file changed, 233 insertions(+), 55 deletions(-) diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" index 38ce89f9..031546b6 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" @@ -5,13 +5,14 @@ 1. 我没找到第一题是什么- -!,谁知道的给补充下吧 -2. 实现单例模式 - 单例的实现分为好几种: +2. 实现单例模式 + 单例的实现分为好几种: - 饿汉式 - 懒汉式 - - 枚举 + - 枚举 - - 饿汉式 + 具体实现: + - 饿汉式 ```java public class Singleton { private Singleton() { @@ -75,20 +76,20 @@ } ``` -3. 二维数组中的查找 +3. 二维数组中的查找 题目描述:一个二维数组,每一行从左到右递增,每一列从上到下递增.输入一个二维数组和一个整数,判断数组中是否含有整数。 - + 分析: 1 6 11 5 9 15 7 13 20 - 假设我们要找7,那怎么找呢? - 我们先从第一行找,从后往前找,因为他是递增的,先是11,这里11>7所以肯定不是第三列的。这时候我们就找第二列, - 这个值是6,6 < 7,所以我们可以从第二列往下找,这个数可能会再第二列或者第一列。把行数加1,来到第二行第二列的9 - 这时候一判断9 > 7,所以不可能是第二列了,这时候把列数再前移,来到第一列,刚才是第二行,所以我们取第一列第二行 - 的数,也就是5,5 < 7,所以还要继续往后找,就是把行数加1,就来到了第三行第一列,也就是7,一判断就是他了。 - 整体思路就是从右上角开始,逐渐前移列数或者增加行数。 + 假设我们要找7,那怎么找呢? + 我们先从第一行找,从后往前找,因为他是递增的,先是11,这里11>7所以肯定不是第三列的。这时候我们就找第二列, + 这个值是6,6 < 7,所以我们可以从第二列往下找,这个数可能会再第二列或者第一列。把行数加1,来到第二行第二列的9 + 这时候一判断9 > 7,所以不可能是第二列了,这时候把列数再前移,来到第一列,刚才是第二行,所以我们取第一列第二行 + 的数,也就是5,5 < 7,所以还要继续往后找,就是把行数加1,就来到了第三行第一列,也就是7,一判断就是他了。 + 整体思路就是从右上角开始,逐渐前移列数或者增加行数。 ```java public static boolean find(int[][] array, int number) { if (array == null) { @@ -115,9 +116,9 @@ } ``` -4. 替换空格 - 请实现一个函数,把字符串中的每个空格替换成`%20`。 - 思路: 很简单,就是判断每个字符是否为空着,但也要注意使用`StringBuilder`会比`StringBuffer`效率稍高。 +4. 替换空格 + 请实现一个函数,把字符串中的每个空格替换成`%20`。 + 思路: 很简单,就是判断每个字符是否为空着,但也要注意使用`StringBuilder`会比`StringBuffer`效率稍高。 ```java public String replaceBlank(String input) { if (input == null) { @@ -137,9 +138,9 @@ } ``` -5. 从尾到头打印链表 - 输入一个链表的头结点,从尾到头反过来打印出每个节点的值。 - 思路: 我们可以从头开始遍历,但是要让先遍历的最后打印,这就是一个吃进去、吐出来的方式,最适合的就是栈. +5. 从尾到头打印链表 + 输入一个链表的头结点,从尾到头反过来打印出每个节点的值。 + 思路: 我们可以从头开始遍历,但是要让先遍历的最后打印,这就是一个吃进去、吐出来的方式,最适合的就是栈. - 遍历的方式 ```java public class ListNodeTest { @@ -190,17 +191,17 @@ } ``` -6. 重建二叉树 - 输入二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设前 - 序遍历和中序遍历结果中都不包含重复的数字,例如输入的前序遍历序列 - {1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}重建出如图所示的二叉树。 +6. 重建二叉树 + 输入二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设前 + 序遍历和中序遍历结果中都不包含重复的数字,例如输入的前序遍历序列 + {1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}重建出如图所示的二叉树。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binary_offer.png) - 思路: 前序遍历序列中,第一个数字总是树的根节点的值。 - 中序遍历根节点在序列的中间,左边是左子树,右边是右子树。 - 所以我们可以根据前序遍历的第一个值去中旬遍历数组中查找,就能找出来中序的分割点是谁。 - 这样在他左边的就都是左子树,右边是右子树。这样也就能知道左子树的个数,再用这个数量去前子树中去前几个数。 - 就这样再递归下去排列二级左子树的根节点。 + 思路: 前序遍历序列中,第一个数字总是树的根节点的值。 + 中序遍历根节点在序列的中间,左边是左子树,右边是右子树。 + 所以我们可以根据前序遍历的第一个值去中旬遍历数组中查找,就能找出来中序的分割点是谁。 + 这样在他左边的就都是左子树,右边是右子树。这样也就能知道左子树的个数,再用这个数量去前子树中去前几个数。 + 就这样再递归下去排列二级左子树的根节点。 ```java import java.util.Arrays; @@ -246,14 +247,14 @@ } ``` -7. 用两个栈实现队列 - 用两个栈实现一个队列,实现对了的两个函数`appendTail`和 - `deleteHead`,分别完成在队列尾插入结点和在队列头部删除结点的功能。 - 思路: 栈是啥?栈是先进后出,因为栈是一个出口啊,先进入的被压在最下面了,出要从上面开始出,也就是吃了吐出来。 - 队列是啥?两头的,就想管道一样,先进先出。不雅的说,就是吃了拉出来。 - 队列尾插入节点好说啊,就是在栈中往里放。 +7. 用两个栈实现队列 + 用两个栈实现一个队列,实现对了的两个函数`appendTail`和 + `deleteHead`,分别完成在队列尾插入结点和在队列头部删除结点的功能。 + 思路: 栈是啥?栈是先进后出,因为栈是一个出口啊,先进入的被压在最下面了,出要从上面开始出,也就是吃了吐出来。 + 队列是啥?两头的,就想管道一样,先进先出。不雅的说,就是吃了拉出来。 + 队列尾插入节点好说啊,就是在栈中往里放。 那队列头部删除怎么弄?因为他在栈的最底部啊,你没法直接删他啊,不要忘了,我们是用两个栈来实现。所以自然想到 - 就是把这个栈中的数据都取出放入到第二个栈中,然后删除第二个栈的最上面的元素就可以了。 + 就是把这个栈中的数据都取出放入到第二个栈中,然后删除第二个栈的最上面的元素就可以了。 ```java public class StackListTest { private Stack stack1 = new Stack(); @@ -289,10 +290,10 @@ } ``` -8. 旋转数组的最小数字 - 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的 - 旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数 - 组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为 1. +8. 旋转数组的最小数字 + 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的 + 旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数 + 组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为 1. 思路: 开始看到这道题,我感觉很简单,就是循环比较下找出最下的就完了,我感觉什么旋转数组都是面试官 放的烟雾弹。后来我发现我错了。旋转数组是有用的。 @@ -345,9 +346,9 @@ } } ``` -9. 斐波那契数列 - 什么是斐波那契数列呢? 就是f(0)=0;f(1)=1;f(n)=f(n-1)+f(n-2); - 写一个函数,输入n,求斐波那契数列的第n项。 +9. 斐波那契数列 + 什么是斐波那契数列呢? 就是f(0)=0;f(1)=1;f(n)=f(n-1)+f(n-2); + 写一个函数,输入n,求斐波那契数列的第n项。 思路:标准的一个递归。 ```java public long fibonacci1(int n) { @@ -384,9 +385,9 @@ } } ``` -10. 2进制中1的个数 - 请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如 把 9 表示成二进制是 1001;有 2 位是 1,因此如果输入 9,函数输出 2. - 思路: 把一个整数减去1,再和原整数做与运算,会把该整数最右边的一个1变成0.那么一个整数的二进制表示中有多少个1,就可以进行多少次运算。 +10. 2进制中1的个数 + 请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如 把 9 表示成二进制是 1001;有 2 位是 1,因此如果输入 9,函数输出 2. + 思路: 把一个整数减去1,再和原整数做与运算,会把该整数最右边的一个1变成0.那么一个整数的二进制表示中有多少个1,就可以进行多少次运算。 ```java public int numberOf1(int n) { int count = 0; @@ -398,9 +399,9 @@ } ``` -11. 数值的整数次方 - 实现函数double Power(double base,int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。 - 思路:就是不断的累计去乘. +11. 数值的整数次方 + 实现函数double Power(double base,int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。 + 思路:就是不断的累计去乘. ```java public double powerWithExponent(double base, int exponent) { double result = 1.0; @@ -410,7 +411,7 @@ return result; } ``` - 本来想着挺简单,其实已经写错了。因为exponent如果是0或者负数呢? + 本来想着挺简单,其实已经写错了。因为exponent如果是0或者负数呢? 思路:当指数为负数的时候,可以先对指数求绝对值,然后算出次方的结果之后再取倒数。既然有求倒数,我们很自然的就要想到有没有可能对0求倒数,如果对0求倒数怎么办?当底数base是零且指数是负数的时候,我们不做特殊的处理,就会发现对0求倒数从而导致程序运行出错。怎么告诉函数的调用者出现了这种错误?在Java中可以抛出异常来解决。 ```java public static double power(double base, int exponent) throws Exception { @@ -457,9 +458,9 @@ } ``` -12. 打印 1 到最大的 n 位数 - 输入数字n,按顺序打印出从1最大的的n位数十进制数。比如输入3,则打印出1,2,3一直到最大的3位数即999. - 思路: 1位数就是10-1,两位数就是10*10-1三位数就是10*10*10-1 +12. 打印 1 到最大的 n 位数 + 输入数字n,按顺序打印出从1最大的的n位数十进制数。比如输入3,则打印出1,2,3一直到最大的3位数即999. + 思路: 1位数就是10-1,两位数就是10*10-1三位数就是10*10*10-1 ```java public void print1ToMaxOfNDigits(int n) { @@ -516,9 +517,9 @@ } ``` -13. 在O(1)时间删除链表节点 - 给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。 - 思路:在单向链表中删除一个节点,最常规的方法无疑是从链表的头结点开始,顺序遍历查找要删除的节点,并在链表中删除该节点。删除就是将这个要被删除的节点的前一节点设置成该要被删除节点的下一节点。- -! +13. 在O(1)时间删除链表节点 + 给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。 + 思路:在单向链表中删除一个节点,最常规的方法无疑是从链表的头结点开始,顺序遍历查找要删除的节点,并在链表中删除该节点。删除就是将这个要被删除的节点的前一节点设置成该要被删除节点的下一节点。- -! ```java public class DeleteListNodeTest { public static void main(String[] args) { @@ -569,8 +570,185 @@ ListNode nextNode; } ``` -14. 调整数组顺序使奇数位于偶数前面 +14. 调整数组顺序使奇数位于偶数前面 + 输入一个整数数组,实现一个函数来调整该函数数组中数字的顺序,使得 +所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。 + 思路: 维护两个指针,一个指向第一个元素,一个指向最后一个元素,然后通过指针的移动,来判断当前元素是奇数还是偶数,来交换位置。    + ```java + public static void order(int[] array) { + if (array == null || array.length == 0) { + return; + } + int start = 0; + int end = array.length - 1; + while (start < end) { + while (start < end && !isEven(array[start])) { + // 如果当前位置是奇数,就下移指针 + start++; + } + while (start < end && isEven(array[end])) { + // 第二个指针如果当前位置是偶数,就向前移动指针 + end--; + } + if (start < end) { + // 说明第一个指针指向的是偶数,第二个指针指向的是奇数,我们来更换他俩的位置。 + int temp = array[start]; + array[start] = array[end]; + array[end] = temp; + } + } + } + + private static boolean isEven(int n) { + return n % 2 == 0; + } + ``` + +15. 链表中倒数第K个结点 + 输入一个链表,输出该链表中倒数第`k`个结点。 + 思路: 拿到倒数第k个节点,我们只需要知道该链表的总长度,然后我们从头开始遍历渠道第`totalLength-k`个就是了。如何拿到总长度,也简单就是遍历一遍就知道了。 + 但是这样会牵扯到两次遍历,效率比较低。那怎么处理呢?也是使用两个指针,我们要保证第一个指针走到链表最后一个位置(totalLength)的时候,第二个指针正好指向倒数第`k`个节点( + 也就是从头开始第`totalLength-k+1个`),那这两个指针之间差多少呢?`totalLength-(totalLength-k+1)`也就是`k-1`个位置,所以让第一个指针移动到第`k-1`个位置后,就让第二个指针 + 开始移动,这样等第一个移动到最后一个元素的时候,第二个正好指向了倒数第`k`个元素。 + ```java + public class ListNodeTailText { + public static void main(String[] args) { + ListNode node1 = new ListNode(); + ListNode node2 = new ListNode(); + ListNode node3 = new ListNode(); + ListNode node4 = new ListNode(); + node1.nextNode = node2; + node2.nextNode = node3; + node3.nextNode = node4; + node1.data = 1; + node2.data = 2; + node3.data = 3; + node4.data = 4; + ListNode resultListNode = findKToTail(node1, 3); + System.out.println(resultListNode.data); + } + + public static ListNode findKToTail(ListNode head, int k) { + if (head == null || k == 0) { + return null; + } + ListNode firstIndex = head; + ListNode secondIndex = head; + for (int i = 0; i < k; ++i) { + if (firstIndex.nextNode != null) { + firstIndex = firstIndex.nextNode; + } else { + return null; + } + } + while (firstIndex != null) { + secondIndex = secondIndex.nextNode; + firstIndex = firstIndex.nextNode; + } + return secondIndex; + } + } + + class ListNode { + int data; + ListNode nextNode; + } + ``` +16. 反转链表 + 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。 + 思路:反转链表问的比较多,整体的思路就是从后往前来,这个问题我也是花了很长时间才弄明白,太笨了。也有两种方式:递归和普通的方式 + ```java + public class LinkedListDemo { + public static void main(String[] args) { + ListNode head = new ListNode(); + ListNode second = new ListNode(); + ListNode third = new ListNode(); + ListNode forth = new ListNode(); + head.nextNode = second; + second.nextNode = third; + third.nextNode = forth; + head.data = 1; + second.data = 2; + third.data = 3; + forth.data = 4; + ListNode resultListNode = reverse1(head); + System.out.println(resultListNode.data); + } + + /** + * 递归 + * + * @param head + * @return + */ + public static ListNode reverse1(ListNode head) { + if (null == head || null == head.getNextNode()) { + return head; + } + // A B C -> A C B -> C B A + ListNode reversedHead = reverse1(head.getNextNode()); + head.getNextNode().setNextNode(head); + head.setNextNode(null); + return reversedHead; + } + + public static ListNode reverse2(ListNode head) { + if (null == head) { + return head; + } + // A B C + ListNode pre = head; // A + ListNode cur = head.getNextNode(); // B + ListNode next; + while (cur != null) { + // next = C + next = cur.getNextNode(); + // B -> A + cur.setNextNode(pre); + // pre = B + pre = cur; + // cur = C + cur = next; + // 第一轮下来就是 A B C -> A B A + // 第二轮下来就是 C B A pre = C cur = null + // 再继续就会跳出循环 + } + + // 虽然已经是C B A 了,但是不要忘了此时A的next还是B,所以我们要将其设置为null + // 将原链表的头节点的下一个节点置为null,再将反转后的头节点赋给head + head.setNextNode(null); + head = pre; + // 到这就是返回C了。 + return head; + } + + } + + class ListNode { + public ListNode nextNode; + public int data; + + public ListNode getNextNode() { + return nextNode; + } + + public void setNextNode(ListNode nextNode) { + this.nextNode = nextNode; + } + + public int getData() { + return data; + } + + public void setData(int data) { + this.data = data; + } + } + ```     +17. 合并两个排序的链表 + 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按 + 照递增排序的。 --- From 3d2c0c525976bdd0e727c42c39005a643baf8c19 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 9 Mar 2016 14:07:45 +0800 Subject: [PATCH 009/373] =?UTF-8?q?Update=20=E5=89=91=E6=8C=87Offer.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\345\211\221\346\214\207Offer.md" | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" index 031546b6..e7272251 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" @@ -749,7 +749,152 @@ 17. 合并两个排序的链表 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按 照递增排序的。 + 思路: 合并两个链表,按照递增顺序,那就是假设第一个链表是1 3 第二个链表是2 4 6那怎么去合并呢? + 先是比较两个链表的头结点,1和2比较,那合并后的新链表头肯定是1了,然后再拿2和3比较看谁是第二个结点,那可定是2了,到这里就确定了新链表的前两个结点, + 就是1 2 然后再用3和4比较确定谁是第三个,这是啥?这是递归。 + ```java + public class MergeListTest { + public static void main(String[] args) { + ListNode head1 = new ListNode(); + ListNode second1 = new ListNode(); + ListNode head2 = new ListNode(); + ListNode second2 = new ListNode(); + ListNode third2 = new ListNode(); + head1.nextNode = second1; + head2.nextNode = second2; + second2.nextNode = third2; + head1.data = 1; + second1.data = 3; + head2.data = 2; + second2.data = 2; + third2.data = 2; + MergeListTest test = new MergeListTest(); + ListNode result = test.mergeList(head1, head2); + System.out.println(result.nextNode.nextNode.nextNode.nextNode.data); + } + + public ListNode mergeList(ListNode head1, ListNode head2) { + if (head1 == null) { + return head2; + } else if (head2 == null) { + return head1; + } + ListNode mergeHead = null; + if (head1.data < head2.data) { + mergeHead = head1; + mergeHead.nextNode = mergeList(head1.nextNode, head2); + } else { + mergeHead = head2; + mergeHead.nextNode = mergeList(head1, head2.nextNode); + } + return mergeHead; + } + } + + class ListNode { + public ListNode nextNode; + public int data; + + public ListNode getNextNode() { + return nextNode; + } + + public void setNextNode(ListNode nextNode) { + this.nextNode = nextNode; + } + + public int getData() { + return data; + } + + public void setData(int data) { + this.data = data; + } + } + ``` +18. 树的子结构 + + 输入两颗二叉树 A 和 B,判断 B 是不是 A 的子结构。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/findchildbinarytree.png) + 思路: 想要判断B树是不是A的子树,那首先要先去在A树种遍历找到与B树根节点相同的结点,然后再从这个结点去遍历子结点 + 看字节点与B树是否一样,如果不一样就继续往下遍历结点,找与B树根节点一直的结点,如此循环。 + 以上图为例,我们先在A中找8的结点,发现A的根节点就是,然后继续看A的左子节点是8,而B的左子节点是9,锁着这 + 肯定不是了,继续往下找为8的结点,发现在A树的第二层找打了,然后再进行判断该结点下的子节点,发现为9和2,正好与B的相同 + 就是他了。 + ```java + public class Problem18 { + public static void main(String args[]) { + BinaryTreeNode root1 = new BinaryTreeNode(); + BinaryTreeNode node1 = new BinaryTreeNode(); + BinaryTreeNode node2 = new BinaryTreeNode(); + BinaryTreeNode node3 = new BinaryTreeNode(); + BinaryTreeNode node4 = new BinaryTreeNode(); + BinaryTreeNode node5 = new BinaryTreeNode(); + BinaryTreeNode node6 = new BinaryTreeNode(); + root1.leftNode = node1; + root1.rightNode = node2; + node1.leftNode = node3; + node1.rightNode = node4; + node4.leftNode = node5; + node4.rightNode = node6; + root1.value = 8; + node1.value = 8; + node2.value = 7; + node3.value = 9; + node4.value = 2; + node5.value = 4; + node6.value = 7; + BinaryTreeNode root2 = new BinaryTreeNode(); + BinaryTreeNode a = new BinaryTreeNode(); + BinaryTreeNode b = new BinaryTreeNode(); + root2.leftNode = a; + root2.rightNode = b; + root2.value = 8; + a.value = 9; + b.value = 2; + System.out.println(hasSubTree(root1, root2)); + } + + public static boolean hasSubTree(BinaryTreeNode root1, BinaryTreeNode root2) { + boolean result = false; + if (root1 != null && root2 != null) { + if (root1.value == root2.value) { + result = doesTree1HavaTree2(root1, root2); + if (!result) { + result = hasSubTree(root1.leftNode, root2); + } + if (!result) { + result = hasSubTree(root1.rightNode, root2); + } + } + } + return result; + } + + private static boolean doesTree1HavaTree2(BinaryTreeNode root1, + BinaryTreeNode root2) { + if (root2 == null) { + return true; + } else if (root1 == null) + return false; + if (root1.value != root2.value) { + return false; + } + return doesTree1HavaTree2(root1.leftNode, root2.leftNode) + && doesTree1HavaTree2(root1.rightNode, root2.rightNode); + } + } + class BinaryTreeNode { + int value; + BinaryTreeNode leftNode; + BinaryTreeNode rightNode; + } + ``` + +19. 二叉树的镜像 + 请完成一个函数,输入一个二叉树,该函数输出它的镜像。 + --- - 邮箱 :charon.chui@gmail.com From 67d0d835a4e28c9c461392fb47c32a388f918793 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 15 Mar 2016 12:58:26 +0800 Subject: [PATCH 010/373] update --- ...211\221\346\214\207Offer(\344\270\212).md" | 532 +++++++++++++++--- ...211\221\346\214\207Offer(\344\270\213).md" | 132 +++++ 2 files changed, 571 insertions(+), 93 deletions(-) rename "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" => "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" (65%) create mode 100644 "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" similarity index 65% rename from "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" rename to "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" index e7272251..cdb8a89f 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer.md" +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" @@ -1,4 +1,4 @@ -剑指Offer +剑指Offer(上) === 最近面试,遇到一些笔试题,写不上来,内心是崩溃的,该好好复习下了,所以决定仔细做一遍,随便也整理下,方便大家学习。 @@ -7,7 +7,7 @@ 2. 实现单例模式 单例的实现分为好几种: - - 饿汉式 + - 饿汉式 - 懒汉式 - 枚举 @@ -655,97 +655,98 @@ } ``` 16. 反转链表 - 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。 - 思路:反转链表问的比较多,整体的思路就是从后往前来,这个问题我也是花了很长时间才弄明白,太笨了。也有两种方式:递归和普通的方式 - ```java - public class LinkedListDemo { - - public static void main(String[] args) { - ListNode head = new ListNode(); - ListNode second = new ListNode(); - ListNode third = new ListNode(); - ListNode forth = new ListNode(); - head.nextNode = second; - second.nextNode = third; - third.nextNode = forth; - head.data = 1; - second.data = 2; - third.data = 3; - forth.data = 4; - ListNode resultListNode = reverse1(head); - System.out.println(resultListNode.data); - } + 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。 + 思路:反转链表问的比较多,整体的思路就是从后往前来,这个问题我也是花了很长时间才弄明白,太笨了。 + 也有两种方式:递归和普通的方式 - /** - * 递归 - * - * @param head - * @return - */ - public static ListNode reverse1(ListNode head) { - if (null == head || null == head.getNextNode()) { - return head; - } - // A B C -> A C B -> C B A - ListNode reversedHead = reverse1(head.getNextNode()); - head.getNextNode().setNextNode(head); - head.setNextNode(null); - return reversedHead; - } - - public static ListNode reverse2(ListNode head) { - if (null == head) { - return head; - } - // A B C - ListNode pre = head; // A - ListNode cur = head.getNextNode(); // B - ListNode next; - while (cur != null) { - // next = C - next = cur.getNextNode(); - // B -> A - cur.setNextNode(pre); - // pre = B - pre = cur; - // cur = C - cur = next; - // 第一轮下来就是 A B C -> A B A - // 第二轮下来就是 C B A pre = C cur = null - // 再继续就会跳出循环 - } - - // 虽然已经是C B A 了,但是不要忘了此时A的next还是B,所以我们要将其设置为null - // 将原链表的头节点的下一个节点置为null,再将反转后的头节点赋给head - head.setNextNode(null); - head = pre; - // 到这就是返回C了。 - return head; - } - - } - - class ListNode { - public ListNode nextNode; - public int data; - - public ListNode getNextNode() { - return nextNode; - } - - public void setNextNode(ListNode nextNode) { - this.nextNode = nextNode; - } - - public int getData() { - return data; - } - - public void setData(int data) { - this.data = data; - } - } - ```     + ```java + public class LinkedListDemo { + public static void main(String[] args) { + ListNode head = new ListNode(); + ListNode second = new ListNode(); + ListNode third = new ListNode(); + ListNode forth = new ListNode(); + head.nextNode = second; + second.nextNode = third; + third.nextNode = forth; + head.data = 1; + second.data = 2; + third.data = 3; + forth.data = 4; + ListNode resultListNode = reverse1(head); + System.out.println(resultListNode.data); + } + + /** + * 递归 + * + * @param head + * @return + */ + public static ListNode reverse1(ListNode head) { + if (null == head || null == head.getNextNode()) { + return head; + } + // A B C -> A C B -> C B A + ListNode reversedHead = reverse1(head.getNextNode()); + head.getNextNode().setNextNode(head); + head.setNextNode(null); + return reversedHead; + } + + public static ListNode reverse2(ListNode head) { + if (null == head) { + return head; + } + // A B C + ListNode pre = head; // A + ListNode cur = head.getNextNode(); // B + ListNode next; + while (cur != null) { + // next = C + next = cur.getNextNode(); + // B -> A + cur.setNextNode(pre); + // pre = B + pre = cur; + // cur = C + cur = next; + // 第一轮下来就是 A B C -> A B A + // 第二轮下来就是 C B A pre = C cur = null + // 再继续就会跳出循环 + } + + // 虽然已经是C B A 了,但是不要忘了此时A的next还是B,所以我们要将其设置为null + // 将原链表的头节点的下一个节点置为null,再将反转后的头节点赋给head + head.setNextNode(null); + head = pre; + // 到这就是返回C了。 + return head; + } + } + + class ListNode { + public ListNode nextNode; + public int data; + + public ListNode getNextNode() { + return nextNode; + } + + public void setNextNode(ListNode nextNode) { + this.nextNode = nextNode; + } + + public int getData() { + return data; + } + + public void setData(int data) { + this.data = data; + } + } + ``` + 17. 合并两个排序的链表 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按 照递增排序的。 @@ -894,9 +895,354 @@ 19. 二叉树的镜像 请完成一个函数,输入一个二叉树,该函数输出它的镜像。 - + 思路:什么事镜像? 就像照镜子一样。打个比方现在的数是 + ``` + 1 1 + 2 3 左图的二叉树的镜像就是 3 2 + 4 5 6 7 7 6 5 4 + ``` + 就是从根节点开始,先前序遍历这棵树的每个结点,先转换它的两个子节点,然后再对这两个子节点下的子节点进行转换。 + ```java + public class mirrorBinaryTreeTest { + public static void main(String[] args) { + BinaryTreeNode root1 = new BinaryTreeNode(); + BinaryTreeNode node1 = new BinaryTreeNode(); + BinaryTreeNode node2 = new BinaryTreeNode(); + + BinaryTreeNode node3 = new BinaryTreeNode(); + BinaryTreeNode node4 = new BinaryTreeNode(); + BinaryTreeNode node5 = new BinaryTreeNode(); + BinaryTreeNode node6 = new BinaryTreeNode(); + root1.leftNode = node1; + root1.rightNode = node2; + node1.leftNode = node3; + node1.rightNode = node4; + node4.leftNode = node5; + node4.rightNode = node6; + root1.value = 8; + node1.value = 8; + node2.value = 7; + node3.value = 9; + node4.value = 2; + node5.value = 4; + node6.value = 7; + BinaryTreeNode rootBinaryTreeNode = mirrorBinaryTree(root1); + } + + public static BinaryTreeNode mirrorBinaryTree(BinaryTreeNode root) { + if (root == null) { + return null; + } + if (root.leftNode == null && root.rightNode == null) + return null; + Stack stack = new Stack(); + while (root != null || !stack.isEmpty()) { + while (root != null) { + BinaryTreeNode temp = root.leftNode; + root.leftNode = root.rightNode; + root.rightNode = temp; + stack.push(root); + root = root.leftNode; + } + root = stack.pop(); + root = root.rightNode; + } + return root; + } + } + + class BinaryTreeNode { + public int value; + public BinaryTreeNode leftNode; + public BinaryTreeNode rightNode; + + public BinaryTreeNode() { + + } + + public BinaryTreeNode(int value) { + this.value = value; + this.leftNode = null; + this.rightNode = null; + } + } + ``` +20. 顺时针打印矩阵 + 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。 + ``` + 1 2 3 4 + 5 6 7 8 + 9 10 11 12 + ``` + 思路:顺时针打印也就是4步,从左往右、从上往下、从右向左、从下往上,然后继续循环如此, + 当然在每次循环的时候都要判断好,以免只有一行或者一列或者一个元素的情况。 + ```java + public class printMatrixTest { + public static void main(String[] args) { + int[][] arr = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }; + printMatrixInCircle(arr); + } + + public static void printMatrixInCircle(int[][] array) { + if (array == null) { + return; + } + int start = 0; + // 循环的次数就是维度要大于指针的2倍 + while (array[0].length > start * 2 && array.length > start * 2) { + printOneCircle(array, start); + start++; + } + } + + private static void printOneCircle(int[][] array, int start) { + int columns = array[0].length; + int rows = array.length; + int endX = columns - 1 - start; + int endY = rows - 1 - start; + // 从左到右打印一行 + for (int i = start; i <= endX; i++) { + int number = array[start][i]; + System.out.print(number + ","); + } + // 从上到下打印一列 + if (start < endY) { + for (int i = start + 1; i <= endY; i++) { + int number = array[i][endX]; + System.out.print(number + ","); + } + } + // 从右到左打印一行 + if (start < endX && start < endY) { + for (int i = endX - 1; i >= start; i--) { + int number = array[endY][i]; + System.out.print(number + ","); + } + } + // 从下到上打印一列 + if (start < endY && start < endY - 1) { + for (int i = endY - 1; i >= start + 1; i--) { + int number = array[i][start]; + System.out.print(number + ","); + } + } + } + } + ``` +21. 包含`min`函数的栈 + 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的`min`函数。 + 在该栈中,调用`min`、`push`及`pop`的时间复杂度都是O(1) + 思路: 最先想到的就是用一个变量记录住最小的元素,但是如果这个最小的元素被取出了呢? + 怎么再返回剩下所有元素中最小的一个呢?很显然用一个变量记住是不行的,我们必须要用 + 一个辅助栈来纪录这小小元素。 + ```java + public class MinInStack { + /** + * 辅助栈,来纪录这些小的元素 + */ + private MyStack minStack = new MyStack<>(); + private MyStack dataStack = new MyStack<>(); + + public void push(Integer item) { + dataStack.push(item); + if (minStack.length == 0 || item <= minStack.head.data) { + minStack.push(item); + } else { + minStack.push(minStack.head.data); + } + } + + public Integer pop() { + if (dataStack.length == 0 || minStack.length == 0) { + return null; + } + minStack.pop(); + return dataStack.pop(); + } + + public Integer min() { + if (minStack.length == 0) { + return null; + } + return minStack.head.data; + } + + public static void main(String[] args) { + MinInStack test = new MinInStack(); + test.push(3); + test.push(2); + test.push(1); + System.out.println(test.pop()); + System.out.println(test.min()); + } + } + + /** + * 链表 + */ + class ListNode { + K data; + ListNode nextNode; + } + + /** + * 自定义栈的数据结构 + */ + class MyStack { + public ListNode head; + /** + * 当前栈的大小 + */ + public int length; + + public void push(K item) { + ListNode node = new ListNode(); + node.data = item; + node.nextNode = head; + head = node; + length++; + } + + public K pop() { + if (head == null) + return null; + ListNode temp = head; + head = head.nextNode; + length--; + return temp.data; + } + + } + ``` +22. 栈的压入、弹出序列 + 题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是 + 否为该栈的弹出序列。假设压入栈的所有数字均不相等。 + 例如压栈序列为 1、2、3、4、5.序列 4、5、3、2、1 是压栈序列对应的一个 + 弹出序列,但 4、3、5、1、2 却不是。 + 思路: 这道题我完全没看懂,看完后实在不理解是什么意思。 - -!搜了很久才弄明白。 + 什么事对应的弹出序列呢? 就是这个入栈后所有的可能弹出的方式。比如我可以完全按照1、2、3、4、5 + 的方式去放入,然后取出就是5、4、3、2、1,也可以在1、2 + 进入的时候先把2弹出,然后再继续添加3、4、5,这样的弹出顺序就是2、5、4、3、1。 + 但是为什么4、3、5、1、2是不对的呢? 这是因为1、2、3、4添加进入的时候,我们先弹出 + 4然后再弹出3,因为以后要弹5,所以我们必须继续添加5,这样栈内就剩下了1、2、5,然后我们再 + 往外弹出就只能是5、2、1,也就是他的顺序只能是4、3、5、2、1。 + 那怎么判断呢? 如果下一个弹出的数字刚好是栈顶数字,那么直接弹出。 + 如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈, + 直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了仍没有 + 找到下一个弹出的数字,那么该序列不可能是一个弹出序列。 + ```java + public class PopOrderTest { + public static void main(String[] args) { + int[] array1 = { 1, 2, 3, 4, 5 }; + int[] array2 = { 4, 3, 5, 1, 2 }; + System.out.println(isPopOrder(array1, array2)); + } + + public static boolean isPopOrder(int[] line1, int[] line2) { + if (line1 == null || line2 == null) { + return false; + } + int index = 0; + // 把line1中的元素都加入该栈 + Stack stack = new Stack(); + for (int i = 0; i < line2.length; i++) { + if (!stack.isEmpty() && stack.peek() == line2[i]) { + // 要取出的元素正好是栈顶的元素,就直接取出。 + stack.pop(); + } else { + if (index == line1.length) { + // line1的元素已经全部加入栈中,且要取出的元素仍然不是栈顶的元素,那就说明line1中不包含要取出的元素。直接返回false + return false; + } else { + // 只要要取出的元素不是栈顶的,就一直往栈里面加 + do { + stack.push(line1[index++]); + } while (stack.peek() != line2[i] && index != line1.length); + + if (stack.peek() == line2[i]) { + stack.pop(); + } else { + return false; + } + } + } + } + return true; + } + } + ``` + +23. 从上往下打印二叉树 + 从上往下打印二叉树的每个结点,同一层的结点按照从左到右的顺序打印。 + 思路: 每一次打印一个结点的时候,如果该结点有子节点,把该结点的子节点放到一个队列的尾。接下来到队列的头部取出最早进入队列的结点,重复前面打印操作,直到队列中所有的结点都被打印出为止。 + ```java + public class Problem23 { + public static void main(String args[]) { + BinaryTreeNode root1 = new BinaryTreeNode(); + BinaryTreeNode node1 = new BinaryTreeNode(); + BinaryTreeNode node2 = new BinaryTreeNode(); + BinaryTreeNode node3 = new BinaryTreeNode(); + BinaryTreeNode node4 = new BinaryTreeNode(); + BinaryTreeNode node5 = new BinaryTreeNode(); + BinaryTreeNode node6 = new BinaryTreeNode(); + + root1.leftNode = node1; + root1.rightNode = node2; + node1.leftNode = node3; + node1.rightNode = node4; + node4.leftNode = node5; + node4.rightNode = node6; + root1.value = 8; + node1.value = 8; + node2.value = 7; + node3.value = 9; + node4.value = 2; + node5.value = 4; + node6.value = 7; + + Problem23 test = new Problem23(); + test.printFromTopToBottom(root1); + } + + public void printFromTopToBottom(BinaryTreeNode root) { + if (root == null) + return; + Queue queue = new LinkedList(); + queue.add(root); + while (!queue.isEmpty()) { + BinaryTreeNode node = queue.poll(); + System.out.print(node.value); + if (node.leftNode != null) { + queue.add(node.leftNode); + } + if (node.rightNode != null) { + queue.add(node.rightNode); + } + } + } + } + + class BinaryTreeNode { + public int value; + public BinaryTreeNode leftNode; + public BinaryTreeNode rightNode; + + public BinaryTreeNode() { + + } + + public BinaryTreeNode(int value) { + this.value = value; + this.leftNode = null; + this.rightNode = null; + } + } + ``` + + --- - 邮箱 :charon.chui@gmail.com - Good Luck! + diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" new file mode 100644 index 00000000..50e15bf7 --- /dev/null +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" @@ -0,0 +1,132 @@ +剑指Offer(下) +=== + +剑指Offer(上)一共是23道题。 + +24. 二叉搜索树的后序遍历序列 + 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。 是则返回`true`,否则返回`false`。假设输入的数组的任意两个数字都互不相同 + 思路: 在后序遍历得到的序列中,最后一个数字是树的根节点的值。 + 数组中前面的数字可以分为两部分:第一部分是左子树结点的值, + 它们都比根节点的值小;第二部分是右子树结点的值,他们都比根节点的值大。 + ```java + public class Problem24 { + public static void main(String[] args) { + int[] array = { 5, 7, 6, 9, 11, 10, 8 }; + System.out.println(verfiySequence(array)); + } + + public static boolean verfiySequence(int[] array) { + if (array == null || array.length == 0) { + return false; + } + int length = array.length; + // 最后一个是根节点 + int root = array[length - 1]; + // 左右子树的分界点,因为左子树都是小于根节点的,右子树都是大于的。 + int cut = 0; + for (int i = 0; i < length - 1; i++) { + if (array[i] > root) { + // 到了右子树的分界点 + cut = i + 1; + break; + } + } + if (cut == 0) { + // 没有右子树,都是左子树,然后就继续判断除了上面根节点后的其他所有节点,这些都是左子树的。 + verfiySequence(Arrays.copyOfRange(array, 0, length - 1)); + } else { + // 有右子树,判断从分界点开始后的所有数是不是都大于根节点。 + for (int j = cut; j < length - 1; j++) { + if (array[j] < root) { + // 有不大于根节点值的数,肯定是错误的。 + return false; + } + } + } + boolean left = true; + if (cut > 0) { + // 判断左子数里面的元素是否都对 + left = verfiySequence(Arrays.copyOfRange(array, 0, cut)); + } + boolean right = true; + if (cut < length - 1) { + // 右子树 + right = verfiySequence(Arrays.copyOfRange(array, cut, length - 1)); + } + return (right && left); + } + } + ``` +25. 二叉树中和为某一值的路径 + 输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶结点所经过的所有的结点形成一条路径。 + 思路: + ```java + public class Problem25 { + public static void main(String args[]) { + BinaryTreeNode root1 = new BinaryTreeNode(); + BinaryTreeNode node1 = new BinaryTreeNode(); + + BinaryTreeNode node2 = new BinaryTreeNode(); + BinaryTreeNode node3 = new BinaryTreeNode(); + BinaryTreeNode node4 = new BinaryTreeNode(); + root1.leftNode = node1; + root1.rightNode = node2; + node1.leftNode = node3; + node1.rightNode = node4; + root1.value = 10; + node1.value = 5; + node2.value = 12; + node3.value = 4; + node4.value = 7; + Problem25 testFindPath = new Problem25(); + testFindPath.findPath(root1, 22); + } + + public void findPath(BinaryTreeNode root, int sum) { + if (root == null) + return; + Stack stack = new Stack(); + int currentSum = 0; + findPath(root, sum, stack, currentSum); + } + + private void findPath(BinaryTreeNode root, int sum, Stack stack, int currentSum) { + currentSum += root.value; + stack.push(root.value); + if (root.leftNode == null && root.rightNode == null) { + // 到节点的尾部了 + if (currentSum == sum) { + for (int path : stack) { + System.out.print(path + " "); + } + System.out.println(); + } + } + if (root.leftNode != null) { + findPath(root.leftNode, sum, stack, currentSum); + } + if (root.rightNode != null) { + findPath(root.rightNode, sum, stack, currentSum); + } + stack.pop(); + } + } + + class BinaryTreeNode { + public static int value; + public BinaryTreeNode leftNode; + public BinaryTreeNode rightNode; + } + ``` + +26. 复杂链表的复制 + 实现函数复制一个复杂链表。在复杂链表中,每个结点除了有一个 next 指针指向下一个结点外,还有一个指向链表中任意结点或 null。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + From 40c5cfaf177dc34b078b5b614c0d3d6cfd7e7c06 Mon Sep 17 00:00:00 2001 From: "xu.chuanren" Date: Thu, 7 Apr 2016 19:39:28 +0800 Subject: [PATCH 011/373] add applicationId vs packageName --- "Android\345\212\240\345\274\272/.DS_Store" | Bin 0 -> 10244 bytes .../ApplicationId vs PackageName.md" | 30 ++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/.DS_Store" create mode 100644 "Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" diff --git "a/Android\345\212\240\345\274\272/.DS_Store" "b/Android\345\212\240\345\274\272/.DS_Store" new file mode 100644 index 0000000000000000000000000000000000000000..4a6d7aa23b217eca98177afb91e4258cb72f6f9b GIT binary patch literal 10244 zcmeHM&u<$=6n-0rHV_;q6h)DGs1XMuB%p9W5tWdehC-EegS~DkxH$3dx`~OqjuJa@ zC`}C#Qmr7RRY5iV3(A2%pqvq)sy#%NP$dpM1W=_)oDf0?6uvjR4YT%ai#Q;PW>&Lr zf6V*l&G+7$^^A$g7_GU_6D5hL2bE?2qc|xFzvtRfx`HQ9LK@H~TA~tFX(jty=5j}C z{XjFI8PE)91~dbjf&T&n_-1qI>Gh>PYX&p}nt^Qwcz#HtvP=cC;Y%@f;3Sp+kllDJ z3a+sZ5IV^~rUKdUrBDT?zj`nNRU=qp7`{5*C%HLfDv%9d`s!r(>SP2x8^H?25WPb$ zDVmd!@})j&1~dbm4Di}LLHo(Uy1gW?-`Br>x0jx!ljI&MR~)-svC)!~>mZV=XG;_T zx2s*dHgVBjy_tED5!QZeZmLj>o6v1ZD1$)p$fX6Uzz!GLpT$)TcFme=<}aBY)_^}SR13n~PIM=twz z4-n>Yl);@t1rZ|^uy;s!Xo(i#r`Pe^p*fsqs7NN{Nk~QHFnWylg}S!3MC2s@G5V?! zpSi08*$D9Cz|Tf<5o7VLSS~fj!Eo1Mz3L9oUOv5-0$!Ioc=I( zto-H07cx6$&M7evn<0!nz^LMwrz2ozf({_tJsivv&qTO`-|n_KAR@4c_f-`-XH5uw za9cAHJ<6ZVe+X+gtScZEcs8mc@;y2MYd9C+4Oml>pG2Vw4!@B&OaQecV&f=ek(Rx4 zb>p7w$=ZW-7#28Y@A<=LJY2bCTUWzTkv(mGujGoyCu1c9)&erv#Tb{#%bhT%?eX+S zN^NFZ*4`zweJ)C{q>NlEVq7K62HxbE+OP_Rb0#)|OzjRb^%0=#rz}TNBnODX*oeen zKQVJIdaH=5`*6s94e7`4YW#aG%WS?xcE-+%xxo13#coZy~0~O2YKd==x;y>XW-2b zCXcLrMi#Ud#2kv5EI)GT6njf$iTJk5mh8;Oj;AK0MhcEX{q3B7s_{ouYxZL3cSF4S zRqMR_dGY0?U(<`Q=*slI)f+0SI0HL1o11mR9AB?AH_g6!S^Zmuqcx<7TU|5*ngPv# zWNF}BZrAXo zP{B!T$3u$kc--)I{4grF-OG{-u2X?*_!3W0{`Eftu)7eoQs)(hUjOU$|6e=ayq^94 Ht^fZ7&L(yN literal 0 HcmV?d00001 diff --git "a/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" "b/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" new file mode 100644 index 00000000..8859c3c2 --- /dev/null +++ "b/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" @@ -0,0 +1,30 @@ +ApplicationId vs PackageName +=== + +曾几何时,自从转入`Studio`阵营后就发现多了个`applicationId "com.xx.xxx"`,虽然知道肯定会有区别,但是我却没有仔细去看,只想着把它和`packageName`设置成相同即可(原谅我的懒惰- -!)。 +直到今天在官网看`Gradle`使用时,终于忍不住要搞明白它俩的区别。 + + +http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename + + + +所有的 Android 应用程序都有一个包名。包名是设备上的这个应用程序的唯一标识,也是在谷歌Play商店上的唯一标识。这意味着,一旦你已发布的程序使用了这个包名, 你就永远都无法改变它;否则会导致你的应用程序被当作是一个全新的应用程序,你之前的应用程序的用户将不会看到作为更新的安装包。 +在此前Android Gradle 构建系统中,您的应用程序的包名由你的manifest文件的根元素里的package属性决定: +AndroidManifest.xml: +然而,这里所定义的包也有第二个目的:它被用来命名你的资源类的包(以及解析任何相关的Activity的类名)。在上面的示例中,生成的 R 类将会是com.example.my.app.R,因此如果您其他包里面的代码需要引用这些资源,就需要导入com.example.my.app.R。 +使用新的 Android Gradle 构建系统,你可以轻松构建多个不同版本的应用程序;例如,您可以构建一个“free”版本和“pro”版本的应用程序 (通过使用flavors),并且这些不同版本的程序在 Google Play 商店上应该有不同的包,这样他们可以被单独安装和购买,或者是同时安装两个,等等。同样,您还可以同时创建“debug”、“alpha”和“beta”版本的应用程序 (使用build types),而这些版本的程序同样可以使用唯一的包名。 +同时,您要在代码中导入的 R 类必须在这段时间内保持不变 ;在您正在构建您的应用程序的不同版本时您的.java 源文件不应该被更改。 +因此,我们解耦了包名称的两种用法: +最终的方案是,在您生成的.apk 的manifest 中,并且用于在你的设备和 Google Play 商店来标识你的应用的包,叫做“application id”。 +用于在源代码中来引用您的R类的,并且是解析任何相关的Activity/Service 注册的包,继续被称为“package”。 +你可以在你 gradle 文件中,指定application id,如下所示: +app/build.gradle: apply plugin: 'com.android.application' +android { compileSdkVersion 19 buildToolsVersion "19.1" +defaultConfig { applicationId "com.example.my.app" minSdkVersion 15 targetSdkVersion 19 versionCode 1 versionName "1.0" } ... +像以前一样,你需要在 Manifest 文件中指定用于代码的包,就如上面的Andr + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From d6810fe12366724895738e5c03893659e05146e5 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 8 Apr 2016 15:42:43 +0800 Subject: [PATCH 012/373] add applicationId vs packageName --- "Android\345\212\240\345\274\272/.DS_Store" | Bin 10244 -> 10244 bytes .../ApplicationId vs PackageName.md" | 91 ++++- .../Git\345\221\275\344\273\244.md" | 354 +++++++++--------- ...211\221\346\214\207Offer(\344\270\213).md" | 7 +- 4 files changed, 258 insertions(+), 194 deletions(-) diff --git "a/Android\345\212\240\345\274\272/.DS_Store" "b/Android\345\212\240\345\274\272/.DS_Store" index 4a6d7aa23b217eca98177afb91e4258cb72f6f9b..e2756d5ea30a65147057cd53d06dc95eba801fe1 100644 GIT binary patch delta 39 vcmZn(XbG6$I9U^hRb(qb#rT6@Ia7UMRxM3;;xh5a$2@ diff --git "a/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" "b/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" index 8859c3c2..7c717c54 100644 --- "a/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" +++ "b/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" @@ -2,27 +2,86 @@ ApplicationId vs PackageName === 曾几何时,自从转入`Studio`阵营后就发现多了个`applicationId "com.xx.xxx"`,虽然知道肯定会有区别,但是我却没有仔细去看,只想着把它和`packageName`设置成相同即可(原谅我的懒惰- -!)。 -直到今天在官网看`Gradle`使用时,终于忍不住要搞明白它俩的区别。 +直到今天在官网看`Gradle`使用时,终于忍不住要搞明白它俩的区别。 -http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename +在`Android`官方文档中有一句是这样描述`applicationId`的:`applicationId : the effective packageName`,真是言简意赅,那既然`applicationId`是有效的包明了,`packageName`算啥? +所有`Android`应用都有一个包名。包名在设备上能唯一的标示一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 +之前的`Android Gradle`构建系统中,应用的包名是由你的`manifest`文件中的根元素中的`package`属性定义的: + +`AndroidManifest.xml: ` + +``` + + +``` +然而,这里定义的包也有第二个目的:就是被用来命名你的`R`资源类(以及解析任何与`Activities`相关的类名)的包。在上面的示例中,生成的`R`类就是`com.example.my.app.R`,所以如果你在其他的包中想引用资源,就需要导入`com.example.my.app.R`。 + +伴随着新的`Android Gradle`构建系统,你可以很简单的为你的应用构建多个不同的版本;例如,你可以同时为你的应用构建一个免费版本和一个专业版(使用`flavors`),并且他们应该在`Google Play`商店中有不同的包,这样才能让他们可以被单独安装和购买,同时安装两个,等等。同样的你也可能同时为你的应用构建`debug`版、`alpha`版和`beta`版(使用`build types`),这些也可以同样使用不同的包名。 + +在这同时,你在代码中导入的`R`类必须一直保持一直;在为应用构建不同的版本时`.java`源文件都不应该发生变化。 + +因此,我们解耦了`package name`的两种用法: + +- 在生成的`.apk`中的`manifest`文件中使用的最终的包名以及在你的设备和`Google Play`商店中用来标示你的包名叫做`application id`的值。 +- 在源代码中指向`R`类的包名以及在解析任何与`activity/service`注册相关的包名继续叫做`package name`。 + +可以在`gradle`文件中像如下指定`application id`: + +`app/build.gradle:` + +``` +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "19.1" + + defaultConfig { + applicationId "com.example.my.app" + minSdkVersion 15 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + ... +``` + +像之前一样,你需要在`Manifest`文件中指定你在代码中使用的'package name',像上面`AndroidManifest.xml`的例子。 +***下面进入关键部分了:***当你按照上面的方式做完后,这两个包就是相互独立的了。你现在可以很简单的重构你的代码-通过修改`Manifest`中的包名来修改在你的`activitise`和`services`中使用的包和在重构你在代码中的引用声明。这不会影响你应用的最终`id`,也就是在`Gradle`文件中的`applicationId`。 + +你可以通过以下`Gradle DSL`方法为应用的`flavors`和`build types`指定不同的`applicationId`: + +`app/buid.gradle: ` + +``` +productFlavors { + pro { + applicationId = "com.example.my.pkg.pro" + } + free { + applicationId = "com.example.my.pkg.free" + } +} + +buildTypes { + debug { + applicationIdSuffix ".debug" + } +} +.... +``` +(在`Android Studio`中你也可以通过图形化的`Project Structure`的对话框来更改上面所有的配置) + +***注意:***为了兼容性,如果你在`build.gradle`文件中没有定义`applicationId` ,那`applicationId`就是与`AndroidManifest.xml`中配置的包名相同的默认值。在这种情况下,这两者显然脱不了干系,如果你试图重构代码中的包就将会导致同时会改变你应用程序的`id`!在`Android Studio`中新创建的项目都是同时指定他们俩。 + +***注意2:***`package name`必须在默认的`AndroidManifest.xml`文件中指定。如果有多个`manifest`文件(例如对每个`flavor`制定一个`manifest`或者每个`build type`制定一个`manifest`)时,`package name`是可选的,但是如果你指定的话,它必须与主`manifest`中指定的`pakcage`相同。 -所有的 Android 应用程序都有一个包名。包名是设备上的这个应用程序的唯一标识,也是在谷歌Play商店上的唯一标识。这意味着,一旦你已发布的程序使用了这个包名, 你就永远都无法改变它;否则会导致你的应用程序被当作是一个全新的应用程序,你之前的应用程序的用户将不会看到作为更新的安装包。 -在此前Android Gradle 构建系统中,您的应用程序的包名由你的manifest文件的根元素里的package属性决定: -AndroidManifest.xml: -然而,这里所定义的包也有第二个目的:它被用来命名你的资源类的包(以及解析任何相关的Activity的类名)。在上面的示例中,生成的 R 类将会是com.example.my.app.R,因此如果您其他包里面的代码需要引用这些资源,就需要导入com.example.my.app.R。 -使用新的 Android Gradle 构建系统,你可以轻松构建多个不同版本的应用程序;例如,您可以构建一个“free”版本和“pro”版本的应用程序 (通过使用flavors),并且这些不同版本的程序在 Google Play 商店上应该有不同的包,这样他们可以被单独安装和购买,或者是同时安装两个,等等。同样,您还可以同时创建“debug”、“alpha”和“beta”版本的应用程序 (使用build types),而这些版本的程序同样可以使用唯一的包名。 -同时,您要在代码中导入的 R 类必须在这段时间内保持不变 ;在您正在构建您的应用程序的不同版本时您的.java 源文件不应该被更改。 -因此,我们解耦了包名称的两种用法: -最终的方案是,在您生成的.apk 的manifest 中,并且用于在你的设备和 Google Play 商店来标识你的应用的包,叫做“application id”。 -用于在源代码中来引用您的R类的,并且是解析任何相关的Activity/Service 注册的包,继续被称为“package”。 -你可以在你 gradle 文件中,指定application id,如下所示: -app/build.gradle: apply plugin: 'com.android.application' -android { compileSdkVersion 19 buildToolsVersion "19.1" -defaultConfig { applicationId "com.example.my.app" minSdkVersion 15 targetSdkVersion 19 versionCode 1 versionName "1.0" } ... -像以前一样,你需要在 Manifest 文件中指定用于代码的包,就如上面的Andr --- diff --git "a/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" "b/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" index fb5d1241..e9915a7f 100644 --- "a/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" +++ "b/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" @@ -1,175 +1,179 @@ -Git命令 -=== - -先上一张图 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.jpg) -图中的index部分就是暂存区 - -- 安装好git后我们要先配置一下。以便`git`跟踪。 - ``` - git config --global "user.name xxx" - git config --global "user.email xxx@xxx.com" - ``` - 上面修改后可以使用`cat ~/.gitconfig`查看 - 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 - `git config --list`来查看所有的配置。 - -- 新建仓库 - `mkdir gitDemo` - `cd gitDemo` - `git init` - 这样就创建完了。 - -- clone仓库 - 在某一目录下执行. - `git clone [git path]` - 只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 - -- `git add`提交文件更改(修改和新增),把当前的修改添加到暂存区 - `git add xxx.txt`添加某一个文件 - `git add .`添加当前目录所有的文件 - -- `git commit`提交,把修改由暂存区提交到仓库中 - `git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 - 或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx - -- `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 - -- `git diff`对比区别 - `git diff` 直接查看所有的区别 - `git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 - `git diff`比较只是当前工作区和暂存区之间的区别。如果想要查看暂存起来的文件和上次提交时的差异可以使用`git diff --staged`。或者想看和远程仓库最新内容的差异可以使用`git diff HEAD`。 - -- `git push` 提交到远程仓库 - 可以直接调用`git push`推送到当前分支 - 或者`git push origin master`推送到远程`master`分支 - `git push origin devBranch`推送到远程`devBranch`分支 - -- `git merge`合并目标分支到当前分支 - `git merge devBrach` - -- `git log`查看当前分支下的提交记录 - -- `git reset`命令回退到某一版本(回退已经暂存的文件) - `Git`中用`HEAD`表示当前版本,上一版本就是`HEAD^`,上上一版本就是`HEAD^^`.如果往前一千个版本呢? 那就是`HEAD~1000`. - `git reset —-hard HEAD^` - `git reset —-hard commit_id` - `git reset HEAD fileName`可以把用`git add`之后但是还没有`commit`之前暂存区中的修改撤销。 - 说到这里就说一个问题,如果你reset到某一个版本之后,发现弄错了,还想返回去,这时候用`git log`已经找不到之前的`commit id`了。那怎么办?这时候可以使用下面的命令来找。 - -- `git reflog` - 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 - -- `git checkout`撤销修改或者切换分支 - `git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: - - - 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 - - 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 - - 总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 - 这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: - - 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 - - 使用`git checkout --file`直接丢弃工作区的修改。 - - `git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 - 为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 - `git checkout origin/developer` // 切换到orgin/developer分支 - -- `git rm`删除文件 - 该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 - 另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上,用 --cached 选项即可:`git rm --cached readme.txt` - -- `git push` - 把本地仓库的内容推送到远程仓库中。 - -- 分支 - `git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) - - 创建分支 - `git branch devBranch`创建名为`devBranch`的分支。 - `git checkout devBranch`切换到`devBranch`分支。 - `git branch`查看当前仓库中的分支 - `git branch -r`查看远程仓库的分支 - ``` - origin/HEAD -> origin/master - origin/developer - origin/developer_sg - origin/master - origin/master_sg - origin/offline - ``` - `git branch -d devBranch`删除`devBranch`分支。 - 当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 - 这时如果你要强行删除的话可以使用命令`git branch -D xxx`. - 如何删除远程分支呢? - ``` - git branch -r -d origin/developer - git push origin :developer - ``` - 如何本地创建分支并推送给远程仓库? - ``` - // 本地创建分支 - git checkout master //进入master分支 - git checkout -b frommaster //以master为源创建分支frommaster - // 推送到远程仓库 - git push origin frommaster// 推送到远程仓库所要使用的名字 - ``` - - 如何切到到远程仓库分支进行开发呢? - git checkout -b frommaster origin/frommaster// 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 - 提交更改的话就用 - git push origin frommaster - - -- `git merge`合并指定分支到当前分支 - `git merge devBranch`将`devBranch`分支合并到`master`。 - -- 打`tag` - `git tag v1.0`来进行打`tag`,默认为`HEAD` - `git tag`查看所有`tag` - 如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` - 当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` - - `git show tagName`来查看某`tag`的详细信息。 -- 打完`tag`后怎么推送到远程仓库 - `git push origin tagName` - -- 删除`tag` - `git tag -d tagName` - -- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 - `git push origin:refs/tags/tagName` - -- 忽略文件 - 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 - 其实不用一个个的去写,具体可以根据项目参考[https://github.com/github/gitignore](https://github.com/github/gitignore) - 当然不要忘了把该文件提交上去 - 在用`linux`的时候会自动生成一些以`~`结尾的备份文件,如果ignore掉呢?[https://github.com/github/gitignore/blob/master/Global/Linux.gitignore](https://github.com/github/gitignore/blob/master/Global/Linux.gitignore) - -- 撤销最后一次提交 - 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以使用--amend选项重新提交:`git commit --amend` - -- 查看远程仓库克隆地址 - `git remote -v` - -关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) - -从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 -其实我一直有点分不开`reset`和`checkout`的区别,从这个图里能明显看出来了: - -- 当执行`git reset HEAD`命令时,暂存区的目录树会被重写,会被`master`分支指向的目录树所替换,但是工作区不受影响。 -- 当执行`git checkout .`或`git checkout -- file`命令是,会用暂存区全部的文件或指定的文件替换工作区的文件。这个操作很危险,会清楚工作区中未添加到暂存区的改动。 -命令时,会用`HEAD`指向的`master`分支中的全部或部分文件替换暂存区和工作区中的文件。这个命令也是极度危险的。因为不但会清楚工作区中未提交的改动,也会清楚暂存区中未提交的改动。 -- `git reset HEAD ` 是在添加到暂存区后,撤出暂存区使用,他只会把文件撤出暂存区,但是你的修改还在,仍然在工作区。当然如果使用`git reset --hard HEAD`这样就完了,工作区所有的内容都会被远程仓库最新代码覆盖。 -- `git checkout -- xxx.txt`是用于修改后未添加到暂存区时使用(如果修改后添加到暂存区后就没效果了,必须要先reset撤销暂存区后再使用checkout),这时候会把之前的修改覆盖掉。所以是危险的。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - \ No newline at end of file +@(Java基础) + +Git命令 +=== + +先上一张图 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.jpg) +图中的index部分就是暂存区 + +- 安装好git后我们要先配置一下。以便`git`跟踪。 + ``` + git config --global user.name "xxx" + git config --global user.email "xxx@xxx.com" + ``` + 上面修改后可以使用`cat ~/.gitconfig`查看 + 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 + `git config --list`来查看所有的配置。 + +- 新建仓库 + `mkdir gitDemo` + `cd gitDemo` + `git init` + 这样就创建完了。 + +- clone仓库 + 在某一目录下执行. + `git clone [git path]` + 只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 + +- `git add`提交文件更改(修改和新增),把当前的修改添加到暂存区 + `git add xxx.txt`添加某一个文件 + `git add .`添加当前目录所有的文件 + +- `git commit`提交,把修改由暂存区提交到仓库中 + `git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 + 或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx + +- `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 + +- `git diff`对比区别 + `git diff` 直接查看所有的区别 + `git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 + `git diff`比较只是当前工作区和暂存区之间的区别。如果想要查看暂存起来的文件和上次提交时的差异可以使用`git diff --staged`。或者想看和远程仓库最新内容的差异可以使用`git diff HEAD`。 + +- `git push` 提交到远程仓库 + 可以直接调用`git push`推送到当前分支 + 或者`git push origin master`推送到远程`master`分支 + `git push origin devBranch`推送到远程`devBranch`分支 + +- `git merge`合并目标分支到当前分支 + `git merge devBrach` + +- `git log`查看当前分支下的提交记录 + +- `git reset`命令回退到某一版本(回退已经暂存的文件) + `Git`中用`HEAD`表示当前版本,上一版本就是`HEAD^`,上上一版本就是`HEAD^^`.如果往前一千个版本呢? 那就是`HEAD~1000`. + `git reset —-hard HEAD^` + `git reset —-hard commit_id` + `git reset HEAD fileName`可以把用`git add`之后但是还没有`commit`之前暂存区中的修改撤销。 + 说到这里就说一个问题,如果你reset到某一个版本之后,发现弄错了,还想返回去,这时候用`git log`已经找不到之前的`commit id`了。那怎么办?这时候可以使用下面的命令来找。 + +- `git reflog` + 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 + +- `git checkout`撤销修改或者切换分支 + `git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: + + - 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 + - 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 + + 总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 + 这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: + - 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 + - 使用`git checkout --file`直接丢弃工作区的修改。 + + `git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 + 为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 + `git checkout origin/developer` // 切换到orgin/developer分支 + +- `git rm`删除文件 + 该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 + 另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上,用 --cached 选项即可:`git rm --cached readme.txt` + +- `git push` + 把本地仓库的内容推送到远程仓库中。 + +- 分支 + `git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) + +创建分支 +`git branch devBranch`创建名为`devBranch`的分支。 +`git checkout devBranch`切换到`devBranch`分支。 +`git branch`查看当前仓库中的分支 +`git branch -r`查看远程仓库的分支 +``` +origin/HEAD -> origin/master +origin/developer +origin/developer_sg +origin/master +origin/master_sg +origin/offline +``` +`git branch -d devBranch`删除`devBranch`分支。 +当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 +这时如果你要强行删除的话可以使用命令`git branch -D xxx`. +如何删除远程分支呢? +``` +git branch -r -d origin/developer +git push origin :developer +``` +如何本地创建分支并推送给远程仓库? +``` +// 本地创建分支 +git checkout master //进入master分支 + git checkout -b frommaster //以master为源创建分支frommaster +// 推送到远程仓库 +git push origin frommaster// 推送到远程仓库所要使用的名字 +``` + +如何切到到远程仓库分支进行开发呢? +`git checkout -b frommaster origin/frommaster` +// 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 +提交更改的话就用 +`git push origin frommaster` + + +- `git merge`合并指定分支到当前分支 + `git merge devBranch`将`devBranch`分支合并到`master`。 + +- 打`tag` + `git tag v1.0`来进行打`tag`,默认为`HEAD` + `git tag`查看所有`tag` + 如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` + 当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` + + `git show tagName`来查看某`tag`的详细信息。 +- 打完`tag`后怎么推送到远程仓库 + `git push origin tagName` + +- 删除`tag` + `git tag -d tagName` + +- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 + `git push origin:refs/tags/tagName` + +- 忽略文件 + 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 + 其实不用一个个的去写,具体可以根据项目参考[https://github.com/github/gitignore](https://github.com/github/gitignore) + 当然不要忘了把该文件提交上去 + 在用`linux`的时候会自动生成一些以`~`结尾的备份文件,如果ignore掉呢?[https://github.com/github/gitignore/blob/master/Global/Linux.gitignore](https://github.com/github/gitignore/blob/master/Global/Linux.gitignore) + +- 撤销最后一次提交 + 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以使用--amend选项重新提交:`git commit --amend` + +- 查看远程仓库克隆地址 + `git remote -v` + +关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) + +从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 +其实我一直有点分不开`reset`和`checkout`的区别,从这个图里能明显看出来了: + +- 当执行`git reset HEAD`命令时,暂存区的目录树会被重写,会被`master`分支指向的目录树所替换,但是工作区不受影响。 +- 当执行`git checkout .`或`git checkout -- file`命令是,会用暂存区全部的文件或指定的文件替换工作区的文件。这个操作很危险,会清楚工作区中未添加到暂存区的改动。 +命令时,会用`HEAD`指向的`master`分支中的全部或部分文件替换暂存区和工作区中的文件。这个命令也是极度危险的。因为不但会清楚工作区中未提交的改动,也会清楚暂存区中未提交的改动。 +- `git reset HEAD ` 是在添加到暂存区后,撤出暂存区使用,他只会把文件撤出暂存区,但是你的修改还在,仍然在工作区。当然如果使用`git reset --hard HEAD`这样就完了,工作区所有的内容都会被远程仓库最新代码覆盖。 +- `git checkout -- xxx.txt`是用于修改后未添加到暂存区时使用(如果修改后添加到暂存区后就没效果了,必须要先reset撤销暂存区后再使用checkout),这时候会把之前的修改覆盖掉。所以是危险的。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" index 50e15bf7..4df119ef 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" +++ "b/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" @@ -8,7 +8,8 @@ 思路: 在后序遍历得到的序列中,最后一个数字是树的根节点的值。 数组中前面的数字可以分为两部分:第一部分是左子树结点的值, 它们都比根节点的值小;第二部分是右子树结点的值,他们都比根节点的值大。 - ```java + + ``` public class Problem24 { public static void main(String[] args) { int[] array = { 5, 7, 6, 9, 11, 10, 8 }; @@ -60,12 +61,12 @@ 25. 二叉树中和为某一值的路径 输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶结点所经过的所有的结点形成一条路径。 思路: - ```java + + ``` public class Problem25 { public static void main(String args[]) { BinaryTreeNode root1 = new BinaryTreeNode(); BinaryTreeNode node1 = new BinaryTreeNode(); - BinaryTreeNode node2 = new BinaryTreeNode(); BinaryTreeNode node3 = new BinaryTreeNode(); BinaryTreeNode node4 = new BinaryTreeNode(); From a8a4f7df8b41a73a794663d463fde223a47be9ff Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 8 Apr 2016 16:52:05 +0800 Subject: [PATCH 013/373] add note --- ...se\346\227\266\346\212\245\351\224\231.md" | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" diff --git "a/Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" "b/Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" new file mode 100644 index 00000000..001a762b --- /dev/null +++ "b/Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" @@ -0,0 +1,71 @@ +Library项目中资源id使用case时报错 +=== + +在平时开发的过程中对一些常量的判断,都是使用`switch case`语句,因为`switch case`比`if else`的效率稍高。 +但是也遇到了问题,就是在`library`中使用时会报错: +```java +public void testClick(View view) { + int id = view.getId(); + switch (id) { + case R.id.bt_pull: + + break; + case R.id.bt_push: + + break; + } +} +``` +我们来看一下错误提示: + +![Image](https://github.com/CharonChui/Pictures/blob/master/library_r.error.png?raw=true) + +意思就是在`Android Library`的`Module`中的`switch`语句不能使用资源`ID`.这是为什么呢? 我们都知道`R`文件中都是常量啊,`public static final`的,为什么不能用在`switch`语句中,继续看下去: +``` +Resource IDs are non final in th library projects since SDK tools r14, means that the library code cannot treat this IDs as constants. +``` +说的灰常明白了,也就是说从14开始,library中的资源`id`就不是`final`类型的了,所以不是常量了。 + +在一个常规的`Android`项目中,资源`R`文件中的常量都是如下这样声明的: +`public static final int main=0x7f030004;` +然后,从`ADT`14开始,在`library`项目中,它们将被这样声明: +`public static int main=0x7f030004;` + +也就是说在`library`项目中这些常量不是`final`的了。为什么这样做的原因也非常简单:当多个`library`项目结合时,这些字段的实际值(必须唯一的值)可能会冲突。在`ADT`14之前,所有的字段都是`final`的,这样就导致了一个结果,就是所有的`libraries`不论何时在被使用时都必须把他们所有的资源和相关的`Java`代码都伴随着主项目一起重新编译。这样做是不合理的,因为它会导致编译非常慢。这也会阻碍一些没有源码的`libraray`项目进行发布,极大的限制了`library`项目的使用范围。 + +那些字段不再是`final`的原因就是意味着`library jars`可以被编译一次,然后在其他项目中直接被服用。也就是意味着允许发布二进制版本的`library`项目(r15中开始支持),这让编译变的更快。 + +然而,这对`library`的源码会有一个影响。类似下面这样的代码就将无法编译: +```java +public void testClick(View view) { + int id = view.getId(); + switch (id) { + case R.id.bt_pull: + + break; + case R.id.bt_push: + + break; + } +} +``` +这是因为`swtich`语句要求所有`case`后的字段,例如`R.di.bt_pull`在编译的时候都应该是常量(就像可以直接把值拷贝进`.class`文件中) + +当然针对这个问题的解决方法也很简单:就是把`switch`语句改成`if-else`语句。幸运的是,在`eclise`中这是非常简单的。只需要在`switch`的关键字上按`Ctrl+1`(`Mac`上是`Cmd+1`)。(`eclipse`都辣么方便了,`Studio`更不用说了啊,直接按修复的快捷键就阔以了)。 +上面的代码就将变成如下: + +```java +public void testClick(View view) { + int id = view.getId(); + if (id == R.id.bt_pull) { + } else if (id == R.id.bt_push) { + } +} +``` +这在UI代码和性能的影响通常是微不足道的 - -!。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + From 04ec0119775da5aa73b435607a46fe12a8caef77 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 8 Apr 2016 19:44:45 +0800 Subject: [PATCH 014/373] =?UTF-8?q?add=20gradle=E4=B8=93=E9=A2=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gradle\344\270\223\351\242\230.md" | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" diff --git "a/Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" "b/Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" new file mode 100644 index 00000000..45691cf7 --- /dev/null +++ "b/Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" @@ -0,0 +1,122 @@ +Gradle专题 +=== + +随着`Google`对`Eclipse`的无情抛弃以及`Studio`的不断壮大,`Android`开发者逐渐拜倒在`Studio`的石榴裙下。 +而作为`Studio`的默认编译方式,`Gradle`已逐渐普及。我最开始是被它的多渠道打包所吸引。关于多渠道打包,请看之前我写的文章[AndroidStudio使用教程(第七弹)](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%9F%BA%E7%A1%80/AndroidStudio%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B(%E7%AC%AC%E4%B8%83%E5%BC%B9).md) + +接下来我们就系统的学习一下`Gradle`。 + +简介 +--- + +`Gradle`是以`Groovy`语言为基础,面向`Java`应用为主。基于`DSL(Domain Specific Language)`语法的自动化构建工具。 + +`Gradle`集合了`Ant`的灵活性和强大功能,同时也集合了`Maven`的依赖管理和约定,从而创造了一个更有效的构建方式。凭借`Groovy`的`DSL`和创新打包方式,`Gradle`提供了一个可声明的方式,并在合理默认值的基础上描述所有类型的构建。 `Gradle`目前已被选作许多开源项目的构建系统。 + +因为`Gradle`是基于`DSL`语法的,如果想看到`build.gradle`文件中全部可以选项的配置,可以看这里 +[DSL Reference](http://google.github.io/android-gradle-dsl/current/) + +基本的项目设置 +--- + +一个`Gradle`项目通过一个在项目根目录中的`build.gradle`文件来描述它的构建。 + +###简单的`Build`文件 + +最简单的`Android`应用中的`build.gradle`都会包含以下几个配置: +`Project`根目录的`build.gradle`: +```java +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.5.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} +``` +`Module`中的`build.gradle`: +``` +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + ... +} +``` + + +- `buildscript { ... }`配置了编译时的代码驱动. 这种情况下,它声明所使用的是`jCenter`仓库。还有一个声明所依赖的在`Maven`文件的路径。这里声明的包含了`Android`插件所使用的1.5.0版本的`Gradle`. ***注意:***这只会影响`build`中运行的代码,不是项目中。项目中需要声明它自己所需要仓库和依赖关系。 +- `apply plugin : com.android.application`,声明使用`com.androdi.application`插件。这是构建`Android`应用所需要的插件。 +- `android{...}`配置了所有`Android`构建时的参数。默认情况下,只有编译的目标版本以及编译工具的版本是需要的。 + +重要: 这里只能使用`com.android.application`插件。如果使用`java`插件将会报错。 + +###目录结构 +`module/src/main`下的目录结构,因为有时候很多人把`so`放到`libs`目录就会报错: + +- java/ +- res/ +- AndroidManifest.xml +- assets/ +- aidl/ +- jniLibs/ +- jni/ +- rs/ + +###配置目录结构 +如果项目的结构不标准的时候,可能就需要去配置它。`Android`插件使用了相似的语法,但是因为它有自己的`sourceSets`,所以要在`android`代码块中进行配置。下面就是一个从`Eclipse`的老项目结构中配置主要代码并且将`androidTest`的`sourceSet`设置给`tests`目录的例子: +``` +android { + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + } + + androidTest.setRoot('tests') + } +} +``` +就像有些人就是要把`so`放到`libs`目录中(这类人有点犟),那就需要这样进行修改。 +***注意:***因为在旧的项目结构中所有的源文件(`Java`,`AIDL`和`RenderScript`)都放到同一个目录中,我们需要将`sourceSet`中的这些新部件都设置给`src`目录。 + +Build Tasks +--- +对构建文件声明插件时通常或自动创建一些列的构建任务去执行。不管`Java`插件还是`Android`插件都是这样。`Android`常规的任务如下: + +- `assemble`生成项目`output`目录中的内容的任务。 +- `check`执行所有的检查的任务。 +- `build`执行`assemble`和`check`的任务。 +- `clean`清理项目`output`目录的任务。 + +在`Android`项目中至少会有两种`output`输出:一个`debug apk`和一个`release apk`。他们都有自己的主任务来分别执行构建: +- `assemble` + - `assembleDebug` + - `assembleRelease` + +***提示:***`Gradle`支持通过命令行执行任务首字母缩写的方式。例如: +在没有其他任务符合`aR`的前提下,`gradle aR`与`gradle assembleRelease`是相同的。 + +最后,构建插件创建了为所有`build type(debug, release, test)`类型安装和卸载的任务,只要他们能被安装(需要签名)。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 4d6a5e9579ea14916d685d2048c6abb4b3532aac Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 11 Apr 2016 16:04:14 +0800 Subject: [PATCH 015/373] add Gradle.md --- "Android\345\212\240\345\274\272/.DS_Store" | Bin 10244 -> 10244 bytes .../Gradle\344\270\223\351\242\230.md" | 442 +++++++++++++++++- 2 files changed, 440 insertions(+), 2 deletions(-) diff --git "a/Android\345\212\240\345\274\272/.DS_Store" "b/Android\345\212\240\345\274\272/.DS_Store" index e2756d5ea30a65147057cd53d06dc95eba801fe1..81e3d82885601432633aecac77c36c74280a5b63 100644 GIT binary patch delta 191 zcmZn(XbG6$C7U^hRb%4Qyc%gln@4DJj?42cXW3^@#`e!??mOl}Yp<-Ebbz*xb+ zz`(ZohOjrIz7&HGLncEKSYZ)E<&4U*cYeCPE_oAxqAC9Mr5X&$KqHD7Qd7SrEuAU} eGJ+Lo1oMi`5~8VGo7ok9u}nTK5.compile`这两种配置。创建一个新的`Build Type`通常会自动基于它的名字创建一个新的配置部分。这样在像`debug`版本而`release`版本不适用的一些特别的`library`时非常有用。 + +#####远程仓库 + +`Gradle`只是使用`Maven`和`Ivy`仓库。但是仓库必须要添加到列表中,并且必须声明所依赖仓库的`Maven`或者`Ivy`定义。 + +``` +repositories { + jcenter() +} + + +dependencies { + compile 'com.google.guava:guava:18.0' +} + + +android { + ... +} +``` + +***注意: ***`jcenter()`是指定仓库`URL`的快捷设置。`Gradle`支持远程和本地仓库。 +***注意: ***`Gradle`会直接识别所有的依赖关系。这就意味着如果一个依赖库自身又依赖别的库时,他们会被一起下下来。 + +#####本地`AAR`库 + +``` +dependencies { + compile(name:'本地aar库的名字,不用加后缀', ext:'aar') +} +``` + +#####多项目设置 + +`Gradle`项目通常使用多项目设置来依赖其他的`gradle`项目。例如: + +- MyProject/ + - app/ + - libraries/ + - lib1/ + - lib2/ + +`Gradle`会通过下面的名字来引用他们: +`:app` +`:libraries:lib1` +`:libraries:lib2` + + +每个项目都会有一个单独的`build`文件,并且在项目的根目录还会有一个`setting.gradle`文件: + +- MyProject/ + - settings.gradle + - app/ + - build.gradle + + libraries/ + + lib1/ + - build.gradle + + lib2/ + - build.gradle + + +`setting.gradle`文件中的内容非常简单。它指定了哪个目录是`Gralde`项目: +``` +include ':app', ':libraries:lib1', ':libraries:lib2' +``` + +`:app`这个项目可能会依赖其他的`libraries`,这样可以通过如下进行声明: +``` +dependencies { + compile project(':libraries:lib1') +} +``` + +###`Library`项目 + +上面用到了`:libraries:lib1`和`:libraries:lib2`可以是`Java`项目,`:app`项目会使用他们俩的输出的`jar`包。但是如果你需要使用`android`资源等,这些`libraries`就不能是普通的`Java`项目了,他们必须是`Android Library`项目。 + +#####创建一个`Library`项目 + +`Library`项目和普通的`Android`项目的区别比较少,由于`libraries`的构建类型与应用程序的构建不同,所有它会使用一个别的构建插件。但是他们所使用的插件内部有很多相同的代码,他们都是由`com.android.tools.build.gradle`这个`jar`包提供的。 + +``` +buildscript { + repositories { + jcenter() + } + + + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + } +} + +apply plugin: 'com.android.library' + + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" +} +``` + +#####普通项目与`Library`项目的区别 + +`Library`项目的主要输出我`.aar`包。它结合了代码(例如`jar`包或者本地`.so`文件)和资源(`manifest`,`res`,`assets`)。每个`library`也可以单独设置`Build Type`等来指定生成不同版本的`aar`。 + +###`Lint Support` + +你可以通过指定对应的变量来设置`lint`的运行。可以通过添加`lintOptions`来进行配置: + +``` +android { + lintOptions { + // turn off checking the given issue id's + disable 'TypographyFractions','TypographyQuotes' + + // turn on the given issue id's + enable 'RtlHardcoded','RtlCompat', 'RtlEnabled' + + // check *only* the given issue id's + check 'NewApi', 'InlinedApi' + } +} +``` + +###`Build`变量 + + +构建系统的一个目标就是能对同一个应用创建多个不同的版本。 + +#####`Product flavors` + +一个`product flavor`可以针对一个项目制定不同的构建版本。一个应用可以有多个不同的`falvors`来改变生成的应用。 +`Product flavors`是通过`DSL`语法中的`productFlavors`来声明的: + +``` +android { + .... + + + productFlavors { + flavor1 { + ... + } + + + flavor2 { + ... + } + } +} +``` + +#####`Build Type + Product Flavor = Build Variant` + +像我们之前看到的,每个`Build Type`都会生成一个`apk`.`Product Flavors`也是同样的:项目的输出僵尸所有`Build Types`与`Product Flavors`的结合。每种结合方式称之为`Build Variant`。例如,如果有`debug`和`release`版本的`Build Types`,上面的例子就会生成4种`Build Variants`: +- `Flavor1` - `debug` +- `Flavor1` - `release` +- `Flavor2` - `debug` +- `Flavor2` - `release` + +没有配置`flavors`的项目仍然有`Build Variants`,它只是用了一个默认的`flavor/config`,没有名字,这导致`variants`的列表和`Build Types`的列表比较相同。 + +#####`Product Flavor`配置 + +``` +android { + ... + defaultConfig { + minSdkVersion 8 + versionCode 10 + } + + + productFlavors { + flavor1 { + applicationId "com.example.flavor1" + versionCode 20 + } + + + flavor2 { + applicationId "com.example.flavor2" + minSdkVersion 14 + } + } +} +``` +注意`android.productFlavors.*`对象`ProductFlavor`有`android.defaultConfig`是相同的类型。这就意味着他们有相同的属性。 +`defaultConfig`为所有的`flavors`提供了一些基本的配置,每个`flavor`都已重写他们。在上面的例子中,这些配置有: + +- `flavor1` + - `applicationId`: `com.example.flavor1` + - `minSdkVersion`: 8 + - `versionCode`: 20 +- `flavor2` + - `applicationId`: `com.example.flavor2` + - `minSdkVersion`: 14 + - `versionCode`: 10 + + +通常,`Build Type`配置会覆盖其他的配置。例如,`Build Type`的`applicationIdSuffix`会添加到`Product Flavor`的`applicationId`上。 + + +最后,就像`Build Types`一样,`Product Flavors`也可以有他们自己的依赖关系。例如,如果有一个单独的`flavors`会使用一些广告或者支付,那这个`flavors`生成的`apk`就会使用广告的依赖,而其他的`flavors`就不需要使用。 +``` +dependencies { + flavor1Compile "..." +} +``` + +###`BuildConfig` + +在编译阶段,`Android Studio`会生成一个叫做`BuildConfig`的类,该类包含了编译时使用的一些变量的值。你可以观看这些值来改变不同变量的行为: + +``` +private void javaCode() { + if (BuildConfig.FLAVOR.equals("paidapp")) { + doIt(); + else { + showOnlyInPaidAppDialog(); + } +} +``` + +下面是`BuildConfig`中包含的一些值: + +- `boolean DEBUG` - `if the build is debuggable` +- `int VERSION_CODE` +- `String VERSION_NAME` +- `String APPLICATION_ID` +- `String BUILD_TYPE`- `Build Type`的名字,例如`release` +- `String FLAVOR` - `flavor`的名字,例如`flavor1` + + +###`ProGuard`配置 + +`Android`插件默认会使用`ProGuard`插件,并且如果`Build Type`中使用`ProGuard`的`minifyEnabled`属性开启的话,会默认创建对应的`task`。 + +``` +android { + buildTypes { + release { + minifyEnabled true + proguardFile getDefaultProguardFile('proguard-android.txt') + } + } + + productFlavors { + flavor1 { + } + flavor2 { + proguardFile 'some-other-rules.txt' + } + } +} +``` + +###`Tasks`控制 + +基本的`Java`项目有一系列的`tasks`一起制作输出文件。 +`classes task`就是编译`Java`源码的任务。 我们可以在`build.gradle`中通过使用`classes`很简单的获取到它。就是`project.tasks.classes`. + +在`Android`项目中,更多的编译`task`,因为他们的名字通过`Build Types`和`Product Flavors`生成。 + +为了解决这个问题,`android`对象有两种属性: +- `applicationVariants` - `only for the app plugin` +- `libraryVariants` - `only for the library plugin` +- `testVariants` - `for both plugins` +这些都会返回一个`ApplicationVariant`, `LibraryVariant`,`TestVariant`的`DomainObjectCollection`接口的实现类对象。 +`DomainObjectCollection`提供了直接获取或者很方便的间接获取所有对象的方法。 +``` +android.applicationVariants.all { variant -> + .... +} +``` + +###设置编译语言版本 + +可以使用`compileOptions`代码块来设置编译时使用的语言版本。默认是基于`compileSdkVersion`的值。 +``` +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_6 + targetCompatibility JavaVersion.VERSION_1_6 + } +} +``` +###`Resource Shrinking` +`Gradle`构建系统支持资源清理:对构建的应用会自动移除无用的资源。不仅会移除项目中未使用的资源,而且还会移除项目所以来的类库中的资源。注意,资源清理只能在与代码清理结合使用(例如`ProGuad`)。这就是为什么它能移除所依赖类库的无用资源。通常,类库中的所有资源都是使用的,只有类库中无用代码被移除后这些资源才会变成没有代码引用的无用资源。 --- From e150d767675651190926b789b3b327765174b992 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 11 Apr 2016 17:04:21 +0800 Subject: [PATCH 016/373] update Gradle.md --- "Android\345\212\240\345\274\272/.DS_Store" | Bin 10244 -> 10244 bytes .../Gradle\344\270\223\351\242\230.md" | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git "a/Android\345\212\240\345\274\272/.DS_Store" "b/Android\345\212\240\345\274\272/.DS_Store" index 81e3d82885601432633aecac77c36c74280a5b63..ad77ce302c12b8ce27815d9f905885a21c031bef 100644 GIT binary patch delta 71 zcmZn(XbG6$aFU^hRb>Si8+AIy_C2(c)zNc;x_1`yz3aAzoDNMuN1$YDtJ6P_`H Wp>FdB;rra1*%f}VY(6Q%%nSg2!5DA= delta 41 xcmZn(XbG6$C7U^hRb%4QycAIy^{h_h^V5IN30vB6?9yTUJ)&6c9f%m6K*4W Date: Tue, 12 Apr 2016 10:07:22 +0800 Subject: [PATCH 017/373] update Gradle.md --- "Java\345\237\272\347\241\200/.DS_Store" | Bin 0 -> 8196 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "Java\345\237\272\347\241\200/.DS_Store" diff --git "a/Java\345\237\272\347\241\200/.DS_Store" "b/Java\345\237\272\347\241\200/.DS_Store" new file mode 100644 index 0000000000000000000000000000000000000000..9eff2101d5c7071e98ea2f89126f556b2600cfe1 GIT binary patch literal 8196 zcmeHMO-vI(6n=vgOA9TMm>?H=Yl(@0M2TWznu5QPmUatROK54k7Gj}jX@d}<31}c_ z@Sp)N;?1i!!^wCtrtxMl9*h?y@#azEL49v_Lw4J#H-iZ?WaevT-#6dRH@mZW0{}2z zKGg}(0svO5giUsA1ck0sttd5m&k;n1e1I-E9(o#m4aZ=jB3gDx2c!ei0qKBrKsxY0 zIDp@5D%L9BeeJ4jr32D|edz%04=${PULA`X@~s0Ko&q4&VmB}N8}k6pap~yQv8W-Z z;+R_Yz;r6pQw*lj@w$t`5xqJVHPq<9G&(R{ndun{X4P`4GkPcKjK)Jge zd@u?*P)+%J>S89FOk}bNjKG(Qg|N>2=7kSqh4U~D)8X6xFFfBbkN;2>-wb11Wej>C z3n@_j4ph7WdF;)gI-MDmjREIyjIRdGCb*!F(T6w{eUf{wY);pO>e3H{{I_NN2DpU0 z0*ns*obj$FR<}1cL_9uMqb~6Vp>Jq$=*-|JwJY5n>Jsv3&PJXU`k)t1?4F4T=2vsY zFh3{84?+(*L)fpBIHIyh4RCfQwjDwk>5I6Box#UznPfu5qPdw^HdGpiiNHeMqef=! zLJrQ$#A!tjv$$%bFpb;{sNs8{z%7`BB(CyJyf%sbv$(z~r8{&~G$yTW%^Snq8$-e5 zK?uM#{8MoZkKYFE)tIpHSEjuR6CE=#8Q>AtR3x6qpjf92Jp8ph zCWw}?$^YI@6TBPa77ddHS8N8CZ7A~|VN zY>suu4(B}`Y%Qx{tW(3gR|!SB{H~3j+SIA!Bt5rEY2~wiq9=HInbqKm<@($sCK{d3 z${1OnZ}UGZS|*n7pDJ2J42DmfX{9W4>zQp~B<)M4vFBgT+B5OO6738k2H#bTJ6YpG zaQ@@!_l&RbBbW=`iEhPD&%YBf>8?y#j>Q+sdw(hHh_ntLinNCvg(Y<--ByijS|yyI zEYg8Jbl`xd-9q{Q+3M&2dw7RZS~?&d_}31w+FqsChbnq*U7<(0whL<YRD Date: Tue, 24 May 2016 19:13:01 +0800 Subject: [PATCH 018/373] Update --- .../Android\345\212\250\347\224\273.md" | 194 ++++++++++++++++ .../WebView\346\200\273\347\273\223.md" | 215 +++++++++++------- 2 files changed, 326 insertions(+), 83 deletions(-) diff --git "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" "b/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" index 398f610e..f290c632 100644 --- "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" +++ "b/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" @@ -57,6 +57,7 @@ Android动画 ``` 6. Frame动画 + 在`SDK`中提到,不要在`onCreate`中调用`start`方法开始播放`Frame`动画,因为`AnimationDrawable`还没有完全跟`Window`相关联,如果想要界面显示时就开始播放帧动画的话,可以在`onWindowFocusChanged()`中调用`start()`。 - 在`drawable`目录下新建一个`xml`文件,内容如下: @@ -101,7 +102,200 @@ Android动画 CycleInterpolator//曲线运动特效,要传递float型的参数。 animation.setInterpolator(new LinearInterpolator());//指定动画的运行效果 ``` + +上面所讲的动画都是`Android 3.0`之前的动画,也就是我们最先熟悉的`Frame`动画和`Tween`动画。 + +从`3.0`开始又引入了一个新的动画叫做`Property`动画。并且针对这三种动画的动画模式分为: + +- `Property Animation` : 属性动画,从`Android 3.0`开始引进,更改的是对象的实际属性,而且属性动画不止可以应用于`View`还可以应用于任何对象。 +- `View Animation` : 指之前的`Tween`动画,`alpha scale translate rotate`等。为什么叫做`View Animation`呢?因为它只能应用于`View`对象,而且只支持一部分属性,如支持缩放旋转而不支持背景颜色的变化。对于`View Animation`而言,它只改变了`View`对象绘制的位置,而没有改变`View`对象本身的属性,比如,有一个200*200大小的`Button`,你用`Tween`动画给放大到500*500但是它的有效点击区域还是200*200。 +- `Drawable Animation` : 指之前的`Frame`动画。因为它是通过一帧帧的图片来播放的。 +下面我们开始仔细讲解一下`Property Animation` +--- + +官方文档中是这样这样介绍属性动画的: + +``` +The property animation system is a robust framework that allows you to animate almost anything. You can define an animation to change any object property over time, regardless of whether it draws to the screen or not. A property animation changes a property's (a field in an object) value over a specified length of time. To animate something, you specify the object property that you want to animate, such as an object's position on the screen, how long you want to animate it for, and what values you want to animate between. +``` +一个强大的框架。 + +在`Property Animation`中,可以对动画应用以下属性: + +- `Duration`: 指定动画持续时间,默认时间是`300ms` +- `TimeInterpolation`: 一些效果,如加速、加速等。 +- `Repeat count and behavior `: 重复次数已经 +- `Animation Set`: 动画合集。用来同时或者顺序播放多个动画。 +- `Frame Refresh Delay`: 多长时间刷新一次,默认是`10ms`。 + +###`ValueAnimator` + + +`ValueAnimator`包含`Property Animation`动画的所有核心功能,如动画时间,开始、结束属性值,相应时间属性值计算方法等。应用`Property Animation`有两个步聚: + +- 计算属性值 +- 根据属性值执行相应的动作,如改变对象的某一属性。 + +`ValuAnimiator`只完成了第一步工作,如果要完成第二步,需要实现`ValueAnimator.onUpdateListener`接口,这个接口只有一个函数`onAnimationUpdate()`,在这个函数中会传入`ValueAnimator`对象做为参数,通过这个`ValueAnimator`对象的`getAnimatedValue()`函数可以得到当前的属性值。 + +###`ObjectAnimator` + +继承自`ValueAnimator`,要指定一个对象及该对象的一个属性,当属性值计算完成时自动设置为该对象的相应属性,即完成了`Property Animation`的全部两步操作。实际应用中一般都会用`ObjectAnimator`来改变某一对象的某一属性,但用`ObjectAnimator`有一定的限制,要想使用`ObjectAnimator`,应该满足以下条件: + +- 对象应该有一个`setter`函数:`set`(驼峰命名法) +- 如上面的例子中,像`ofFloat`之类的工场方法,第一个参数为对象名,第二个为属性名,后面的参数为可变参数,如果`values…`参数只设置了一个值的话,那么会假定为目的值,属性值的变化范围为当前值到目的值,为了获得当前值,该对象要有相应属性的`getter`方法:`get` +- 如果有`getter`方法,其应返回值类型应与相应的`setter`方法的参数类型一致。 +- `object`的`setXxx`对属性`xxx`所做的改变必须能够通过某种方法反映出来,比如会带来`ui`的改变啥的(如果这条不满足,动画无效果),例如我对`TextView`或者`Button`使用`width`的`ObjectAnimator`动画,就会发现无效,虽然他们都有`setWidth`和`getWidth`方法,但是`setWidth`方法的内部实现是改变`TextView`的最大宽度和最小宽度的,和`TextView`的宽度不是一个东西。所以动画就会无效。确切的说`TextView`的宽度对应的是`xml`中`android:layout_width`属性,而`TextView`还有另外一个属性:`android:width`,而`android:width` 属性对应的就是`TextView`中的`setWidth`方法。 + +如果上述条件不满足,则不能用`ObjectAnimator`,应用`ValueAnimator`代替。 +也就是说`ObjectAnimator`内部的工作机制是通过寻找特定属性的`get`和`set`方法,然后通过方法不断地对值进行改变,从而实现动画效果的。 + +###`AnimationSet` + + +`AnimationSet`提供了一个把多个动画组合成一个组合的机制,并可设置组中动画的时序关系,如同时播放,顺序播放等。 + +以下例子同时应用5个动画: + +- 播放anim1; +- 同时播放anim2,anim3,anim4; +- 播放anim5。 + +```java +AnimatorSet bouncer = new AnimatorSet(); +bouncer.play(anim1).before(anim2); +bouncer.play(anim2).with(anim3); +bouncer.play(anim2).with(anim4) +bouncer.play(anim5).after(amin2); +animatorSet.start(); +``` + +###TypeEvalutors + + +根据属性的开始、结束值与`TimeInterpolation`计算出的比例值来计算当前时间对应的属性值,`Android`提供了一下几种`evalutor`: + +- `IntEvalutor`:`int`类型的属性值 +- `FloatEvaluator`: +- `ArgbEvaluator`: 属性的值类型为十六进制颜色值; +- `TypeEvaluator`: 一个接口,可以通过实现该接口自定义Evaluator。 +自定义`TypeEvaluator`也很简单,只需要实现一个方法,如`FloatEvalutor`的定义: +```java +public class FloatEvaluator implements TypeEvaluator { + public Object evaluate(float fraction, Object startValue, Object endValue) { + float startFloat = ((Number) startValue).floatValue(); + return startFloat + fraction * (((Number) endValue).floatValue() - startFloat); + } +} +``` +`evaluate()`方法当中传入了三个参数,第一个参数`fraction`非常重要,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。那么上述代码的逻辑就比较清晰了,用结束值减去初始值,算出它们之间的差值,然后乘以`fraction`这个系数,再加上初始值,那么就得到当前动画的值了。 + + +###`TimeInterplator` +Time interplator定义了属性值变化的方式,如线性均匀改变,开始慢然后逐渐快等。在Property Animation中是TimeInterplator,在View Animation中是Interplator,这两个是一样的,在3.0之前只有Interplator,3.0之后实现代码转移至了TimeInterplator。Interplator继承自TimeInterplator,内部没有任何其他代码。 + +- AccelerateInterpolator      加速,开始时慢中间加速 +- DecelerateInterpolator       减速,开始时快然后减速 +- AccelerateDecelerateInterolator  先加速后减速,开始结束时慢,中间加速 +- AnticipateInterpolator       反向 ,先向相反方向改变一段再加速播放 +- AnticipateOvershootInterpolator  反向加回弹,先向相反方向改变,再加速播放,会超出目的值然后缓慢移动至目的值 +- BounceInterpolator        跳跃,快到目的值时值会跳跃,如目的值100,后面的值可能依次为85,77,70,80,90,100 +- CycleIinterpolator         循环,动画循环一定次数,值的改变为一正弦函数:Math.sin(2 * mCycles * Math.PI * input) +- LinearInterpolator         线性,线性均匀改变 +- OvershottInterpolator       回弹,最后超出目的值然后缓慢改变到目的值 +- TimeInterpolator         一个接口,允许你自定义interpolator,以上几个都是实现了这个接口 + + +###`PropertyValuesHolder` + +如果要实现一个对象不同属性的动画效果,除了`Set`,我们还可以利用`PropertyValuesHolder`和`ViewPropertyAnimator`对象来实现,具体做法如下: +``` +PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f); +PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f); +ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvyY).start(); +``` + +###`ViewPropertyAnimator` + +`*

This class is not constructed by the caller, but rather by the View whose properties + * it will animate. Calls to {@link android.view.View#animate()} will return a reference + * to the appropriate ViewPropertyAnimator object for that View.

` + +如果需要对一个View的多个属性进行动画可以用ViewPropertyAnimator类,该类对多属性动画进行了优化,会合并一些invalidate()来减少刷新视图,该类在3.1中引入。 + +`view.animate()`方法会返回`ViewPropertyAnimator`类。 +```java +myView.animate().x(50f).y(100f); +``` +的效果与上面的`PropertyValuesHolder`例子中的效果完全一致。 +但是你有没有发现我们自始至终没有调用过`start()`方法,这是因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成之后,动画就会自动启动。并且这个机制对于组合动画也同样有效,只要我们不断地添加新的方法,那么动画就不会立刻执行,等到所有在`ViewPropertyAnimator`上设置的方法都执行完毕后,动画就会自动启动。当然如果不想使用这一默认机制的话,我们也可以显式地调用`start()`方法来启动动画。 + + + +###XML中定义 + +在`res/animator`中定义对应的动画`xml` +- 对应代码中的ValueAnimator +- 对应代码中的ObjectAnimator +- 对应代码中的AnimatorSet +例如: + +```xml + + + +``` +这里说明一下`valueFrom`和`valueTo`:是动画开始和结束值,如果我们缩放,则它们对应的是倍数,如果我们平移则对应的就是距离了。 + +接下来就是调用了 +```java +scaleXAnimator = (ObjectAnimator)AnimatorInflater.loadAnimator(this, R.animator.scalex); +scaleXAnimator.setTarget(btnScaleX); +scaleXAnimator.start(); +``` + +那如果我们要播放多个动画怎么办? +```xml + + + + + + + +``` +```java +animatorScaleSet = (AnimatorSet)AnimatorInflater.loadAnimator(this, R.animator.scale); +animatorScaleSet.setTarget(btnScale); +animatorScaleSet.start(); + +``` + + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" "b/Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" index da2671a2..e0e09f02 100644 --- "a/Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" +++ "b/Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" @@ -1,83 +1,132 @@ -WebView总结 -=== - -在`Android`中有`WebView Widget`,它内置了`WebKit`引擎,同时,`WebKit`也是`Mac OS X`的`Safari`网页浏览器的基础。`WebKit`是一个开源的浏览器引擎, -`Chrome`浏览器也是基于它的。所以很多表现`WebView`和`Chrome`是一样的。 - -很多文章中多会说在使用`WebView`之前,要在`AndroidManifest.xml`中添加 如下权限: -`` -否则会出`Web page not available`错误。其实这是不全面的,如果我加载本地的页面是不用该权限的。 - -- 设置WevView要显示的网页方法有很多: - - `mWebView.loadUrl(“http://www.google.com“);` // 网络 - - `mWebView.loadUrl(“file:///android_asset/XX.html“);` // 本地页面,这里的格式是固定的,文件要放到`assets`目录下 - - `mWebview.postUrl(String url, byte[] postData); // 加载页面使用`Post`方式,`postData`为参数` - ```java - String postData = "password=password&username=username"; - mWebview.postUrl(url, EncodingUtils.getBytes(postData, "base64")); - ``` - - mWebView.loadData(htmlString, "text/html", "utf-8"); // 加载Html数据 - ```java - String htmlString = "

Title

This is HTML text
Formatted in italics
Anothor Line

"; - mWebView.loadData(htmlString, "text/html", "utf-8"); // 加载Html数据 - ``` - - `loadData()`不能加载图片内容,如果要加载图片内容或者获得更强大的Web支持请使用`loadDataWithBaseURL()`。 - - 显示乱码 - `WebView`一般为了节省资源使用`UTF-8`编码,而`String`类型的数据主要是`Unicode`编码, - 因此在`loadData()`的时候需要设置相应编码让其将`Unicode`编码转成`UTF-8`但是有些时候设置后还是会出现乱码,这是因为还需要为`WebView`中的`Text`设置编码, - - ```java - WebView mWebView = (WebView)findViewById(R.id.webview) ; - String content = getUnicodeContent() ; - mWebView.getSettings().setDefaultTextEncodingName(“UTF -8”) ; - mWebView.loadData(content, “text/html”, “UTF-8”) ; - ``` - -- 设置WebView基本信息: - - 如果访问的页面中有`Javascript`,则`webview`必须设置支持`Javascript`。 - `webview.getSettings().setJavaScriptEnabled(true); ` - - 触摸焦点起作用 - `requestFocus() // 如果不设置的话,会出现不能弹出软键盘等问题。` - - 取消滚动条 - `this.setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);` - -- Back键的处理 - 如果用`webview`点链接看了很多页以后,如果不做任何处理,点击系统`Back`键,整个浏览器会调用`finish()`而结束自身, - 如果希望浏览的网页回退而不是退出浏览器,需要在当前`Activity`中处理并消费掉该`Back`事件。 - - ```java - public boolean onKeyDown(int keyCoder,KeyEvent event){ - if(webView.canGoBack() && keyCoder == KeyEvent.KEYCODE_BACK){ - webview.goBack(); //goBack()表示返回webView的上一页面 - return true; - } - return false; - } - ``` - -- WebView中Padding没有效果 - `WebView`中使用`Padding`没有效果,我们在`WebView`外层包上一层布局就会有所改进,但是不能完全解决问题,正确的做法是在`WebView`的加载`css`中增加`Padding` - -- WebViewClient - 如果希望点击链接由自己处理,而不是新开`Android`的系统`browser`中响应该链接。 - 给`WebView`添加一个事件监听对象`WebViewClient`并重写其中的一些方法: `shouldOverrideUrlLoading`对网页中超链接按钮的响应。 - 当按下某个连接时`WebViewClient`会调用这个方法,并传递按下的url。 - 1. 接收到 Http 请求的事件 - `onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) ` - - 2. 打开链接前的事件 - `public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } ` - 这个函数我们可以做很多操作,比如我们读取到某些特殊的URL,于是就可以不打开地址,取消这个操作,进行预先定义的其他操作,这对一个程序是非常必要的。 - - 3. 载入页面完成的事件 - `public void onPageFinished(WebView view, String url){ } ` - 页面载入完成,于是我们可以关闭`loading`条,切换程序动作。 - - 4. 载入页面开始的事件 - `public void onPageStarted(WebView view, String url, Bitmap favicon)` - 这个事件就是开始载入页面调用的,通常我们可以在这设定一个loading的页面,告诉用户程序在等待网络响应。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +WebView总结 +=== + +在`Android`中有`WebView Widget`,它内置了`WebKit`引擎,同时,`WebKit`也是`Mac OS X`的`Safari`网页浏览器的基础。`WebKit`是一个开源的浏览器引擎, +`Chrome`浏览器也是基于它的。所以很多表现`WebView`和`Chrome`是一样的。 + +很多文章中多会说在使用`WebView`之前,要在`AndroidManifest.xml`中添加 如下权限: +`` +否则会出`Web page not available`错误。其实这是不全面的,如果我加载本地的页面是不用该权限的。 + +- 设置WevView要显示的网页方法有很多: + - `mWebView.loadUrl(“http://www.google.com“);` // 网络 + - `mWebView.loadUrl(“file:///android_asset/XX.html“);` // 本地页面,这里的格式是固定的,文件要放到`assets`目录下 + - `mWebview.postUrl(String url, byte[] postData); // 加载页面使用`Post`方式,`postData`为参数` + ```java + String postData = "password=password&username=username"; + mWebview.postUrl(url, EncodingUtils.getBytes(postData, "base64")); + ``` + - mWebView.loadData(htmlString, "text/html", "utf-8"); // 加载Html数据 + ```java + String htmlString = "

Title

This is HTML text
Formatted in italics
Anothor Line

"; + mWebView.loadData(htmlString, "text/html", "utf-8"); // 加载Html数据 + ``` + - `loadData()`不能加载图片内容,如果要加载图片内容或者获得更强大的Web支持请使用`loadDataWithBaseURL()`。 + - 显示乱码 + `WebView`一般为了节省资源使用`UTF-8`编码,而`String`类型的数据主要是`Unicode`编码, + 因此在`loadData()`的时候需要设置相应编码让其将`Unicode`编码转成`UTF-8`但是有些时候设置后还是会出现乱码,这是因为还需要为`WebView`中的`Text`设置编码, + ```java + WebView mWebView = (WebView)findViewById(R.id.webview) ; + String content = getUnicodeContent() ; + mWebView.getSettings().setDefaultTextEncodingName(“UTF -8”) ; + mWebView.loadData(content, “text/html”, “UTF-8”) ; + ``` + +- 设置WebView基本信息: + + - 如果访问的页面中有`Javascript`,则`webview`必须设置支持`Javascript`。 + `webview.getSettings().setJavaScriptEnabled(true); ` + - 触摸焦点起作用 + `requestFocus() // 如果不设置的话,会出现不能弹出软键盘等问题。` + - 取消滚动条 + `this.setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);` + +- Back键的处理 + 如果用`webview`点链接看了很多页以后,如果不做任何处理,点击系统`Back`键,整个浏览器会调用`finish()`而结束自身, + 如果希望浏览的网页回退而不是退出浏览器,需要在当前`Activity`中处理并消费掉该`Back`事件。 + ```java + public boolean onKeyDown(int keyCoder,KeyEvent event){ + if(webView.canGoBack() && keyCoder == KeyEvent.KEYCODE_BACK){ + webview.goBack(); //goBack()表示返回webView的上一页面 + return true; + } + return false; + } + ``` + +- WebView中Padding没有效果 + `WebView`中使用`Padding`没有效果,我们在`WebView`外层包上一层布局就会有所改进,但是不能完全解决问题,正确的做法是在`WebView`的加载`css`中增加`Padding` + +- WebViewClient + 如果希望点击链接由自己处理,而不是新开`Android`的系统`browser`中响应该链接。 + 给`WebView`添加一个事件监听对象`WebViewClient`并重写其中的一些方法: `shouldOverrideUrlLoading`对网页中超链接按钮的响应。 + 当按下某个连接时`WebViewClient`会调用这个方法,并传递按下的url。 + 1. 接收到 Http 请求的事件 + `onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) ` + + 2. 打开链接前的事件 + `public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } ` + 这个函数我们可以做很多操作,比如我们读取到某些特殊的URL,于是就可以不打开地址,取消这个操作,进行预先定义的其他操作,这对一个程序是非常必要的。 + + 3. 载入页面完成的事件 + `public void onPageFinished(WebView view, String url){ } ` + 页面载入完成,于是我们可以关闭`loading`条,切换程序动作。 + + 4. 载入页面开始的事件 + `public void onPageStarted(WebView view, String url, Bitmap favicon)` + 这个事件就是开始载入页面调用的,通常我们可以在这设定一个loading的页面,告诉用户程序在等待网络响应。 + + +`WebView`与`Js`交互 +--- + +- `Android`调用`WebView`中的`js`脚本。 + ```java + // 启用javascript mWebView.getSettings().setJavaScriptEnabled(true); + // 加载web页面 + mWebView.loadUrl("file:///android_asset/wst.html"); + // 调用js + mWebView.loadUrl("javascript:test('" + aa+ "')"); //aa是js的函数test()的参数 + ``` + +- `Js`调用`Android`中的方法 + ```java + public WebViewActivity extends Activity { + + public void onCreate() { + // 启用javascript + mWebView.getSettings().setJavaScriptEnabled(true); + // 加载web页面 + mWebView.loadUrl("file:///android_asset/wst.html"); + // 首先要对webview绑定javascriptInterface,js脚本通过这个接口来调用java代码。javascriptInterface实际就是一个普通的java类,里面是我们本地实现的java代码, 将JavascriptInterface传递给webview,并指定别名,这样js脚本就可以通过我们给的这个别名来调用我们的方法,在这里,this是实例化的对象,wst是这个对象在js中的别名 + mWebView.addJavascriptInterface(this, "wst"); + } + + // js所调用的native方法的本地实现,安卓4.2及以上版本(API >= 17),在注入类中为可调用的方法添加@JavascriptInterface注解,无注解的方法不能被调用,这种方式可以防范注入漏洞 + @JavascriptInterface + public void startFunction(final String str) { + Toast.makeText(this, str, Toast.LENGTH_SHORT).show(); + runOnUiThread(new Runnable() { + + @Override + public void run() { + msgView.setText(msgView.getText() + "\njs调用了java函数传递参数:" + str); + + } + }); + } + } + ``` + + `js`中通过如下代码调用即可 + ```html + 点击调用java代码并传递参数 + ``` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From 00e67dc6b559a54027cfef28e77128f3a7b0d159 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 24 May 2016 19:23:50 +0800 Subject: [PATCH 019/373] =?UTF-8?q?Update=20Android=E5=8A=A8=E7=94=BB.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Android\345\212\250\347\224\273.md" | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" "b/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" index f290c632..812386db 100644 --- "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" +++ "b/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" @@ -158,16 +158,22 @@ The property animation system is a robust framework that allows you to animate a 以下例子同时应用5个动画: -- 播放anim1; -- 同时播放anim2,anim3,anim4; -- 播放anim5。 +- Plays bounceAnim. +- Plays squashAnim1, squashAnim2, stretchAnim1, and stretchAnim2 at the same time. +- Plays bounceBackAnim. +- Plays fadeAnim. ```java AnimatorSet bouncer = new AnimatorSet(); -bouncer.play(anim1).before(anim2); -bouncer.play(anim2).with(anim3); -bouncer.play(anim2).with(anim4) -bouncer.play(anim5).after(amin2); +bouncer.play(bounceAnim).before(squashAnim1); +bouncer.play(squashAnim1).with(squashAnim2); +bouncer.play(squashAnim1).with(stretchAnim1); +bouncer.play(squashAnim1).with(stretchAnim2); +bouncer.play(bounceBackAnim).after(stretchAnim2); +ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f); +fadeAnim.setDuration(250); +AnimatorSet animatorSet = new AnimatorSet(); +animatorSet.play(bouncer).before(fadeAnim); animatorSet.start(); ``` From 2ae860d9441d4cd1d810d3558214e94fcb763431 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 25 May 2016 18:22:52 +0800 Subject: [PATCH 020/373] Add file --- "Android\345\212\240\345\274\272/.DS_Store" | Bin 10244 -> 0 bytes ...1\253\230Build\351\200\237\345\272\246.md" | 84 ++++++++++++++++++ 2 files changed, 84 insertions(+) delete mode 100644 "Android\345\212\240\345\274\272/.DS_Store" create mode 100644 "Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" diff --git "a/Android\345\212\240\345\274\272/.DS_Store" "b/Android\345\212\240\345\274\272/.DS_Store" deleted file mode 100644 index ad77ce302c12b8ce27815d9f905885a21c031bef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHM-)kII6h6~U(`cGsMnbd?T_40CRI3jXEXX#E)sl2#cD6q>x=D6-nylIGhRtrW zYi+s;LPM0K)Y7%@LVfTLsL!IM=tC$-!3Q6rA_#qwBBI3aoSB3>**l>xl?wL`GiP@8 ze&0R!JLlfndzXl;fpX>rq9_ry;AGpm7Z*+8@A+&fyZk3l!&<--%~O_2R82e|zur*W zJTM9v1&jhl0i%FX;9*bz?`$qDt)4YkqkvJsD6pjf&ks?YY-7Hyc~+(lTx1CV+l0qb zaF2C>V4}W_`L^a+fr8UpJy<@}@|PHv2gmnOt_~aXZOyYDoGcGcmfy1FuTU&$9k{4e zCoASzb2SPW1sWCLwR;C0rZm>=S#|xs^6h)A^csYKiz}6L(9LJk23aZ{T^FW^kP%mu%Rxu$s(a_6Y9_?cUgs%xMFAoDz0B z4FZ*u?3>XcMb+Q7MP!bu@Fa~qnS>uRVTww2sMuZ_kSv=-AGl9*=v5pZr*ZNKaH%-9 zG&+6dZoRJaMKlWX9hHpDpa;OFE;~O2bx#dVz3k4W^UiK9A4go}>!R1dSE6a;Y|4Jt z{>na7FE6f*T|4ztqD{xZ>g+nLsM89DB6=ns+1>hnFsi9nLG5ioE$Uj646Pu3&3+Ug zqu0?Ek6r2i*xw)gG5XQi>D<>>UyL_Tt?D>9CIOH4gQJ9FmQElR!*m2=KaYc>!t)Y7 zAxk#v9Fcr<@qJ0h&K?HXd$y3F>|xo%(GGC!gLjk2Jf53M7zcSe18;a_AolPkt3C;1 zm5AL`w0{z;SxQONid3yv=kMAFJ{iof;%R2&Lb=b+RM)+i`uxfhVZDxAS2#O_le7CI zSX^4juU4!bBcgiLL&wppwB35?_=P;~GL&*|1@k&_-u_wZ32UZubqH4xb3_KZEl_^! zj6IPW8vR&{=IE*DZb5tcX5mQ=vxSRMo5g&S7i~0i4QE)SA6eSuXX&G0IZO%8%uo$5 zA|m~Yf}NsV2CWtDM?=n)0KRP#bw6)I@iwwGGoD17IUFukcZ?hRtVX?!-hwi-qLZgk zuhXN`Srk%{kHq&S{~G^2z2M#$?+8OspQ6v>@+RVU!&c_T4@$In7R}qY!{0Iio~}1P z967Q41vx+!1@IN5*g97jv%iTrv{J=v3tyd4_}>nSt}w6|yCnc6>LwK9^IHYw0K+R6_4sh/.gradle/ (Linux)` + - `/Users//.gradle/ (Mac)` + - `C:\Users\\.gradle (Windows)` + + 文件的内容为: + ``` + org.gradle.daemon=true + org.gradle.parallel=true + ``` + + 经过上面的这一步修改是对所有工程都有效果的,如果你只想对某一个工程配置的话,那就在该工程目录下的`gralde.properties`中进行配置。 + ``` + # Project-wide Gradle settings. + + # IDE (e.g. Android Studio) users: + # Gradle settings configured through the IDE *will override* + # any settings specified in this file. + + # For more details on how to configure your build environment visit + # http://www.gradle.org/docs/current/userguide/build_environment.html + + # Specifies the JVM arguments used for the daemon process. + # The setting is particularly useful for tweaking memory settings. + # Default value: -Xmx10248m -XX:MaxPermSize=256m + # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + + # When configured, Gradle will run in incubating parallel mode. + # This option should only be used with decoupled projects. More details, visit + # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects + # org.gradle.parallel=true + ``` + 打开时默认如上。我们给加添加上面的配置就好。 + + 当然你也可以通过`Studio`的设置中进行修改。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_speed.png?raw=true) + + +- 使用`offline`模式 + 下一步就是开始`offline`模式,因为我们经常会在`gradle`中使用一下依赖库时用`+`这样的话就能保证你的依赖库是最新的版本,但是这样在每次`build`的时候都会去检查是不是最新的版本,所以就会耗时。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_offline.png?raw=true) + +- 增加内存使用`SSD` + 首先是增大内存, `Mac`中在`Applications`中找到`Sutio`然后右键显示包内容`Contents/bin/studio.vmoptions`。 + 打开该文件后修改就可以了,我是的是: + ``` + # + # *DO NOT* modify this file directly. If there is a value that you would like to override, + # please add it to your user specific configuration file. + # + # See http://tools.android.com/tech-docs/configuration + # + -Xms1024m + -Xmx4096m + -XX:MaxPermSize=768m + -XX:ReservedCodeCacheSize=768m + -XX:+UseCompressedOops + ``` + 我没看见`DO NOT`的提示- -! + + - Xms 是JVN启动起始时的堆内存,堆内存是分配给对象的内容。 + - Xmx 是能使用的最大堆内存。 + + +- 使用`Instant Run` + `Instantt Run`放在这里说可能不合适,但是用他确实能大大的减少运行时间。 + 如果还不了解的话可以参考[Instant Run](http://tools.android.com/tech-docs/instant-run) + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_instantrun.png?raw=true) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From aa71d4b54eb9ea4115e63d4cdf5e78706369590f Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 2 Jun 2016 17:24:42 +0800 Subject: [PATCH 021/373] add files --- .../RecyclerView\344\270\223\351\242\230.md" | 596 ++++++++++++++++++ .../RxJava\350\257\246\350\247\243.md" | 9 + ...50\347\224\273\346\200\247\350\203\275.md" | 129 ++++ 3 files changed, 734 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" create mode 100644 "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" create mode 100644 "Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" diff --git "a/Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" "b/Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" new file mode 100644 index 00000000..2de826df --- /dev/null +++ "b/Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" @@ -0,0 +1,596 @@ +RecyclerView专题 +=== + +### 简介 + +`RecyclerView`是`Android 5.0`提供的新控件,已经用了很长时间了,但是一直没有时间去仔细的梳理一下。现在项目不太紧,决定来整理下。 + +官方文档中是这样介绍的: +`A flexible view for providing a limited window into a large data set.` + +RecyclerView比listview更先进更灵活,对于很多的视图它就是一个容器,可以有效的重用和滚动。当数据动态变化的时候请使用它。 + + + +#####专业术语: + +- `Adapter`: `A subclass of RecyclerView.Adapter responsible for providing views that represent items in a data set.` +- `Position`: `The position of a data item within an Adapter.` +- `Index`: `The index of an attached child view as used in a call to getChildAt(int). Contrast with Position.` +- `Binding`: `The process of preparing a child view to display data corresponding to a position within the adapter.` +- `Recycle (view)`: `A view previously used to display data for a specific adapter position may be placed in a cache for later reuse to display the same type of data again later. This can drastically improve performance by skipping initial layout inflation or construction.` +- `Scrap (view)`: `A child view that has entered into a temporarily detached state during layout. Scrap views may be reused without becoming fully detached from the parent RecyclerView, either unmodified if no rebinding is required or modified by the adapter if the view was considered dirty.` +- `Dirty (view)`: `A child view that must be rebound by the adapter before being displayed.` + +#####`RecyclerView`中的位置: + +`RecyclerView`在`RecyclerView.Adapter`和`RecyclerView.LayoutManager`中引进了一个抽象的额外中间层来保证在布局计算的过程中能批量的监听到数据变化。这样介绍了`LayoutManager`追踪`adapter`数据变化来计算动画的时间。因为所有的`View`绑定都是在同一时间执行,所以这样也提高了性能和避免了一些非必要的绑定。 +因为这个原因,在`RecylcerView`中有两种`position`类型相关的方法: +- `layout position`: 在最近一次`layout`计算是`item`的位置。这是`LayoutManager`角度中的位置。 +- `adapter position`: `item`在`adapter`中的位置。这是从`Adapter`的角度中的位置。 + +这两种`position`除了在分发`adapter.notify*`事件与之后计算布局更新的这段时间之内外都是相同的。 +可以通过`getLayoutPosition(),findViewHolderForLayoutPosition(int)`方法来获取最近一次布局计算的`LayoutPosition`。这些`positions`包括从最近一次布局计算的所有改变。你可以根据这些位置来方便的得到用户当前从屏幕上所看到的。例如,如果在屏幕上有一个列表,用户请求的是第五个条目,你可以通过该方法来匹配当前用户正在看的内容。 + +另一种`AdapterPosition`相关的方法是`getAdapterPosition(),findViewHolderForAdapterPosition(int)`,当及时一些数据可能没有来得及被展现到布局上时便需要获取最新的`adapter`位置可以使用这些相关的方法。例如,如果你想获取一个条目的`ViewHOlder`的`click`事件时,你应该使用`getAdapterPosition()`。需要知道这些方法在`notifyDataSetChange()`方法被调用和新布局还没有被计算之前是不能使用的。鉴于这个原因,你应该小心的去处理这些方法有可能返回`NO_POSITION`或者`null`的情况。 + + + +###结构 + +- `RecyclerView.Adapter`: 创建View并将数据集合绑定到View上 +- `ViewHolder`: 持有所有的用于绑定数据或者需要操作的View +- `LayoutManager`: 布局管理器,负责摆放视图等相关操作 +- `ItemDecoration`: 负责绘制`Item`附近的分割线,通过`RecyclerView.addItemDecoration()`使用 +- `ItemAnimator`: 为`Item`的操作添加动画效果,如,增删条目等,通过`RecyclerView.setItemAnimator(new DefaultItemAnimator());`使用 + +下图能更直观的了解: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/RecyclerView.png?raw=true) + +#####`RecyclerView`提供这些内置布局管理器: + +- `LinearLayoutManager`: 以垂直或水平滚动列表方式显示项目。 +- `GridLayoutManager`: 在网格中显示项目。 +- `StaggeredGridLayoutManager`: 在分散对齐网格中显示项目。 + +#####`RecyclerView.ItemDecoration`是一个抽象类,可以通过重写以下三个方法,来实现Item之间的偏移量或者装饰效果: + +- `public void onDraw(Canvas c, RecyclerView parent)` 装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡 +- `public void onDrawOver(Canvas c, RecyclerView parent)` 装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上 +- `public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)` 与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量。 + + +#####`ItemAnimator`触发于以下三种事件: + +- 某条数据被插入到数据集合中 +- 从数据集合中移除某条数据 +- 更改数据集合中的某条数据 + +在之前的版本中,当时据集合发生改变时通过调用`notifyDataSetChanged()`,来刷新列表,因为这样做会触发列表的重绘,所以并不会出现任何动画效果,因此需要调用一些以`notifyItem*()`作为前缀的特殊方法,比如: + +- `public final void notifyItemInserted(int position)` 向指定位置插入`Item` +- `public final void notifyItemRemoved(int position)` 移除指定位置`Item` +- `public final void notifyItemChanged(int position)` 更新指定位置`Item` + + + +###使用介绍: + +- 添加依赖库 + ``` + dependencies { + compile 'com.android.support:recyclerview-v7:23.4.0' + } + ``` +- 示例代码 + + ```java + public class MainActivity extends AppCompatActivity { + private RecyclerView mRecyclerView; + private LinearLayoutManager mLayoutManager; + private RecyclerView.Adapter mAdapter; + + private String [] mDatas = {"Android","ios","jack","tony","window","mac","1234","hehe","495948", "89757", "66666"}; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + findView(); + initView(); + } + + private void findView() { + mRecyclerView = (RecyclerView) findViewById(R.id.rv); + } + + private void initView() { + // use this setting to improve performance if you know that changes + // in content do not change the layout size of the RecyclerView + mRecyclerView.setHasFixedSize(true); + + // use a linear layout manager + mLayoutManager = new LinearLayoutManager(this); + mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mRecyclerView.setLayoutManager(mLayoutManager); + mAdapter = new MyAdapter(mDatas); + mRecyclerView.setAdapter(mAdapter); + } + + private class MyAdapter extends RecyclerView.Adapter { + private String[] mData; + + public MyAdapter(String[] data) { + mData = data; + } + + @Override + public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) { + // create a new view + View v = LayoutInflater.from(parent.getContext()).inflate( + R.layout.item, parent, false); + MyHolder holder = new MyHolder(v); + return holder; + } + + @Override + public void onBindViewHolder(MyHolder holder, int position) { + holder.mTitleTv.setText(mData[position]); + } + + @Override + public int getItemCount() { + return mData == null ? 0 : mData.length; + } + } + + static class MyHolder extends RecyclerView.ViewHolder { + public TextView mTitleTv; + + public MyHolder(View itemView) { + super(itemView); + mTitleTv = (TextView) itemView; + } + } + } + ``` + + `activity_main的内容如下: ` + ```xml + + ``` + + `item的内容如下:` + ```xml + + + + + ``` + + + +###点击事件 + +之前在使用`ListView`的时候,设置点击事件是非常方便的。 +```java +mListView.setOnItemClickListener(); +mListView.setOnItemLongClickListener(); +``` +但是`RecylcerView`确没有提供类似的方法。那我们只能是自己去处理。处理的方式也有两种: + + +- 通过`itemView.onClickListener()`以及`onLongClickListener()` + + ```java + public class MainActivity extends AppCompatActivity { + private RecyclerView mRecyclerView; + private LinearLayoutManager mLayoutManager; + private MyAdapter mAdapter; + + private String[] mDatas = {"Android", "ios", "jack", "tony", "window", "mac", "1234", "hehe", "495948", "89757", "66666"}; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + findView(); + initView(); + } + + private void findView() { + mRecyclerView = (RecyclerView) findViewById(R.id.rv); + } + + private void initView() { + // use this setting to improve performance if you know that changes + // in content do not change the layout size of the RecyclerView + mRecyclerView.setHasFixedSize(true); + + // use a linear layout manager + mLayoutManager = new LinearLayoutManager(this); + mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mRecyclerView.setLayoutManager(mLayoutManager); + mAdapter = new MyAdapter(mDatas); + mAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + Toast.makeText(MainActivity.this, "click " + mDatas[position], Toast.LENGTH_SHORT).show(); + } + }); + mAdapter.setOnItemLongClickListener(new OnItemLongClickListener() { + @Override + public void onItemLongClick(View view, int position) { + Toast.makeText(MainActivity.this, "long click " + mDatas[position], Toast.LENGTH_SHORT).show(); + } + }); + mRecyclerView.setAdapter(mAdapter); + } + + class MyAdapter extends RecyclerView.Adapter { + private String[] mData; + + public MyAdapter(String[] data) { + mData = data; + } + + @Override + public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) { + // create a new view + View v = LayoutInflater.from(parent.getContext()).inflate( + R.layout.item, parent, false); + MyHolder holder = new MyHolder(v); + return holder; + } + + @Override + public void onBindViewHolder(final MyHolder holder, final int position) { + holder.mTitleTv.setText(mData[position]); + if (mOnItemClickListener != null) { + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mOnItemClickListener.onItemClick(holder.itemView, position); + } + }); + } + if (mOnItemLongClickListener != null) { + holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + mOnItemLongClickListener.onItemLongClick(holder.itemView, position); + return true; + } + }); + } + } + + @Override + public int getItemCount() { + return mData == null ? 0 : mData.length; + } + + private OnItemClickListener mOnItemClickListener; + private OnItemLongClickListener mOnItemLongClickListener; + + public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) { + this.mOnItemClickListener = mOnItemClickListener; + } + + public void setOnItemLongClickListener(OnItemLongClickListener mOnItemLongClickListener) { + this.mOnItemLongClickListener = mOnItemLongClickListener; + } + + } + + static class MyHolder extends RecyclerView.ViewHolder { + public TextView mTitleTv; + + public MyHolder(View itemView) { + super(itemView); + mTitleTv = (TextView) itemView; + } + } + + public interface OnItemClickListener { + void onItemClick(View view, int position); + } + + public interface OnItemLongClickListener { + void onItemLongClick(View view, int position); + } + + } + ``` +- 使用`RecyclerView.OnItemTouchListener` + + 虽然没有提供现成的监听器,但是提供了一个内部接口`OnItemTouchListener`。 + 先来看看它的介绍: + ``` + /** + * An OnItemTouchListener allows the application to intercept touch events in progress at the + * view hierarchy level of the RecyclerView before those touch events are considered for + * RecyclerView's own scrolling behavior. + * + *

This can be useful for applications that wish to implement various forms of gestural + * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept + * a touch interaction already in progress even if the RecyclerView is already handling that + * gesture stream itself for the purposes of scrolling.

+ * + * @see SimpleOnItemTouchListener + */ + public static interface OnItemTouchListener { + ... + } + ``` + 说的和明白了,而且还让你看`SimpleOnItemTouchListener`,猜也能猜出来是一个默认的实现类。 + 好了直接上代码: + ```java + public class MainActivity extends AppCompatActivity { + private RecyclerView mRecyclerView; + private LinearLayoutManager mLayoutManager; + private MyAdapter mAdapter; + + private String[] mDatas = {"Android", "ios", "jack", "tony", "window", "mac", "1234", "hehe", "495948", "89757", "66666"}; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + findView(); + initView(); + } + + private void findView() { + mRecyclerView = (RecyclerView) findViewById(R.id.rv); + } + + private void initView() { + // use this setting to improve performance if you know that changes + // in content do not change the layout size of the RecyclerView + mRecyclerView.setHasFixedSize(true); + + // use a linear layout manager + mLayoutManager = new LinearLayoutManager(this); + mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mRecyclerView.setLayoutManager(mLayoutManager); + mAdapter = new MyAdapter(mDatas); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.addOnItemTouchListener(new RecyclerViewClickListener(this, mRecyclerView, new OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + Toast.makeText(MainActivity.this, mDatas[position], Toast.LENGTH_SHORT).show(); + } + + @Override + public void onItemLongClick(View view, int position) { + Toast.makeText(MainActivity.this, "Long Click " + mDatas[position], Toast.LENGTH_SHORT).show(); + } + })); + } + + class RecyclerViewClickListener extends RecyclerView.SimpleOnItemTouchListener { + private GestureDetector mGestureDetector; + private OnItemClickListener mListener; + + public RecyclerViewClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + View childView = recyclerView.findChildViewUnder(e.getX(), e.getY()); + if (childView != null && mListener != null) { + mListener.onItemClick(childView, recyclerView.getChildLayoutPosition(childView)); + return true; + } + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + View childView = recyclerView.findChildViewUnder(e.getX(), e.getY()); + if (childView != null && mListener != null) { + mListener.onItemLongClick(childView, recyclerView.getChildLayoutPosition(childView)); + } + } + }); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + if (mGestureDetector.onTouchEvent(e)) { + return true; + } else + return super.onInterceptTouchEvent(rv, e); + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) { + super.onTouchEvent(rv, e); + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + super.onRequestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + interface OnItemClickListener { + void onItemClick(View view, int position); + + void onItemLongClick(View view, int position); + } + ``` + 上面的实现稍微有些缺陷,就是如果我手指按住某个条目一直不抬起,他也会执行`Long click`事件,这显然是不合理的,至于怎么解决,就是可以不用`GestureDetector`,自己在`DOWN`和`UP`事件中去判断处理。 + +### Headerview FooterView + +之前在`ListView`中提供了`addHeaderView()`和`addFooterView()`等方法,但是在`RecyclerView`中并没有提供类似的方法,那我们该如何添加呢? 也很简单,就是通过`Adapter`中去添加,利用不同的`itemViewType`,然后根据不同的类型去在`onCreateViewHOlder`中创建不同的视图,通过这种方式来达到`headerview`和`FooterView`的效果。 + +上一段简单的示例代码: +```java + public class HeaderAdapter extends RecyclerView.Adapter { + private static final int TYPE_HEADER = 0; + private static final int TYPE_ITEM = 1; + String[] data; + + public HeaderAdapter(String[] data) { + this.data = data; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == TYPE_ITEM) { + //inflate your layout and pass it to view holder + return new VHItem(null); + } else if (viewType == TYPE_HEADER) { + //inflate your layout and pass it to view holder + return new VHHeader(null); + } + + throw new RuntimeException("there is no type that matches the type " + viewType + " + make sure your using types correctly"); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (holder instanceof VHItem) { + String dataItem = getItem(position); + //cast holder to VHItem and set data + } else if (holder instanceof VHHeader) { + //cast holder to VHHeader and set data for header. + } + } + + @Override + public int getItemCount() { + return data.length + 1; + } + + @Override + public int getItemViewType(int position) { + if (isPositionHeader(position)) + return TYPE_HEADER; + + return TYPE_ITEM; + } + + private boolean isPositionHeader(int position) { + return position == 0; + } + + private String getItem(int position) { + return data[position - 1]; + } + + class VHItem extends RecyclerView.ViewHolder { + TextView title; + + public VHItem(View itemView) { + super(itemView); + } + } + + class VHHeader extends RecyclerView.ViewHolder { + Button button; + + public VHHeader(View itemView) { + super(itemView); + } + } +} +``` +上面的代码对`LinearLayoutManger`是没问题的,但是使用`GridLayoutManager`呢? 如果是两列,那添加的`HeaderView`并不是占据上第一行,而是`HeaderView`与第二个`ItemView`一起占据第一行。那该怎么处理呢? +那就是使用`setSpanSizeLookup()`方法。 +比如: +```java +recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); +``` +在上面的基本设置中,我们的`spanCount`为2,每个`item`的`span size`为1,因此一个`header`需要的`span size`则为2。在我尝试着添加`header`之前,我想先看看如何设置`span size`。其实很简单。 +```java +final GridLayoutManager manager = new GridLayoutManager(this, 2); +recyclerView.setLayoutManager(manager); +manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return adapter.isHeader(position) ? manager.getSpanCount() : 1; + } +}); +``` + +### 下拉刷新、自动加载 + +#####实现下拉刷新 +实现下拉刷新也很简单了,可以使用`SwipeRefrshLayout`,`SwipeRefrshLayout`是`Google`官方提供的组件,可以实现下拉刷新的功能。已包含到`support.v4`包中。 + +主要方法有: + +- `setOnRefreshListener(OnRefreshListener)`:添加下拉刷新监听器 +- `setRefreshing(boolean)`:显示或者隐藏刷新进度条 +- `isRefreshing()`:检查是否处于刷新状态 +- `setColorSchemeResources()`:设置进度条的颜色主题,最多设置四种。 + +```xml + + + +``` +具体实现就不写了。 + +#####实现滑动自动加载更多功能 + +实现方式和`ListView`的实现方式类似,就是通过监听`scroll`时间,然后判断当前显示的`item`。 + +```java +//RecyclerView滑动监听 +mRecylcerView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState ==RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 ==adapter.getItemCount()) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + List newDatas = new ArrayList(); + for (int i = 0; i< 5; i++) { + int index = i +1; + newDatas.add("more item" + index); + } + adapter.addMoreItem(newDatas); + } + },1000); + } + } + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView,dx, dy); + lastVisibleItem =linearLayoutManager.findLastVisibleItemPosition(); + } +}); +``` + +然后再通过结合`FooterView`以及增加几种状态就可以实现自动加载更多了。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" new file mode 100644 index 00000000..6b48dbd7 --- /dev/null +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" @@ -0,0 +1,9 @@ +RxJava详解 +=== + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" "b/Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" new file mode 100644 index 00000000..a1d430a8 --- /dev/null +++ "b/Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" @@ -0,0 +1,129 @@ +通过Hardware Layer提高动画性能 +=== + +项目中越来越多的动画,越来越多的效果导致了应用性能越来越低。该如何提升。 + +###简介 + +在`View`播放动画的过程中每一帧都需要被重绘。如果使用`view layers`,就不用每帧都去重绘,因为`View`渲染一旦离开屏幕缓冲区就可以被重用。 + +而且,`hardware layers`会在`GPU`上缓存,这样就会让一些动画过程中的操作变得更快。通过`hardware layers`可以快速的渲染一些简单的转变(位移、选中、缩放、颜色渐变)。由于很多动画都是这些动作的结合,所以`hardware layers`可以显著的提高动画性能。 + +在`View`当中提供了三种类型的`Layer type`: + +- LAYER_TYPE_HARDWARE + > Indicates that the view has a hardware layer. A hardware layer is backed by a hardware specific texture (generally Frame Buffer Objects or FBO on OpenGL hardware) and causes the view to be rendered using Android's hardware rendering pipeline, but only if hardware acceleration is turned on for the view hierarchy. When hardware acceleration is turned off, hardware layers behave exactly as software layers. + + > A hardware layer is useful to apply a specific color filter and/or blending mode and/or translucency to a view and all its children. + + > A hardware layer can be used to cache a complex view tree into a texture and reduce the complexity of drawing operations. For instance, when animating a complex view tree with a translation, a hardware layer can be used to render the view tree only once. + + > A hardware layer can also be used to increase the rendering quality when rotation transformations are applied on a view. It can also be used to prevent potential clipping issues when applying 3D transforms on a view. + + +- LAYER_TYPE_SOFTWARE + > Indicates that the view has a software layer. A software layer is backed by a bitmap and causes the view to be rendered using Android's software rendering pipeline, even if hardware acceleration is enabled. + + > Software layers have various usages: + + > When the application is not using hardware acceleration, a software layer is useful to apply a specific color filter and/or blending mode and/or translucency to a view and all its children. + + > When the application is using hardware acceleration, a software layer is useful to render drawing primitives not supported by the hardware accelerated pipeline. It can also be used to cache a complex view tree into a texture and reduce the complexity of drawing operations. For instance, when animating a complex view tree with a translation, a software layer can be used to render the view tree only once. + + > Software layers should be avoided when the affected view tree updates often. Every update will require to re-render the software layer, which can potentially be slow (particularly when hardware acceleration is turned on since the layer will have to be uploaded into a hardware texture after every update.) + +- LAYER_TYPE_NONE + > Indicates that the view does not have a layer. + 默认值。 + +### 使用 + +首先使用的前提是在清单文件中开启了硬件加速。否则将无法使用`hardware layer`。这一点在上面的文档中也有说明。 + +`API`也是非常简单的,直接使用`View.setLayerType()`就好。使用时应该只是暂时的设置`Hardware Layer`,因为它们无法自动释放。 +基本的使用步骤: + +- 对每个想要在动画过程中进行缓存的`view`调用`View.setLayerType(View.LAYER_TYPE_HARDWARE, null)`方法。 +- 执行动画。 +- 在动画执行结束后调用`View.setLayerType(View.LAYER_TYPE_NONE, null)`方法来进行清除。 + +示例: +```java +mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + +animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mView.setLayerType(View.LAYER_TYPE_NONE, null); + } +}); + +animator.start(); + +``` +但是如果在`4.0.x`的版本中使用上面的代码会本亏,必须要把`setLayerType`放到`Runnable`中。如下: +```java +mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + +animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + //This will work successfully + post(new Runnable() { + @Override + public void run () { + setLayerType(LAYER_TYPE_NONE, null); + } + } + } +}); + +animator.start(); +``` + +如果你基于`minSdkVersion 16`以上并且使用`ViewPropertyAnimator`时,你可以使用`withLayer()`方法替代如上的操作: + +```java +mView.animate().translationX(150).withLayer().start(); +``` + +或者在`api 14`以上时使用`ViewCompat.animate().withLayer()` +这样做,你的动画就会变得更流畅! + + +###注意事项 + +你应该知道,事情没那么简单。 +`Hardware layers`有着惊人的提升动画性能的能力。然而,如果滥用,它的危害更大。**不要盲目的使用`layers`** + +- 首先,在有些情况下,`hardware layers`除了`view`渲染外还会执行更多的工作。缓存`layer`将会需要时间,因为首选第一步就需要两个过程: 先将这些`view`渲染到`GPU`的一个`layer`中然后`GPU`再渲染该`layer`到`Window`上。如果要渲染的`View`非常简单(例如一个纯色值),那么这样在初始化的时候就会增加`Hardware Layer`不必要的开销。 + +- 其次,对所有的缓存来讲,都有一个缓存失效的可能性。任何时候如果在动画过程中调用`view.invalidate()`,那么`layer`就必须要重新渲染。经常的废弃`hardware layers`会比没有`layers`的情况下更糟糕,因为如同上面讲到的`hardware layers`在设置缓存时会有额外的开销。如果你需要经常的重新缓存`layer`,那就会有极大的损害。 + + 这个问题也是非常容易出现的,因为动画经常有多个移动的部分。假如现在有一个三个部分移动的动画: + ```java + Parent ViewGroup + —-> Child View1 (往左移动) + —-> Child View2 (往右移动) + —-> Child View3 (往上移动) + ``` + + 如果你只在父布局`ViewGroup`上设置一个`layer`,那就将经常的缓存失效,因为`ViewGroup`会随着子`View`不断地改变。然而对每个单独的子`Views`而言,他们只是在位移。这种情况下,最好是对每个子`View上`设置`Hardware Layer`(而不是在父布局上)。 + + ***再次重申,通常是对多个子`View上`适当的设置`Hardware Layer`,这样他们就不会在动画运行时失效。*** + + 在手机开发者选项中的*显示硬件层更新(Show hardware layers updates)*功能是追踪这个问题的开发利器。当`View`渲染`Hardware Layer`的时候闪烁绿色,它应该在动画开始的时候闪烁一次(也就是`Layer`渲染初始化的时候),然而,如果你的`View`在整个动画期间都是绿色,那就是遇到失效的问题了。 + +- 最后,`hardware layers`使用`GPU`内存,你当然不想出现内存泄漏的问题。所以你应该在必要的时候再去使用`hardware layers`,就想播放动画时。 + + +这里也没有硬性规则。`Android`渲染系统是非常复杂的。就像所有性能问题一样,测试才是关键。通过使用“显示硬件层更新”开发者选项来确定`layers`是在帮你还是害你。 + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From c483d21da0ac31e08c11690e234778cb6c9b4a82 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 2 Jun 2016 19:28:10 +0800 Subject: [PATCH 022/373] add MVP.md --- ...41\345\274\217\350\257\246\350\247\243.md" | 170 ++++++++++++++++++ .../MVC\344\270\216MVP\345\217\212MVVM.md" | 5 +- 2 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" new file mode 100644 index 00000000..a97c07f6 --- /dev/null +++ "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -0,0 +1,170 @@ +Android开发中的MVP模式详解 +=== + +[MVC、MVP、MVVM介绍](https://github.com/CharonChui/AndroidNote/blob/master/Java%E5%9F%BA%E7%A1%80/MVC%E4%B8%8EMVP%E5%8F%8AMVVM.md) + + +在`Android`开发中,如果不注重架构的话,`Activity`类就会变得愈发庞大。这是因为在`Android`开发中`View`和其他的线程可以共存于`Activity`内。那最大的问题是什么呢? 其实就是**`Activity`中同事存在业务逻辑和`UI`逻辑。这导致增加了单元测试和维护的成本。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_is_god.png?raw=true) + +这就是为什么要清晰架构的原因之一。不仅是因为`Activity`类变得臃肿,也是其他的一些问题,例如`Activity`和`Fragment`相结合时的生命周期、数据绑定等等。 + +###MVP简介 + +`MVP(Model,View,Presenter)` + +- `View`:负责处理用户时间和视图展现。在`Android`中就可能是`Activity`或者`Fragment`。 +- `Model`: 负责数据访问。数据可以是从接口或者本地数据库中获取。 +- `Presenter`: 负责连接`View`和`Model`。 + +用一句话来说:`MVP`其实就是面向接口编程,`V`实现接口,`P`使用接口。 + +清晰的架构: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mvp_a.png?raw=true) + + +举个栗子: +在`Android Studio`中新建一个`Activity`,系统提供了`LoginActivity`,直接用它是极好的。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/loginactivity.png?raw=true) + +不得不说,`Material Design`的效果真是美美哒! + +好,那我们就用用户登录页来按照`MVP`的模式实现一下: + +- M: 很显然Model应该是`User`类。 +- V: `View`就是`LoginActivity`。 +- P: P那我们一会就创建一个`LoginPresenter`类。 + +齐了,那接下来就详细分析下他们这三部分: + +- `User`: 应该有`email`, `password`, `boolean login(email, password)`。 +- `LoginActivity`:点击登录应该要出`loading`页。登录成功后要进入下一个页面。如果登录失败应该弹`toast`提示。那就需要`void showLoading()`,`void hideLoading()`,`void showErrorTip()`,`void doLoginSuccess()`这四个方法。 +- `LoginPresenter`:这是`Model`和`View`的桥梁。他需要做的处理业务逻辑,直接与`Model`打交道,然后将`UI`的逻辑交给`LoginActivity`处理。 +那怎么做呢? 按照我上面总结的那一句话,、`MVP`其实就是面向接口编程,`V`实现接口,`P`使用接口。很显然我们需要提供一个接口。那就新建一个`ILoginView`的接口。这里面有哪些方法呢? 当然是上面我们在分析`LoginActiity`时提出的那四个方法。这样`LoginActivity`直接实现`ILoginView`接口就好。 + + +开始做: + +- 先把`Model`做好吧,创建`User`类。 + ```java + public class User { + private String email; + private String password; + + public User(String email, String password) { + this.email = email; + this.password = password; + } + + public boolean login() { + // do login request.. + return true; + } + } + ``` +- 创建`ILoginView`接口,定义登录所需要的`ui`逻辑。 + ```java + public interface ILoginView { + void showLoading(); + void hideLoading(); + void showErrorTip(); + void doLoginSuccess(); + } + ``` + +- 创建`LoginPresenter`类,使用`ILoginView`接口,那该类主要有什么功能呢? 它主要是处理业务逻辑的, + 对于登录的话,当然是用户在`UI`页面输入邮箱和密码,然后`Presenter`去开线程、请求接口。然后得到登录结果再去让`UI`显示对应的视图。那自然就是有一个`void login(String email, String passowrd)`的方法了 + ```java + public class LoginPresenter { + private ILoginView mLoginView; + + public LoginPresenter(ILoginView loginView) { + mLoginView = loginView; + } + + public void login(String email, String password) { + if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password)) { + // + mLoginView.showErrorTip(); + return; + } + mLoginView.showLoading(); + User user = new User(email, password); + + // do network request.... + // .... + onSuccess() { + boolean login = user.login(); + if (login) { + mLoginView.doLoginSuccess(); + } else { + mLoginView.showErrorTip(); + } + mLoginView.hideLoading(); + } + + onFailde() { + mLoginView.showErrorTip(); + mLoginView.hideLoading(); + } + } + } + ``` +- 创建`LoginActivity`,实现`ILoginView`的接口,然后内部调用`LoginPresenter`来处理业务逻辑。 + ```java + public class LoginActivity extends AppCompatActivity implements ILoginView { + private LoginPresenter mLoginPresenter; + + private AutoCompleteTextView mEmailView; + private EditText mPasswordView; + private View mProgressView; + private View mLoginButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + mEmailView = (AutoCompleteTextView) findViewById(R.id.email); + mPasswordView = (EditText) findViewById(R.id.password); + mLoginButton = findViewById(R.id.email_sign_in_button); + mProgressView = findViewById(R.id.login_progress); + + mLoginPresenter = new LoginPresenter(this); + + mLoginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLoginPresenter.login(mEmailView.getText().toString().trim(), mPasswordView.getText().toString().trim()); + } + }); + } + + @Override + public void showLoading() { + mProgressView.setVisibility(View.VISIBLE); + } + + @Override + public void hideLoading() { + mProgressView.setVisibility(View.GONE); + } + + @Override + public void showErrorTip() { + Toast.makeText(this, "login faled", Toast.LENGTH_SHORT).show(); + } + + @Override + public void doLoginSuccess() { + Toast.makeText(this, "login success", Toast.LENGTH_SHORT).show(); + } + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" "b/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" index d08971cf..031a3198 100644 --- "a/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" +++ "b/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" @@ -24,6 +24,8 @@ MVC 分层同时也简化了分组开发。不同的开发人员可同时开发 - 视图与控制器间的过于紧密的连接 - 增加系统结构和实现的复杂性 +![image](https://github.com/CharonChui/Pictures/blob/master/mvc_model.png?raw=true) + MVP --- @@ -34,8 +36,7 @@ MVP 在`MVC`模型里,更关注的`Model`的不变,而同时有多个对`Model`的不同显示及`View`。所以,在`MVC`模型里,`Model`不依赖于`View`,但是`View`是依赖于`Model`的。 不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 -![image](https://github.com/CharonChui/Pictures/blob/master/MVP.jpg?raw=true) -![image](https://github.com/CharonChui/Pictures/blob/master/MVC.jpg?raw=true) +![image](https://github.com/CharonChui/Pictures/blob/master/is-activity-god-the-mvp-architecture-10-638.jpg?raw=true) 在`MVP`里,`Presenter`完全把`Model`和`View`进行了分离,主要的程序逻辑在`Presenter`里实现。而且`Presenter`与具体的`View`是没有直接关联的, 而是通过定义好的接口进行交互,从而使得在变更`View`时候可以保持`Presenter`的不变,即重用! From 381ce86555f6e6cef022a6b54b096a225df19561 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 3 Jun 2016 11:51:46 +0800 Subject: [PATCH 023/373] add file --- ...71\345\272\224\345\212\237\350\203\275.md" | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" new file mode 100644 index 00000000..7d1ac6df --- /dev/null +++ "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" @@ -0,0 +1,56 @@ +Android开发不申请权限来使用对应功能 +=== + +从用户角度来说很难获取到正确的`android`权限。通常你只需要做一些很基础的事(例如编辑一个联系人)但实际你申请的权限却远远比这更强大(例如可以获取到所有的联系人明细等)。 + +这样能很容易的理解到用户会怀疑到你的应用。如果你的应用不是开源的,那他们就没有方法来验证你会不会下载所有的联系人数据并上传到服务器。及时你去解释为什么需要这个权限,但是人们不会相信你。原来我会选择不去使用这些敏感的权限来防止用户产生不信任。(当然很多应用他们申请权限只是为了后台获取你的联系人数据上传- -!以及之前被爆的某宝使用摄像头拍照的问题) + +这就是说,有一件事在困扰着我,**如何能在做一些操作时不去申请权限。** + +打个比方说:`android.permission.CALL_PHONE`这个权限。你需要它来在应用中拨打电话,是吗?这就是你怎么去实现拨号的吗? +```java +Intent intent = new Intent(Intent.ACTION_CALL); +intent.setData(Uri.parse("tel:1234567890")) +startActivity(intent); +``` +**错!**,你通过这段代码需要该权限的原因是因为你可以在任何时间在不需要用户操作的情况下打电话。也就是说如果我的应用申请了这个权限,我可以在你不知情的情况下每天凌晨三点去拨打骚扰电话。 + +正确的方式是使用`ACTION_VIEW`或者`ACTION_DIAL`: +```java +Intent intent = new Intent(Intent.ACTION_DIAL); +intent.setData(Uri.parse("tel:1234567890")) +startActivity(intent); +``` + +**这个问题的完美解决方案就是不需要申请权限了。**原因就是你不是直接拨号,而是用指定的号码调起拨号器,仍然需要用户点击”拨号”来开始打电话。老实的说,这样让人感觉更好。 + +简单的说就是如果我想要的操作不是让用户在应用内点击某个按钮就直接开始拨打电话,而是让用户点击在应用内点击某个按钮是我们去调起拨号程序,并且显示指定号码,让用户在拨号器中点击拨号后再开始拨打电话。这样的话我们就完全不用申请拨号权限了。 + +另一个例子: 我想获取某一个联系人的号码,你可能会想这需要申请获取所有联系人的权限。这是错的!。 +```java +Intent intent = new Intent(Intent.ACTION_PICK); +intent.setType(StructuredPostal.CONTENT_TYPE); +startActivityForResult(intent, 1); +``` +我们可以使用上面的代码,来启动联系人管理器,让用户来选择某一个联系人。这样不仅是不需要申请任何权限,也不需要提供任何联系人相关的`UI`。这样也能完全保证你选择联系人时的体验。 + + +`Android`系统最酷的部分之一就是`Intent`系统,这意味着我不需要自己来实现所有的东西。应用可以注册处理它所擅长的指定数据,像电话号码、短信或者联系人。如果这些都要自己在一个应用中去实现,那这将会是很大的工作量,也会让应用变得臃肿。 + +`Android`系统的另一个优势就是你可以使用其他应用申请的权限,而不用自己申请。这样才保证了上面的情况。拨号器需要申请拨打电话的权限,我只需要一个能调起拨号器的`Intent`就好了。用户信任拨号器拨打电话,而不是我们的应用。他们无论如何都宁愿使用系统的拨号器。 + +写这篇文章的意义是**在你想要申请一个权限的时候,你需要至少看看[Intent的官方文档](https://developer.android.com/reference/android/content/Intent.html)看能否请求另外一个应用来帮我们做这些操作。**如果想要深入的研究,可以学习下[关于权限的详细介绍](https://developer.android.com/guide/topics/security/permissions.html),这里面包含了很多精细的权限。 + +使用更少的权限可以不但可以让用户更加信任你,而且可以让用户有一个更好的体验,因为他们仍然在使用他们所期望的应用。 + +1. 遗憾的是,不是一个真实的号码。 +2. 不幸的是,`Intent`系统的属性也建立了[可能会被滥用的漏洞](http://css.csail.mit.edu/6.858/2012/projects/ocderby-dennisw-kcasteel.pdf),但你也不会写一个滥用的应用,是吗? + + +- (译)[感谢Dan Lew](http://blog.danlew.net/2014/11/26/i-dont-need-your-permission/) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 105e78c49f638e524722eeef9e41b2129dffaab1 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 3 Jun 2016 14:38:11 +0800 Subject: [PATCH 024/373] update studio shortcut --- ...347\250\213(\347\254\254\344\270\200\345\274\271).md" | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" "b/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" index 0e3173aa..02163749 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" +++ "b/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" @@ -102,8 +102,15 @@ AndroidStudio使用教程(第一弹) 不用再通过新开一个页面查看了,看代码实现的时候非常方便。 | `Ctrl+Shift+I` | `Option+Space` | | 全局搜索 | `Ctrl+Shift+F` | `Command+Shift+F` | | 上一步、下一步 | `` | `Command+[或]` | + | 高亮所有相同变量 | `Ctrl+Shift+F7` | `Command+Shift+F7` | + | 方法调用层级弹窗 | `Ctrl+Alt+H` | `Control+Option+H` | + |立马知道某个类或者方法的来源同时又不想丢掉当前的编码环境 | `Ctrl+Shift+I` | `Option+Space` | +|书签(在当前行打上书签) | `F11` | `F3` | +|展示书签 | `Shift+F11` | `Command+F3` | +|整行代码上下移动 | `Alt+Shift++↑或↓` | `Option+Shift+↑或↓` | + + - --- - 邮箱 :charon.chui@gmail.com From 3ee7fdd66b044b09a1123fd1a2f2e4e3472a86d2 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 3 Jun 2016 14:40:13 +0800 Subject: [PATCH 025/373] update studio shortcut --- ...\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" "b/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" index 02163749..905df36c 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" +++ "b/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" @@ -98,13 +98,11 @@ AndroidStudio使用教程(第一弹) | 进入父类方法的实现 UP | `Ctrl+U` | `Command+U` | | 抽取某一个块代码为单独的变量或者方法 | `Ctrl+Alt+V` | `Command+Option+V`  方法是`Command+Option+M` | | 选择最近所有复制过内容的列表 | `Ctrl+Shift+V` | `Command+Shift+V` | - | 通过卡片的方式,直接查看该方法的具体内容或者图片, - 不用再通过新开一个页面查看了,看代码实现的时候非常方便。 | `Ctrl+Shift+I` | `Option+Space` | + | 通过卡片的方式,直接查看该方法的具体内容或者图片 | `Ctrl+Shift+I` | `Option+Space` | | 全局搜索 | `Ctrl+Shift+F` | `Command+Shift+F` | | 上一步、下一步 | `` | `Command+[或]` | | 高亮所有相同变量 | `Ctrl+Shift+F7` | `Command+Shift+F7` | | 方法调用层级弹窗 | `Ctrl+Alt+H` | `Control+Option+H` | - |立马知道某个类或者方法的来源同时又不想丢掉当前的编码环境 | `Ctrl+Shift+I` | `Option+Space` | |书签(在当前行打上书签) | `F11` | `F3` | |展示书签 | `Shift+F11` | `Command+F3` | |整行代码上下移动 | `Alt+Shift++↑或↓` | `Option+Shift+↑或↓` | From 3b98ea47e26331f5fc90ef099aba2964bd064687 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 3 Jun 2016 15:12:47 +0800 Subject: [PATCH 026/373] update improve gradle build speed --- ...7\220\351\253\230Build\351\200\237\345\272\246.md" | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git "a/Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" "b/Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" index b03e7fb6..24c94c5d 100644 --- "a/Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" +++ "b/Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" @@ -47,10 +47,16 @@ AndroidStudio提高Build速度 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_speed.png?raw=true) -- 使用`offline`模式 +- 使用`offline`模式 + 下一步就是开始`offline`模式,因为我们经常会在`gradle`中使用一下依赖库时用`+`这样的话就能保证你的依赖库是最新的版本,但是这样在每次`build`的时候都会去检查是不是最新的版本,所以就会耗时。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_offline.png?raw=true) + 在开发过程中是不建议使用动态版本的,在`Studio`中使用动态版本的`gradle`中间中使用`ALT+ENTER`键进行修复。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_daymaic_version_tip.png?raw=true) + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_dymaic_version_fix.png?raw=true) + 详细有关为什么不要使用动态版本的介绍,请参考[Don't use dynamic versions for your dependencies](http://blog.danlew.net/2015/09/09/dont-use-dynamic-versions-for-your-dependencies/) + - 增加内存使用`SSD` 首先是增大内存, `Mac`中在`Applications`中找到`Sutio`然后右键显示包内容`Contents/bin/studio.vmoptions`。 打开该文件后修改就可以了,我是的是: @@ -73,7 +79,8 @@ AndroidStudio提高Build速度 - Xmx 是能使用的最大堆内存。 -- 使用`Instant Run` +- 使用`Instant Run` + `Instantt Run`放在这里说可能不合适,但是用他确实能大大的减少运行时间。 如果还不了解的话可以参考[Instant Run](http://tools.android.com/tech-docs/instant-run) ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_instantrun.png?raw=true) From 58f2ae88f8cc65b805f14985675dfc316854f61d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 3 Jun 2016 19:07:07 +0800 Subject: [PATCH 027/373] update improve gradle build speed --- ...44\271\211View\350\257\246\350\247\243.md" | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" new file mode 100644 index 00000000..42329752 --- /dev/null +++ "b/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" @@ -0,0 +1,400 @@ +自定义View详解 +=== + +虽然之前也分析过[View回执过程](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/View%E7%BB%98%E5%88%B6%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.md),但是如果让我自己集成`ViewGroup`然后自己重新`onMeasure,onLayout,onDraw`方法自定义`View`我还是会头疼。今天索性来系统的学习下。 + +###onMeasure + +```java +/** + *

+ * Measure the view and its content to determine the measured width and the + * measured height. This method is invoked by {@link #measure(int, int)} and + * should be overridden by subclasses to provide accurate and efficient + * measurement of their contents. + *

+ * + *

+ * CONTRACT: When overriding this method, you + * must call {@link #setMeasuredDimension(int, int)} to store the + * measured width and height of this view. Failure to do so will trigger an + * IllegalStateException, thrown by + * {@link #measure(int, int)}. Calling the superclass' + * {@link #onMeasure(int, int)} is a valid use. + *

+ * + *

+ * The base class implementation of measure defaults to the background size, + * unless a larger size is allowed by the MeasureSpec. Subclasses should + * override {@link #onMeasure(int, int)} to provide better measurements of + * their content. + *

+ * + *

+ * If this method is overridden, it is the subclass's responsibility to make + * sure the measured height and width are at least the view's minimum height + * and width ({@link #getSuggestedMinimumHeight()} and + * {@link #getSuggestedMinimumWidth()}). + *

+ * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent. + * The requirements are encoded with + * {@link android.view.View.MeasureSpec}. + * @param heightMeasureSpec vertical space requirements as imposed by the parent. + * The requirements are encoded with + * {@link android.view.View.MeasureSpec}. + * + * @see #getMeasuredWidth() + * @see #getMeasuredHeight() + * @see #setMeasuredDimension(int, int) + * @see #getSuggestedMinimumHeight() + * @see #getSuggestedMinimumWidth() + * @see android.view.View.MeasureSpec#getMode(int) + * @see android.view.View.MeasureSpec#getSize(int) + */ +protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); + } +``` + +注释说的非常清楚。但是我还是要强调一下这两个参数:`widthMeasureSpec`和`heightMeasureSpec`这两个int类型的参数,看名字应该知道是跟宽和高有关系,但它们其实不是宽和高,而是由宽、高和各自方向上对应的模式来合成的一个值:其中,在int类型的32位二进制位中,31-30这两位表示模式,0~29这三十位表示宽和高的实际值.其中模式一共有三种,被定义在Android中的View类的一个内部类中:View.MeasureSpec: + +```java +android.view +public static class View.MeasureSpec +extends Object +A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode. There are three possible modes: +UNSPECIFIED +The parent has not imposed any constraint on the child. It can be whatever size it wants. +EXACTLY +The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be. +AT_MOST +The child can be as large as it wants up to the specified size. +MeasureSpecs are implemented as ints to reduce object allocation. This class is provided to pack and unpack the tuple into the int. +``` + +- MeasureSpec.UNSPECIFIED The parent has not imposed any constraint on the child. It can be whatever size it wants. 这种情况比较少,一般用不到。标示父控件没有给子View任何显示- - - -对应的二进制表示: 00 +- MeasureSpec.EXACTLY The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be. +理解成MATCH_PARENT或者在布局中指定了宽高值,如layout:width=’50dp’. - - - - 对应的二进制表示:01 +- MeasureSpec.AT_MOST The child can be as large as it wants up to the specified size.理解成WRAP_CONTENT,这是的值是父View可以允许的最大的值,只要不超过这个值都可以。- - - - 对应的二进制表示:10 + + +那具体`MeasureSpec`是怎么把宽和高的实际值以及模式组合起来变成一个int类型的值呢? 这部分是在`MeasureSpce.makeMeasureSpec()`方法中处理的: +```java +public static int makeMeasureSpec(int size, int mode) { + if (sUseBrokenMakeMeasureSpec) { + return size + mode; + } else { + return (size & ~MODE_MASK) | (mode & MODE_MASK); + } + } +``` +那我们如何从MeasureSpec值中提取模式和大小呢?该方法内部是采用位移计算. +```java +/** + * Extracts the mode from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the mode from + * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, + * {@link android.view.View.MeasureSpec#AT_MOST} or + * {@link android.view.View.MeasureSpec#EXACTLY} + */ + public static int getMode(int measureSpec) { + return (measureSpec & MODE_MASK); + } + + /** + * Extracts the size from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the size from + * @return the size in pixels defined in the supplied measure specification + */ + public static int getSize(int measureSpec) { + return (measureSpec & ~MODE_MASK); + } +``` + + +###onLayout + +为了能合理的去绘制定义`View`,你需要制定它的大小。复杂的自定义`View`通常需要根据屏幕的样式和大小来进行复杂的布局计算。你不应该假设你的屏幕上的`View`的大小。即使只有一个应用使用你的自定义`View`,也需要处理不同的屏幕尺寸、屏幕密度和横屏以及竖屏下的多种比率等。 + +虽然`View`有很多处理测量的方法,但他们中的大部分都不需要被重写。如果你的`View`不需要特别的控制它的大小,你只需要重写一个方法:`onSizeChanged()`。 + +`onSizeChanged()`方法会在你的`View`第一次指定大小后调用,在因某些原因改变大小后会再次调用。在上面`PieChart`的例子中,`onSizeChanged()`方法就是它需要重新计算表格样式和大小以及其他元素的地方。 +下面就是`PieChart.onSizeChanged()`方法的内容: + +```java +// Account for padding + float xpad = (float)(getPaddingLeft() + getPaddingRight()); + float ypad = (float)(getPaddingTop() + getPaddingBottom()); + + // Account for the label + if (mShowText) xpad += mTextWidth; + + float ww = (float)w - xpad; + float hh = (float)h - ypad; + + // Figure out how big we can make the pie. + float diameter = Math.min(ww, hh); +``` + + +###onDraw + +自定义`View`最重要的就是展现样式。 + +#####重写`onDraw()`方法 + +绘制自定义`View`最重要的步骤就是重写`onDraw()`方法。`onDraw()`方法的参数是`Canvas`对象。可以用它来绘制自身。`Canvas`类定义了绘制文字、线、位图和很多其他图形的方法。你可以在`onDraw()`方法中使用这些方法来指定`UI`. + +在使用任何绘制方法之前,你都必须要创建一个`Paint`对象。 + +#####创建绘制的对象 + +`android.graphics`框架将绘制分为两步: + +- 绘制什么,由`Canvas`处理。 +- 怎么去绘制,由`Paint`处理。 + +例如,`Canvas`提供了一个画一条线的方法,而`Paint`提供了指定这条线的颜色的方法。`Canvas`提供了绘制长方形的方法,而`Paint`提供了是用颜色填充整个长方形还是空着的方法。简单的说,`Canvas`指定了你想在屏幕上绘制的形状,而`Paint`指定了你要绘制的形状的颜色、样式、字体和样式等等。 + +所以,在你`draw`任何东西之前,你都需要创建一个或者多个`Paint`对象。下面的`PieChart`例子就是在构造函数中调用的`init`方法: + +```java +private void init() { + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(mTextColor); + if (mTextHeight == 0) { + mTextHeight = mTextPaint.getTextSize(); + } else { + mTextPaint.setTextSize(mTextHeight); + } + + mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPiePaint.setStyle(Paint.Style.FILL); + mPiePaint.setTextSize(mTextHeight); + + mShadowPaint = new Paint(0); + mShadowPaint.setColor(0xff101010); + mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); + + ... +``` + +下面是`PieChart`完整的`onDraw()`方法: +```java +protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the shadow + canvas.drawOval( + mShadowBounds, + mShadowPaint + ); + + // Draw the label text + canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint); + + // Draw the pie slices + for (int i = 0; i < mData.size(); ++i) { + Item it = mData.get(i); + mPiePaint.setShader(it.mShader); + canvas.drawArc(mBounds, + 360 - it.mEndAngle, + it.mEndAngle - it.mStartAngle, + true, mPiePaint); + } + + // Draw the pointer + canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint); + canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint); +} +``` + + +下面是一张`View`绘制过程中框架调用的一些标准方法概要图: + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/custom_view_methods.png?raw=true) + +下面来几个例子: + +自定义开关: + +```java +public class ToogleView extends View { + private int mSlideMarginLeft = 0; + private Bitmap backgroundBitmap; + private Bitmap slideButton; + + + public ToogleView(Context context) { + super(context); + init(context); + } + + public ToogleView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ToogleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + backgroundBitmap = BitmapFactory.decodeResource(getResources(), + R.drawable.toogle_bg); + slideButton = BitmapFactory.decodeResource(getResources(), + R.drawable.toogle_slide); + this.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mSlideMarginLeft == 0) { + mSlideMarginLeft = backgroundBitmap.getWidth() - slideButton.getWidth(); + } else { + mSlideMarginLeft = 0; + } + invalidate(); + } + }); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + Paint paint = new Paint(); + paint.setAntiAlias(true); + // 先画背景图 + canvas.drawBitmap(backgroundBitmap, 0, 0, paint); + // 再画滑块,用mSlideMarginLeft来控制滑块距离左边的距离。 + canvas.drawBitmap(slideButton, mSlideMarginLeft, 0, paint); + } +``` + +```xml + + + + +``` + + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toogle_1.png?raw=true) +很明显显示的不对,因为高设置为`warp_content`了,但是界面显示的确实整个屏幕,而且`paddingLeft`也没生效,那该怎么做呢? 当然是重写`onMeasure()` 方法: +```java +public class ToogleView extends View { + private int mSlideMarginLeft = 0; + private Bitmap backgroundBitmap; + private Bitmap slideButton; + + + public ToogleView(Context context) { + super(context); + init(context); + } + + public ToogleView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ToogleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + backgroundBitmap = BitmapFactory.decodeResource(getResources(), + R.drawable.toogle_bg); + slideButton = BitmapFactory.decodeResource(getResources(), + R.drawable.toogle_slide); + this.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mSlideMarginLeft == 0) { + mSlideMarginLeft = backgroundBitmap.getWidth() - slideButton.getWidth(); + } else { + mSlideMarginLeft = 0; + } + invalidate(); + } + }); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measureWidth = MeasureSpec.getSize(widthMeasureSpec); + int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); + + int measureHeight = MeasureSpec.getSize(heightMeasureSpec); + int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); + int width; + int height; + if (MeasureSpec.EXACTLY == measureWidthMode) { + width = measureWidth; + } else { + width = backgroundBitmap.getWidth(); + } + + if (MeasureSpec.EXACTLY == measureHeightMode) { + height = measureHeight; + } else { + height = backgroundBitmap.getHeight(); + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + Paint paint = new Paint(); + paint.setAntiAlias(true); + canvas.drawBitmap(backgroundBitmap, getPaddingLeft(), 0, paint); + canvas.drawBitmap(slideButton, mSlideMarginLeft + getPaddingLeft(), 0, paint); + } + +} + +``` +这样就可以了。简单的说明一下,就是如果当前的模式是`EXACTLY`那就把父`View`传递进来的宽高设置进来,如果是`AT_MOST`或者`UNSPECIFIED`的话就使用背景图片的宽高。 + + + + + + + + +http://blog.csdn.net/cyp331203/article/details/40736027 + +http://blog.csdn.net/lmj623565791/article/details/24529807 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 32b306089604307988a8408e8387e9c56b2c679e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 7 Jun 2016 12:25:56 +0800 Subject: [PATCH 028/373] update customize view --- ...44\271\211View\350\257\246\350\247\243.md" | 156 ++++++++++++++---- 1 file changed, 122 insertions(+), 34 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" index 42329752..456705de 100644 --- "a/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" +++ "b/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" @@ -93,26 +93,26 @@ public static int makeMeasureSpec(int size, int mode) { 那我们如何从MeasureSpec值中提取模式和大小呢?该方法内部是采用位移计算. ```java /** - * Extracts the mode from the supplied measure specification. - * - * @param measureSpec the measure specification to extract the mode from - * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, - * {@link android.view.View.MeasureSpec#AT_MOST} or - * {@link android.view.View.MeasureSpec#EXACTLY} - */ - public static int getMode(int measureSpec) { - return (measureSpec & MODE_MASK); - } + * Extracts the mode from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the mode from + * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, + * {@link android.view.View.MeasureSpec#AT_MOST} or + * {@link android.view.View.MeasureSpec#EXACTLY} + */ +public static int getMode(int measureSpec) { + return (measureSpec & MODE_MASK); +} - /** - * Extracts the size from the supplied measure specification. - * - * @param measureSpec the measure specification to extract the size from - * @return the size in pixels defined in the supplied measure specification - */ - public static int getSize(int measureSpec) { - return (measureSpec & ~MODE_MASK); - } +/** + * Extracts the size from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the size from + * @return the size in pixels defined in the supplied measure specification + */ +public static int getSize(int measureSpec) { + return (measureSpec & ~MODE_MASK); +} ``` @@ -127,17 +127,17 @@ public static int makeMeasureSpec(int size, int mode) { ```java // Account for padding - float xpad = (float)(getPaddingLeft() + getPaddingRight()); - float ypad = (float)(getPaddingTop() + getPaddingBottom()); +float xpad = (float)(getPaddingLeft() + getPaddingRight()); +float ypad = (float)(getPaddingTop() + getPaddingBottom()); - // Account for the label - if (mShowText) xpad += mTextWidth; +// Account for the label +if (mShowText) xpad += mTextWidth; - float ww = (float)w - xpad; - float hh = (float)h - ypad; +float ww = (float)w - xpad; +float hh = (float)h - ypad; - // Figure out how big we can make the pie. - float diameter = Math.min(ww, hh); +// Figure out how big we can make the pie. +float diameter = Math.min(ww, hh); ``` @@ -158,6 +158,48 @@ public static int makeMeasureSpec(int size, int mode) { - 绘制什么,由`Canvas`处理。 - 怎么去绘制,由`Paint`处理。 + +#####`Canvas` + +> The Canvas class holds the "draw" calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, + a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), +and a paint (to describe the colors and styles for the drawing). + + +- `Canvas()`:创建一个空的画布,可以使用`setBitmap()`方法来设置绘制的具体画布; +- `Canvas(Bitmap bitmap)`:以`bitmap`对象创建一个画布,则将内容都绘制在`bitmap`上,`bitmap`不得为`null`; +- `canvas.drawRect(RectF,Paint)`方法用于画矩形,第一个参数为图形显示区域,第二个参数为画笔,设置好图形显示区域`Rect`和画笔`Paint`后,即可画图; +- `canvas.drawRoundRect(RectF, float, float, Paint)`方法用于画圆角矩形,第一个参数为图形显示区域,第二个参数和第三个参数分别是水平圆角半径和垂直圆角半径。 +- `canvas.drawLine(startX, startY, stopX, stopY, paint)`:前四个参数的类型均为`float`,最后一个参数类型为`Paint`。表示用画笔`paint`从点`(startX,startY)`到点`(stopX,stopY)`画一条直线; +- `canvas.drawLines (float[] pts, Paint paint)``pts`:是点的集合,大家下面可以看到,这里不是形成连接线,而是每两个点形成一条直线,`pts`的组织方式为`{x1,y1,x2,y2,x3,y3,……}`,例如`float []pts={10,10,100,100,200,200,400,400};`就是有四个点:(10,10)、(100,100),(200,200),(400,400)),两两连成一条直线; +- `canvas.drawArc(oval, startAngle, sweepAngle, useCenter, paint)`:第一个参数`oval`为`RectF`类型,即圆弧显示区域,`startAngle`和`sweepAngle`均为`float`类型,分别表示圆弧起始角度和圆弧度数,3点钟方向为0度,`useCenter`设置是否显示圆心,`boolean`类型,`paint`为画笔; +- `canvas.drawCircle(float,float, float, Paint)`方法用于画圆,前两个参数代表圆心坐标,第三个参数为圆半径,第四个参数是画笔; +- `canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)` 位图,参数一就是我们常规的`Bitmap`对象,参数二是源区域(这里是`bitmap`),参数三是目标区域(应该在`canvas`的位置和大小),参数四是`Paint`画刷对象,因为用到了缩放和拉伸的可能,当原始`Rect`不等于目标`Rect`时性能将会有大幅损失。 +- `canvas.drawText(String text, float x, floaty, Paint paint)`渲染文本,`Canvas`类除了上 +面的还可以描绘文字,参数一是`String`类型的文本,参数二`x`轴,参数三`y`轴,参数四是`Paint`对象。 +- `canvas.drawPath (Path path, Paint paint)`,根据`Path`去画. + ```java + Path path = new Path(); + path.moveTo(10, 10); //设定起始点 + path.lineTo(10, 100);//第一条直线的终点,也是第二条直线的起点 + path.lineTo(300, 100);//画第二条直线 + path.lineTo(500, 100);//第三条直线 + path.close();//闭环 + canvas.drawPath(path, paint); + ``` + +#####`Paint` + +- `setARGB(int a, int r, int g, int b)` 设置`Paint`对象颜色,参数一为`alpha`透明值 +- `setAlpha(int a)` 设置`alpha`不透明度,范围为0~255 +- `setAntiAlias(boolean aa)`是否抗锯齿 +- `setColor(int color)`设置颜色 +- `setTextScaleX(float scaleX)`设置文本缩放倍数,1.0f为原始 +- `setTextSize(float textSize)`设置字体大小 +- `setUnderlineText(String underlineText)`设置下划线 + + + 例如,`Canvas`提供了一个画一条线的方法,而`Paint`提供了指定这条线的颜色的方法。`Canvas`提供了绘制长方形的方法,而`Paint`提供了是用颜色填充整个长方形还是空着的方法。简单的说,`Canvas`指定了你想在屏幕上绘制的形状,而`Paint`指定了你要绘制的形状的颜色、样式、字体和样式等等。 所以,在你`draw`任何东西之前,你都需要创建一个或者多个`Paint`对象。下面的`PieChart`例子就是在构造函数中调用的`init`方法: @@ -276,9 +318,9 @@ public class ToogleView extends View { super.onDraw(canvas); Paint paint = new Paint(); paint.setAntiAlias(true); - // 先画背景图 + // 先画背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); - // 再画滑块,用mSlideMarginLeft来控制滑块距离左边的距离。 + // 再画滑块,用mSlideMarginLeft来控制滑块距离左边的距离。 canvas.drawBitmap(slideButton, mSlideMarginLeft, 0, paint); } ``` @@ -297,7 +339,7 @@ public class ToogleView extends View { ``` - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toogle_1.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toogle_1.png?raw=true) 很明显显示的不对,因为高设置为`warp_content`了,但是界面显示的确实整个屏幕,而且`paddingLeft`也没生效,那该怎么做呢? 当然是重写`onMeasure()` 方法: ```java public class ToogleView extends View { @@ -383,18 +425,64 @@ public class ToogleView extends View { 这样就可以了。简单的说明一下,就是如果当前的模式是`EXACTLY`那就把父`View`传递进来的宽高设置进来,如果是`AT_MOST`或者`UNSPECIFIED`的话就使用背景图片的宽高。 +最后再来一个自定义`ViewGroup`的例子: +之前的引导页面都是通过类似`ViewPager`这种方法左右滑动,现在想让他上下滑动,该怎么弄呢? +```java +public class VerticalLayout extends ViewGroup { + public VerticalLayout(Context context) { + super(context); + } + public VerticalLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public VerticalLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + } +} +``` +继承`ViewGroup`必须要重写`onLayout`方法。其实这也很好理解,因为每个`ViewGroup`的排列方式不一样,所以让子类来自己实现是最好的。 +当然畜类重写`onLayout`之外,也要重写`onMeasure`。 +代码如下,滑动手势处理的部分就不贴了。 +```java + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measureSpec = MeasureSpec.makeMeasureSpec(mScreenHeight + * getChildCount(), MeasureSpec.getMode(heightMeasureSpec)); + super.onMeasure(widthMeasureSpec, measureSpec); + measureChildren(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // 就像猴子捞月一样,让他们一个个的从上往下排就好了 + if (changed) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + child.layout(l, i * mScreenHeight, r, (i + 1) + * mScreenHeight); + } + } + } + } +``` - -http://blog.csdn.net/cyp331203/article/details/40736027 -http://blog.csdn.net/lmj623565791/article/details/24529807 +参考部分: +- http://blog.csdn.net/cyp331203/article/details/40736027 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! I \ No newline at end of file From c32bb5331c88884fbd3c0525a3a837fd6e6a6514 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 7 Jun 2016 19:16:40 +0800 Subject: [PATCH 029/373] add percent support file --- ...71\346\241\210\350\257\246\350\247\243.md" | 560 ++++++++++++++++++ ...17\345\271\225\351\200\202\351\205\215.md" | 25 +- 2 files changed, 576 insertions(+), 9 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" new file mode 100644 index 00000000..9718775d --- /dev/null +++ "b/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" @@ -0,0 +1,560 @@ +屏幕适配之百分比方案详解 +=== + +`Android`设备碎片化十分严重,在开发过程中的适配工作也非常很繁琐,有关屏幕适配的介绍请看之前的文章[屏幕适配](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%9F%BA%E7%A1%80/%E5%B1%8F%E5%B9%95%E9%80%82%E9%85%8D.md)。 + +最近看到`DrawerLayout`,`support v4`中提供的类,想到对`google`提供的这些支持库,自己一点都不熟悉,想着看看`Google`提供的支持库都有什么内容。结果看着看着在最后忽然看到了`Percent Support Library`。寻思怎么还百分比呢?仔细一看介绍,我擦,真是太有用了。 +> ###Percent Support Library +> The Percent package provides APIs to support adding and managing percentage based dimensions in your app. + +> The Percent Support library adds support for the PercentLayoutHelper.PercentLayoutParams interface and various classes, such as PercentFrameLayout and PercentRelativeLayout. + +> After you download the Android Support Libraries, this library is located in the /extras/android/support/percent directory. For more information on how to set up your project, follow the instructions in Adding libraries with resources. + +> The Gradle build script dependency identifier for this library is as follows: +> `com.android.support:percent:23.3.0` + +看到了吗? 说提供了`PercentFrameLayout`和`PercentRelativeLayout`来支持百分比了。这样不就完美的解决了适配的问题嘛。啥也不说了,立马配置`cradle`来瞧瞧。 + +> Subclass of FrameLayout that supports percentage based dimensions and margins. You can specify dimension or a margin of child by using attributes with "Percent" suffix. + +上代码: +```xml + + + +``` +效果如下: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/percent_frame.png?raw=true) +完美. + +支持的属性: + +- `layout_widthPercent` +- `layout_heightPercent` +- `layout_marginPercent` +- `layout_marginLeftPercent` +- `layout_marginTopPercent` +- `layout_marginRightPercent` +- `layout_marginBottomPercent` +- `layout_marginStartPercent` +- `layout_marginEndPercent` +- `layout_aspectRatio` + +> It is not necessary to specify layout_width/height if you specify layout_widthPercent. However, if you want the view to be able to take up more space than what percentage value permits, you can add layout_width/height="wrap_content". In that case if the percentage size is too small for the View's content, it will be resized using wrap_content rule. + +如果指定了`layout_widthPercent`就不用指定`layout_width/height`属性了。(`Studio`可能会提示错误,设置忽略就好)。然而,如果你想要该`View`能够占用比设置的百分比值更大的空间时,你可以指定`layout_widht/height=“wrap_content”`。在这种情况下,如果设置的百分比值在显示内容时太小时,将会使用`wrap_content`的值重新计算。 + +就是这个意思:如果指定的百分比太小怕显示不开的话,也可以给它指定`wrap_content`属性,这样当显示不开的时候就会使用`wrap_content`的值。 +如下: +```xml + + + +``` + +效果: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/percent_wrap.png?raw=true) + +加入`wrap_content`后一点效果也没有啊,还是显示不全啊,还没加`wrap_content`一样,- -! + + +你也可以通过只设置`width`或者`height`和`layout_aspectRatio`这种比例值的方式来让第另一个值自动计算。例如,如果你想要使用`16:9`的比例,你可以使用: +```xml +android:layout_width="300dp" +app:layout_aspectRatio="178%" +``` + +这样将会在宽固定为`300dp`的基础上以16:9(1.78:1)来计算高度。 + +`PercentRelativeLayout`的使用都是一样的,这里就不贴了。 + +那它是怎么实现的呢?其实就是内部给通过属性换算,把本布局的宽高和百分比去计算每个`view`的大小和位置。但是还有为什么上面设置的`wrap_content`无效呢?是我理解错了吗? + +带着这两个疑问我们来看下源码: +```java +public class PercentFrameLayout extends FrameLayout { + private final PercentLayoutHelper mHelper = new PercentLayoutHelper(this); + + public PercentFrameLayout(Context context) { + super(context); + } + + public PercentFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PercentFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + // 从名字上就能看出来下面就是处理如果指定百分比过小不足显示内容时,就去使用`wrap_content`的逻辑 + if (mHelper.handleMeasuredStateTooSmall()) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mHelper.restoreOriginalParams(); + } + + public static class LayoutParams extends FrameLayout.LayoutParams + implements PercentLayoutHelper.PercentLayoutParams { + + private PercentLayoutHelper.PercentLayoutInfo mPercentLayoutInfo; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + // PercentLayoutInfo中去解析自定义属性的值 + mPercentLayoutInfo = PercentLayoutHelper.getPercentLayoutInfo(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + super(width, height, gravity); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(FrameLayout.LayoutParams source) { + super((MarginLayoutParams) source); + gravity = source.gravity; + } + + public LayoutParams(LayoutParams source) { + this((FrameLayout.LayoutParams) source); + mPercentLayoutInfo = source.mPercentLayoutInfo; + } + + @Override + public PercentLayoutHelper.PercentLayoutInfo getPercentLayoutInfo() { + if (mPercentLayoutInfo == null) { + mPercentLayoutInfo = new PercentLayoutHelper.PercentLayoutInfo(); + } + + return mPercentLayoutInfo; + } + + @Override + protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { + PercentLayoutHelper.fetchWidthAndHeight(this, a, widthAttr, heightAttr); + } + } +} +``` +源码不长,主要是三个部分: + +- 重写`onMeasure`,根据百分比转换成对应的尺寸 +- 重写`onLayout` +- 自定义`LayoutParams`,在`FrameLayout.LayoutParams`的基础上多实现了一个接口。包含了`PercentLayoutHelper.PercentLayoutInfo `属性,而文档中对`PercentLayoutInfo`的介绍是`Container for information about percentage dimensions and margins. It acts as an extension for LayoutParams.` + + +我们也先一步步的来分析,首先看下`mHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec);`: +```java +/** + * Iterates over children and changes their width and height to one calculated from percentage + * values. + * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup. + * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup. + */ +public void adjustChildren(int widthMeasureSpec, int heightMeasureSpec) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "adjustChildren: " + mHost + " widthMeasureSpec: " + + View.MeasureSpec.toString(widthMeasureSpec) + " heightMeasureSpec: " + + View.MeasureSpec.toString(heightMeasureSpec)); + } + + int widthHint = View.MeasureSpec.getSize(widthMeasureSpec); + int heightHint = View.MeasureSpec.getSize(heightMeasureSpec); + // mHost是由构造函数传递过来的,就是PercentFrameLayout自身。 + for (int i = 0, N = mHost.getChildCount(); i < N; i++) { + View view = mHost.getChildAt(i); + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should adjust " + view + " " + params); + } + if (params instanceof PercentLayoutParams) { + // 通过PercentLayoutParams. getPercentLayoutInfo()方法得到PercentLayoutInfo,至于PercentLayoutInfo类在上面也说过了。 + PercentLayoutInfo info = + ((PercentLayoutParams) params).getPercentLayoutInfo(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "using " + info); + } + if (info != null) { + if (params instanceof ViewGroup.MarginLayoutParams) { + // PercentFrameLayout.LayoutParams继承自FrameLayout.LayoutParams,而FrameLayout.LayoutParams又继承自ViewGroup.MarginLayoutParams + info.fillMarginLayoutParams(view, (ViewGroup.MarginLayoutParams) params, + widthHint, heightHint); + } else { + info.fillLayoutParams(params, widthHint, heightHint); + } + } + } + } + } +``` +所以上面代码的意思就是通过对每个子`View`调用`getLayoutParams`方法,然后从该`LayoutParams`中获取到它的`PercentLayoutInfo`属性,然后再调用`PercentLayoutInfo.fillMarginLayoutParams()`方法。 + +那我们继续看一下`PercentLayoutInfo.fillMarginLayoutParams()`方法的实现,该方法是根据百分比去设置尺寸和margin: +```java +/** + * Fills {@code ViewGroup.MarginLayoutParams} dimensions and margins based on percentage + * values. + */ + public void fillMarginLayoutParams(View view, ViewGroup.MarginLayoutParams params, + int widthHint, int heightHint) { + // 根据百分比值设置View的尺寸 + fillLayoutParams(params, widthHint, heightHint); + + // 从注释中就能知道,fillLayoutParams是计算尺寸,那下面这部分就是处理margin了 + // mPreservedParams来记录原始的margin值 + // Preserve the original margins, so we can restore them after the measure step. + mPreservedParams.leftMargin = params.leftMargin; + mPreservedParams.topMargin = params.topMargin; + mPreservedParams.rightMargin = params.rightMargin; + mPreservedParams.bottomMargin = params.bottomMargin; + MarginLayoutParamsCompat.setMarginStart(mPreservedParams, + MarginLayoutParamsCompat.getMarginStart(params)); + MarginLayoutParamsCompat.setMarginEnd(mPreservedParams, + MarginLayoutParamsCompat.getMarginEnd(params)); + + if (leftMarginPercent >= 0) { + params.leftMargin = (int) (widthHint * leftMarginPercent); + } + if (topMarginPercent >= 0) { + params.topMargin = (int) (heightHint * topMarginPercent); + } + if (rightMarginPercent >= 0) { + params.rightMargin = (int) (widthHint * rightMarginPercent); + } + if (bottomMarginPercent >= 0) { + params.bottomMargin = (int) (heightHint * bottomMarginPercent); + } + boolean shouldResolveLayoutDirection = false; + if (startMarginPercent >= 0) { + MarginLayoutParamsCompat.setMarginStart(params, + (int) (widthHint * startMarginPercent)); + shouldResolveLayoutDirection = true; + } + if (endMarginPercent >= 0) { + MarginLayoutParamsCompat.setMarginEnd(params, + (int) (widthHint * endMarginPercent)); + shouldResolveLayoutDirection = true; + } + if (shouldResolveLayoutDirection && (view != null)) { + // Force the resolve pass so that start / end margins are propagated to the + // matching left / right fields + MarginLayoutParamsCompat.resolveLayoutDirection(params, + ViewCompat.getLayoutDirection(view)); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "after fillMarginLayoutParams: (" + params.width + ", " + params.height + + ")"); + } + } +``` + +再看一下`PercentLayoutInfo.fillLayoutParams()`的实现,该方法是通过百分比设置`View`的尺寸: + +```java + /** + * Fills {@code ViewGroup.LayoutParams} dimensions based on percentage values. + */ + public void fillLayoutParams(ViewGroup.LayoutParams params, int widthHint, + int heightHint) { + // mPreservedParams来记录原始的宽高值 + // Preserve the original layout params, so we can restore them after the measure step. + mPreservedParams.width = params.width; + mPreservedParams.height = params.height; + + // We assume that width/height set to 0 means that value was unset. This might not + // necessarily be true, as the user might explicitly set it to 0. However, we use this + // information only for the aspect ratio. If the user set the aspect ratio attribute, + // it means they accept or soon discover that it will be disregarded. + final boolean widthNotSet = + (mPreservedParams.mIsWidthComputedFromAspectRatio + || mPreservedParams.width == 0) && (widthPercent < 0); + final boolean heightNotSet = + (mPreservedParams.mIsHeightComputedFromAspectRatio + || mPreservedParams.height == 0) && (heightPercent < 0); + + // 如果指定了百分比属性,就用宽高值和百分比去算出具体的宽高 + if (widthPercent >= 0) { + params.width = (int) (widthHint * widthPercent); + } + + if (heightPercent >= 0) { + params.height = (int) (heightHint * heightPercent); + } + // 如果指定了宽高的比例值,就用比例值去算出宽高,从这里能看出`aspectRatio`的优先级比百分比要高。他可以覆盖百分比之前设置的值。 + if (aspectRatio >= 0) { + if (widthNotSet) { + params.width = (int) (params.height * aspectRatio); + // Keep track that we've filled the width based on the height and aspect ratio. + mPreservedParams.mIsWidthComputedFromAspectRatio = true; + } + if (heightNotSet) { + params.height = (int) (params.width / aspectRatio); + // Keep track that we've filled the height based on the width and aspect ratio. + mPreservedParams.mIsHeightComputedFromAspectRatio = true; + } + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "after fillLayoutParams: (" + params.width + ", " + params.height + ")"); + } + } + +``` + +到这里`onMeasure`中通过百分比值设置宽高以及`margin`的部分就看完了,那我们接着看一下关于百分比值过小时对`wrap_content`的处理: +```java +if (mHelper.handleMeasuredStateTooSmall()) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); +} +``` + +看一下`PercentLayoutHelper.handleMeasuredStateTooSmall()`方法的实现: +```java +/** + * Iterates over children and checks if any of them would like to get more space than it + * received through the percentage dimension. + * + * If you are building a layout that supports percentage dimensions you are encouraged to take + * advantage of this method. The developer should be able to specify that a child should be + * remeasured by adding normal dimension attribute with {@code wrap_content} value. For example + * he might specify child's attributes as {@code app:layout_widthPercent="60%p"} and + * {@code android:layout_width="wrap_content"}. In this case if the child receives too little + * space, it will be remeasured with width set to {@code WRAP_CONTENT}. + * + * @return True if the measure phase needs to be rerun because one of the children would like + * to receive more space. + */ + public boolean handleMeasuredStateTooSmall() { + boolean needsSecondMeasure = false; + for (int i = 0, N = mHost.getChildCount(); i < N; i++) { + View view = mHost.getChildAt(i); + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should handle measured state too small " + view + " " + params); + } + if (params instanceof PercentLayoutParams) { + PercentLayoutInfo info = + ((PercentLayoutParams) params).getPercentLayoutInfo(); + if (info != null) { + // shouldHandleMeasuredWidthTooSmall方法来进行判断 + if (shouldHandleMeasuredWidthTooSmall(view, info)) { + needsSecondMeasure = true; + params.width = ViewGroup.LayoutParams.WRAP_CONTENT; + } + if (shouldHandleMeasuredHeightTooSmall(view, info)) { + needsSecondMeasure = true; + params.height = ViewGroup.LayoutParams.WRAP_CONTENT; + } + } + } + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should trigger second measure pass: " + needsSecondMeasure); + } + return needsSecondMeasure; + } +``` + +那继续看一下`shouldHandleMeasuredWidthTooSmall()`方法: +```java +private static boolean shouldHandleMeasuredWidthTooSmall(View view, PercentLayoutInfo info) { + int state = ViewCompat.getMeasuredWidthAndState(view) & ViewCompat.MEASURED_STATE_MASK; + return state == ViewCompat.MEASURED_STATE_TOO_SMALL && info.widthPercent >= 0 && + info.mPreservedParams.width == ViewGroup.LayoutParams.WRAP_CONTENT; + } +``` + +继续看`ViewCompat.getMeasuredWidthAndState()`的源码: +```java +public static int getMeasuredWidthAndState(View view) { + return IMPL.getMeasuredWidthAndState(view); + } +``` +继续往下看,这个`IMPL`是什么呢? +```java +static final ViewCompatImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 23) { + IMPL = new MarshmallowViewCompatImpl(); + } else if (version >= 21) { + IMPL = new LollipopViewCompatImpl(); + } else if (version >= 19) { + IMPL = new KitKatViewCompatImpl(); + } else if (version >= 17) { + IMPL = new JbMr1ViewCompatImpl(); + } else if (version >= 16) { + IMPL = new JBViewCompatImpl(); + } else if (version >= 15) { + IMPL = new ICSMr1ViewCompatImpl(); + } else if (version >= 14) { + IMPL = new ICSViewCompatImpl(); + } else if (version >= 11) { + IMPL = new HCViewCompatImpl(); + } else if (version >= 9) { + IMPL = new GBViewCompatImpl(); + } else if (version >= 7) { + IMPL = new EclairMr1ViewCompatImpl(); + } else { + IMPL = new BaseViewCompatImpl(); + } + } +``` +继续看,`IMPL.getMeasuredWidthAndState()`,方法其实最终就是使用的`BaseViewCompatImpl.getMeasuredWidthAndState()`方法,接着看它的的源码: +```java + @Override + public int getMeasuredWidthAndState(View view) { + return view.getMeasuredWidth(); + } +``` +最后就是调用了`getMeasuredWidth()`方法。 没毛病- -! + + +`onMeasure`的到这里就分析完了。 +那接着来看一下`onLayout`方法,`onLayout`方法直接调用了`PercentLayoutHelper.restoreOriginalParams`,: +```java +/** + * Iterates over children and restores their original dimensions that were changed for + * percentage values. Calling this method only makes sense if you previously called + * {@link PercentLayoutHelper#adjustChildren(int, int)}. + */ + public void restoreOriginalParams() { + for (int i = 0, N = mHost.getChildCount(); i < N; i++) { + View view = mHost.getChildAt(i); + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should restore " + view + " " + params); + } + if (params instanceof PercentLayoutParams) { + PercentLayoutInfo info = + ((PercentLayoutParams) params).getPercentLayoutInfo(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "using " + info); + } + if (info != null) { + if (params instanceof ViewGroup.MarginLayoutParams) { + info.restoreMarginLayoutParams((ViewGroup.MarginLayoutParams) params); + } else { + info.restoreLayoutParams(params); + } + } + } + } + } +``` + +调用了`PercentLayoutInfo.restoreMarginLayoutParams()`方法,我们看下它的源码: +```java +/** + * Restores original dimensions and margins after they were changed for percentage based + * values. Calling this method only makes sense if you previously called + * {@link PercentLayoutHelper.PercentLayoutInfo#fillMarginLayoutParams}. + */ + public void restoreMarginLayoutParams(ViewGroup.MarginLayoutParams params) { + restoreLayoutParams(params); + // mPreservedParams的值在之前ad + params.leftMargin = mPreservedParams.leftMargin; + params.topMargin = mPreservedParams.topMargin; + params.rightMargin = mPreservedParams.rightMargin; + params.bottomMargin = mPreservedParams.bottomMargin; + MarginLayoutParamsCompat.setMarginStart(params, + MarginLayoutParamsCompat.getMarginStart(mPreservedParams)); + MarginLayoutParamsCompat.setMarginEnd(params, + MarginLayoutParamsCompat.getMarginEnd(mPreservedParams)); + } + +``` +意思是说,再他们被以百分比为基础的数据更改之后恢复成原始的尺寸和margin。 +然后继续看一下`restoreLayoutParams()`: +```java +/** + * Restores original dimensions after they were changed for percentage based values. Calling + * this method only makes sense if you previously called + * {@link PercentLayoutHelper.PercentLayoutInfo#fillLayoutParams}. + */ + public void restoreLayoutParams(ViewGroup.LayoutParams params) { + if (!mPreservedParams.mIsWidthComputedFromAspectRatio) { + // Only restore the width if we didn't compute it based on the height and + // aspect ratio in the fill pass. + params.width = mPreservedParams.width; + } + if (!mPreservedParams.mIsHeightComputedFromAspectRatio) { + // Only restore the height if we didn't compute it based on the width and + // aspect ratio in the fill pass. + params.height = mPreservedParams.height; + } + + // Reset the tracking flags. + mPreservedParams.mIsWidthComputedFromAspectRatio = false; + mPreservedParams.mIsHeightComputedFromAspectRatio = false; + } +``` + +好了到这里也分析完了`onLayout`的方法,大意就是在`onMeasure`之前先将原始的宽高和`margin`都备份一下,然后在`onMeasure`中根据百分比去设置对应的宽高和`margin`,等到设置完之后在`onLayout`方法中再去将这些值恢复到之前备份的起始数据。说实话我没看明白为什么要这样做? + +通过上面的代码分析我们知道对布局影响力的优先顺序: `app:layout_aspectRatio > app:layout_heightPercent > android:layout_height`如果我们同时都设置这三个参数值的话,最终会用`app:layout_aspectRatio`的值。 + +遗留问题: + +- 为什么在`onLayout`方法中去恢复数据,这有什么作用? +- 看代码在处理如果指定的百分比过小但又指定`wrap_content`时,会重新根据`wrap_content`去重新计算的逻辑没有错,但是为什么我在上面测试的时候确没效果? + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" index 570e77cb..9d8cda0f 100644 --- "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" +++ "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" @@ -7,7 +7,7 @@ Px不同设备显示效果相同。这里的“相同”是指像素数不会变 也正是因为如此才造成了UI在小分辨率设备上被放大而失真,在大分辨率上被缩小。 #### Screen Size(屏幕尺寸) -一般所说的手机屏幕大小如1.6英寸、1.9英寸、2.2英寸,都是指的对角线的长度,而不是手机面积。我们可以根据勾股定理获取手机的宽和长,当然还有面积。单位为inch +一般所说的手机屏幕大小如5寸、5.5英寸,都是指的屏幕对角线的长度,而不是手机面积。我们可以根据勾股定理获取手机的宽和长,当然还有面积。单位为inch,1英寸=2.54厘米 #### Resolution(分辨率) 指手机屏幕垂直和水平方向上的像素个数。比如分辨率是480*320,则指设备垂直方向有480个像素点,水平方向有320个像素点。单位px * px,指的是一屏显示多少像素点。 @@ -18,7 +18,7 @@ Px不同设备显示效果相同。这里的“相同”是指像素数不会变 #### Dip(Device-independent pixel,设备独立像素) 同dp,可作长度单位,不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA 推荐使用这个,不依赖像素。 -dip和具体像素值的对应公式是dip值 =设备密度/160* pixel值,可以看出在dpi(像素密度)为160dpi的设备上1px=1dip +dip和具体像素值的对应公式是dip值 =设备密度/160* pixel值,可以看出在dpi(像素密度)为160dpi的设备上1px=1dip,在Android中,规定以160dpi为基准,1dip=1px,如果密度是320dpi,则1dip=2px,以此类推。 单位dp/dip,指的是自适应屏幕密度的像素,用于指定控件宽高。就所屏幕密度变大,1dp所对应的px也会变大。 #### Sp(ScaledPixels放大像素) @@ -144,13 +144,20 @@ QHD 960*540 1080p 1920*1080 高清 -分辨率对应DPI -HVGA mdpi -WVGA hdpi -FWVGA hdpi -QHD hdpi -720P xhdpi -1080P xxhdpi +分辨率对应DPI 像素密度范围 +HVGA mdpi 120dpi-160dpi +WVGA hdpi 160dpi-240dpi +720P xhdpi 240dpi-320dpi +1080P xxhdpi 320dpi-480dpi + xxxhdpi 480dpi-640dpi + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xhdpi.jpg?raw=true) + + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/icon_size.jpg?raw=true) + + 为了解决适配的问题有时候还会使用9patch图,这里随便说一下。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/9patch.jpg?raw=true) +`Stretchable area`代表拉伸的部分。 +`Padding box`代码内容所在区域。外面的就是`padding`部分。 --- From 426eb8e75303f38592f89ef964cb10ccc889eff7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 8 Jun 2016 18:37:36 +0800 Subject: [PATCH 030/373] add somethings you don't know with studio --- ...23\347\232\204\346\223\215\344\275\234.md" | 54 ++ .../InstantRun\350\257\246\350\247\243.md" | 15 + ...71\346\241\210\350\257\246\350\247\243.md" | 526 +++++++++--------- ...(\347\254\254\344\270\200\345\274\271).md" | 4 +- .../Android\345\212\250\347\224\273.md" | 78 +++ ...17\345\271\225\351\200\202\351\205\215.md" | 380 +++++++------ 6 files changed, 626 insertions(+), 431 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" create mode 100644 "Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" "b/Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" new file mode 100644 index 00000000..bf89c72d --- /dev/null +++ "b/Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" @@ -0,0 +1,54 @@ +Android Studio你可能不知道的操作 +=== + +今天看在`Youtube`看视频,看到`Reto Meier`在讲解`Studio`, +一查才知道他现在是`Studio`的开发人员。 +想起刚开始学`Android`时买的他写的书`Professional Android 4 Application Development`, +当时很多内容没看懂。不过看了这个视频才发现大神写代码如此之快… + +现在天天用着大神开发的工具,没有理由不去好好学习下。 + +工欲善其事必先利其器,一个好的开发工具可以让你事半功倍。`Android Studio`是一个非常好的开发工具,但是虽然都在用,你可能还是了解的不全面,今天就来说一下一些你可能不知道的功能。 + +熟练使用快捷键是非常有必要的: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/use_shortcut.gif?raw=true) + + +- 自动导入 +经常听到同事抱怨,`Studio`怎么没有`Eclipse`那种批量导包啊,那么多类要到,费劲了。其实不用一个个导的。 +使用`Command+Shift+A(Windows或Linux是Ctrl+Shift+A)`快速的查找设置命令。我们输入`auto import`后将`Add unambiguous imports on fly`选项开启就好了,很爽有木有?你的是不是也没开啊? +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/auto_import.gif?raw=true) +你可以快速打开一些设置,例如你想在`finder`中查看该文件,直接输入`find`就好了。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/action_find.png?raw=true) + +- 在自动完成代码时使用`Tab`键来替换已存在的方法和参数。 +经常如我们想要修改一个已经存在的方法时,我们移动到对象的`.`的提示区域,如果使用`Command+Space`来补充代码选择后按`enter`的话,会把之前已经存在的代码往后移动,这样还要再去删除,很不方便。但是如果我们直接使用`Tag`键,那就会直接替换当前已经存在的方法。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/tab_tip.gif?raw=true) + +- 内容选择技巧 +使用`Control+↑或↓`能在方法间移动。使用`Shift+↑或↓`来选择至上一行或者至下一行代码的代码?那么使用`option++↑或↓(Windows或Linux是alt++↑或↓)`呢?它能选择或者取消选择和你鼠标所在位置的代码区域。同时使用`option+shift++↑或↓(Windows或Linux是alt+shift++↑或↓)可以交换选中代码块与上一行的位置。这样我们就不需要剪切和粘贴了。 + +- 使用模板补全代码 +你可以在代码后加用后缀的方式补充代码块,也可以用`Command+J(Windows是Ctrl+J)`来提示。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/command_j.gif?raw=true) +对于一些更多的模式,在代码自动完成时也支持生成对应的模板,例如使用`Toast`的快捷键可以很方便的生成一个新的`toast`对象,你只需要指定文字就可以了。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toast_autocomp.gif?raw=true) +用`Command+Shift+A`然后输入`live template`然后打开对应的页面,可以看到目前已经存在的模板。当然你也可以添加新的模板。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/live_templete.png?raw=true) + +- 在`Evaluating Expressions`中为`Object`对象指定显示内容 +如果我们在`debug`的时候查看断点处的变量值或者`evaluating expressions`,你会发现`objects`会显示他们的`toString()`值。如果你的变量是`String`类型或者基础类型那不会有问题,但是大多数其他对象,这样没有什么意义。 +尤其是在集合对象时,你看的是一个`CallsName:HashValue`的列表。而为了需要看清数据,我们需要知道每个对象的内容。 +你当然可以去对每个对象类型指定索要显示的内容。在对应的对象上邮件`View as`然后创建你想要显示的内容就可以了。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/debug_eva.gif?raw=true) + +- 结构性的搜索、替换、和检查 +`Command+shift+A`然后输入`structural `后选择`search structurally`或者`replace structurally`,然后可以对应的结构性搜索的模板,完成之后所有符合该模板的代码都会提示`warning`。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/structural_search.gif?raw=true) +更有用的是可以使用快速修复来替换一些代码,例如一些废弃的代码或者你在`review`时发现的其他团队成员提交的一些普遍的错误。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" new file mode 100644 index 00000000..a593f788 --- /dev/null +++ "b/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" @@ -0,0 +1,15 @@ +InstantRun详解 +=== + +之前在写[AndroidStudio提高Build速度](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/AndroidStudio%E6%8F%90%E9%AB%98Build%E9%80%9F%E5%BA%A6.md)这篇文章的时候写到,想要快,就用`Instant Run`。最近有朋友发来邮件讨论它的原理,最近项目不忙,索性就来系统的学习下。 + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" index 9718775d..b95ccb7c 100644 --- "a/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" +++ "b/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" @@ -79,7 +79,7 @@ 效果: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/percent_wrap.png?raw=true) -加入`wrap_content`后一点效果也没有啊,还是显示不全啊,还没加`wrap_content`一样,- -! +加入`wrap_content`后一点效果也没有啊,还是显示不全啊,和没加`wrap_content`一样,- -! 你也可以通过只设置`width`或者`height`和`layout_aspectRatio`这种比例值的方式来让第另一个值自动计算。例如,如果你想要使用`16:9`的比例,你可以使用: @@ -200,159 +200,159 @@ public class PercentFrameLayout extends FrameLayout { 我们也先一步步的来分析,首先看下`mHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec);`: ```java /** - * Iterates over children and changes their width and height to one calculated from percentage - * values. - * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup. - * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup. - */ + * Iterates over children and changes their width and height to one calculated from percentage + * values. + * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup. + * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup. + */ public void adjustChildren(int widthMeasureSpec, int heightMeasureSpec) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "adjustChildren: " + mHost + " widthMeasureSpec: " + + View.MeasureSpec.toString(widthMeasureSpec) + " heightMeasureSpec: " + + View.MeasureSpec.toString(heightMeasureSpec)); + } + + int widthHint = View.MeasureSpec.getSize(widthMeasureSpec); + int heightHint = View.MeasureSpec.getSize(heightMeasureSpec); + // mHost是由构造函数传递过来的,就是PercentFrameLayout自身。 + for (int i = 0, N = mHost.getChildCount(); i < N; i++) { + View view = mHost.getChildAt(i); + ViewGroup.LayoutParams params = view.getLayoutParams(); if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "adjustChildren: " + mHost + " widthMeasureSpec: " - + View.MeasureSpec.toString(widthMeasureSpec) + " heightMeasureSpec: " - + View.MeasureSpec.toString(heightMeasureSpec)); + Log.d(TAG, "should adjust " + view + " " + params); } - - int widthHint = View.MeasureSpec.getSize(widthMeasureSpec); - int heightHint = View.MeasureSpec.getSize(heightMeasureSpec); - // mHost是由构造函数传递过来的,就是PercentFrameLayout自身。 - for (int i = 0, N = mHost.getChildCount(); i < N; i++) { - View view = mHost.getChildAt(i); - ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof PercentLayoutParams) { + // 通过PercentLayoutParams. getPercentLayoutInfo()方法得到PercentLayoutInfo,至于PercentLayoutInfo类在上面也说过了。 + PercentLayoutInfo info = + ((PercentLayoutParams) params).getPercentLayoutInfo(); if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "should adjust " + view + " " + params); + Log.d(TAG, "using " + info); } - if (params instanceof PercentLayoutParams) { - // 通过PercentLayoutParams. getPercentLayoutInfo()方法得到PercentLayoutInfo,至于PercentLayoutInfo类在上面也说过了。 - PercentLayoutInfo info = - ((PercentLayoutParams) params).getPercentLayoutInfo(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "using " + info); - } - if (info != null) { - if (params instanceof ViewGroup.MarginLayoutParams) { - // PercentFrameLayout.LayoutParams继承自FrameLayout.LayoutParams,而FrameLayout.LayoutParams又继承自ViewGroup.MarginLayoutParams - info.fillMarginLayoutParams(view, (ViewGroup.MarginLayoutParams) params, - widthHint, heightHint); - } else { - info.fillLayoutParams(params, widthHint, heightHint); - } + if (info != null) { + if (params instanceof ViewGroup.MarginLayoutParams) { + // PercentFrameLayout.LayoutParams继承自FrameLayout.LayoutParams,而FrameLayout.LayoutParams又继承自ViewGroup.MarginLayoutParams + info.fillMarginLayoutParams(view, (ViewGroup.MarginLayoutParams) params, + widthHint, heightHint); + } else { + info.fillLayoutParams(params, widthHint, heightHint); } } } } +} ``` 所以上面代码的意思就是通过对每个子`View`调用`getLayoutParams`方法,然后从该`LayoutParams`中获取到它的`PercentLayoutInfo`属性,然后再调用`PercentLayoutInfo.fillMarginLayoutParams()`方法。 那我们继续看一下`PercentLayoutInfo.fillMarginLayoutParams()`方法的实现,该方法是根据百分比去设置尺寸和margin: ```java /** - * Fills {@code ViewGroup.MarginLayoutParams} dimensions and margins based on percentage - * values. - */ - public void fillMarginLayoutParams(View view, ViewGroup.MarginLayoutParams params, - int widthHint, int heightHint) { - // 根据百分比值设置View的尺寸 - fillLayoutParams(params, widthHint, heightHint); - - // 从注释中就能知道,fillLayoutParams是计算尺寸,那下面这部分就是处理margin了 - // mPreservedParams来记录原始的margin值 - // Preserve the original margins, so we can restore them after the measure step. - mPreservedParams.leftMargin = params.leftMargin; - mPreservedParams.topMargin = params.topMargin; - mPreservedParams.rightMargin = params.rightMargin; - mPreservedParams.bottomMargin = params.bottomMargin; - MarginLayoutParamsCompat.setMarginStart(mPreservedParams, - MarginLayoutParamsCompat.getMarginStart(params)); - MarginLayoutParamsCompat.setMarginEnd(mPreservedParams, - MarginLayoutParamsCompat.getMarginEnd(params)); - - if (leftMarginPercent >= 0) { - params.leftMargin = (int) (widthHint * leftMarginPercent); - } - if (topMarginPercent >= 0) { - params.topMargin = (int) (heightHint * topMarginPercent); - } - if (rightMarginPercent >= 0) { - params.rightMargin = (int) (widthHint * rightMarginPercent); - } - if (bottomMarginPercent >= 0) { - params.bottomMargin = (int) (heightHint * bottomMarginPercent); - } - boolean shouldResolveLayoutDirection = false; - if (startMarginPercent >= 0) { - MarginLayoutParamsCompat.setMarginStart(params, - (int) (widthHint * startMarginPercent)); - shouldResolveLayoutDirection = true; - } - if (endMarginPercent >= 0) { - MarginLayoutParamsCompat.setMarginEnd(params, - (int) (widthHint * endMarginPercent)); - shouldResolveLayoutDirection = true; - } - if (shouldResolveLayoutDirection && (view != null)) { - // Force the resolve pass so that start / end margins are propagated to the - // matching left / right fields - MarginLayoutParamsCompat.resolveLayoutDirection(params, - ViewCompat.getLayoutDirection(view)); - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "after fillMarginLayoutParams: (" + params.width + ", " + params.height - + ")"); - } - } + * Fills {@code ViewGroup.MarginLayoutParams} dimensions and margins based on percentage + * values. + */ +public void fillMarginLayoutParams(View view, ViewGroup.MarginLayoutParams params, + int widthHint, int heightHint) { + // 根据百分比值设置View的尺寸 + fillLayoutParams(params, widthHint, heightHint); + + // 从注释中就能知道,fillLayoutParams是计算尺寸,那下面这部分就是处理margin了 + // mPreservedParams来记录原始的margin值 + // Preserve the original margins, so we can restore them after the measure step. + mPreservedParams.leftMargin = params.leftMargin; + mPreservedParams.topMargin = params.topMargin; + mPreservedParams.rightMargin = params.rightMargin; + mPreservedParams.bottomMargin = params.bottomMargin; + MarginLayoutParamsCompat.setMarginStart(mPreservedParams, + MarginLayoutParamsCompat.getMarginStart(params)); + MarginLayoutParamsCompat.setMarginEnd(mPreservedParams, + MarginLayoutParamsCompat.getMarginEnd(params)); + + if (leftMarginPercent >= 0) { + params.leftMargin = (int) (widthHint * leftMarginPercent); + } + if (topMarginPercent >= 0) { + params.topMargin = (int) (heightHint * topMarginPercent); + } + if (rightMarginPercent >= 0) { + params.rightMargin = (int) (widthHint * rightMarginPercent); + } + if (bottomMarginPercent >= 0) { + params.bottomMargin = (int) (heightHint * bottomMarginPercent); + } + boolean shouldResolveLayoutDirection = false; + if (startMarginPercent >= 0) { + MarginLayoutParamsCompat.setMarginStart(params, + (int) (widthHint * startMarginPercent)); + shouldResolveLayoutDirection = true; + } + if (endMarginPercent >= 0) { + MarginLayoutParamsCompat.setMarginEnd(params, + (int) (widthHint * endMarginPercent)); + shouldResolveLayoutDirection = true; + } + if (shouldResolveLayoutDirection && (view != null)) { + // Force the resolve pass so that start / end margins are propagated to the + // matching left / right fields + MarginLayoutParamsCompat.resolveLayoutDirection(params, + ViewCompat.getLayoutDirection(view)); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "after fillMarginLayoutParams: (" + params.width + ", " + params.height + + ")"); + } +} ``` 再看一下`PercentLayoutInfo.fillLayoutParams()`的实现,该方法是通过百分比设置`View`的尺寸: ```java - /** - * Fills {@code ViewGroup.LayoutParams} dimensions based on percentage values. - */ - public void fillLayoutParams(ViewGroup.LayoutParams params, int widthHint, - int heightHint) { - // mPreservedParams来记录原始的宽高值 - // Preserve the original layout params, so we can restore them after the measure step. - mPreservedParams.width = params.width; - mPreservedParams.height = params.height; - - // We assume that width/height set to 0 means that value was unset. This might not - // necessarily be true, as the user might explicitly set it to 0. However, we use this - // information only for the aspect ratio. If the user set the aspect ratio attribute, - // it means they accept or soon discover that it will be disregarded. - final boolean widthNotSet = - (mPreservedParams.mIsWidthComputedFromAspectRatio - || mPreservedParams.width == 0) && (widthPercent < 0); - final boolean heightNotSet = - (mPreservedParams.mIsHeightComputedFromAspectRatio - || mPreservedParams.height == 0) && (heightPercent < 0); - - // 如果指定了百分比属性,就用宽高值和百分比去算出具体的宽高 - if (widthPercent >= 0) { - params.width = (int) (widthHint * widthPercent); - } - - if (heightPercent >= 0) { - params.height = (int) (heightHint * heightPercent); - } - // 如果指定了宽高的比例值,就用比例值去算出宽高,从这里能看出`aspectRatio`的优先级比百分比要高。他可以覆盖百分比之前设置的值。 - if (aspectRatio >= 0) { - if (widthNotSet) { - params.width = (int) (params.height * aspectRatio); - // Keep track that we've filled the width based on the height and aspect ratio. - mPreservedParams.mIsWidthComputedFromAspectRatio = true; - } - if (heightNotSet) { - params.height = (int) (params.width / aspectRatio); - // Keep track that we've filled the height based on the width and aspect ratio. - mPreservedParams.mIsHeightComputedFromAspectRatio = true; - } - } +/** + * Fills {@code ViewGroup.LayoutParams} dimensions based on percentage values. + */ +public void fillLayoutParams(ViewGroup.LayoutParams params, int widthHint, + int heightHint) { + // mPreservedParams来记录原始的宽高值 + // Preserve the original layout params, so we can restore them after the measure step. + mPreservedParams.width = params.width; + mPreservedParams.height = params.height; + + // We assume that width/height set to 0 means that value was unset. This might not + // necessarily be true, as the user might explicitly set it to 0. However, we use this + // information only for the aspect ratio. If the user set the aspect ratio attribute, + // it means they accept or soon discover that it will be disregarded. + final boolean widthNotSet = + (mPreservedParams.mIsWidthComputedFromAspectRatio + || mPreservedParams.width == 0) && (widthPercent < 0); + final boolean heightNotSet = + (mPreservedParams.mIsHeightComputedFromAspectRatio + || mPreservedParams.height == 0) && (heightPercent < 0); + + // 如果指定了百分比属性,就用宽高值和百分比去算出具体的宽高 + if (widthPercent >= 0) { + // 看到了吗?是根据父`View `的宽高来乘以百分比,不是根据整个屏幕的宽高,这样可能会有问题,要是ListView这种的高度没法算了 + params.width = (int) (widthHint * widthPercent); + } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "after fillLayoutParams: (" + params.width + ", " + params.height + ")"); - } + if (heightPercent >= 0) { + params.height = (int) (heightHint * heightPercent); + } + // 如果指定了宽高的比例值,就用比例值去算出宽高,从这里能看出`aspectRatio`的优先级比百分比要高。他可以覆盖百分比之前设置的值。 + if (aspectRatio >= 0) { + if (widthNotSet) { + params.width = (int) (params.height * aspectRatio); + // Keep track that we've filled the width based on the height and aspect ratio. + mPreservedParams.mIsWidthComputedFromAspectRatio = true; } + if (heightNotSet) { + params.height = (int) (params.width / aspectRatio); + // Keep track that we've filled the height based on the width and aspect ratio. + mPreservedParams.mIsHeightComputedFromAspectRatio = true; + } + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "after fillLayoutParams: (" + params.width + ", " + params.height + ")"); + } +} ``` 到这里`onMeasure`中通过百分比值设置宽高以及`margin`的部分就看完了,那我们接着看一下关于百分比值过小时对`wrap_content`的处理: @@ -365,101 +365,101 @@ if (mHelper.handleMeasuredStateTooSmall()) { 看一下`PercentLayoutHelper.handleMeasuredStateTooSmall()`方法的实现: ```java /** - * Iterates over children and checks if any of them would like to get more space than it - * received through the percentage dimension. - * - * If you are building a layout that supports percentage dimensions you are encouraged to take - * advantage of this method. The developer should be able to specify that a child should be - * remeasured by adding normal dimension attribute with {@code wrap_content} value. For example - * he might specify child's attributes as {@code app:layout_widthPercent="60%p"} and - * {@code android:layout_width="wrap_content"}. In this case if the child receives too little - * space, it will be remeasured with width set to {@code WRAP_CONTENT}. - * - * @return True if the measure phase needs to be rerun because one of the children would like - * to receive more space. - */ - public boolean handleMeasuredStateTooSmall() { - boolean needsSecondMeasure = false; - for (int i = 0, N = mHost.getChildCount(); i < N; i++) { - View view = mHost.getChildAt(i); - ViewGroup.LayoutParams params = view.getLayoutParams(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "should handle measured state too small " + view + " " + params); - } - if (params instanceof PercentLayoutParams) { - PercentLayoutInfo info = - ((PercentLayoutParams) params).getPercentLayoutInfo(); - if (info != null) { - // shouldHandleMeasuredWidthTooSmall方法来进行判断 - if (shouldHandleMeasuredWidthTooSmall(view, info)) { - needsSecondMeasure = true; - params.width = ViewGroup.LayoutParams.WRAP_CONTENT; - } - if (shouldHandleMeasuredHeightTooSmall(view, info)) { - needsSecondMeasure = true; - params.height = ViewGroup.LayoutParams.WRAP_CONTENT; - } + * Iterates over children and checks if any of them would like to get more space than it + * received through the percentage dimension. + * + * If you are building a layout that supports percentage dimensions you are encouraged to take + * advantage of this method. The developer should be able to specify that a child should be + * remeasured by adding normal dimension attribute with {@code wrap_content} value. For example + * he might specify child's attributes as {@code app:layout_widthPercent="60%p"} and + * {@code android:layout_width="wrap_content"}. In this case if the child receives too little + * space, it will be remeasured with width set to {@code WRAP_CONTENT}. + * + * @return True if the measure phase needs to be rerun because one of the children would like + * to receive more space. + */ +public boolean handleMeasuredStateTooSmall() { + boolean needsSecondMeasure = false; + for (int i = 0, N = mHost.getChildCount(); i < N; i++) { + View view = mHost.getChildAt(i); + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should handle measured state too small " + view + " " + params); + } + if (params instanceof PercentLayoutParams) { + PercentLayoutInfo info = + ((PercentLayoutParams) params).getPercentLayoutInfo(); + if (info != null) { + // shouldHandleMeasuredWidthTooSmall方法来进行判断 + if (shouldHandleMeasuredWidthTooSmall(view, info)) { + needsSecondMeasure = true; + params.width = ViewGroup.LayoutParams.WRAP_CONTENT; + } + if (shouldHandleMeasuredHeightTooSmall(view, info)) { + needsSecondMeasure = true; + params.height = ViewGroup.LayoutParams.WRAP_CONTENT; } } } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "should trigger second measure pass: " + needsSecondMeasure); - } - return needsSecondMeasure; } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should trigger second measure pass: " + needsSecondMeasure); + } + return needsSecondMeasure; +} ``` 那继续看一下`shouldHandleMeasuredWidthTooSmall()`方法: ```java private static boolean shouldHandleMeasuredWidthTooSmall(View view, PercentLayoutInfo info) { - int state = ViewCompat.getMeasuredWidthAndState(view) & ViewCompat.MEASURED_STATE_MASK; - return state == ViewCompat.MEASURED_STATE_TOO_SMALL && info.widthPercent >= 0 && - info.mPreservedParams.width == ViewGroup.LayoutParams.WRAP_CONTENT; - } + int state = ViewCompat.getMeasuredWidthAndState(view) & ViewCompat.MEASURED_STATE_MASK; + return state == ViewCompat.MEASURED_STATE_TOO_SMALL && info.widthPercent >= 0 && + info.mPreservedParams.width == ViewGroup.LayoutParams.WRAP_CONTENT; +} ``` 继续看`ViewCompat.getMeasuredWidthAndState()`的源码: ```java public static int getMeasuredWidthAndState(View view) { - return IMPL.getMeasuredWidthAndState(view); - } + return IMPL.getMeasuredWidthAndState(view); +} ``` 继续往下看,这个`IMPL`是什么呢? ```java static final ViewCompatImpl IMPL; - static { - final int version = android.os.Build.VERSION.SDK_INT; - if (version >= 23) { - IMPL = new MarshmallowViewCompatImpl(); - } else if (version >= 21) { - IMPL = new LollipopViewCompatImpl(); - } else if (version >= 19) { - IMPL = new KitKatViewCompatImpl(); - } else if (version >= 17) { - IMPL = new JbMr1ViewCompatImpl(); - } else if (version >= 16) { - IMPL = new JBViewCompatImpl(); - } else if (version >= 15) { - IMPL = new ICSMr1ViewCompatImpl(); - } else if (version >= 14) { - IMPL = new ICSViewCompatImpl(); - } else if (version >= 11) { - IMPL = new HCViewCompatImpl(); - } else if (version >= 9) { - IMPL = new GBViewCompatImpl(); - } else if (version >= 7) { - IMPL = new EclairMr1ViewCompatImpl(); - } else { - IMPL = new BaseViewCompatImpl(); - } +static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 23) { + IMPL = new MarshmallowViewCompatImpl(); + } else if (version >= 21) { + IMPL = new LollipopViewCompatImpl(); + } else if (version >= 19) { + IMPL = new KitKatViewCompatImpl(); + } else if (version >= 17) { + IMPL = new JbMr1ViewCompatImpl(); + } else if (version >= 16) { + IMPL = new JBViewCompatImpl(); + } else if (version >= 15) { + IMPL = new ICSMr1ViewCompatImpl(); + } else if (version >= 14) { + IMPL = new ICSViewCompatImpl(); + } else if (version >= 11) { + IMPL = new HCViewCompatImpl(); + } else if (version >= 9) { + IMPL = new GBViewCompatImpl(); + } else if (version >= 7) { + IMPL = new EclairMr1ViewCompatImpl(); + } else { + IMPL = new BaseViewCompatImpl(); } +} ``` 继续看,`IMPL.getMeasuredWidthAndState()`,方法其实最终就是使用的`BaseViewCompatImpl.getMeasuredWidthAndState()`方法,接着看它的的源码: ```java - @Override - public int getMeasuredWidthAndState(View view) { - return view.getMeasuredWidth(); - } +@Override + public int getMeasuredWidthAndState(View view) { + return view.getMeasuredWidth(); + } ``` 最后就是调用了`getMeasuredWidth()`方法。 没毛病- -! @@ -468,80 +468,79 @@ static final ViewCompatImpl IMPL; 那接着来看一下`onLayout`方法,`onLayout`方法直接调用了`PercentLayoutHelper.restoreOriginalParams`,: ```java /** - * Iterates over children and restores their original dimensions that were changed for - * percentage values. Calling this method only makes sense if you previously called - * {@link PercentLayoutHelper#adjustChildren(int, int)}. - */ - public void restoreOriginalParams() { - for (int i = 0, N = mHost.getChildCount(); i < N; i++) { - View view = mHost.getChildAt(i); - ViewGroup.LayoutParams params = view.getLayoutParams(); + * Iterates over children and restores their original dimensions that were changed for + * percentage values. Calling this method only makes sense if you previously called + * {@link PercentLayoutHelper#adjustChildren(int, int)}. + */ +public void restoreOriginalParams() { + for (int i = 0, N = mHost.getChildCount(); i < N; i++) { + View view = mHost.getChildAt(i); + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "should restore " + view + " " + params); + } + if (params instanceof PercentLayoutParams) { + PercentLayoutInfo info = + ((PercentLayoutParams) params).getPercentLayoutInfo(); if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "should restore " + view + " " + params); + Log.d(TAG, "using " + info); } - if (params instanceof PercentLayoutParams) { - PercentLayoutInfo info = - ((PercentLayoutParams) params).getPercentLayoutInfo(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "using " + info); - } - if (info != null) { - if (params instanceof ViewGroup.MarginLayoutParams) { - info.restoreMarginLayoutParams((ViewGroup.MarginLayoutParams) params); - } else { - info.restoreLayoutParams(params); - } + if (info != null) { + if (params instanceof ViewGroup.MarginLayoutParams) { + info.restoreMarginLayoutParams((ViewGroup.MarginLayoutParams) params); + } else { + info.restoreLayoutParams(params); } } } } +} ``` 调用了`PercentLayoutInfo.restoreMarginLayoutParams()`方法,我们看下它的源码: ```java /** - * Restores original dimensions and margins after they were changed for percentage based - * values. Calling this method only makes sense if you previously called - * {@link PercentLayoutHelper.PercentLayoutInfo#fillMarginLayoutParams}. - */ - public void restoreMarginLayoutParams(ViewGroup.MarginLayoutParams params) { - restoreLayoutParams(params); - // mPreservedParams的值在之前ad - params.leftMargin = mPreservedParams.leftMargin; - params.topMargin = mPreservedParams.topMargin; - params.rightMargin = mPreservedParams.rightMargin; - params.bottomMargin = mPreservedParams.bottomMargin; - MarginLayoutParamsCompat.setMarginStart(params, - MarginLayoutParamsCompat.getMarginStart(mPreservedParams)); - MarginLayoutParamsCompat.setMarginEnd(params, - MarginLayoutParamsCompat.getMarginEnd(mPreservedParams)); - } - + * Restores original dimensions and margins after they were changed for percentage based + * values. Calling this method only makes sense if you previously called + * {@link PercentLayoutHelper.PercentLayoutInfo#fillMarginLayoutParams}. + */ +public void restoreMarginLayoutParams(ViewGroup.MarginLayoutParams params) { + restoreLayoutParams(params); + // mPreservedParams的值在之前ad + params.leftMargin = mPreservedParams.leftMargin; + params.topMargin = mPreservedParams.topMargin; + params.rightMargin = mPreservedParams.rightMargin; + params.bottomMargin = mPreservedParams.bottomMargin; + MarginLayoutParamsCompat.setMarginStart(params, + MarginLayoutParamsCompat.getMarginStart(mPreservedParams)); + MarginLayoutParamsCompat.setMarginEnd(params, + MarginLayoutParamsCompat.getMarginEnd(mPreservedParams)); +} ``` 意思是说,再他们被以百分比为基础的数据更改之后恢复成原始的尺寸和margin。 然后继续看一下`restoreLayoutParams()`: ```java /** - * Restores original dimensions after they were changed for percentage based values. Calling - * this method only makes sense if you previously called - * {@link PercentLayoutHelper.PercentLayoutInfo#fillLayoutParams}. - */ - public void restoreLayoutParams(ViewGroup.LayoutParams params) { - if (!mPreservedParams.mIsWidthComputedFromAspectRatio) { - // Only restore the width if we didn't compute it based on the height and - // aspect ratio in the fill pass. - params.width = mPreservedParams.width; - } - if (!mPreservedParams.mIsHeightComputedFromAspectRatio) { - // Only restore the height if we didn't compute it based on the width and - // aspect ratio in the fill pass. - params.height = mPreservedParams.height; - } - - // Reset the tracking flags. - mPreservedParams.mIsWidthComputedFromAspectRatio = false; - mPreservedParams.mIsHeightComputedFromAspectRatio = false; - } +* Restores original dimensions after they were changed for percentage based values. Calling +* this method only makes sense if you previously called +* {@link PercentLayoutHelper.PercentLayoutInfo#fillLayoutParams}. +*/ +public void restoreLayoutParams(ViewGroup.LayoutParams params) { + if (!mPreservedParams.mIsWidthComputedFromAspectRatio) { + // Only restore the width if we didn't compute it based on the height and + // aspect ratio in the fill pass. + params.width = mPreservedParams.width; + } + if (!mPreservedParams.mIsHeightComputedFromAspectRatio) { + // Only restore the height if we didn't compute it based on the width and + // aspect ratio in the fill pass. + params.height = mPreservedParams.height; + } + + // Reset the tracking flags. + mPreservedParams.mIsWidthComputedFromAspectRatio = false; + mPreservedParams.mIsHeightComputedFromAspectRatio = false; +} ``` 好了到这里也分析完了`onLayout`的方法,大意就是在`onMeasure`之前先将原始的宽高和`margin`都备份一下,然后在`onMeasure`中根据百分比去设置对应的宽高和`margin`,等到设置完之后在`onLayout`方法中再去将这些值恢复到之前备份的起始数据。说实话我没看明白为什么要这样做? @@ -552,8 +551,9 @@ static final ViewCompatImpl IMPL; - 为什么在`onLayout`方法中去恢复数据,这有什么作用? - 看代码在处理如果指定的百分比过小但又指定`wrap_content`时,会重新根据`wrap_content`去重新计算的逻辑没有错,但是为什么我在上面测试的时候确没效果? +- 在上面分析代码时看到`fillLayoutParams`中是根据父`View`的宽高来乘以百分比,不是根据整个屏幕的宽高,这样可能会有问题,要是ListView这种的高度没法算了。 - + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" "b/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" index 905df36c..37affaa8 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" +++ "b/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" @@ -105,8 +105,8 @@ AndroidStudio使用教程(第一弹) | 方法调用层级弹窗 | `Ctrl+Alt+H` | `Control+Option+H` | |书签(在当前行打上书签) | `F11` | `F3` | |展示书签 | `Shift+F11` | `Command+F3` | -|整行代码上下移动 | `Alt+Shift++↑或↓` | `Option+Shift+↑或↓` | - +|整行代码上下移动 | `Alt+Shift++↑或↓` | `Option+Shift+↑或↓` | +|搜索设置操作命令 | `Ctrl+Shift+A` | `Command+Shift+A` | --- diff --git "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" "b/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" index 812386db..36c4f9e4 100644 --- "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" +++ "b/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" @@ -299,8 +299,86 @@ animatorScaleSet.start(); ``` +`Android L`又增加了一种动画样式,叫做`Reveal Animation`。 +首先来看一下效果: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reveal.gif?raw=true) +可以直接通过`ViewAnimationUtils.createCircularReveal()`方法来创建。 + +```java +/** + * Returns an Animator which can animate a clipping circle. + *

+ * Any shadow cast by the View will respect the circular clip from this animator. + *

+ * Only a single non-rectangular clip can be applied on a View at any time. + * Views clipped by a circular reveal animation take priority over + * {@link View#setClipToOutline(boolean) View Outline clipping}. + *

+ * Note that the animation returned here is a one-shot animation. It cannot + * be re-used, and once started it cannot be paused or resumed. It is also + * an asynchronous animation that automatically runs off of the UI thread. + * As a result {@link AnimatorListener#onAnimationEnd(Animator)} + * will occur after the animation has ended, but it may be delayed depending + * on thread responsiveness. + * + * @param view The View will be clipped to the animating circle. + * @param centerX The x coordinate of the center of the animating circle, relative to + * view. + * @param centerY The y coordinate of the center of the animating circle, relative to + * view. + * @param startRadius The starting radius of the animating circle. + * @param endRadius The ending radius of the animating circle. + */ +public static Animator createCircularReveal(View view, + int centerX, int centerY, float startRadius, float endRadius) { + return new RevealAnimator(view, centerX, centerY, startRadius, endRadius); +} +``` + +代码如下: + +```java +void enterReveal() { + final View myView = findViewById(R.id.my_view); + + int cx = myView.getMeasuredWidth() / 2; + int cy = myView.getMeasuredHeight() / 2; + + int finalRadius = Math.max(myView.getWidth(), myView.getHeight()) / 2; + + Animator anim = + ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius); + + anim.start(); +} + +void exitReveal() { + final View myView = findViewById(R.id.my_view); + + int cx = myView.getMeasuredWidth() / 2; + int cy = myView.getMeasuredHeight() / 2; + + int initialRadius = myView.getWidth() / 2; + + Animator anim = + ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0); + + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + myView.setVisibility(View.INVISIBLE); + } + }); + + anim.start(); +} + +``` + +有关如何提高动画性能请看:[通过Hardware Layer提高动画性能](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E9%80%9A%E8%BF%87Hardware%20Layer%E6%8F%90%E9%AB%98%E5%8A%A8%E7%94%BB%E6%80%A7%E8%83%BD.md) --- diff --git "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" index 9d8cda0f..05f79d6f 100644 --- "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" +++ "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" @@ -1,167 +1,215 @@ -屏幕适配 -=== - -## 基本概念 - -Px不同设备显示效果相同。这里的“相同”是指像素数不会变,比如指定UI长度是100px,那不管分辨率是多少UI长度都是100px。 -也正是因为如此才造成了UI在小分辨率设备上被放大而失真,在大分辨率上被缩小。 - -#### Screen Size(屏幕尺寸) -一般所说的手机屏幕大小如5寸、5.5英寸,都是指的屏幕对角线的长度,而不是手机面积。我们可以根据勾股定理获取手机的宽和长,当然还有面积。单位为inch,1英寸=2.54厘米 - -#### Resolution(分辨率) -指手机屏幕垂直和水平方向上的像素个数。比如分辨率是480*320,则指设备垂直方向有480个像素点,水平方向有320个像素点。单位px * px,指的是一屏显示多少像素点。 - -#### Dpi(dots per inch屏幕密度) -单位dpi,指的是每inch上可以显示多少像素点即px。(每英寸中的像素数)如160dpi指手机水平或垂直方向上每英寸距离有160个像素点。 -假定设备分辨率为320*240,屏幕长2英寸宽1.5英寸,dpi=320/2=240/1.5=160 - -#### Dip(Device-independent pixel,设备独立像素) -同dp,可作长度单位,不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA 推荐使用这个,不依赖像素。 -dip和具体像素值的对应公式是dip值 =设备密度/160* pixel值,可以看出在dpi(像素密度)为160dpi的设备上1px=1dip,在Android中,规定以160dpi为基准,1dip=1px,如果密度是320dpi,则1dip=2px,以此类推。 -单位dp/dip,指的是自适应屏幕密度的像素,用于指定控件宽高。就所屏幕密度变大,1dp所对应的px也会变大。 - -#### Sp(ScaledPixels放大像素) -主要用于字体显示(best for textsize)。根据 google 的建议,TextView 的字号最好使用 sp 做单位,而且查看TextView的源码可知 Android 默认使用 sp 作为字号单位。 - - -我们可以用下面的思路来解释为什么用dip代替px作单位: -设备最终会以px作为长度单位。 -如果我们直接用px作为单位会造成UI在不同分辨率设备上出现不合适的缩放。因此我们需要一种新的单位,这种单位要最终能够以合适的系数换算成px使UI表现出合适的大小。 -Dip符合这种要求吗? -由dip和具体像素值的对应公式dip值 =设备密度/160* pixel值 可以知 -pixel值=dip值/(设备密度/160),其中dip值是我们指定的长度大小,那么pixel值,160也是定植,也就是说UI最终的pixel值只受像素密度dip的影响, -这个dip就相当于那个换算系数,这个系数的值是多少有设备商去决定。因此dip符合这种要求。 - - 以我自己的Haier W910超级战舰(宽高比16:9)为例,上述单位的换算如下: -  已知数据:屏幕尺寸4.5, 分辨率1280 * 720, 屏幕密度320 - -  (1)16:9的4.5寸屏幕由勾股定理计算其高约为3.9寸,宽约为2.2寸 - -  (2)则竖向dpi为1280 / 3.9 ≈ 328, 横向dpi为720 / 2.2 ≈ 327 - -  (3)工业上切割液晶板时取整为320 - -  那么既然dip是自适应屏幕密度的,与px之间又是如何换算呢: - -  120dpi(ldpi低密度屏)   1dp = 0.75px (由于像素点是物理点,所以用2个像素点来显示3个dp的内容) - -  160dpi(mdpi中密度屏)   1dp = 1px - -  213dpi(tvdpi电视密度屏)  1dp = 1.33px - -  240dpi(hdpi高密度屏)   1dp = 1.5px - -  320dpi(xhdpi极高密度屏)  1dp = 2px - -  由上述分析结果可知,控件使用dp,文字使用sp即可满足自适应的需求。 - -注1:分辨率限定符的匹配是向下匹配,如果没有values-land-mdpi-1024x552,比如,分辨率values-land-mdpi-1024x600的屏幕, -当rom不把虚拟键计算到屏幕尺寸时,实际显示的屏幕应该是values-land-mdpi-1024x552,无法适配到values-land-mdpi-1024x600, -那这样就可能适配到下一级,比如values-land-mdpi-800x480,但是现在的平板已经没有这么低的分辨率了,所以是配到无限定符的values-mdpi里,造成界面显示上的瑕疵。 - -注2:由于分辨率限定符的匹配是向下匹配,所以如果有非主流mdpi屏幕不能精确适配到上述指定值时,values-mdpi至少可以保证app运行时不至于崩溃, -同理values可以保证ldpi屏幕的平板不会因生成view而又取不到相应值而崩溃。 - -##android为什么引进dip - -The reason for dip to exist is simple enough. Take for instance the T-Mobile G1. It has a pixel resolution of 320x480 pixels. -Now image another device, with the same physical screen size, but more pixels, for instance 640x480. -This device would have a higher pixel density than the G1. - -—If you specify, in your application, a button with a width of 100 pixels, it will look at lot smaller on the 640x480 device than on the 320x480 device. -Now, if you specify the width of the button to be 100 dip, the button will appear to have exactly the same size on the two devices. - -—The density-independent pixel is equivalent to one physical pixel on a 160 dpi screen, the baseline density assumed by the -platform (as described later in this document). At run time, the platform transparently handles any scaling of the dip units needed, -based on the actual density of the screen in use. The conversion of dip units to screen pixels is simple: pixels = dips * (density / 160). -For example, on 240 dpi screen, 1 dip would equal 1.5 physical pixels.Using dip units to define your application's UI is highly recommended, -as a way of ensuring proper display of your UI on different screens. - -dip是一种能自适应屏幕密度的像素,这个屏幕密度就是屏幕中一英寸里面含有的像素数,打个比方说,现在有两个4寸的手机,一个分辨率是320*480,它的屏幕密度是1, -二另一个是480*800的分辨率,这样他的屏幕密度就是1.5,如果我们在320*480的手机中弄了一个160dp的横线,由于他的屏幕密度是1,所以他里面dp和px是一比一的计算。 -这样这条横线正好占屏幕的一半,而我们将这个横线的视图发布到480*800的手机上时,虽然也是160dp, -但是由于他的屏幕密度是1.5.按照dp与px的计算公式pixel值=dip值/(设备密度/160),可以得到将160dp要乘以1.5倍,这样得到的px就是240px, -而这个手机的分辨率正好是480*800,所以看起来仍然是占屏幕的一半。注意:不论多少dp最后都是要通过转换成px来显示的手机的屏幕上的, -至于这个dp的大小在屏幕上到底能占多少位置,就要看这个转换后的px与屏幕的宽度像素的比值了。 - -另一个例子: 打个比方一个是4寸 480*800 的手机,屏幕密度为1.5. 另一个是7寸的1280*800的平板。屏幕密度也为1. 这样用160dp的横线, -在480*800的手机上换算成px是160*1.5 = 240,这样横向正好占屏幕的一半,但是在1280*800的平板上,160dp换算后的px为,160*1 = 160px, -而屏幕的宽是1280.这样这条横线显示在1280*800的平板上后,是160/1280 正好是八分之一。 - -所以引入dp解决的是什么问题呢?同一个屏幕尺寸下,对于不同的分辨率,由于尺寸相同,分辨率不同,所以它的屏幕密度就会不一样, -这样在同一尺寸的不同分辨率下的设备上,显示出来会看到所占的比例基本一样。如两个7寸的平板,一个是1280*800,一个是2560*1600, -这样用dp后,虽然2560*1600的分辨率比那个要大一倍,但是它尺寸一样啊,所以屏幕的分辨率会是另一个的2倍,这样dp转成px后会乘以2. -这样显示到这两个设备上所占的比例会一样 - -一个是7寸1280*800的板,密度为1,一个是5寸 1024*600的板,密度为1.这样你用160dp的线,在前面是8分之一,在后面是六分之一,你就悲剧了,所以要做屏幕适配了 - -## 代码 - -```java -DisplayMetrics displayMetrics = new DisplayMetrics(); -getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); -Log.e("@@@", "density:"+displayMetrics.density); //1 -Log.e("@@@", "densityDpi:"+displayMetrics.densityDpi); //160 -Log.e("@@@", "widthPixels:"+displayMetrics.widthPixels); //1024 -Log.e("@@@", "heightPixels:"+displayMetrics.heightPixels); //600 -``` -这个displayMetrics.densityDpi是真正的屏幕密度,可以为160dpi(mdpi标准的屏幕密度),240dpi(hdpi),320dpi(xhdpi). -displayMetrics.density,这个不叫屏幕密度,但是在dp和px转换的时候要使用到,这个是屏幕密度的比例值, -是用该设备的屏幕密度除以标准的屏幕密度得到的一个比值,如240dpi/160dpi = 1.5 - -## 屏幕适配文件夹命名规则 - -- 命名不区分大小写; -- 命名形式:资源名-属性1-属性2-属性3-属性4-属性5..... - 资源名就是资源类型名,包括:drawable, values, layout, anim, raw, menu, color, animator, xml; - 属性1-属性2-属性3-属性4-属性5.....就是上述的属性集内的属性,如:-en-port-hdpi; - 注意:各属性的位置顺序必须遵守优先级从高到低排列!否则编译不过 - -- 实例说明 - - 把全部属性都用上的例子(各属性是按优先级先后排列出来的) - values-mcc310-en-sw320dp-w720dp-h720dp-large-long-port-car-night-ldpi-notouch-keysexposed-nokeys-navexposed-nonav-v7 - - 上述例子属性的中文说明 -values-mcc310(sim卡运营商)-en(语言)-sw320dp(屏幕最小宽度)-w720dp(屏幕最佳宽度)-h720dp(屏幕最佳高度)-large(屏幕尺寸)-long(屏幕长短边模式) --port(当前屏幕横竖屏显示模式)-car(dock模式)-night(白天或夜晚)-ldpi(屏幕最佳dpi)-notouch(触摸屏模类型)-keysexposed(键盘类型)-nokey(硬按键类型) --navexposed(方向键是否可用)-nonav(方向键类型)-v7(android版本) - -## 手机常见分辨率 -4:3 -VGA 640*480 (Video Graphics Array) -QVGA 320*240 (Quarter VGA) -HVGA 480*320 (Half-size VGA) -SVGA 800*600 (Super VGA) - -5:3 -WVGA 800*480 (Wide VGA) - -16:9 -FWVGA 854*480 (Full Wide VGA) -HD 1920*1080 High Definition -QHD 960*540 -720p 1280*720 标清 -1080p 1920*1080 高清 - - -分辨率对应DPI 像素密度范围 -HVGA mdpi 120dpi-160dpi -WVGA hdpi 160dpi-240dpi -720P xhdpi 240dpi-320dpi -1080P xxhdpi 320dpi-480dpi - xxxhdpi 480dpi-640dpi - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xhdpi.jpg?raw=true) - - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/icon_size.jpg?raw=true) - - 为了解决适配的问题有时候还会使用9patch图,这里随便说一下。 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/9patch.jpg?raw=true) -`Stretchable area`代表拉伸的部分。 -`Padding box`代码内容所在区域。外面的就是`padding`部分。 - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +屏幕适配 +=== + +## 基本概念 + +`Px`不同设备显示效果相同。这里的“相同”是指像素数不会变,比如指定`UI`长度是`100px`,那不管分辨率是多少`UI`长度都是`100px`。 +也正是因为如此才造成了`UI`在小分辨率设备上被放大而失真,在大分辨率上被缩小。 + +#### Screen Size(屏幕尺寸) + +一般所说的手机屏幕大小如5寸、5.5英寸,都是指的屏幕对角线的长度,而不是手机面积。我们可以根据勾股定理获取手机的宽和长,当然还有面积。单位为`inch`,1英寸=2.54厘米 + +#### Resolution(分辨率) + +指手机屏幕垂直和水平方向上的像素个数。比如分辨率是`480*320`,则指设备垂直方向有480个像素点,水平方向有320个像素点。单位`px * px`,指的是一屏显示多少像素点。 + +#### Dpi(dots per inch屏幕密度) + +单位`dpi`,指的是每`inch`上可以显示多少像素点即`px`。(每英寸中的像素数)如`160dpi`指手机水平或垂直方向上每英寸距离有`160`个像素点。`dpi`越高,每个像素点越小也就越清晰。 +简单地说,`dpi`越高就意味着每英寸显示的细节越多,而不是直接取决于高分辨。举个例子, +`Galaxy Nexus`(对角线 4.65”)分辨率为:`720×1280`,`Nexus 7`(对角线 7”)分辨率为:`800×1280`. +常见的错误观点是认为他们拥有相同的屏幕像素密度,因为它们的分辨率几乎一样。 +然而,`Galaxy Nexus` 的屏幕像素密度约为`316dpi`,`Nexus 7`的屏幕像素密度为`216dpi`,相差甚远。 +这是因为虽然它们拥有一样的分辨率,但是它们显示尺寸却不一样。 +再次说明,屏幕像素密度是分辨率和显示尺寸的比值,两个因素一起决定屏幕像素密度。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/dpi_calculate.png?raw=true) +例如:计算`Nexus5`的屏幕像素密度: 屏幕尺寸:`4.95inch`、分辨率:`1920×1080`,屏幕像素密度:`445` +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/dpi_nexus5.png?raw=true) + + +#### Dip(Device-independent pixel,设备独立像素) + +同`dp`,指的是自适应屏幕密度的像素,用于指定控件宽高。就所屏幕密度变大,1dp所对应的px也会变大。 +不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持`WVGA、HVGA`和`QVGA`推荐使用这个, +不依赖像素。 +`dip`和具体像素值的对应公式是`dip值 =设备密度/160* pixel`值,可以看出在`dpi`(像素密度)为`160dpi`的设备上`1px=1dip`, +在`Android`中,规定以`160dpi`为基准,`1dip=1px`,如果密度是`320dpi`,则`1dip=2px`, +以此类推。 + +#### Sp(ScaledPixels放大像素) +主要用于字体显示`(best for textsize)`。根据`google`的建议,`TextView`的字号最好使用`sp`做单位, +而且查看`TextView`的源码可知`Android`默认使用`sp`作为字号单位。`Google`推荐我们使用`12sp`以上的大小, +通常可以使用`12sp`,`14sp`,`18sp`,`22sp`,最好不要使用奇数和小数。 + + +我们可以用下面的部分来解释为什么用dip代替px作单位: +设备最终会以`px`作为长度单位。 +如果我们直接用`px`作为单位会造成`UI`在不同分辨率设备上出现不合适的缩放。 +因此我们需要一种新的单位,这种单位要最终能够以合适的系数换算成`px`使`UI`表现出合适的大小。 +`Dip`符合这种要求吗? +由`dip`和具体像素值的对应公式`dip值 =设备密度/160* pixel`值可以知 +也就是说`UI`最终的pixel值只受像素密度dip的影响, +这个`dip`就相当于那个换算系数,这个系数的值是多少有设备商去决定。因此`dip`符合这种要求。 + +##android为什么引进dip + +> The reason for dip to exist is simple enough. Take for instance the T-Mobile G1. It has a pixel resolution of 320x480 pixels. +Now image another device, with the same physical screen size, but more pixels, for instance 640x480. +This device would have a higher pixel density than the G1. + +> —If you specify, in your application, a button with a width of 100 pixels, it will look at lot smaller on the 640x480 device than on the 320x480 device. +Now, if you specify the width of the button to be 100 dip, the button will appear to have exactly the same size on the two devices. + +> —The density-independent pixel is equivalent to one physical pixel on a 160 dpi screen, the baseline density assumed by the +platform (as described later in this document). At run time, the platform transparently handles any scaling of the dip units needed, +based on the actual density of the screen in use. The conversion of dip units to screen pixels is simple: pixels = dips * (density / 160). +For example, on 240 dpi screen, 1 dip would equal 1.5 physical pixels.Using dip units to define your application's UI is highly recommended, +as a way of ensuring proper display of your UI on different screens. + +`dip`是一种能自适应屏幕密度的像素,这个屏幕密度就是屏幕中一英寸里面含有的像素数,打个比方说,现在有两个4寸的手机,一个分辨率是`320*480`, +它的屏幕密度是1, +而另一个是`480*800`的分辨率,这样他的屏幕密度就是1.5,如果我们在320*480的手机中弄了一个160dp的横线, +由于他的屏幕密度是1,所以他里面dp和px是一比一的计算。 +这样这条横线正好占屏幕的一半,而我们将这个横线的视图发布到480*800的手机上时, +虽然也是160dp, +但是由于他的屏幕密度是1.5.按照dp与px的计算公式pixel值=dip值/(设备密度/160), +可以得到将160dp要乘以1.5倍,这样得到的px就是240px, +而这个手机的分辨率正好是480*800,所以看起来仍然是占屏幕的一半。 +注意:不论多少`dp`最后都是要通过转换成`px`来显示的手机的屏幕上的, +至于这个`dp`的大小在屏幕上到底能占多少位置,就要看这个转换后的`px`与屏幕的宽度像素的比值了。 + +另一个例子: 打个比方一个是4寸`480*800`的手机,屏幕密度为1.5. 另一个是7寸的`1280*800`的平板。屏幕密度也为1. +这样用160dp的横线,在`480*800`的手机上换算成`px`是`160*1.5 = 240`,这样横向正好占屏幕的一半, +但是在`1280*800`的平板上,`160dp`换算后的`px`为,`160*1 = 160px`, +而屏幕的宽是`1280`.这样这条横线显示在`1280*800`的平板上后,是`160/1280`正好是八分之一。 + +所以引入dp解决的是什么问题呢?同一个屏幕尺寸下,对于不同的分辨率,由于尺寸相同,分辨率不同,所以它的屏幕密度就会不一样, +这样在同一尺寸的不同分辨率下的设备上,显示出来会看到所占的比例基本一样。如两个7寸的平板,一个是`1280*800`,一个是`2560*1600`, +这样用`dp`后,虽然`2560*1600`的分辨率比那个要大一倍,但是它尺寸一样啊, +所以屏幕的分辨率会是另一个的2倍,这样dp转成px后会乘以2.这样显示到这两个设备上所占的比例会一样 + +一个是`7`寸`1280*800`的手机,密度为1,一个是5寸`1024*600的`手机,密度为1. +这样你用160dp的线,在前面是8分之一,在后面是六分之一,你就悲剧了,所以要做屏幕适配了 + +## 代码 + +```java +DisplayMetrics displayMetrics = new DisplayMetrics(); +getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); +Log.e("@@@", "density:"+displayMetrics.density); //1 +Log.e("@@@", "densityDpi:"+displayMetrics.densityDpi); //160 +Log.e("@@@", "widthPixels:"+displayMetrics.widthPixels); //1024 +Log.e("@@@", "heightPixels:"+displayMetrics.heightPixels); //600 +``` +`displayMetrics.densityDpi`是真正的屏幕密度,可以为`160dpi`(`mdpi`标准的屏幕密度),`240dpi`(`hdpi`), +`320dpi(xhdpi)`. +`displayMetrics.density`,这个不叫屏幕密度,但是在`dp`和`px`转换的时候要使用到, +这个是屏幕密度的比例值,是用该设备的屏幕密度除以标准的屏幕密度得到的一个比值, +如`240dpi/160dpi = 1.5` + +## 屏幕适配文件夹命名规则 + +- 命名不区分大小写; +- 命名形式:资源名-属性1-属性2-属性3-属性4-属性5..... + 资源名就是资源类型名,包括:drawable, values, layout, anim, raw, menu, color, animator, xml; + 属性1-属性2-属性3-属性4-属性5.....就是上述的属性集内的属性,如:-en-port-hdpi; + 注意:各属性的位置顺序必须遵守优先级从高到低排列!否则编译不过 + +- 实例说明 + - 把全部属性都用上的例子(各属性是按优先级先后排列出来的) + values-mcc310-en-sw320dp-w720dp-h720dp-large-long-port-car-night-ldpi-notouch-keysexposed-nokeys-navexposed-nonav-v7 + - 上述例子属性的中文说明 +values-mcc310(sim卡运营商)-en(语言)-sw320dp(屏幕最小宽度)-w720dp(屏幕最佳宽度)-h720dp(屏幕最佳高度)-large(屏幕尺寸)-long(屏幕长短边模式) +-port(当前屏幕横竖屏显示模式)-car(dock模式)-night(白天或夜晚)-ldpi(屏幕最佳dpi)-notouch(触摸屏模类型)-keysexposed(键盘类型)-nokey(硬按键类型) +-navexposed(方向键是否可用)-nonav(方向键类型)-v7(android版本) + +## 手机常见分辨率 +4:3 +VGA 640*480 (Video Graphics Array) +QVGA 320*240 (Quarter VGA) +HVGA 480*320 (Half-size VGA) +SVGA 800*600 (Super VGA) + +5:3 +WVGA 800*480 (Wide VGA) + +16:9 +FWVGA 854*480 (Full Wide VGA) +HD 1920*1080 High Definition +QHD 960*540 +720p 1280*720 标清 +1080p 1920*1080 高清 + + + +|分辨率对应DPI | 像素密 | 通常的分辨率 | 比例大小| +|HVGA| mdpi | 160dpi | 320*480 | 1| +|WVGA | hdpi | 240dpi | 480*800 | 1.5| +720P |xhdpi | 320dpi | 720*1280 | 2 | +1080P |xxhdpi | 480dpi | 1080*1920 | 3 | + | xxxhdpi | 640dpi | | 4 | + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xhdpi.jpg?raw=true) + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/icon_size.jpg?raw=true) + +为了解决适配的问题有时候还会使用9patch图,这里随便说一下。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/9patch.jpg?raw=true) + +- `Stretchable area`代表拉伸的部分。 +- `Padding box`代码内容所在区域。外面的就是`padding`部分。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/dpi_drawable.png?raw=true) + +### Android图片选择策略 +上面说到,如果屏幕所对应的文件夹没有要找的图片怎么办。 +这是很常见的,我们开发项目时一般不会去为每一个级别的屏幕去切一套图片。 +那样做只会让`apk`很大。所以一般性的图片我们只切一两个典型密度屏幕的图片。 +但是`apk`是有可能会运行在从`ldpi`到`xxhdpi`的各种级别的手机上。 +这个时候就需要根据一定的策略去寻找图片了。 + +`Android`系统寻找图片的步骤是这样的: + +- 去屏幕密度对应的目录去找。如果找到就拿来用。 +- 如果没找到,就去比这个密度高一级的目录里面去找,如果找到就拿来用。 +- 如果没找到就继续往上找。以此类推。 +- 如果到了xxhdpi目录还没有找到的话,就会去比自身屏幕密度低一级的目录去找,如果低一级的目录>=hdpi,找到了就拿来用。 +- 如果没找到, 就去mdpi目录去找, 如果找到了,就拿来用。 +- 如果没找到,就去默认的drawble目录里去找, 如果找到了就拿来用。 +- 如果没找到,再去最低的ldpi目录里去找。如果找到了,就拿来用。 +- 如果没找到, 那就是没找到了, 图片无法显示。(不过一般不会出现这种现象,因为如果每个目录都没有这个图片的话,你是编译不过的) + +这里有两点需要注意: + +- 先会去比自己密度高的目录里去找,这是因为因为系统相信,你在密度更高的目录里会放置分辨率更大的图片,这样的话这个图片会被缩小,但同时显示效果不会有损失,但是如果优先去低一级别的目录去找的话, 找到的图片就会被放大,这样的话这个图片就会被拉扯模糊了。 +e.g. 同一张图片,你在mdpi和xxhdpi目录各放了一份, 这个应用你现在运行在hdpi的手机上, 那应用会选择哪张图片呢。答案是xxhdpi目录里的。即便hdpi离mdpi更近一点! + +- 如果在mdpi里找不到是不会直接去ldpi里找的, 而是先去默认的drawble目录里找,这是drawble目录和drawble-mdpi是一个级别的。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/scale_drawable.png?raw=true) +上表几点值得注意的地方: + +- `rawable`目录和`drawable-mdpi`目录和`dp`到`px`的转换关系是一样的。 +- 当你放一个`120px*180px`的图片到`drawable-hdpi`目录,如果此应用运行在一个`xhdpi`的手机上,则这个图片会被拉扯到`160px*240px`。 +- 最后一行`dp->px`, 说明了在代码或者布局文件中声明一个`dp`值, 这个值在不同屏幕密度的手机中会被乘以不同的倍数。 +比如你在布局文件中写了一个宽和高分别为`120dp`和`180dp`的`LinearLayout`, 那么当这个应用运行在`xhdpi`的手机上时 +它的实际像素就会被转换为`240px*360px`。 如果运行在`ldpi`的手机上,就变成了`90px*135px`。 但是在这两个手机中显示的区域大小从肉眼看,是一模一样大的。 + +UI给工程师切多大图是合适的。 +直接基于`720*1280`的视觉稿切一版图片就可以了。 将图片只放到`xhdpi`目录中, +这样系统会在不同密度屏幕的手机中对图片进行合理的缩放,如果想在`xxhdpi`的手机上显示的很好, 也可以基于`1080P`的屏幕设计然后放到`xxhdpi`中, +这样的话就兼容所有低密度屏幕的手机, 而且也不会出现图片被拉扯的现象。 + +***Note:***` The mipmap-xxxhdpi qualifier is necessary only to provide a launcher icon that can appear larger than usual on an xxhdpi device. You do not need to provide xxxhdpi assets for all your app's images.` + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From f90bdb731170d0485ce10ccdd8a3ed1371d8ec8e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 8 Jun 2016 18:42:53 +0800 Subject: [PATCH 031/373] add somethings you don't know with studio --- .../\345\261\217\345\271\225\351\200\202\351\205\215.md" | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" index 05f79d6f..a087bea1 100644 --- "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" +++ "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" @@ -145,11 +145,12 @@ QHD 960*540 |分辨率对应DPI | 像素密 | 通常的分辨率 | 比例大小| +| ---------- |:----------:|:----------:|-----------:| |HVGA| mdpi | 160dpi | 320*480 | 1| |WVGA | hdpi | 240dpi | 480*800 | 1.5| -720P |xhdpi | 320dpi | 720*1280 | 2 | -1080P |xxhdpi | 480dpi | 1080*1920 | 3 | - | xxxhdpi | 640dpi | | 4 | +|720P |xhdpi | 320dpi | 720*1280 | 2 | +|1080P |xxhdpi | 480dpi | 1080*1920 | 3 | +| xx | xxxhdpi | 640dpi | | 4 | ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xhdpi.jpg?raw=true) From 8f0cc43a04a60962748d8581b40ff444378f3563 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sun, 12 Jun 2016 19:40:08 +0800 Subject: [PATCH 032/373] add systrace part --- .../InstantRun\350\257\246\350\247\243.md" | 172 ++++++++++++++- ...70\345\205\263\345\267\245\345\205\267.md" | 204 ++++++++++++++++++ ...44\271\211View\350\257\246\350\247\243.md" | 42 +++- ...17\345\271\225\351\200\202\351\205\215.md" | 3 +- 4 files changed, 417 insertions(+), 4 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" diff --git "a/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" index a593f788..ebba5262 100644 --- "a/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" +++ "b/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" @@ -4,11 +4,181 @@ InstantRun详解 之前在写[AndroidStudio提高Build速度](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/AndroidStudio%E6%8F%90%E9%AB%98Build%E9%80%9F%E5%BA%A6.md)这篇文章的时候写到,想要快,就用`Instant Run`。最近有朋友发来邮件讨论它的原理,最近项目不忙,索性就来系统的学习下。 +`Android Studio`2.0开始引入了`Instant Run`,它主要是在`Run`和`Debug`的时候可以去减少更新应用的时间。虽然第一次`Build`的时候可能会消耗稍长的时间来完成,但是`Instant Run`可以把更新内容推送到设备上,而无需重新`build`一个新的`apk`,这样就会很快速的让我们观察到改变。 +`Instant Run`只支持在`build.gralde`文件中配置的`Gradle`版本是2.0.0以上并且`minSdkVersion`是15以上才可以。为了能更好的使用,请将`minSdkVrsion`设置到21以上。 +部署完应用后,会在`Run`![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/as-irrun.png?raw=true)(或者(Debug![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/as-irdebug.png?raw=true)))图标上面出现一个小黄色的闪电符号,这就意味着`Instant Run`已经准备就绪,在你下次点击按钮的时候可以推送更新内容。它不需要重新构建一个新的`APK`,它只推送那些新改变的地方,有些情况下`app`都不需要重启就可以直接显示新改变的下效果。 - +###配置你的应用来使用`Instant Run` + +`Android Stuido`中项目使用`Gralde`2.0.0及以上版本会默认使用`Instant Run`。 +让项目更新到最新的版本: + +1. 点击`Settings`中的`Preferences`按钮。 +2. 找到`Build,Execution,Deployment`中的`Instant Run`然后点击`Update Project`,如果`Update Project`部分没有显示,那说明你当前已经是最新版本了. +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/update-project-dialog.png?raw=true) + + + +###修复类型 + +`Instant Run`通过热修复、暖修复或者冷修复来推送改变的代码和资源到你的设备或者模拟器上。它会根据你更改的内容来自动选择对应的修复类型。 + +- 热修复: 更改的内容不需要重启应用甚至不需要重启当前的`activity`就可以让显示内容的改变,对于大部分通过方法内容改变的更改都可以通过这种方式来修改。 +- 暖修复: 需要重启`activity`才能让改变生效,在改变资源时会通过该方式。 +- 冷修复: 应用需要重启(不是重新安装)。对于一些方法方法结构和实现等结构性的改变需要通过该方式。 + + + +那具体更改内容和修复方式是怎么对应的呢? + +- 改变现在已经存在的方法 + 这将通过***热修复***来执行,这是最快的一种修复方式。你的应用会在运行的过程中,在下次调用该方法时直接使用新实现的方法。 + 热修复不需要重新初始化对象。在看到具体的更改之前,你可能会需要重启当前的`activity`或者应用。默认情况下`Android Studio`在执行热修复后会自动重启当前的`activity`。如果你不想要它自动重启`activity`你也可以在设置中禁用。 + +- 更改或者移除已经存在的资源 + 这将通过***暖修复***来执行:这也是非常快的,但是`Instant Run`在推送这些更新内容时必须要重启当前的`activity`。你的应用仍然会继续运行,但是在重启`activity`时屏幕可能会闪动-这是正常滴。 + +- 结构性的改变例如: + + - 增删或者改变: + - 一个注解 + - 一个变量 + - 一个静态变量 + - 一个方法结构 + - 一个静态方法结构 + + - 更改该类集成的父类 + - 更改接口的实现列表 + - 更改类的静态修饰符 + - 使用动态资源`id`重新布局 + + 上面的这些改变都将通过***冷修复(API 21以上才支持)***来执行.冷修复会稍微有些慢,因为虽然不需要构建一个新的`APK`但是必须要重启整个`app`才能推送这些结构性的代码改变。对于`API` 20及以下的版本,`Android Studio`将重新部署`APK`。 + +- 更改`manifest`文件或者更改`manifest`文件中引用的资源或者更改`Android`桌面`widget`的`UI`实现(需要Clean and Rerun) + 在更改清单文件或者清单文件中引用的资源时,`Android Studio`会自动的重新部署引用来实现这些内容的改变。这是因为例如应用名称、图标和`intent filters`这些东西是要在应用安装到设备时根据清单文件来决定的。 + 在更新`Android UI widget`时,你需要执行`Clean and Rerun`才能看到更改的内容,因为在使用`Instant Run`时执行`clean`会需要很长的时间,所以你在更新`UI widget`时可以禁用`Instant Run`。 + + +#####使用Rerun + +如果修改了一些会影响到初始化的内容时,例如修改了应用的`onCreate()`方法,你需要重启你的应用来让这些改变生效,你可以点击`Rerun`图标。它将会停止应用运行,并且执行`clean`操作后重新部署一个新的`APK`到你的设备。 + + +###实现原理 + +正常情况下,我们修改了内容,想让让它生效,那就需要如下的步骤: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/change_run_list.png?raw=true) + +那`Instant Run`的目标也非常简单: +> 尽可能多的移除上面的步骤,来让剩下的部分尽可能更快。 + +这就意味着: + +- 只构建和部署新更改的部分。 +- 不去重新安装应用。 +- 不去重启应用。 +- 甚至不去重启`activity`。 + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/instant_run_list.png?raw=true) + + +普通情况下在你点击`Run`或者`Debug`按钮时,会执行如下的操作: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/apk_progress.png?raw=true) + +清单文件会被整合然后与你应用的资源重启打包成`APK`.同样的,`.java`的源代码会被编译成二进制,然后转换成`.dex`文件,他们也会被包含到`APK`中。 + +在使用`Instant Run`的情况下,第一次点击`Run`和`Debug`时`Gradle`会执行一些额外的操作: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/instant_apk_progress.png?raw=true) +`instrumentation`相关的二进制内容会被增加到`.class`文件中,并且一个新的`App Server`类会被注入到应用中。 +同时也会增加一个新的`Application`类,来注入一些自定义的`class loader`并且能启动`App Server`。因此`minifest`文件会被修改来保证使用该新加的`Application`类(如果你已经创建了自己的`Application`类,`Instant Run`的版本将会用你定义的来进行代理扩展) + +经过上面的操作`Instant Run`就开始执行了,以后如果你改变了代码部分,在重新点击`Run`或者`Debug`时,`Instant Run`将会通过热、暖、冷修复来尽可能的减少上面的构建过程。 + +在`Instant Run`更改内容前,`Android Studio`会你的应用版本是否支持`Instant Run`并且`App Server`是否运行在对其有用的端口(内部使用了Socket)。 这样来确定你的应用是否在前台运行,并且找到`Studio`所需要的构建`ID`. + +总结一下,其实就是内部会对每个`class`文件注入`instant run`相关的代码,然后自定义一个`application`内部指定自定义的`classLoader`(也就是说不使用默认的classLoader了,只要用了Instant Run就需要使用它自定义的classLoader),然后在应用程序里面开启一个服务器,`Studio`将修改的代码发送到该服务器,然后再通过自定义的`classLoader`加载代码的时候会去请求该服务器判断代码是否有更新,如果有更新就会通过委托机制加载新更新的代码然后注入到应用程序中,这样就完成了替换的操作。 + +###热修复过程 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hot_swapping.png?raw=true) + +`Android Studio`会监听在开发过程中哪个文件发生了改变,并且通过一个自定义的`Gralde task`来只对修改的`class`文件生成对应的`.dex`文件。 +这些新的`.dex`文件会通过`Studio`传递发布给应用中运行的`App Server`。 + +因为开始时这些文件的`class`已经存在到运行中的应用中-`Gralde`需要保证更新的版本来保证他们能覆盖之前已经存在的文件。这些更新文件的转换是由`App Server`通过自定义的`class loader`来完成的。 + +从现在开始,每次一个方法被调用时(不管在应用中的任何地方),被注入到最初的`class`文件中的`instrumentation`都讲去连接`App Server`来检查它们是否有更新了。 如果有更新,执行操作将被指派到新的充在的`class`文件,这样就会执行新修改的方法。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/instant_run_app_server.png?raw=true) + +如果你设置断点,你会发现你调用的是`override`名字的类中的方法。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/instant_debug_override.gif?raw=true) + + +这种重新指定方法的方式在改变方法实现时非常有效,但是对于那种需要在`Activity`启动时加载的改变呢? + +###暖修复 + +暖修复会重启`Activity`。资源文件会在`Activity`启动时被加载,所以修改它们需要`activity`重新启动来进行加载。 + +现在,更改任何资源都会导致所有的资源都被重新打包病转移到应用中-但是以后会支持增量包的功能来让只打包和部署那些新修改的资源。 + +> 注意,暖修复在更改`manifest`件或者`manifest`文件中引用的内容时无效,因为`manifest`文件中的内容需要在`apk`安装时读取。更改`Manifest`文件(或者它所引用的内容)需要被全部构建和部署。 + + + +非常不幸的是,重启`activity`在做一些结构性的改变时无效。对于这些改变需要使用冷修复。 + +###冷修复 + +在部署时,你的应用和他的子项目会被分派到10个不同的部分,每个都在自己单独的`dex`文件。不同部分的类会通过包名来分配。在使用冷修复时,修改的类需要其他所有相关的`class`文件都被重新打包后才能被部署到设备上。 +这就需要依赖`Android Runtime`能支持加载多个`.dex`文件,这是一个在`ART`中新增的功能,它只有在`Android 5.0(API 21)`以上的设备总才支持。 +对于`API 20`一下的设备,他们仍在使用`Dalvik`虚拟机,`Android Studio`需要部署整个`APK`。 + + + +有些时候通过热修复来改变的代码,但是它会被应用首次运行时的初始化所影响,这时你就需要`restart`你的应用来让其生效(快捷键是Command + Ctrl + R)。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/instant_run_restart.gif?raw=true) + + +###Instant Run使用技巧及提示 + +`Instant Run`是由`Android Studio`控制的,所以你只能通过`IDE`来`start/restart`你的`debug`实例,不要直接在设备中`start/restart`你的应用,不然的话就会发生错乱。 + + +#####Instant Run限制条件 + +- 部署到多个设备 + + `Instant Run`针对设备的不同`API Level`使用不同的技术来进行热、暖、冷修复。因此,如果同时部署一个应用到多个设备时,`Studio`会暂时关闭`Instant Run`功能。 + + +- Multidex + + 如果你的项目支持传统的`Multidex`-也就是在`build.gradle`中配置了`multiDexEnabled true`和`minSdkVersion 20`及以下-当你部署应用到`4.4`以下的设备上时,`Android Studio`将会禁用`Instant Run`。 + + 如果`minSdkVersion`设置为21或者更高时,`Instant Run`将自动配置来支持`multidex`,因为`Instant Run`只支持`debug`版本,有需要在发布`release`版的时候配置你的`build`变量来支持`multidex`。 + +- 使用第三方插件 + `Android Studio`在使用`Instant Run`时会暂时禁用`Java Code Coverage Library`和`ProGuard`。因为`Instant Run`只支持`debug`版本,这样也不会影响`release`版的构建。 + +- 只能在主进程中运行 + 目前热修复只能在主进程中进行,如果在其他进程中热、暖修复将无法使用,只能用冷修复来替代。 + + + +参考: + +- [官网](https://developer.android.com/studio/run/index.html#instant-run) +- [Instant Run: How Does it Work?!](https://medium.com/google-developers/instant-run-how-does-it-work-294a1633367f#.8xmpk8xvc) + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" new file mode 100644 index 00000000..471e7390 --- /dev/null +++ "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" @@ -0,0 +1,204 @@ +性能优化相关工具 +=== + +有关性能优化的文章请参考[性能优化](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.md)和[布局优化](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E5%B8%83%E5%B1%80%E4%BC%98%E5%8C%96.md) + + +首先要讲到的是`Systrace`,之前在淘宝面试的时候被问到,如有查找`UI`绘制卡顿的问题,我没答上来。早点知道`Systrace`就好了(当然只知道工具是不够的,也要懂得里面的原理)。 + +### Systrace + +`Systrace`有什么用呢?官方文档中有一片文章的标题是:使用`Systrace`进行`UI`性能分析。 + +在应用程序开发的过程中,你需要检查用户交互是否平滑流畅,运行在稳定的60fps。如果中间出了些问题,导致某一帧延迟,我们想要解决该问题的第一步就是理解系统如何操作的。 + +`Systrace`工具可以让你来手机和观察整个`android`设备的时间信息,也称为`trace`。它会显示每个时间点上的`CPU`消耗图,显示每个线程在显示的内容以及每个进程当时在做的操作。 + +在使用`Systracv`来分析应用之前,你需要手机应用的`trace log`信息。生成的`trace`能够让你清晰的观察系统在该时间内所做的任何事情。 + +##### 生成Trace + +为了能创建一个`trace`,你必须要执行如下几个操作。 首先,你要有一个`Android 4.1`及以上的设备。将该设备设置为`debugging`,连接你的设备并且安装应用。有些类型的信息,特别是一些硬盘的活动和内核工作队列,需要设备获取`root`权限才可以。 然而,大多数的`Systrace log`的数据只需要设备开启开发者`debugging`就可以了。 + +`Systrace`可以通过命令行或者图形化界面的方式来运行,在`Studio`中打开`Android Device Monitor`然后选择`Systracv`图标![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-button.png?raw=true)。 + + +下面以命令行的方式为例,这里只讲解一下`Android 4.3`及以上的使用方式: + +- 确保设备已经通过`USB`连接并且开启了`debugging`模式。 +- 设置一些参数并执行`trace`命令,例如: + ``` + $ cd android-sdk/platform-tools/systrace + $ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm + ``` + 执行完上面的命令后就会在`sdk/platform-tools/systrace/mynewtrace.html`生成对应的`html`文件。 +- 在设备上执行一些你想要`trace`的过程。 + +悲剧了,生成了对应的`html`文件,但是我死活打不开。打开是白屏,折腾了我半天,最后终于找到了解决方法: + +> Firstly, if anyone is using Chrome v50.0+ on OS X, just try this please. + +> open chrome browser and go to "chrome://tracing" +> in the tracing page, click load and select the systrace generated html file. +> Secondly, I think it's a bug which is confirmed by Google. + +`OK`了。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace_file.png?raw=true)。 + +看不懂!!! + +#####分析Trace + +用浏览器打开上面生成的`mynewtrace.html`。 下面为了能明显的说明,我就用官网提供的例子来说明了。 + +######查看Frames +从打开的文件中我们能看到每个应用都有一行`frame`的圆圈来标示渲染的帧,通常都是绿色的。黄色或者红的圆圈标示超过了我们对保障60fps所需的16毫秒绘制时间。。 可以在文件上面按`w`键来放大文件以便能更好的观看查找。 +> ***提示:***在文件上面按`?`键可以查看对应的快捷键。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame-unselected.png?raw=true)。 + +在`Android 5.0`以上的设备上,显示工作呗分为`UI Thread`和`Render Thread`。在之前的版本,所有工作都是在`UI Thread`进行的。 点击某个单独的`frame`图标来查看它们所需的时间。 + +######观看Alerts + +`Systracv`会自动分析`trace`过程的时间,并且通过`alerts`提示该展现问题,以及建议如何处理。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame-selected.png?raw=true) + +如上图所示,在你点击一个比较慢的`frame`时,下面就会显示一个`alert`。在这种情况下,它直说可能是`ListView`服用以及重复渲染的问题。 如果你发现`UI Thread`做了太多的工作,你可以使用`TraceView`进行代码分析,来找到具体哪些操作导致了消耗时间。 + +如下,你也可以通过点击窗口右边的`Alerts`来查看当前所有的`alert`。这样会直接展开`Alert`窗口。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame-selected-alert-tab.png?raw=true) + +下面我们以另外一个例子来分析一下它的`Frame`: + +我们点击某一红色`frame`来查看这一阵的一些相关警告: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-frame-zoomin.png?raw=true) +可以看到这里说耗时32毫秒,这已经远远超过了16毫秒的绘制时间。 我们可以通过`Description`来查看描述内容。 +下面是另外一个渲染过慢的例子: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-2-frame.png?raw=true) +从上图,我们发现了`Scheduling delay`的警告,加起来能看到总的绘制时间大约是19毫秒。 +`Scheduling delay`的意思是调度延迟,也就是说一个县城在处理这部分操作时,在很长时间内没有被分配到`CPU`上面进行运算,这样就导致了很长时间内没有完成操作。 我们选择这一帧中最长的一块,来观察下: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-2-slice.png?raw=true) + +我们在上图看到了`Wall duration`,他代表着这一块开始到结束的总耗时。而在`CPU Duration`这里显示了`CPU`在处理该部分消耗的时间。 很明显,真个区域用了18毫秒,但是`CPU`实际处理只用了4毫秒,也就是说剩下的14毫秒就可能有问题了。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-2-cpu.png?raw=true) + +可以看到,4个线程都比较忙。 + +选择其中一个线程来查看是哪个应用在使用它,这里看到了一个包名为`com.udinic.keepbusyapp`的应用。也就是说由于另外一个应用占用了`CPU`,,导致了我们的应用未能获取到足够的`CPU`资源。 + + +#####追踪代码 + +在`Android 4.3`以上你可以使用`Trace`类来在代码中添加(很熟悉有木有,在看源码的时候经常看到)。这样能查看到此时你的应用线程都做了哪些操作。 +下面的代码展示了如何使用`Trace`类来追踪两个代码块部分: +```java +public void ProcessPeople() { + Trace.beginSection("ProcessPeople"); + try { + Trace.beginSection("Processing Jane"); + try { + // code for Jane task... + } finally { + Trace.endSection(); // ends "Processing Jane" + } + + Trace.beginSection("Processing John"); + try { + // code for John task... + } finally { + Trace.endSection(); // ends "Processing John" + } + } finally { + Trace.endSection(); // ends "ProcessPeople" + } +} +``` + + + + +###Traceview + +`Traceview`是一个性能测试工具,展示了所有方法的运行时间。 +> Traceview is a graphical viewer for execution logs that you create by using the Debug class to log tracing information in your code. Traceview can help you debug your application and profile its performance. + + +#####创建Trace文件 + +想要使用`Traceview`你需要创建一个包含你想分析部分的`trace`信息的`log`文件。 +有两种方式来生成`trace logs`: + +- 在你测试的类中添加`startMethodTracing()`和`stopMethodTracing()`的代码,来指定开始和结束获取`trace`信息。这种方式是非常精确的,因为你可以在代码中指定开始和结束的位置。 + +- 使用`DDMS`中的方法来生成。这种方式就不太精确。虽然这种方式不能精确的指定起始和结束位置,但是如果在你无法修改源代码或者不需要精确时间的时候是非常有用的。 + +在开始生成`trace log`信息时,你需要知道如下的限制条件: + +- 如果你在测试代码中使用,你的应用必须要用`WRITE_EXTERNAL_STORAGE`的权限。 +- 如果你使用`DDMS`生成: + + - `Android 2.1`之前必须有`SD`卡,并且你的应用也要有写入`SD`卡的权限。 + - `Android 2.2`之后不需要`SD`卡,`trace log`文件会直接生成到你的开发机上。 + +在测试代码中调用`startMethodTracing()`方法的时候,你可以指定系统生成`trace`文件的名字。结束的时候调用`stopMethodTracing()`方法。这些方法开始和结束时贯穿整个虚拟中的。例如你可以在你`activity`的`onCreate()`方法中调用`startMethodTracing()`方法,然后在`activity`的`onDestroy()`方法中调用`stopMethodTracing()`方法。 +```java + // start tracing to "/sdcard/calc.trace" + Debug.startMethodTracing("calc"); + // ... + // stop tracing + Debug.stopMethodTracing(); +``` + +在调用`startMethodTracing()`方法的时候,系统创建一个名为`.trace`的文件。它包含方法的二进制`trace`数据和一个线程与方法名的对应集合。 +然后系统就开始生成`trace`数据,直到调用`stopMethodTracing()`方法。如果在你调用`stopMethodTracing()`方法之前系统已经达到了最大的缓冲大小,系统就会停止`trace`并且在控制台发出一个通知。 + +#####拷贝Trace文件到电脑上 + +在模拟器或者机器上生成`.trace`文件后,你需要拷贝他们到你的电脑上,你可以使用`adb pull`命令来拷贝: +`adb pull /sdcard/calc.trace /tmp` + +#####在Traceview中查看trace文件 + +运行`Traceview`并且查看`trace`文件: + +1. 打开`Android Device Monitor`。 +2. 在`Android Device Monitor`的状态栏中点击`DDMS`并且选择一个进程。 +3. 点击`Start Method Profiling`图标开始查看。 +4. 在查看完后点击`Stop Method Profiling`图标来显示`traceview`。 + + + +#####Traceview Layout + +如果你有一个`trace log`文件(通过添加`tracing`代码或者用DDMS生成),你可以把该文件加载到`Traceview`中,这将会把`log`数据显示为两部分: + +- `timeline panel`-展示了每个线程和方法的起始和结束 +- `profile panel`-提供了一个方法中的执行内容的简述 + + + + + +###Hierarchy Viewer + + +###GPU Profiling + + +###Hardware Acceleration + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" index 456705de..66449531 100644 --- "a/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" +++ "b/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" @@ -318,9 +318,9 @@ public class ToogleView extends View { super.onDraw(canvas); Paint paint = new Paint(); paint.setAntiAlias(true); - // 先画背景图 + // 先画背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); - // 再画滑块,用mSlideMarginLeft来控制滑块距离左边的距离。 + // 再画滑块,用mSlideMarginLeft来控制滑块距离左边的距离。 canvas.drawBitmap(slideButton, mSlideMarginLeft, 0, paint); } ``` @@ -475,6 +475,44 @@ public class VerticalLayout extends ViewGroup { } ``` +上面介绍了通过继承`View`以及`ViewGroup`的方式来自定义`View`,平时开发过程中有时不需要继承他俩,我们直接继承功能接近 +的类进行扩展就好,例如:我想自定义一个`Meterial Design`样式的`EditText`。那我们该怎么实现呢? 当然是继承`EditText`了,它比`EditText`多了一条底下的线,那我们给它`draw`上就可以了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/meterial_edittext.png?raw=true) + +```java +public class MetrailEditText extends EditText { + private NinePatchDrawable mDrawable; + + public MetrailEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public MetrailEditText(Context context) { + super(context); + init(); + } + + private void init() { + setBackgroundResource(0); + mDrawable = (NinePatchDrawable) getResources().getDrawable(R.drawable.edittext_meterial_bg_activated); + } + + @Override + protected void onDraw(final Canvas canvas) { + super.onDraw(canvas); + mDrawable.setBounds(-getCompoundPaddingLeft(), 0, getWidth() + getCompoundPaddingRight(), getHeight()); + mDrawable.draw(canvas); + } +} +``` + +看到这里你可能会糊涂,这哪行啊? 我们的`edittext_meterial_bg_activated`可不是普通的图,当然是`9 patch`图了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/meterial_edittext_line.png?raw=true) + +当然你可以在`onDraw()`的时候加一个自定义线的颜色`mDrawable.setColorFilter(mLineColor, PorterDuff.Mode.SRC_ATOP);`等。 diff --git "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" index a087bea1..bbc18e9f 100644 --- "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" +++ "b/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" @@ -156,7 +156,8 @@ QHD 960*540 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/icon_size.jpg?raw=true) -为了解决适配的问题有时候还会使用9patch图,这里随便说一下。 +为了解决适配的问题有时候还会使用9patch图,这里随便说一下。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/9patch.jpg?raw=true) - `Stretchable area`代表拉伸的部分。 From b81bc349f9a75b7fbb1dd86539115ea009d8f199 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 15 Jun 2016 18:02:34 +0800 Subject: [PATCH 033/373] add images --- ...03\351\231\220\347\263\273\347\273\237.md" | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" diff --git "a/Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" "b/Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" new file mode 100644 index 00000000..9e7f8c7a --- /dev/null +++ "b/Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" @@ -0,0 +1,602 @@ +Android6.0权限系统 +=== + +`Android`权限系统是一个非常重要的安全问题,因为它只有在安装时会询问一次。一旦软件本安装之后,应用程序可以在用户毫不知情的情况下使用这些权限来获取所有的内容。 + +很多坏蛋会通过这个安全缺陷来收集用户的个人信息并使用它们来做坏事的情况就不足为奇了。 + +`Android`团队也意识到了这个问题。在经过了7年后,权限系统终于被重新设置了。从`Anroid 6.0(API Level 23)`开始,应用程序在安装时不会被授予任何权限,取而代之的是在运行时应用回去请求用户授予对应的权限。这样可以让用户能更好的去控制应用功能。例如,一个用户可能会同一个拍照应用使用摄像头的权限,但是不同授权它获取设备位置的权限。用户可以在任何时候通过去应用的设置页面来撤销授权。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/runtimepermission.jpg?raw=true) + +**注意:**上面请求权限的对话框不会自动弹出。开发者需要手动的调用。如果开发者调用了一些需要权限的功能,但是用户又拒绝授权的话,应用就会`crash`。 + +系统权限被分为两类,`normal`和`dangerous`: + +- `Normal Permissions`不需要用户直接授权,如果你的应用在清单文件中声明了Normal Permissions`,系统会自动授权该权限。 +- `Dangerous Permissions`可以让应用获取用户的私人数据。如果你的应用在清单文件中申请了`Dangerous Permissions`,那就必须要用户来授权给应用。 + +`Normal Permissions`: +`Normal Permission`是当用户安装或更新应用时,系统将授予应用所请求的权限,又称为`PROTECTION_NORMAL`(安装时授权的一类基本权限)。该类权限只需要在`manifest`文件中声明即可,安装时就授权,不需要每次使用时进行检查,而且用户不能取消以上授权。 + + +- ACCESS_LOCATION_EXTRA_COMMANDS +- ACCESS_NETWORK_STATE +- ACCESS_NOTIFICATION_POLICY +- ACCESS_WIFI_STATE +- BLUETOOTH +- BLUETOOTH_ADMIN +- BROADCAST_STICKY +- CHANGE_NETWORK_STATE +- CHANGE_WIFI_MULTICAST_STATE +- CHANGE_WIFI_STATE +- DISABLE_KEYGUARD +- EXPAND_STATUS_BAR +- GET_PACKAGE_SIZE +- INSTALL_SHORTCUT +- INTERNET +- KILL_BACKGROUND_PROCESSES +- MODIFY_AUDIO_SETTINGS +- NFC +- READ_SYNC_SETTINGS +- READ_SYNC_STATS +- RECEIVE_BOOT_COMPLETED +- REORDER_TASKS +- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +- REQUEST_INSTALL_PACKAGES +- SET_ALARM +- SET_TIME_ZONE +- SET_WALLPAPER +- SET_WALLPAPER_HINTS +- TRANSMIT_IR +- UNINSTALL_SHORTCUT +- USE_FINGERPRINT +- VIBRATE +- WAKE_LOCK +- WRITE_SYNC_SETTINGS + + + +下图为`Dangerous permissions and permission groups`: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/permgroup.png?raw=true) + + +在所有的`Android`版本中,你的应用都需要在`manifest`文件中声明`normal`和`dangerous`权限。然而声明所影响的效果会因系统版本和你应用的`target SDK lever`有关: + +- 如果设备运行的是`Android 5.1`或者之前的系统,或者应用的`targetSdkVersion`是22或者之前版本:如果你在`manifest`中声明了`dangerous permission`,用户需要在安装应用时授权这些权限。如果用户不授权这些权限,系统就不会安装该应用。(我试了下发现即使`targetSdkVersion`是22及以下,在6.0的手机上时,如果你安装时你不同意一些权限,也仍然可以安装的) +- 如果设备运行的是`Android 6.0`或者更高的系统,并且你应用的`targetSdkVersion`是23或者更高:应用必须在`manifest`文件中申请这些权限,而且必须要在运行时对所需要的`dangerous permission`申请授权。用户可以统一或者拒绝授权,并且及时用户拒绝了授权,应用在无法使用一些功能的情况下也要保证能继续运行。 + +也就是说新的运行时权限仅当我们设置`targetSdkVersion`是23及以上时才会起作用。 +如果你的`targtSdkVersion`低于23,那将被认为该应用没有经过`android 6.0`的测试,当该应用被安装到了6.0的手机上时,仍然会使用之前的旧权限规则,在安装时会提示所有需要的权限(这样做是有道理的,不然对于之前开发的应用,我们都要立马去修改让它适应6.0,来不及的话就导致6.0的手机都无法使用了,显然Android开发团队不会考虑不到这种情况),当然用户可以在安装的界面不允许一些权限,那当程序使用到了这些权限时,会崩溃吗?答案是在`Android 6.0`及以上的手机会直接`crash`,但是在`23`之前的手机上不会`crash`。 + +所以如果你的应用没有支持运行时权限的功能,那千万不要讲`targetSdkVersion`设置为23,否则就麻烦了。 + +> ***注意:***从`Android 6.0(API Level 23)`开始,即使应用的`targetSdkVersion`是比较低的版本,但是用户仍然可以在任何时候撤销对应用的授权。所以不管应用的`targetSdkVerison`是什么版本,你都要测试你的应用在不能获取权限时能不能正常运行。 + +下面介绍下如何使用`Android Support Library`来检查和请求权限。`Android`框架在`6.0`开始也提供了相同的方法。然而使用`support`包会比较简单,因为这样你就不需要在请求方法时判断当前的系统版本。(后面说的这几个类都是`android.support.v4`中的) + +###检查权限 + +如果应用需要使用`dangerous permission`,在任何时候执行需要该权限的操作时你都需要检查是否已经授权。用户可能会经常取消授权,所以即使昨天应用已经使用了摄像头,这也不能保证今天仍然有使用摄像头的权限。 + +为了检查是否可以使用该权限,调用`ContextCompat.checkSelfPermission()`。 +例如: +```java +// Assume thisActivity is the current activity +int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, + Manifest.permission.WRITE_CALENDAR); +``` +如果应用有该权限,该方法将返回`PackageManager.PERMISSION_GRANTED`,应用可以进行相关的操作。如果应用不能使用该权限,该方法将返回`PERMISSION_DENIED`,这是应用将必须要向用户申请该权限。 + + +###申请使用权限 + +如果应用需要使用清单文件中申明的`dangerous permission`,它必须要向用户申请来授权。`Android`提供了几种申请授权的方法。使用这些方法时将会弹出一个标准的系统对话框,该对话框不能自定义。 + +#####说明为什么应用需要使用这些权限 + +在一些情况下,你可能需要帮助用力理解为什么需要该权限。例如,一个用户使用了一个照相的应用,用户不会奇怪为什么应用申请使用摄像头的权限,但是用户可能会不理解为什么应用需要获取位置或者联系人的权限。在请求一个权限之前,你需要该用户一个说明。一定要切记不要通过说明来压倒用户。如果你提供了太多的说明,用户可能会感觉沮丧并且会卸载它。 + +一个你需要提供说明的合适时机就是在用户之前已经不同意授权该权限的情况下。如果一个用户继续尝试使用需要权限的功能时,但是之前确禁止了该权限的请求,这就可能是因为用户不理解为什么该功能需要使用该权限。在这种情况下,提供一个说明是非常合适的。 + +为了能找到用户可能需要说明的情况,`android`提供了一个工具类方法`ActivityCompat.shouldShowRequestPermissionRationale().`。如果应用之前申请了该权限但是用户拒绝授权后该方法会返回`true`。(在Android 6.0之前调用的时候会直接返回false) + +> ***注意:***如果用户之前拒绝了权限申请并且选择了请求权限对话框中的`Don’t ask again`选项,该方法就会返回`false`。如果设备策略禁止了该应用使用该权限,该方法也会返回`false`。(我测试的时候发现请求权限的对话框中并没有`Don’t asdk again`这一项) +> ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/request_permission_dialog.png?raw=true) + +#####申请需要的权限 + +如果应用没有所需的权限时,应用必须调用`ActivityCompat.requestPermissions (Activity activity, + String[] permissions, + int requestCode)`方法来申请对用的权限。参数传递对应所需的权限以及一个整数型的`request code`来标记该权限申请。 该方法是异步的:该方法会立即返回,在用户响应了请求权限的对话框之后,系统会调用对用的回调方法来通知结果,并且会传递在`reqeustPermissions()`方法中的`request code`。(在Android 6.0之前调用的时候会直接去调用`onRequestPermissionsResult()`的回调方法) +如图: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/requestpermission.jpg?raw=true) + + +下面是检查是否读取联系人权限,并且在必要时申请权限的代码: + +```java +// Here, thisActivity is the current activity +if (ContextCompat.checkSelfPermission(thisActivity, + Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + + // Should we show an explanation? + if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, + Manifest.permission.READ_CONTACTS)) { + + // Show an expanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + + } else { + + // No explanation needed, we can request the permission. + + ActivityCompat.requestPermissions(thisActivity, + new String[]{Manifest.permission.READ_CONTACTS}, + MY_PERMISSIONS_REQUEST_READ_CONTACTS); + + // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an + // app-defined int constant. The callback method gets the + // result of the request. + } +} +``` + +> ***注意:***当调用`requestPermissions()`方法时,系统会显示一个标准的对话框。应用不能指定或者改变该对话框。如果你想提供一些信息或者说明给用户,你需要在调用`requestPermissions()`之前处理。 + +#####处理请求权限的的结果 + +如果应用申请权限,系统会显示一个对话框。当用户相应后,系统会调用应用中的`onRequestPermissionsResult (int requestCode, + String[] permissions, + int[] grantResults)`方法并传递用户的操作结果。在应用中必须要重写该方法来查找授权了什么权限。该回调方法会传递你在`requestPermisssions()`方法中传递的`request code`。直接在`Activity`或者`Fragment`中重写`onRequestPermissionsResult()`方法即可。例如,申请`READ_CONTACTS`的权限可能会有下面的回到方法: + +```java +@Override +public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case MY_PERMISSIONS_REQUEST_READ_CONTACTS: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + // permission was granted, yay! Do the + // contacts-related task you need to do. + + } else { + + // permission denied, boo! Disable the + // functionality that depends on this permission. + } + return; + } + + // other 'case' lines to check for other + // permissions this app might request + } +} + +``` + +系统提示的对话框会描述应用所需的`permission groud`。它不会列出特定的权限。例如,如果你申请了`READ_CONTACTS`权限,系统的对话框只会说你的应用需要获取设备的联系人信息。用户只需要授权每个`permission group`一次。如果你应用需要申请其他任何一个在该`permission group`中的权限时,系统会自动授权。在申请这些授权时,系统会像用户明确通过系统对话框统一授权时一样去调用`onRequestPermissionsResult()`方法并且传递`PERMISSION_GRANTED`参数。 + +> ***注意:***虽然用户已经授权了同一`permission group`中其他的任何权限,但是应用仍然需要明确申请每个需要的权限。例外,`permission group`中的权限在以后可能会发生变化。 + +例如,假设在应用的`manifest`文件中同时声明了`READ_CONTACTS`和`WRITE_CONTACTS`权限。如果你申请`READ_CONTACTS`权限而且用户同意了该权限,如果你想继续申请`WRITE_CONTACTS`权限,系统不会与用户有任何交互就会直接进行授权。 + +如果用户拒绝了一个权限申请,你的应用进行合适的处理。例如,你的应用可能显示一个对话框来表明无法执行用户请求的需要该权限的操作。 + +如果系统向用户申请权限授权,用户选择了让系统以后不要再申请该权限。 在这种情况下,应用在任何时间调用`reqeustPermissions()`方法来再次申请权限时,系统都会直接拒绝该请求。系统会直接调用`onRequestPermissionResult()`回调方法并且传递`PERMISSION_DENIED`参数,和用户明确拒绝应用申请该权限时一样。 这就意味着在你调用`requestPermissions()`方法是,你无法确定是否会和用户有直接的交互操作。 + + +示例代码: +```java + +final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124; + +private void insertDummyContactWrapper() { + List permissionsNeeded = new ArrayList(); + + final List permissionsList = new ArrayList(); + if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION)) + permissionsNeeded.add("GPS"); + if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS)) + permissionsNeeded.add("Read Contacts"); + if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS)) + permissionsNeeded.add("Write Contacts"); + + if (permissionsList.size() > 0) { + if (permissionsNeeded.size() > 0) { + // Need Rationale + String message = "You need to grant access to " + permissionsNeeded.get(0); + for (int i = 1; i < permissionsNeeded.size(); i++) + message = message + ", " + permissionsNeeded.get(i); + showMessageOKCancel(message, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + requestPermissions(permissionsList.toArray(new String[permissionsList.size()]), + REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); + } + }); + return; + } + requestPermissions(permissionsList.toArray(new String[permissionsList.size()]), + REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); + return; + } + + insertDummyContact(); +} + +private boolean addPermission(List permissionsList, String permission) { + if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { + permissionsList.add(permission); + // Check for Rationale Option + if (!shouldShowRequestPermissionRationale(permission)) + return false; + } + return true; +} +``` + + +上面讲到的都是`Activity`中的使用方法,那`Fragment`中怎么授权呢? +如果在`Fragment`中使用,用`v13`包中的`FragmentCompat.requestPermissions()`和`FragmentCompat.shouldShowRequestPermissionRationale()`。 + + + +在`Fragment`中申请权限,不要使用`ActivityCompat.requestPermissions`, 直接使用`Fragment.requestPermissions`方法, +否则会回调到`Activity`的`onRequestPermissionsResult`。但是虽然你使用`Fragment.requestPermissions`方法,也照样回调不到`Fragment.onRequestPermissionsResult`中。这是`Android`的`Bug`,[详见](https://code.google.com/p/android/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Status%20Priority%20Owner%20Summary%20Stars%20Reporter%20Opened&groupby=&sort=&id=189121),`Google`已经在`23.3.0`修复了该问题,所以要尽快升级。 + +所以升级到`23.3.0`及以上就没问题了。如果不升级该怎么处理呢?就是在`Activity.onRequestPermissionsResult`方法中去手动调用每个`Fragment`的方法(当然你要判断下权限个数,不然申请一个权限的情况下会重复调用). +```java +@Override +public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + List fragments = getSupportFragmentManager().getFragments(); + if (fragments != null) { + for (Fragment fragment : fragments) { + fragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } +} +``` + +我简单的写了一个工具类: + +```java +public class PermissionUtil { + /** + * 在调用需要权限的功能时使用该方法进行检查。 + * + * @param activity + * @param requestCode + * @param iPermission + * @param permissions + */ + public static void checkPermissions(Activity activity, int requestCode, IPermission iPermission, String... permissions) { + handleRequestPermissions(activity, requestCode, iPermission, permissions); + } + + public static void checkPermissions(Fragment fragment, int requestCode, IPermission iPermission, String... permissions) { + handleRequestPermissions(fragment, requestCode, iPermission, permissions); + } + + public static void checkPermissions(android.app.Fragment fragment, int requestCode, IPermission iPermission, String... permissions) { + handleRequestPermissions(fragment, requestCode, iPermission, permissions); + } + + /** + * 在Actvitiy或者Fragment中重写onRequestPermissionsResult方法后调用该方法。 + * + * @param activity + * @param requestCode + * @param permissions + * @param grantResults + * @param iPermission + */ + public static void onRequestPermissionsResult(Activity activity, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + requestResult(activity, requestCode, permissions, grantResults, iPermission); + + } + + public static void onRequestPermissionsResult(Fragment fragment, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + requestResult(fragment, requestCode, permissions, grantResults, iPermission); + } + + public static void onRequestPermissionsResult(android.app.Fragment fragment, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + requestResult(fragment, requestCode, permissions, grantResults, iPermission); + } + + public static void requestPermission(T t, int requestCode, String... permission) { + List permissions = new ArrayList<>(); + for (String s : permission) { + permissions.add(s); + } + requestPermissions(t, requestCode, permissions); + } + + /** + * 在检查权限后自己处理权限说明的逻辑后调用该方法,直接申请权限。 + * + * @param t + * @param requestCode + * @param permissions + * @param + */ + public static void requestPermission(T t, int requestCode, List permissions) { + if (permissions == null || permissions.size() == 0) { + return; + } + requestPermissions(t, requestCode, permissions); + } + + public static boolean checkSelfPermission(Context context, String permission) { + if (context == null || TextUtils.isEmpty(permission)) { + throw new IllegalArgumentException("invalidate params: the params is null !"); + } + + context = context.getApplicationContext(); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + int result = ContextCompat.checkSelfPermission(context, permission); + if (PackageManager.PERMISSION_DENIED == result) { + return false; + } + } + + return true; + } + + private static void handleRequestPermissions(T t, int requestCode, IPermission iPermission, String... permissions) { + if (t == null || permissions == null || permissions.length == 0) { + throw new IllegalArgumentException("invalidate params"); + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + Activity activity = getActivity(t); + List deniedPermissions = getDeniedPermissions(activity, permissions); + if (deniedPermissions != null && deniedPermissions.size() > 0) { + + List rationalPermissions = new ArrayList<>(); + for (String deniedPermission : deniedPermissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, + deniedPermission)) { + rationalPermissions.add(deniedPermission); + } + } + + boolean showRational = false; + if (iPermission != null) { + showRational = iPermission.showRational(requestCode); + } + + if (rationalPermissions.size() > 0 && showRational) { + if (iPermission != null) { + iPermission.onRational(requestCode, deniedPermissions); + } + } else { + requestPermissions(t, requestCode, deniedPermissions); + } + } else { + if (iPermission != null) { + iPermission.onGranted(requestCode); + } + } + } else { + if (iPermission != null) { + iPermission.onGranted(requestCode); + } + } + } + + @Nullable + private static Activity getActivity(T t) { + Activity activity = null; + if (t instanceof Activity) { + activity = (Activity) t; + } else if (t instanceof Fragment) { + activity = ((Fragment) t).getActivity(); + } else if (t instanceof android.app.Fragment) { + activity = ((android.app.Fragment) t).getActivity(); + } + return activity; + } + + @TargetApi(Build.VERSION_CODES.M) + private static void requestPermissions(T t, int requestCode, List deniedPermissions) { + if (deniedPermissions == null || deniedPermissions.size() == 0) { + return; + } + // has denied permissions + if (t instanceof Activity) { + ((Activity) t).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } else if (t instanceof Fragment) { + ((Fragment) t).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } else if (t instanceof android.app.Fragment) { + ((android.app.Fragment) t).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } + } + + private static List getDeniedPermissions(Context context, String... permissions) { + if (context == null || permissions == null || permissions.length == 0) { + return null; + } + List denyPermissions = new ArrayList<>(); + for (String permission : permissions) { + if (!checkSelfPermission(context, permission)) { + denyPermissions.add(permission); + } + } + return denyPermissions; + } + + + private static void requestResult(T t, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + List deniedPermissions = new ArrayList<>(); + for (int i = 0; i < grantResults.length; i++) { + if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { + deniedPermissions.add(permissions[i]); + } + } + if (deniedPermissions.size() > 0) { + if (iPermission != null) { + iPermission.onDenied(requestCode); + } + } else { + if (iPermission != null) { + iPermission.onGranted(requestCode); + } + } + } + +} + +interface IPermission { + void onGranted(int requestCode); + + void onDenied(int requestCode); + + void onRational(int requestCode, List permissions); + + /** + * 是否需要提示用户该权限的作用,提示后需要再调用requestPermission()方法来申请。 + * + * @return true 为提示,false为不提示 + */ + boolean showRational(int requestCode); +} + +``` + +使用方法: +```java +public class MainFragment extends Fragment implements View.OnClickListener { + private Button mReqCameraBt; + private Button mReqContactsBt; + private Button mReqMoreBt; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_main, container, false); + findView(view); + initView(); + return view; + } + + private void findView(View view) { + mReqCameraBt = (Button) view.findViewById(R.id.bt_requestCamera); + mReqContactsBt = (Button) view.findViewById(R.id.bt_requestContacts); + mReqMoreBt = (Button) view.findViewById(R.id.bt_requestMore); + } + + private void initView() { + mReqCameraBt.setOnClickListener(this); + mReqContactsBt.setOnClickListener(this); + mReqMoreBt.setOnClickListener(this); + } + + + public static final int REQUEST_CODE_CAMERA = 0; + public static final int REQUEST_CODE_CONTACTS = 1; + public static final int REQUEST_CODE_MORE = 2; + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + PermissionUtil.onRequestPermissionsResult(this, requestCode, permissions, grantResults, mPermission); + } + + public void requestCamera() { + PermissionUtil.checkPermissions(this, REQUEST_CODE_CAMERA, mPermission, Manifest.permission.CAMERA); + } + + public void requestReadContacts() { + PermissionUtil.checkPermissions(this, REQUEST_CODE_CONTACTS, mPermission, Manifest.permission.READ_CONTACTS); + } + + public void requestMore() { + PermissionUtil.checkPermissions(this, REQUEST_CODE_MORE, mPermission, Manifest.permission.READ_CONTACTS, Manifest.permission.READ_CALENDAR, Manifest.permission.CALL_PHONE); + } + + private void showPermissionTipDialog(final int requestCode, final List permissions) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage("I want you permissions"); + builder.setTitle("Hello Permission"); + builder.setPositiveButton("确认", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + PermissionUtil.requestPermission(MainFragment.this, requestCode, permissions); + } + }); + builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + builder.create().show(); + } + + public IPermission mPermission = new IPermission() { + @Override + public void onGranted(int requestCode) { + Toast.makeText(getActivity(), "onGranted :" + requestCode, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onDenied(int requestCode) { + Toast.makeText(getActivity(), "onDenied :" + requestCode, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onRational(int requestCode, List permission) { + showPermissionTipDialog(requestCode, permission); + } + + @Override + public boolean showRational(int requestCode) { + switch (requestCode) { + case REQUEST_CODE_MORE: + return true; + + default: + break; + } + return false; + } + }; + + @Override + public void onClick(View v) { + int id = v.getId(); + switch (id) { + case R.id.bt_requestCamera: + requestCamera(); + break; + case R.id.bt_requestContacts: + requestReadContacts(); + break; + case R.id.bt_requestMore: + requestMore(); + break; + } + } +} + +``` + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 2fe0e964945fc2233cf355386ccf8b3a93aa87c3 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 15 Jun 2016 20:39:09 +0800 Subject: [PATCH 034/373] add tools.md --- ...70\345\205\263\345\267\245\345\205\267.md" | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" index 471e7390..a0cdcb9f 100644 --- "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" +++ "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" @@ -172,7 +172,9 @@ public void ProcessPeople() { 3. 点击`Start Method Profiling`图标开始查看。 4. 在查看完后点击`Stop Method Profiling`图标来显示`traceview`。 +大体的样子如下: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_1.png?raw=true) #####Traceview Layout @@ -181,20 +183,115 @@ public void ProcessPeople() { - `timeline panel`-展示了每个线程和方法的起始和结束 - `profile panel`-提供了一个方法中的执行内容的简述 +#####Timeline Panel +每个线程都会以时间从左往右递增的方式在单独的一行中显示它的执行情况, +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_timeline.png?raw=true) +#####Profile Panel + +显示了一个方法所消耗的时间的概要情况。在表格中会同时显示`inclusive`和`exclusive`的时间。`Exclusive`的时间是该方法所消耗的时间。`InClusive`的时间是该方法消耗的时间加上任何调的方法所消耗的时间。我们简单的将调用的方法叫做`parents`,被调用的方法叫做`children`。如果一个方法被调用,它会显示对应的`parents`和`children`。`parents`会显示一个紫色的背景,`children`会显示一个黄色的背景。最后一列显示了该方法所调用的总数的调用数。在下面的图中我们能看到一共有14个地方调用了`LoadListener.nativeFinished()` . + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_profile.png?raw=true) + +- Name 方法名 +- Inclusive CPU Time, CPU在处理该方法以及所有子方法(被它调用的所有方法)所消耗的时间。 +- Exlusive CPU Time, CPU在处理该方法的耗时。 +- Inclusive/Exclusive Real Time, 从方法开始执行到执行结束的耗时。 +- Cal+Rec, 这个方法被调用的次数,以及递归被调用的次数。 +- CPU/Real time per Call, 在处理这个方法时的`CPU`耗时的平均值。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview-getview.png?raw=true) + +上图中`getView`方法被调用了12次,每次`CPU`消耗2.8秒,但是每次调用的总耗时是162秒,这里肯定有问题。 +而看看这个方法的children,我们可以看到这其中的每个方法在耗时方面是如何分布的。Thread.join()方法战局了98%的inclusive real time。这个方法在等待另一个线程结束的时候被调用。在Children中另外一个方法就是Tread.start()方法,而之所以整个方法耗时很长,我猜测是因为在getView()方法中启动了线程并且在等待它的结束。 + +但是这个线程在哪儿? + +我们在getView()方法中并不能看到这个线程做了什么,因为这段逻辑不在getView()方法之中。于是我找到了Thread.run()方法,就是在线程被创建出来时候所运行的方法。而跟随这个方法一路向下,我找到了问题的元凶。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview-thread.png?raw=true) + +我发现了BgService.doWork()方法的每次调用花费了将近14毫秒,并且有四十个这东西!而且getView()中还有可能调用多次这个方法,这就解释了为什么getView()方法执行时间如此之长。这个方法让CPU长时间的保持在了繁忙状态。而看看Exclusive CPU time,我们可以看到他占据了80%的CPU时间!此外,根据Exclusive CPU time排序,可以帮我们更好的定位那些耗时很长的方法,而他们很有可能就是造成性能问题的罪魁祸首。 + +关注这些耗时方法,例如getView(),View#onDraw()等方法,可以很好的帮助我们寻找为什么应用运行缓慢的原因。但有些时候,还会有一些其他的东西来占用宝贵的CPU资源,而这些资源如果被运用在UI的绘制上,也许我们的应用会更加流畅。Garbage Collector垃圾回收机制会不时的运行,回收那些没用的对象,通常来讲这不会影响我们在前台运行的程序。但如果GC被运行的过于频繁,他同样可以影响我们应用的执行效率。而我们该如何知道回收的是否过于频繁了呢… + +###Monitors + +在`Android Studio`中下方的`Android Monitor`中可以看到`Monitors`工具栏,它能不断的去检测内存、网络、CPU的消耗情况。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/monitor.png?raw=true) + +我们可以直接点击`Dump Java Heap`或者`Call GC`等按钮,以便更好的去观察内存的使用情况。点击后会生成一个`.href`的文件。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/dump_href.png?raw=true) +在左边能看到所有堆内存中的实例。后面会显示他所占用的内存大小。 +对于内存泄漏的分析可以使用`MAT`或者`LeakCanary`来进行。这里就不仔细说了。 + +上面也显示了`GPU`的使用情况,这里要说一句,如果想要显示它,必须要在手机的开发者中心中开启`GPU显示配置文件`选项,将其设置为显示与`adb shell dumpsys gfxinfo`。然后再点击`Studio`中的按钮重新开始就可以看到了。 每一条线意味着一帧被绘制出来了。而线的颜色又代表不同的阶段: + +- Draw(蓝色)代表着`View.onDraw()`方法。如果这个值很高就说明可能是该`View`比较复杂。 在这个环节会创建/刷新DisplayList中的对象,这些对象在后面会被转换成GPU可以明白的OpenGL命令。而这个值比较高可能是因为view比较复杂,需要更多的时间去创建他们的display list,或者是因为有太多的view在很短的时间内被创建。 +- Prepare(紫色),从`Android 6.0`开始,一个新的线程被引进来帮助`UI`线程进行绘制。 这个线程叫做`Render Thread`。它负责转换它负责转换display list到OpenGL命令并且送至GPU。在这过程中,UI线程可以继续开始处理后面的帧。而在UI线程将所有资源传递给RenderThread过程中所消耗的时间,就是紫色阶段所消耗的时间。如果在这过程中有很多的资源都要进行传递,display list会变得过多过于沉重,从而导致在这一阶段过长的耗时。 + +- Process(红色) 执行Display list中的内容并创建OpenGL命令。如果有过多或者过于复杂的display list需要执行的话,那么这阶段会消耗较长的时间,因为这样的话会有很多的view被重绘。而重绘往往发生在界面的刷新或是被移动出了被覆盖的区域。 + +- Execute (黄色) – 发送OpenGL命令到GPU。这个阶段是一个阻塞调用,因为CPU在这里只会发送一个含有一些OpenGL命令的缓冲区给GPU,并且等待GPU返回空的缓冲区以便再次传递下一帧的OpenGL命令。而这些缓冲区的总量是一定的,如果GPU太过于繁忙,那么CPU则会去等待下一个空缓冲区。所以,如果我们看到这一阶段耗时比较长,那可能是因为GPU过于繁忙的绘制UI,而造成这个的原因则可能是在短时间内绘制了过于复杂的view。 + +- Measure/Layout(绿色) 代表`Measure`和`Layout`的时间。 ###Hierarchy Viewer +布局分析工具,非常常用。 + +在`Android Device Monitor`中打开`Hierarchy Viewer`即可。 + +- 连接你的手机或者模拟器。 + 出于安全性考虑,`Hierarchy Viewer`只能连接开发者版的系统的手机。 +- 运行程序,并且让界面显示出来。 +- 启动`hierarchy view`工具,接着就能看到左边栏显示出了对应的设备,展开后可以看到一些组件的名称。这个页面包含了应用的界面以及系统的界面。选择你的应用中想要查看的界面即可。 + +但是我并打不开。 +如果你的手机是`Android 4.1`及以上版本你必须要在你的电脑上设置一个`ANDROID_HVPROTO`DE 环境变量才可以。 + +- Windows + 增加一个名为`ANDROID_HVPROTO`值为`ddm`的环境变量就可以了。 +- Mac + - 打开`.bash_profile` + - `touch .bash_profile`创建 + - `open -e .bash_profile`打开 + - 添加 + ``` + #Hierarchy Viewer Variable + export ANDROID_HVPROTO=ddm + + ``` + - `source .bash_profile` + + +###过度绘制 + +在开发者选项中将调试GPU过度绘制设置为显示过度绘制区域,就能看到程序的绘制情况。 +过度绘制往往发生在我们需要在一个东西上面绘制另外一个东西,例如在一个红色的背景上画一个黄色的按钮。那么GPU就需要先画出红色背景,再在他上面绘制黄色按钮,此时过度绘制就是不可避免的了。如果我们有太多层需要绘制,那么则会过度的占用GPU导致我们每帧消耗的时间超过16毫秒。 +这些过度绘制可能发生在我们给Activity或Fragment设置了全屏的背景,同时又给ListView以及ListView的条目设置了背景色。而通过只设置一次背景色即可解决这样的问题。 + +注意:默认的主题会为你指定一个默认的全屏背景色,如果你的activity又一个不透平的背景盖住了默认的背景色,那么你可以移除主题默认的背景色,这样也会移除一层的过度绘制。这可以通过配置主题配置或是通过代码的方法,在onCreate()方法中调用getWindow().setBackgroundDrawable(null)方法来实现。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/overdraw-gif.gif?raw=true) + +越红说明绘制 -###GPU Profiling ###Hardware Acceleration +在Honeycomb版本中引入了硬件加速(Hardware Accleration)后,我们的应用在绘制的时候就有了全新的绘制模型。它引入了DisplayList结构,用来记录View的绘制命令,以便更快的进行渲染。但还有一些很好的功能开发者们往往会忽略或者使用不当——View layers。 + +使用View layers(硬件层),我们可以将view渲染入一个非屏幕区域缓冲区(off-screen buffer,前面透明度部分提到过),并且根据我们的需求来操控它。这个功能主要是针对动画,因为它能让复杂的动画效果更加的流畅。而不使用硬件层的话,View会在动画属性(例如coordinate, scale, alpha值等)改变之后进行一次刷新。而对于相对复杂的view,这一次刷新又会连带它所有的子view进行刷新,并各自重新绘制,相当的耗费性能。使用View layers,通过调用硬件层,GPU直接为我们的view创建一个结构,并且不会造成view的刷新。而我们可以在避免刷新的情况下对这个结构进行进行很多种的操作,例如x/y位置变换,旋转,透明度等等。总之,这意味着我们可以对一个让一个复杂view执行动画的同时,又不会刷新!这会让动画看起来更加的流畅。下面这段代码我们该如何操作: + +在阅读了ViewPager的源码后,我发现了在滑动的时候会自动为左右两页启动一个硬件层,并且在滑动结束后移除掉。 +在两页间滑动的时候创建硬件层也是可以理解的,但对我来说小有不幸。通常来讲加入硬件层是为了让ViewPager的滑动更加流畅,毕竟它们相对复杂。 +有关如何使用`Hardware Layer`请参考之前写的文章:[通过Hardware Layer提高动画性能](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E9%80%9A%E8%BF%87Hardware%20Layer%E6%8F%90%E9%AB%98%E5%8A%A8%E7%94%BB%E6%80%A7%E8%83%BD.md) From 342511f41c9a230cc4f69b87aaa20dbce0923a8e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 16 Jun 2016 20:19:19 +0800 Subject: [PATCH 035/373] add files --- ...70\345\205\263\345\267\245\345\205\267.md" | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" index a0cdcb9f..50b9ca75 100644 --- "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" +++ "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" @@ -248,7 +248,12 @@ public void ProcessPeople() { - 连接你的手机或者模拟器。 出于安全性考虑,`Hierarchy Viewer`只能连接开发者版的系统的手机。 - 运行程序,并且让界面显示出来。 -- 启动`hierarchy view`工具,接着就能看到左边栏显示出了对应的设备,展开后可以看到一些组件的名称。这个页面包含了应用的界面以及系统的界面。选择你的应用中想要查看的界面即可。 +- 启动`hierarchy view`工具,接着就能看到左边栏显示出了对应的设备,展开后可以看到一些组件的名称。这个页面包含了应用的界面以及系统的界面。选择你的应用中想要查看的界面直接双击就可以了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gettingstarted_image005.png?raw=true) +如果你的界面显示的不是这个样子,少一些东西的话,你可以使用`Window>Reset Perspective`来重置样式。 + + 但是我并打不开。 如果你的手机是`Android 4.1`及以上版本你必须要在你的电脑上设置一个`ANDROID_HVPROTO`DE 环境变量才可以。 @@ -267,6 +272,56 @@ public void ProcessPeople() { ``` - `source .bash_profile` +如果配置完成后仍然不能用的话,你可以: + +- 关闭`Android Studio`. +- 执行`add kill-server`,然后`adb start-server`. +- 通过命令行开始`hierarchyviewer`. + +好了,我们直接打开自己的页面进行查看布局吧(有点慢)。 +它是介个样子滴 : +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hierarchy_vierwe_page.png?raw=true) + +我们可以看到所有结构,点击对一个的节点,能直接看到该界面的`UI`效果,并展示该布局包含多少个`View`,以及该布局`measure,draw,layout`所消耗的时间。选中`Show Extras`选项可以看到在右下角看到界面效果。 + +选中要查看的节点后点击`Profile Node`选项,可以看到如下界面: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hierarchy_profile_node.png?raw=true) + +可以看到每个布局都出现了三个点。有不同的颜色,从左到右这三个点分别表示: + +- 左边的点代表`Draw`阶段。 +- 中间的点代表了`Layout`阶段。 +- 右边的点代表了`Execute`阶段。 + +这三个点有分别对应了`pipeline`中的不同阶段,如下图: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gettingstarted_image015.png?raw=true) + +不同的颜色代表不同的性能: + +- 绿色代表渲染的非常快,至少是其他`View`的一半。 +- 黄色代表`View`渲染比最后那一半的`View`快。 +- 红色代表`View`渲染是几乎是在最慢的那部分中间。 + + + +Interpreting Hierarchy Viewer Profiling Results +Hierarchy Viewer measures the relative performance of a node, so there are always red nodes in a profile, and it doesn't necessarily mean that view is too slow for the users of your app. + +Hierarchy Viewer software rasterizes your Activity to acquire the timing information. Rasterization is the process of taking a high-level primitive, such as a circle or a vector font, and turning it into pixels on the screen. Typically, rasterization is done by the GPU on your device, but in the case of software rasterization, rendering is done on the CPU with ordinary software. This means that the absolute reported timings are correct relative to each other, but are bloated and vary depending on the overall and changing CPU workload on your device and PC. Profile several times to get a feel for the average measurements. + +The following are guidelines for interpreting Hierarchy Viewer profiling output. + +A red node is a potential problem in any situation where your app has unexpectedly slow performance. In a relative setting, there is always a slowest node; make sure it is the node you expect. The following examples illustrate how to interpret red dots. + +Look for red dots in leaf nodes or view groups with only a few children. This might point to a problem. Your app may not be slow, or it may not be slow on your device, but you need to be aware of why that dot is red. Systrace or Traceview can give you additional information. +If you have a view group with many children and a red measure phase, take a look at the children to see how they are performing. +A view with yellow or even red dots might not be performing slowly on the device. That's where the actual numbers are helpful. Systrace or Traceview can give you additional information. +If the root view of a hierarchy has a red measure phase, red layout phase, and yellow draw phase, this is somewhat typical, because it's the parent of all the other views. +If a leaf node in a tree with 20+ views has a red draw phase, this is a problem. Check your OnDraw method for code that shouldn't be there. + + + ###过度绘制 @@ -285,12 +340,17 @@ public void ProcessPeople() { 在Honeycomb版本中引入了硬件加速(Hardware Accleration)后,我们的应用在绘制的时候就有了全新的绘制模型。它引入了DisplayList结构,用来记录View的绘制命令,以便更快的进行渲染。但还有一些很好的功能开发者们往往会忽略或者使用不当——View layers。 -使用View layers(硬件层),我们可以将view渲染入一个非屏幕区域缓冲区(off-screen buffer,前面透明度部分提到过),并且根据我们的需求来操控它。这个功能主要是针对动画,因为它能让复杂的动画效果更加的流畅。而不使用硬件层的话,View会在动画属性(例如coordinate, scale, alpha值等)改变之后进行一次刷新。而对于相对复杂的view,这一次刷新又会连带它所有的子view进行刷新,并各自重新绘制,相当的耗费性能。使用View layers,通过调用硬件层,GPU直接为我们的view创建一个结构,并且不会造成view的刷新。而我们可以在避免刷新的情况下对这个结构进行进行很多种的操作,例如x/y位置变换,旋转,透明度等等。总之,这意味着我们可以对一个让一个复杂view执行动画的同时,又不会刷新!这会让动画看起来更加的流畅。下面这段代码我们该如何操作: - +使用View layers(硬件层),我们可以将view渲染入一个非屏幕区域缓冲区(off-screen buffer,前面透明度部分提到过),并且根据我们的需求来操控它。这个功能主要是针对动画,因为它能让复杂的动画效果更加的流畅。而不使用硬件层的话,View会在动画属性(例如coordinate, scale, alpha值等)改变之后进行一次刷新。而对于相对复杂的view,这一次刷新又会连带它所有的子view进行刷新,并各自重新绘制,相当的耗费性能。使用View layers,通过调用硬件层,GPU直接为我们的view创建一个结构,并且不会造成view的刷新。而我们可以在避免刷新的情况下对这个结构进行进行很多种的操作,例如x/y位置变换,旋转,透明度等等。总之,这意味着我们可以对一个让一个复杂view执行动画的同时,又不会刷新!这会让动画看起来更加的流畅。 在阅读了ViewPager的源码后,我发现了在滑动的时候会自动为左右两页启动一个硬件层,并且在滑动结束后移除掉。 在两页间滑动的时候创建硬件层也是可以理解的,但对我来说小有不幸。通常来讲加入硬件层是为了让ViewPager的滑动更加流畅,毕竟它们相对复杂。 + +是的,但是再使用硬件layers的时候还是有几点要牢记在心: + +- 回收 – 硬件层会占用GPU中的一块内存。只在必要的时候使用他们,比如动画,并且事后注意回收。例如在上面ObjectAnimator的例子中,我们增加了一个动画结束监听以便在动画结束后可以移除硬件层。而在Property animator的例子中,我们使用了withLayers(),这会在动画开始时候自动创建硬件层并且在结束的时候自动移除。 +- 如果你在调用了硬件View layers后改变了View,那么会造成硬件硬件层的刷新并且再次重头渲染一遍view到非屏幕区域缓存中。这种情况通常发生在我们使用了硬件层暂时还不支持的属性(目前为止,硬件层只针对以下几种属性做了优化:otation、scale、x/y、translation、pivot和alpha)。例如,如果你另一个view执行动画,并且使用硬件层,在屏幕滑动他们的同时改变他的背景颜色,这就会造成硬件层的持续刷新。而以硬件层的持续刷新所造成的性能消耗来说,可能让它在这里的使用变得并不那么值。 + 有关如何使用`Hardware Layer`请参考之前写的文章:[通过Hardware Layer提高动画性能](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E9%80%9A%E8%BF%87Hardware%20Layer%E6%8F%90%E9%AB%98%E5%8A%A8%E7%94%BB%E6%80%A7%E8%83%BD.md) From d93fb5c0c129c0ec2009c71e595843ea631ea8dc Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 20 Jun 2016 11:51:55 +0800 Subject: [PATCH 036/373] =?UTF-8?q?add=20=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9B=B8=E5=85=B3=E5=B7=A5=E5=85=B7.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...70\345\205\263\345\267\245\345\205\267.md" | 118 +++++++++++++----- 1 file changed, 86 insertions(+), 32 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" index 50b9ca75..5bcbc604 100644 --- "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" +++ "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" @@ -4,6 +4,69 @@ 有关性能优化的文章请参考[性能优化](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.md)和[布局优化](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E5%B8%83%E5%B1%80%E4%BC%98%E5%8C%96.md) +###DDMS(百宝箱) + +#####查看进程的内存使用 + +`DDMS`可以让你查看到一个进程使用的堆内存。 +1. 在`Device`了表选择你想查看的进程。 +2. 点击`Update Heap`按钮来开启查看进程堆内存信息。 +3. 在`Heap`页面,点击`Cause GC`来执行垃圾回收。 +4. 在列表中点击一个对象类型来查看它所分配的内存大小。 + +#####追踪对象的内存分配情况 + +1. 在`Device`了表选择你想查看的进程。 +2. 在`Allocation Tracker`页面,点击`Start Tracking`按钮来开始追踪。 +3. 点击`Get Allocations`来查看从你点击`Start Tracking`之后分配内存的对象列表。 你可以再次点击`Get Allocations`来添加新分配内存的对象列表。 +4. 停止追踪或者清除数据可以点击`Stop Tracking`按钮。 +5. 在列表中单独点击一行来查看详细的信息。 + +#####使用文件系统 + +`DDMS`提供了一个文件浏览器来供你查看、拷贝、删除文件。 +1. 在`Device`页面,选中你想要查看的设备。 +2. 从设备中拷贝文件,用文件浏览器选择文件后点击`Pull file`按钮。 +3. 拷贝文件到设备中,点击`Push file`按钮。 + +#####检查线程信息 + + +`Thread`页面可以显示指定进程中当前正在运行的线程。 + +1. 在`Device`页面,选择想要查看的进程。 +2. 点击`Update Threads`按钮。 +3. 在`Thread`页面,你可以查看对应的线程信息。 + +#####启动方法分析 + +方法分析可以追踪一个方法的特定信息,例如调用数量,执行时间、耗时等。如果想要更精确的控制想要收集的数据,可以使用`startMethodTracing()`和`stopMethodTracing()`方法。 + +在你开始使用方法分析之前请知晓如下限制: + +- 在`Android 2.1`及以前的设备必须有`SD`卡,并且你的应用必须有写入`SD`卡的权限。 +- 在`Android 2.2`及以后的设备中不需要`SD`卡,`trace log`文件会直接被导入到开发机上。 + +开启方法分析: + +1. 在`Device`页面,选择想要开启方法分析的进程。 +2. 点击`Start Method Profiling`按钮。 +3. 在`Android 4.4`及以后,可以选择`trace_based profiling`或者基本的`sample-based profiling`选项。在之前的版本智能使用`trace-based profiling`。 +4. 在应用中操作界面来执行你想要分析的方法。 +5. 点击`Stop Method Profiling`按钮。`DDMS`会停止分析并且将在`Start Method Profiling`和`Stop Method Profiling`中间手机的方法分析数据使用`Traceview`打开。 + + +##### 使用网络流量工具 + +从`Android 4.0`开始,`DDMS`包含了一个网络使用情况的页面来支持跟踪应用的网络请求。 + +如图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ddms-network.png?raw=true)。 + +通过检测数据交互的频繁性以及在每次连接中数据的传递量,你可以知道应用中具体可以做的更好更高效的地方。 +想要更好的查看引起数据交互的地方,可以使用`TrafficsStats`这个`API`,它也可以使用`setThreadStatsTag()`方法来设置数据使用情况的`tag`, + 首先要讲到的是`Systrace`,之前在淘宝面试的时候被问到,如有查找`UI`绘制卡顿的问题,我没答上来。早点知道`Systrace`就好了(当然只知道工具是不够的,也要懂得里面的原理)。 ### Systrace @@ -45,7 +108,7 @@ `OK`了。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace_file.png?raw=true)。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace_file.png?raw=true =)。 看不懂!!! @@ -120,9 +183,6 @@ public void ProcessPeople() { } ``` - - - ###Traceview `Traceview`是一个性能测试工具,展示了所有方法的运行时间。 @@ -174,7 +234,7 @@ public void ProcessPeople() { 大体的样子如下: -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_1.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_1.png?raw=true) #####Traceview Layout @@ -223,11 +283,27 @@ public void ProcessPeople() { ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/monitor.png?raw=true) -我们可以直接点击`Dump Java Heap`或者`Call GC`等按钮,以便更好的去观察内存的使用情况。点击后会生成一个`.href`的文件。 +我们可以直接点击`Dump Java Heap`或者`Call GC`等按钮,以便更好的去观察内存的使用情况。点击后会生成一个`.hprof`的文件。生成后直接使用`Studio`打开`.hprof`文件即可。 + +说到这里插一嘴,[有关Java垃圾回收机制请参考](https://github.com/CharonChui/AndroidNote/blob/master/Java%E5%9F%BA%E7%A1%80/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md) + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/dump_href.png?raw=true) -在左边能看到所有堆内存中的实例。后面会显示他所占用的内存大小。 +在左边能看到所有堆内存中的实例。后面会显示他所占用的内存大小。 + 对于内存泄漏的分析可以使用`MAT`或者`LeakCanary`来进行。这里就不仔细说了。 +#####不同虚拟机的内存管理 + +`Android Monitor`使用的`Virtual Machine(VM)`: + +- `Android 4.3(API Level 18)`及以前的版本使用`Dalvik VM` . +- `Android 4.4(API Level 19)`默认使用`Dalvik VM`,`Android RunTime(ART)`是可选的。 +- `Android 4.3(API Level 18)`及更高的版本使用`ART VM`. + +虚拟之执行垃圾回收。Dalvik虚拟机使用一个标记和清除垃圾收集方案。`ART`虚拟机使用分代收集算法与标记和清楚手机算法相结合的方式。 `Logcat`会显示一些垃圾回收相关的信息。 + +#####GPU使用情况 + 上面也显示了`GPU`的使用情况,这里要说一句,如果想要显示它,必须要在手机的开发者中心中开启`GPU显示配置文件`选项,将其设置为显示与`adb shell dumpsys gfxinfo`。然后再点击`Studio`中的按钮重新开始就可以看到了。 每一条线意味着一帧被绘制出来了。而线的颜色又代表不同的阶段: - Draw(蓝色)代表着`View.onDraw()`方法。如果这个值很高就说明可能是该`View`比较复杂。 在这个环节会创建/刷新DisplayList中的对象,这些对象在后面会被转换成GPU可以明白的OpenGL命令。而这个值比较高可能是因为view比较复杂,需要更多的时间去创建他们的display list,或者是因为有太多的view在很短的时间内被创建。 @@ -254,7 +330,6 @@ public void ProcessPeople() { 如果你的界面显示的不是这个样子,少一些东西的话,你可以使用`Window>Reset Perspective`来重置样式。 - 但是我并打不开。 如果你的手机是`Android 4.1`及以上版本你必须要在你的电脑上设置一个`ANDROID_HVPROTO`DE 环境变量才可以。 @@ -268,7 +343,6 @@ public void ProcessPeople() { ``` #Hierarchy Viewer Variable export ANDROID_HVPROTO=ddm - ``` - `source .bash_profile` @@ -304,24 +378,7 @@ public void ProcessPeople() { - 红色代表`View`渲染是几乎是在最慢的那部分中间。 - -Interpreting Hierarchy Viewer Profiling Results -Hierarchy Viewer measures the relative performance of a node, so there are always red nodes in a profile, and it doesn't necessarily mean that view is too slow for the users of your app. - -Hierarchy Viewer software rasterizes your Activity to acquire the timing information. Rasterization is the process of taking a high-level primitive, such as a circle or a vector font, and turning it into pixels on the screen. Typically, rasterization is done by the GPU on your device, but in the case of software rasterization, rendering is done on the CPU with ordinary software. This means that the absolute reported timings are correct relative to each other, but are bloated and vary depending on the overall and changing CPU workload on your device and PC. Profile several times to get a feel for the average measurements. - -The following are guidelines for interpreting Hierarchy Viewer profiling output. - -A red node is a potential problem in any situation where your app has unexpectedly slow performance. In a relative setting, there is always a slowest node; make sure it is the node you expect. The following examples illustrate how to interpret red dots. - -Look for red dots in leaf nodes or view groups with only a few children. This might point to a problem. Your app may not be slow, or it may not be slow on your device, but you need to be aware of why that dot is red. Systrace or Traceview can give you additional information. -If you have a view group with many children and a red measure phase, take a look at the children to see how they are performing. -A view with yellow or even red dots might not be performing slowly on the device. That's where the actual numbers are helpful. Systrace or Traceview can give you additional information. -If the root view of a hierarchy has a red measure phase, red layout phase, and yellow draw phase, this is somewhat typical, because it's the parent of all the other views. -If a leaf node in a tree with 20+ views has a red draw phase, this is a problem. Check your OnDraw method for code that shouldn't be there. - - - +`Hierarchy Viewer`测量的是相对的表现能力,所以总会有一个红色的节点,它并不意味者该`View`一定是绘制的太慢。 ###过度绘制 @@ -332,9 +389,6 @@ If a leaf node in a tree with 20+ views has a red draw phase, this is a problem. 注意:默认的主题会为你指定一个默认的全屏背景色,如果你的activity又一个不透平的背景盖住了默认的背景色,那么你可以移除主题默认的背景色,这样也会移除一层的过度绘制。这可以通过配置主题配置或是通过代码的方法,在onCreate()方法中调用getWindow().setBackgroundDrawable(null)方法来实现。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/overdraw-gif.gif?raw=true) -越红说明绘制 - - ###Hardware Acceleration @@ -354,8 +408,8 @@ If a leaf node in a tree with 20+ views has a red draw phase, this is a problem. 有关如何使用`Hardware Layer`请参考之前写的文章:[通过Hardware Layer提高动画性能](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E9%80%9A%E8%BF%87Hardware%20Layer%E6%8F%90%E9%AB%98%E5%8A%A8%E7%94%BB%E6%80%A7%E8%83%BD.md) - + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 080aae2113a6848bcc6c25de5e3b77b85eb392b6 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 20 Jun 2016 19:47:11 +0800 Subject: [PATCH 037/373] add file --- ...50\350\247\243\344\275\277\347\224\250.md" | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" new file mode 100644 index 00000000..185131f0 --- /dev/null +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -0,0 +1,72 @@ +RxJava详解 +=== + +###简介 + +> Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. + +更通俗的意思是为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且是供指定的工具或框架使用的。 +`Annontation`像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。 + +`Annotation`其实是一种接口。通过反射来访问`annotation`信息。相关类(框架或工具中的类)根据这些信息来决定如何使用该程序元素或改变它们的行为。 +`Annotation`是不会影响程序代码的执行,无论`annotation`怎么变化,代码都始终如一地执行。 +`Java`语言解释器在工作时会忽略这些`annotation`,因此在`JVM`中这些`annotation`是“不起作用”的,只能通过配套的工具才能对这些`annontaion`类型的信息进行访问和处理。 + +###说明 +- `Annotation`的声明是通过关键字`@interface`。这个关键字会去继承`Annotation`接口。 +- `Annotation`的方法定义是独特的、受限制的。 + `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 + +例如: +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface UseCase { + public int id(); + public String description() default "no description"; +} +``` + +###作用 + +`Annotation`一般作为一种辅助途径,应用在软件框架或者工具中。让这些工具类可以根据不同的`Annotation`注解信息来采取不同的处理过程或者改变相应程的行为。 + + +###Annotation分类 + + +#####标准的`Annotaion` + +从`jdk 1.5`开始,自带了三种标准的`annotation`类型: + +- `Override` + 它是一种`marker`类型的`Annotation`,用来标注方法,说明被它标注的方法是重载了父类中的方法。如果我们使用了该注解到一个没有覆盖父类方法的方法时,编译器就会提示一个编译错误的警告。 + +- `Deprecated` + 它也是一种`marker`类型的`Annotation`。当方法或者变量使用该注解时,编译器就会提示该方法已经废弃。 + +- `SuppressWarnings` + 它不是`marker`类型的`Annotation`。用户告诉编译器不要再对该类、方法或者成员变量进行警告提示。 + + +#####元`Annotation` + +元`Annotation`是指用来定义`Annotation`的`Annotation`。 + +- `@Retention + +- `@Target` + +- `@Inherited` + +- `@Documented` + +#####自定义`Annotation` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From b10f00d0e5898bc841cab782640eedd915e9d050 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 21 Jun 2016 20:00:08 +0800 Subject: [PATCH 038/373] =?UTF-8?q?add=20=E6=B3=A8=E8=A7=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\350\247\243\344\275\277\347\224\250.md" | 165 +++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 185131f0..769e1a90 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -15,7 +15,9 @@ RxJava详解 ###说明 - `Annotation`的声明是通过关键字`@interface`。这个关键字会去继承`Annotation`接口。 - `Annotation`的方法定义是独特的、受限制的。 - `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 + `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 + 注解如果只有一个默认属性,可直接用`value()`函数。一个属性也没有则表示该`Annotation`为`Mark Annotation`。 + 例如: ```java @@ -31,6 +33,12 @@ public @interface UseCase { `Annotation`一般作为一种辅助途径,应用在软件框架或者工具中。让这些工具类可以根据不同的`Annotation`注解信息来采取不同的处理过程或者改变相应程的行为。 +具体可分为如下三类: + +- 标记,用于告诉编译器一些信息 +- 编译时动态处理,如动态生成代码 +- 运行时动态处理,如得到注解信息 + ###Annotation分类 @@ -54,17 +62,166 @@ public @interface UseCase { 元`Annotation`是指用来定义`Annotation`的`Annotation`。 - `@Retention + 保留时间,可为`RetentionPolicy.SOURCE(源码时)`、`RetentionPolicy.CLASS(编译时)`、`RetentionPolicy.RUNTIME(运行时)`,默认为`CLASS`。如果值为`RetentionPolicy.SOURCE`那大多都是`Mark Annotation`,例如:`Override`、`Deprecated`、`Suppress Warnings`。`SOURCE`表示仅存在于源码中,在`class`文件中不会包含。`CLASS`表示会在`class`文件中存在,但是运行时无法获取。`RUNTIME`表示会在`class`文件中存在,并且在运行时可以通过反射获取。 - `@Target` - + 用来标记可进行修饰哪些元素,例如`ElementType.TYPE`、`ElementType.METHOD`、`ElementType.CONSTRUCTOR`、`ElementType.FIELD`、`ElementType.PARAMETER`等,如果未指定则默认为可修饰所有。 - `@Inherited` - + 子类是否可以继承父类中的该注解。 - `@Documented` + 是否会保存到`javadoc`文档中。 + +###自定义`Annotation` + + + +###`Annotation`解析 + + +当Java源代码被编译时,编译器的一个插件annotation处理器则会处理这些annotation。处理器可以产生报告信息,或者创建附加的Java源文件或资源。如果annotation本身被加上了RententionPolicy的运行时类,则Java编译器则会将annotation的元数据存储到class文件中。然后,Java虚拟机或其他的程序可以查找这些元数据并做相应的处理。 + +当然除了annotation处理器可以处理annotation外,我们也可以使用反射自己来处理annotation。Java SE 5有一个名为AnnotatedElement的接口,Java的反射对象类Class,Constructor,Field,Method以及Package都实现了这个接口。这个接口用来表示当前运行在Java虚拟机中的被加上了annotation的程序元素。通过这个接口可以使用反射读取annotation。AnnotatedElement接口可以访问被加上RUNTIME标记的annotation,相应的方法有getAnnotation,getAnnotations,isAnnotationPresent。由于Annotation类型被编译和存储在二进制文件中就像class一样,所以可以像查询普通的Java对象一样查询这些方法返回的Annotation。 + + +#####运行时`Annotation`解析 +该类是指`@Retention`为`RUNTIME`的`Annotation`。 +该类型的解析其实本质的使用反射。反射执行的效率是很低的 +如果不是必要,应当尽量减少反射的使用,因为它会大大拖累你应用的执行效率。 + +例如`Target`为`Method`的注解就可以通过`Method`中的如下方法进行解析: + +```java + +public A getAnnotation(Class annotationType) { + ... +} + +public Annotation[] getAnnotations() { + ... +} + +public boolean isAnnotationPresent(Class annotationType) { + ... +} +``` + +当然如果`Target`为`Field`、`Class`等那就要去使用`Filed`或`Class`中的方法来解析了。 +```java +public static void main(String[] args) { + try { + Class cls = Class.forName("cn.trinea.java.test.annotation.App"); + for (Method method : cls.getMethods()) { + MethodInfo methodInfo = method.getAnnotation( +MethodInfo.class); + if (methodInfo != null) { + System.out.println("method name:" + method.getName()); + System.out.println("method author:" + methodInfo.author()); + System.out.println("method version:" + methodInfo.version()); + System.out.println("method date:" + methodInfo.date()); + } + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } +} +``` -#####自定义`Annotation` +#####编译时`Annotation`解析 + +该类值`@Retention`为`CLASS`的`Annotation`,由`APT(Annotaion Processing Tool)`自动进行解析。是在编译时注入,所以不会像反射一样影响效率问题。 + +我们需要做的是: + +- 自定义类继承`AbstractProcessor` +- 重写`process`方法 + +例如: + +```java +@SupportedAnnotationTypes({ "cn.trinea.java.test.annotation.MethodInfo" }) +public class MethodInfoProcessor extends AbstractProcessor { + + @Override + public boolean process(Set annotations, RoundEnvironment env) { + HashMap map = new HashMap(); + for (TypeElement te : annotations) { + for (Element element : env.getElementsAnnotatedWith(te)) { + MethodInfo methodInfo = element.getAnnotation(MethodInfo.class); + map.put(element.getEnclosingElement().toString(), methodInfo.author()); + } + } + return false; + } +} +``` +SupportedAnnotationTypes 表示这个 Processor 要处理的 Annotation 名字。 +process 函数中参数 annotations 表示待处理的 Annotations,参数 env 表示当前或是之前的运行环境 +process 函数返回值表示这组 annotations 是否被这个 Processor 接受,如果接受后续子的 rocessor 不会再对这个 Annotations 进行处理 + + + +###使用注解提高代码的检查性 + +`Google`提供了`Support-Annotations library`来支持更多的注解功能。 +可以直接在`build.gradle`中添加如下代码: + +``` +dependencies { + compile 'com.android.support:support-annotations:23.3.0' +} +``` + +`Android`提供了很多注解来支持在方法、参数和返回值上面使用,例如: + +- `@Nullable` + 可以为`null` + +- `@NonNull` + 不能为`null` +- `@StringRes` + `R.string`类型的资源。 +- `@DrawableRes` + `Drawable`类型的资源。 +- `@ColorRes` + `Color`类型的资源。 +- `@InterpolatorRes` + `Interpolatro`类型。 +- `@AnyRes` + `R.`类型。 +- `@UiThread` + 从`UI thread`调用。 +- `@RequiresPermission` + 来验证该方法的调用者所需要有的权限。检查一个列表中的任何一个权限可以使用`anyOf`属性。想要检查一个权限集合 + + +例如: + +```java +import android.support.annotation.NonNull; +... + + /** Add support for inflating the tag. */ + @NonNull + @Override + public View onCreateView(String name, @NonNull Context context, + @NonNull AttributeSet attrs) { + ... + } +... +``` + +```java +import android.support.annotation.StringRes; +... + public abstract void setTitle(@StringRes int resId); + ... + +遇到那种你写了个`setTitle(int resId)`他确给你传`setTitle(R.drawable.xxx)`的选手,用这种方式能很好的去提示下。 +``` + --- From 0ec9e3c73a006873d686bb54a823bf1c717908a7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 21 Jun 2016 20:18:30 +0800 Subject: [PATCH 039/373] add --- ...50\350\247\243\344\275\277\347\224\250.md" | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 769e1a90..c43d3260 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -73,6 +73,58 @@ public @interface UseCase { ###自定义`Annotation` +假设现在有个开发团队在每个类的开始都要提供一些信息,例如: + +```java +public class Generation3List extends Generation2List { + + // Author: John Doe + // Date: 3/17/2002 + // Current revision: 6 + // Last modified: 4/12/2004 + // By: Jane Doe + // Reviewers: Alice, Bill, Cindy + + // class code goes here + +} +``` + +我们可以声明一个注解来保存这些相同的元数据。如下: +```java +@interface ClassPreamble { + String author(); + String date(); + int currentRevision() default 1; + String lastModified() default "N/A"; + String lastModifiedBy() default "N/A"; + // Note use of array + String[] reviewers(); +} +``` +声明完注解之后我们就可以填写一些参数来使用它,如下: + +```java +@ClassPreamble ( + author = "John Doe", + date = "3/17/2002", + currentRevision = 6, + lastModified = "4/12/2004", + lastModifiedBy = "Jane Doe", + // Note array notation + reviewers = {"Alice", "Bob", "Cindy"} +) +public class Generation3List extends Generation2List { + +// class code goes here + +} +``` + + + + + ###`Annotation`解析 @@ -194,7 +246,23 @@ dependencies { - `@UiThread` 从`UI thread`调用。 - `@RequiresPermission` - 来验证该方法的调用者所需要有的权限。检查一个列表中的任何一个权限可以使用`anyOf`属性。想要检查一个权限集合 + 来验证该方法的调用者所需要有的权限。检查一个列表中的任何一个权限可以使用`anyOf`属性。想要检查多个权限时,可以使用`allOf`属性。如下: +```java +@RequiresPermission(Manifest.permission.SET_WALLPAPER) +public abstract void setWallpaper(Bitmap bitmap) throws IOException; +``` +检查多个权限: + +```java +@RequiresPermission(allOf = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}) +public static final void copyFile(String dest, String source) { + ... +} + +``` + 例如: From c9d8b38a2e2d9954b5a60cd113f9cd2f7867b1fe Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 22 Jun 2016 19:50:47 +0800 Subject: [PATCH 040/373] update --- ...50\350\247\243\344\275\277\347\224\250.md" | 200 ++++++++++++++++-- 1 file changed, 178 insertions(+), 22 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index c43d3260..4e027f22 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -16,8 +16,14 @@ RxJava详解 - `Annotation`的声明是通过关键字`@interface`。这个关键字会去继承`Annotation`接口。 - `Annotation`的方法定义是独特的、受限制的。 `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 - 注解如果只有一个默认属性,可直接用`value()`函数。一个属性也没有则表示该`Annotation`为`Mark Annotation`。 - + 注解如果只有一个默认属性,可直接用`value()`函数。一个属性也没有则表示该`Annotation`为`Mark Annotation`。 + 例如: +```java +public @interface UnitTest { + String value(); +} +``` + 在使用时可以直接使用`@UnitTest("GCD")`,`@UnitTest("GCD"`实际上就是是 @UnitTest(value="GCD)的简单写法。 例如: ```java @@ -31,7 +37,7 @@ public @interface UseCase { ###作用 -`Annotation`一般作为一种辅助途径,应用在软件框架或者工具中。让这些工具类可以根据不同的`Annotation`注解信息来采取不同的处理过程或者改变相应程的行为。 +`Annotation`一般作为一种辅助途径,应用在软件框架或者工具中。让这些工具类可以根据不同的`Annotation`注解信息来采取不同的处理过程或者改变相应程的行为。具有“让编译器进行编译检查的作用”。 具体可分为如下三类: @@ -67,7 +73,27 @@ public @interface UseCase { - `@Target` 用来标记可进行修饰哪些元素,例如`ElementType.TYPE`、`ElementType.METHOD`、`ElementType.CONSTRUCTOR`、`ElementType.FIELD`、`ElementType.PARAMETER`等,如果未指定则默认为可修饰所有。 - `@Inherited` - 子类是否可以继承父类中的该注解。 + 子类是否可以继承父类中的该注解。它所标注的`Annotation`将具有继承性。 + 例如: + ```java +java.lang.annotation.Inherited + +@Inherited +public @interface MyAnnotation { + +} +``` +```java +@MyAnnotation +public class MySuperClass { ... } +``` + +```java +public class MySubClass extends MySuperClass { ... } +``` + +在这个例子中`MySubClass`类继承了`@MyAnnotation`注解,因为`MySubClass`继承了`MySuperClass`类,而`MySuperClass`类使用了`@MyAnnotation`注解。 + - `@Documented` 是否会保存到`javadoc`文档中。 @@ -129,21 +155,49 @@ public class Generation3List extends Generation2List { ###`Annotation`解析 - 当Java源代码被编译时,编译器的一个插件annotation处理器则会处理这些annotation。处理器可以产生报告信息,或者创建附加的Java源文件或资源。如果annotation本身被加上了RententionPolicy的运行时类,则Java编译器则会将annotation的元数据存储到class文件中。然后,Java虚拟机或其他的程序可以查找这些元数据并做相应的处理。 当然除了annotation处理器可以处理annotation外,我们也可以使用反射自己来处理annotation。Java SE 5有一个名为AnnotatedElement的接口,Java的反射对象类Class,Constructor,Field,Method以及Package都实现了这个接口。这个接口用来表示当前运行在Java虚拟机中的被加上了annotation的程序元素。通过这个接口可以使用反射读取annotation。AnnotatedElement接口可以访问被加上RUNTIME标记的annotation,相应的方法有getAnnotation,getAnnotations,isAnnotationPresent。由于Annotation类型被编译和存储在二进制文件中就像class一样,所以可以像查询普通的Java对象一样查询这些方法返回的Annotation。 #####运行时`Annotation`解析 + 该类是指`@Retention`为`RUNTIME`的`Annotation`。 该类型的解析其实本质的使用反射。反射执行的效率是很低的 如果不是必要,应当尽量减少反射的使用,因为它会大大拖累你应用的执行效率。 -例如`Target`为`Method`的注解就可以通过`Method`中的如下方法进行解析: +- 类注解 + +可以通过`Class`、`Method`、`Field`类来在运行时获取注解。下面是通过`Class`类获取注解的示例: ```java +Class aClass = TheClass.class; +Annotation[] annotations = aClass.getAnnotations(); + +for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } +} +``` +也可以获取一个制定注解类型: +```java +Class aClass = TheClass.class; +Annotation annotation = aClass.getAnnotation(MyAnnotation.class); + +if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); +} +``` + +`JDK`提供的主要方法有: + +```java public A getAnnotation(Class annotationType) { ... } @@ -157,34 +211,128 @@ public boolean isAnnotationPresent(Class annotationType) { } ``` -当然如果`Target`为`Field`、`Class`等那就要去使用`Filed`或`Class`中的方法来解析了。 +- 方法注解 + +下面是一个方法使用注解的例子: ```java -public static void main(String[] args) { - try { - Class cls = Class.forName("cn.trinea.java.test.annotation.App"); - for (Method method : cls.getMethods()) { - MethodInfo methodInfo = method.getAnnotation( -MethodInfo.class); - if (methodInfo != null) { - System.out.println("method name:" + method.getName()); - System.out.println("method author:" + methodInfo.author()); - System.out.println("method version:" + methodInfo.version()); - System.out.println("method date:" + methodInfo.date()); - } - } - } catch (ClassNotFoundException e) { - e.printStackTrace(); +public class TheClass { + @MyAnnotation(name="someName", value = "Hello World") + public void doSomething(){} +} +``` + +你可以通过如下方式获取方法注解: + +```java +Method method = ... //obtain method object +Annotation[] annotations = method.getDeclaredAnnotations(); + +for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); } } ``` +也可以获取一个指定的方法注解,如下: +```java +Method method = ... // obtain method object +Annotation annotation = method.getAnnotation(MyAnnotation.class); +if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); +} +``` + +- 参数注解 + +在方法的参数中声明注解,如下: +```java +public class TheClass { + public static void doSomethingElse( + @MyAnnotation(name="aName", value="aValue") String parameter){ + } +} +``` + +可以通过`Method`对象获取到参数的注解,如下: +```java +Method method = ... //obtain method object +Annotation[][] parameterAnnotations = method.getParameterAnnotations(); +Class[] parameterTypes = method.getParameterTypes(); + +int i=0; +for(Annotation[] annotations : parameterAnnotations){ + Class parameterType = parameterTypes[i++]; + + for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("param: " + parameterType.getName()); + System.out.println("name : " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } + } +} +``` +注意,`Method.getParameterAnnotations()`方法会返回一个二维的`Annotation`数组,包含每个方法参数的一个注解数组。 + +- 变量注解 + +下面是一个变量使用注解的例子: +```java +public class TheClass { + + @MyAnnotation(name="someName", value = "Hello World") + public String myField = null; +} +``` + +你可以像下面这样获取变量的注解: +```java +Field field = ... //obtain field object +Annotation[] annotations = field.getDeclaredAnnotations(); + +for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } +} +``` + +当然也可以获取一个指定的变量注解,如下: +```java +Field field = ... // obtain method object +Annotation annotation = field.getAnnotation(MyAnnotation.class); + +if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); +} +``` #####编译时`Annotation`解析 +在刚才介绍的运行时注解中,很多人肯定会说使用反射会影响性能,那有没有不影响性能的方式呢?当然有了,那就是编译时注解。在编译时会通过注解标示来动态生成一些类或者`xml`,而在运行时,这里注解是没有的,它会依靠动态生成的类来进行操作。所以它就和直接调用方法一样,当然不会有效率影响了。 + 该类值`@Retention`为`CLASS`的`Annotation`,由`APT(Annotaion Processing Tool)`自动进行解析。是在编译时注入,所以不会像反射一样影响效率问题。 +根据sun官方的解释,APT(annotation processing tool)是一个命令行工具,它对源代码文件进行检测找出其中的annotation后,使用annotation processors来处理annotation。而annotation processors使用了一套反射API并具备对JSR175规范的支持。 + +annotation processors处理annotation的基本过程如下:首先,APT运行annotation processors根据提供的源文件中的annotation生成源代码文件和其它的文件(文件具体内容由annotation processors的编写者决定),接着APT将生成的源代码文件和提供的源文件进行编译生成类文件。 + +简单的和前面所讲的annotation实例BRFW相比,APT就像一个在编译时处理annotation的javac。而且从sun开发者的blog中看到,java1.6 beta版中已将APT的功能写入到了javac中,这样只要执行带有特定参数的javac就能达到APT的功能。 + + + 我们需要做的是: - 自定义类继承`AbstractProcessor` @@ -192,6 +340,14 @@ MethodInfo.class); 例如: +```java +public class Processor extends AbstractProcessor{ +} +``` +我用`Android Studio`死活提示找不到`AbstractProcessor`类,但是它明明就在`jdk`中。我是这样解决的,新建一个`Moduel`,在选择类型时将该`Moduel`的类型选为`Java Library`。然后在该`Module`中创建就好了,完美解决。 + + + ```java @SupportedAnnotationTypes({ "cn.trinea.java.test.annotation.MethodInfo" }) public class MethodInfoProcessor extends AbstractProcessor { From fee26c177def2cd86ac1f127b3c29633fc1b3644 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 24 Jun 2016 20:19:14 +0800 Subject: [PATCH 041/373] update --- ...50\350\247\243\344\275\277\347\224\250.md" | 488 ++++++++++++++++++ 1 file changed, 488 insertions(+) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 4e027f22..176de113 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -331,6 +331,8 @@ annotation processors处理annotation的基本过程如下:首先,APT运行a 简单的和前面所讲的annotation实例BRFW相比,APT就像一个在编译时处理annotation的javac。而且从sun开发者的blog中看到,java1.6 beta版中已将APT的功能写入到了javac中,这样只要执行带有特定参数的javac就能达到APT的功能。 +pt(Annotation Processing Tool)在编译时自动查找所有继承自AbstractProcessor的类,然后调用他们的process方法去处理,这样就拥有了在编译过程中执行代码的能力 + 我们需要做的是: @@ -338,6 +340,485 @@ annotation processors处理annotation的基本过程如下:首先,APT运行a - 自定义类继承`AbstractProcessor` - 重写`process`方法 +相信很多人都知道`Butter Knife`。它里面就是使用了注解: +```java + @BindView(R.id.user) EditText username; + @BindView(R.id.pass) EditText password; + + @BindString(R.string.login_error) String loginErrorMessage; + + @OnClick(R.id.submit) void submit() { + // TODO call server... + } +``` + +那我们就以`onClick`事件为例模仿着他去写一下。 + +```java +public class InjectorProcessor { + public void process(final Object object) { + Class class_=object.getClass(); + Method[] methods=class_.getDeclaredMethods(); + for (final Method method : methods) { + onClick clickMethod=method.getAnnotation(onClick.class); + if (clickMethod!=null) { + if (object instanceof Activity) { + for (int id : clickMethod.value()) { + View view=((Activity) object).findViewById(id); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + method.invoke(object); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + }); + } + } + } + } + } +} + +使用: + +```java +public class MainActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + InjectorProcessor processor=new InjectorProcessor(); + processor.process(MainActivity.this); + } + + @onClick({R.id.textview}) + public void click() { + Toast.makeText(this, "HHH", Toast.LENGTH_SHORT).show(); + } +} +``` + +很显然大神`JakeWharton`不会这样做的,毕竟反射会影响性能。 +我们来看一下`ButterKnife`的源码: +```java +@Target(METHOD) +@Retention(CLASS) +@ListenerClass( + targetType = "android.view.View", + setter = "setOnClickListener", + type = "butterknife.internal.DebouncingOnClickListener", + method = @ListenerMethod( + name = "doClick", + parameters = "android.view.View" + ) +) +public @interface OnClick { + /** View IDs to which the method will be bound. */ + @IdRes int[] value() default { View.NO_ID }; +} +``` +看到了吗?是编译型的注解。这样不会影响性能。全局所以下`OnClick`,很容易找到它的处理类`ButterKnifeProccessor`: + +```java +@AutoService(Processor.class) +public final class ButterKnifeProcessor extends AbstractProcessor { + static final Id NO_ID = new Id(-1); + static final String VIEW_TYPE = "android.view.View"; + private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList"; + private static final String BITMAP_TYPE = "android.graphics.Bitmap"; + private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable"; + private static final String TYPED_ARRAY_TYPE = "android.content.res.TypedArray"; + private static final String NULLABLE_ANNOTATION_NAME = "Nullable"; + private static final String STRING_TYPE = "java.lang.String"; + private static final String LIST_TYPE = List.class.getCanonicalName(); + private static final String R = "R"; + private static final List> LISTENERS = Arrays.asList(// + OnCheckedChanged.class, // + OnClick.class, // + OnEditorAction.class, // + OnFocusChange.class, // + OnItemClick.class, // + OnItemLongClick.class, // + OnItemSelected.class, // + OnLongClick.class, // + OnPageChange.class, // + OnTextChanged.class, // + OnTouch.class // + ); +@Override public boolean process(Set elements, RoundEnvironment env) { + parseRClass(env); + + Map targetClassMap = findAndParseTargets(env); + + for (Map.Entry entry : targetClassMap.entrySet()) { + TypeElement typeElement = entry.getKey(); + BindingClass bindingClass = entry.getValue(); + + for (JavaFile javaFile : bindingClass.brewJava()) { + try { + javaFile.writeTo(filer); + } catch (IOException e) { + error(typeElement, "Unable to write view binder for type %s: %s", typeElement, + e.getMessage()); + } + } + } + + return true; + } +private Map findAndParseTargets(RoundEnvironment env) { + Map targetClassMap = new LinkedHashMap<>(); + Set erasedTargetNames = new LinkedHashSet<>(); + + // Process each @BindArray element. + for (Element element : env.getElementsAnnotatedWith(BindArray.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceArray(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindArray.class, e); + } + } + + // Process each @BindBitmap element. + for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceBitmap(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindBitmap.class, e); + } + } + + // Process each @BindBool element. + for (Element element : env.getElementsAnnotatedWith(BindBool.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceBool(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindBool.class, e); + } + } + + // Process each @BindColor element. + for (Element element : env.getElementsAnnotatedWith(BindColor.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceColor(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindColor.class, e); + } + } + + // Process each @BindDimen element. + for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceDimen(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindDimen.class, e); + } + } + + // Process each @BindDrawable element. + for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceDrawable(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindDrawable.class, e); + } + } + + // Process each @BindInt element. + for (Element element : env.getElementsAnnotatedWith(BindInt.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceInt(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindInt.class, e); + } + } + + // Process each @BindString element. + for (Element element : env.getElementsAnnotatedWith(BindString.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceString(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindString.class, e); + } + } + + // Process each @BindView element. + for (Element element : env.getElementsAnnotatedWith(BindView.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseBindView(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindView.class, e); + } + } + + // Process each @BindViews element. + for (Element element : env.getElementsAnnotatedWith(BindViews.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseBindViews(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindViews.class, e); + } + } + + // Process each annotation that corresponds to a listener. + for (Class listener : LISTENERS) { + findAndParseListener(env, listener, targetClassMap, erasedTargetNames); + } + + // Try to find a parent binder for each. + for (Map.Entry entry : targetClassMap.entrySet()) { + TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames); + if (parentType != null) { + BindingClass bindingClass = entry.getValue(); + BindingClass parentBindingClass = targetClassMap.get(parentType); + bindingClass.setParent(parentBindingClass); + } + } + + return targetClassMap; + } +``` +`findAndParseTargets()`方法中回去调用`findAndParseListener()`,那我们继续看`findAndParseListener()`方法: + +```java +private void findAndParseListener(RoundEnvironment env, + Class annotationClass, Map targetClassMap, + Set erasedTargetNames) { + for (Element element : env.getElementsAnnotatedWith(annotationClass)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseListenerAnnotation(annotationClass, element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + StringWriter stackTrace = new StringWriter(); + e.printStackTrace(new PrintWriter(stackTrace)); + + error(element, "Unable to generate view binder for @%s.\n\n%s", + annotationClass.getSimpleName(), stackTrace.toString()); + } + } + } + + private void parseListenerAnnotation(Class annotationClass, Element element, + Map targetClassMap, Set erasedTargetNames) + throws Exception { + // This should be guarded by the annotation's @Target but it's worth a check for safe casting. + if (!(element instanceof ExecutableElement) || element.getKind() != METHOD) { + throw new IllegalStateException( + String.format("@%s annotation must be on a method.", annotationClass.getSimpleName())); + } + + ExecutableElement executableElement = (ExecutableElement) element; + TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); + + // Assemble information on the method. + Annotation annotation = element.getAnnotation(annotationClass); + Method annotationValue = annotationClass.getDeclaredMethod("value"); + if (annotationValue.getReturnType() != int[].class) { + throw new IllegalStateException( + String.format("@%s annotation value() type not int[].", annotationClass)); + } + + int[] ids = (int[]) annotationValue.invoke(annotation); + String name = executableElement.getSimpleName().toString(); + boolean required = isListenerRequired(executableElement); + + // Verify that the method and its containing class are accessible via generated code. + boolean hasError = isInaccessibleViaGeneratedCode(annotationClass, "methods", element); + hasError |= isBindingInWrongPackage(annotationClass, element); + + Integer duplicateId = findDuplicate(ids); + if (duplicateId != null) { + error(element, "@%s annotation for method contains duplicate ID %d. (%s.%s)", + annotationClass.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), + element.getSimpleName()); + hasError = true; + } + + ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); + if (listener == null) { + throw new IllegalStateException( + String.format("No @%s defined on @%s.", ListenerClass.class.getSimpleName(), + annotationClass.getSimpleName())); + } + + for (int id : ids) { + if (id == NO_ID.value) { + if (ids.length == 1) { + if (!required) { + error(element, "ID-free binding must not be annotated with @Optional. (%s.%s)", + enclosingElement.getQualifiedName(), element.getSimpleName()); + hasError = true; + } + + // Verify target type is valid for a binding without an id. + String targetType = listener.targetType(); + if (!isSubtypeOfType(enclosingElement.asType(), targetType) + && !isInterface(enclosingElement.asType())) { + error(element, "@%s annotation without an ID may only be used with an object of type " + + "\"%s\" or an interface. (%s.%s)", + annotationClass.getSimpleName(), targetType, + enclosingElement.getQualifiedName(), element.getSimpleName()); + hasError = true; + } + } else { + error(element, "@%s annotation contains invalid ID %d. (%s.%s)", + annotationClass.getSimpleName(), id, enclosingElement.getQualifiedName(), + element.getSimpleName()); + hasError = true; + } + } + } + + ListenerMethod method; + ListenerMethod[] methods = listener.method(); + if (methods.length > 1) { + throw new IllegalStateException(String.format("Multiple listener methods specified on @%s.", + annotationClass.getSimpleName())); + } else if (methods.length == 1) { + if (listener.callbacks() != ListenerClass.NONE.class) { + throw new IllegalStateException( + String.format("Both method() and callback() defined on @%s.", + annotationClass.getSimpleName())); + } + method = methods[0]; + } else { + Method annotationCallback = annotationClass.getDeclaredMethod("callback"); + Enum callback = (Enum) annotationCallback.invoke(annotation); + Field callbackField = callback.getDeclaringClass().getField(callback.name()); + method = callbackField.getAnnotation(ListenerMethod.class); + if (method == null) { + throw new IllegalStateException( + String.format("No @%s defined on @%s's %s.%s.", ListenerMethod.class.getSimpleName(), + annotationClass.getSimpleName(), callback.getDeclaringClass().getSimpleName(), + callback.name())); + } + } + + // Verify that the method has equal to or less than the number of parameters as the listener. + List methodParameters = executableElement.getParameters(); + if (methodParameters.size() > method.parameters().length) { + error(element, "@%s methods can have at most %s parameter(s). (%s.%s)", + annotationClass.getSimpleName(), method.parameters().length, + enclosingElement.getQualifiedName(), element.getSimpleName()); + hasError = true; + } + + // Verify method return type matches the listener. + TypeMirror returnType = executableElement.getReturnType(); + if (returnType instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) returnType; + returnType = typeVariable.getUpperBound(); + } + if (!returnType.toString().equals(method.returnType())) { + error(element, "@%s methods must have a '%s' return type. (%s.%s)", + annotationClass.getSimpleName(), method.returnType(), + enclosingElement.getQualifiedName(), element.getSimpleName()); + hasError = true; + } + + if (hasError) { + return; + } + + Parameter[] parameters = Parameter.NONE; + if (!methodParameters.isEmpty()) { + parameters = new Parameter[methodParameters.size()]; + BitSet methodParameterUsed = new BitSet(methodParameters.size()); + String[] parameterTypes = method.parameters(); + for (int i = 0; i < methodParameters.size(); i++) { + VariableElement methodParameter = methodParameters.get(i); + TypeMirror methodParameterType = methodParameter.asType(); + if (methodParameterType instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) methodParameterType; + methodParameterType = typeVariable.getUpperBound(); + } + + for (int j = 0; j < parameterTypes.length; j++) { + if (methodParameterUsed.get(j)) { + continue; + } + if (isSubtypeOfType(methodParameterType, parameterTypes[j]) + || isInterface(methodParameterType)) { + parameters[i] = new Parameter(j, TypeName.get(methodParameterType)); + methodParameterUsed.set(j); + break; + } + } + if (parameters[i] == null) { + StringBuilder builder = new StringBuilder(); + builder.append("Unable to match @") + .append(annotationClass.getSimpleName()) + .append(" method arguments. (") + .append(enclosingElement.getQualifiedName()) + .append('.') + .append(element.getSimpleName()) + .append(')'); + for (int j = 0; j < parameters.length; j++) { + Parameter parameter = parameters[j]; + builder.append("\n\n Parameter #") + .append(j + 1) + .append(": ") + .append(methodParameters.get(j).asType().toString()) + .append("\n "); + if (parameter == null) { + builder.append("did not match any listener parameters"); + } else { + builder.append("matched listener parameter #") + .append(parameter.getListenerPosition() + 1) + .append(": ") + .append(parameter.getType()); + } + } + builder.append("\n\nMethods may have up to ") + .append(method.parameters().length) + .append(" parameter(s):\n"); + for (String parameterType : method.parameters()) { + builder.append("\n ").append(parameterType); + } + builder.append( + "\n\nThese may be listed in any order but will be searched for from top to bottom."); + error(executableElement, builder.toString()); + return; + } + } + } + + MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required); + BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); + for (int id : ids) { + if (!bindingClass.addMethod(getId(id), listener, method, binding)) { + error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)", + id, enclosingElement.getQualifiedName(), element.getSimpleName()); + return; + } + } + + // Add the type-erased version to the valid binding targets set. + erasedTargetNames.add(enclosingElement); + } +``` + + + + +``` + + 例如: ```java @@ -345,6 +826,13 @@ public class Processor extends AbstractProcessor{ } ``` 我用`Android Studio`死活提示找不到`AbstractProcessor`类,但是它明明就在`jdk`中。我是这样解决的,新建一个`Moduel`,在选择类型时将该`Moduel`的类型选为`Java Library`。然后在该`Module`中创建就好了,完美解决。 +这是因为注解是`javase`中`javax`包里面的,`android.jar`默认是不包含的,所以会编译报错。新建`Java`类型的`Library`后就可以直接使用`javax`包中注解相关的类了。 + + + + + + From f26db031e5771239f97411a255ed9e864d242d9d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 27 Jun 2016 19:59:25 +0800 Subject: [PATCH 042/373] =?UTF-8?q?update=20=E6=B3=A8=E8=A7=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\350\247\243\344\275\277\347\224\250.md" | 272 +++++++++++++++++- 1 file changed, 258 insertions(+), 14 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 176de113..e01819de 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -318,6 +318,13 @@ if(annotation instanceof MyAnnotation){ ``` +一张图总结一下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_annotation.jpg?raw=true) + + + + #####编译时`Annotation`解析 @@ -829,33 +836,270 @@ public class Processor extends AbstractProcessor{ 这是因为注解是`javase`中`javax`包里面的,`android.jar`默认是不包含的,所以会编译报错。新建`Java`类型的`Library`后就可以直接使用`javax`包中注解相关的类了。 +好,那我们就开始写个编译时处理的`demo` : +- `Android Studio`中创建一个`Android`工程。 +- 新建一个`Module`,然后选择`Java Library`类型(我的名字为`annotations`),并且让`app`依赖该`module`。 +- 在`annotations`的`module`中昔年一个注解类: + ```java +package com.charon; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface AnnotationTest { + String value() default ""; +} +``` +- 然后在`annotations`的`module`自定义`Processor`类: + ```java +package com.charon; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; - - -```java -@SupportedAnnotationTypes({ "cn.trinea.java.test.annotation.MethodInfo" }) -public class MethodInfoProcessor extends AbstractProcessor { - +@SupportedAnnotationTypes("com.charon.AnnotationTest") +@SupportedSourceVersion(SourceVersion.RELEASE_7) +public class TestProcessor extends AbstractProcessor { @Override - public boolean process(Set annotations, RoundEnvironment env) { - HashMap map = new HashMap(); + public boolean process(Set annotations, RoundEnvironment roundEnv) { + System.out.println("process"); for (TypeElement te : annotations) { - for (Element element : env.getElementsAnnotatedWith(te)) { - MethodInfo methodInfo = element.getAnnotation(MethodInfo.class); - map.put(element.getEnclosingElement().toString(), methodInfo.author()); + for (Element element : roundEnv.getElementsAnnotatedWith(te)) { + AnnotationTest annotation = element.getAnnotation(AnnotationTest.class); + String value = annotation.value(); + System.out.println("type : " + value); } } + return true; + } +} +``` +***注意:***`@SupportedAnnotationTypes("com.charon.AnnotationTest")`来指定要处理的注解类。`@SupportedSourceVersion(SourceVersion.RELEASE_7)`指定编译的版本。 +- 注册处理器 + 我们自定义了`Processor`那如何才能让其生效呢?就是在`annotations`的`java`同级目录新建`resources/META-INF/services/javax.annotation.processing.Processor +```java + - java + - META-INF + - services + - javax.annotation.processing.Processor +``` +然后在`javax.annotation.processing.Processor`文件中指定自定义的处理器: +```java +com.charon.TestProcessor +``` +如果多个话就分行写。 + +然后我们`Build`一下(命令行执行`./gradlew build`),就能看到控制台打印出来如下的信息: +```java +:app:compileReleaseJavaWithJavac +:app:compileReleaseJavaWithJavac - is not incremental (e.g. outputs have changed, no previous execution, etc.). +process +value : haha +process +``` + +注意,千万不要去`logcat`中找信息,这是编译时注解。 +主要使用`jdk1.7`,`1.8`有bug。 + + +上面只是一个简单的例子,如果你想用编译时注解去做一些更高级的事情,生成一些代码,那你可能就会用到如下几个类库: +- [android-apt](https://bitbucket.org/hvisser/android-apt) +- [Google Auto](https://github.com/google/auto) +- [Square javapoet](https://github.com/square/javapoet) + + +`Android Studio`原本是不支持注解处理器的, 但是用`android-apt`这个插件后, 我们就可以使用注解处理器了, 这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到APK里面去, 而且它还可以让最终编译出来的APK里面不包含注解处理器本身的代码, 因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。 + +也就是说它主要有两个目的: +- 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library +- 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用 + +那在什么情况下我们会需要使用它呢? + +当你需要引入Processors 生成的源代码到你的代码中时。例如,当你使用Dagger 2 或 AndroidAnnotaition.该插件使得Android Studio可以配置生成资源的build path,避免IDE报错。当使用 apt添加添加依赖,它将不会被包含到最终的APK里。 + + + +`Google Auto`的主要作用是注解`Processor`类,并对其生成`META-INF`的配置信息,可以让你不用去写`META-INF`这些配置文件,只要在自定义的`Processor`上面加上`@AutoService(Processor.class)` + +`javapoet`是一个`A Java API for generating .java source files.`可以更方便的生成代码,它可以帮助我们通过类调用的形式来生成代码。 + + + + + + +###项目结构 + +在自定义注解时,一般来说可能会建三个`modules`: + +- `app module`:写一些使用注解的`android`应用逻辑。 +- `api module`:定义一些可以在`app`中使用的注解。它会被`app`以及`compiler`使用。 +- `compiler module`:定义`Processor`该`module`不会被包含到应用中,它只会在构建过程中被使用,它会生成一些`java`文件,来打包进`apk`中。我们可以在该`module`中使用`auto`以及`javapoet`。 + + +下面开始配置`android-apt`: + +//配置在Project下的build.gradle中 +buildscript { + repositories { + mavenCentral() + } + dependencies { + //替换成最新的 gradle版本 + classpath 'com.android.tools.build:gradle:1.3.0' + //替换成最新android-apt版本 + classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' + } +} +//配置到`app module`下的build.gradle中 +```java +apply plugin: 'com.android.application' +apply plugin: 'com.neenbedankt.android-apt' +... +dependencies { + compile project(':api') + apt project(':compiler') +} +``` + +dddddddddddd + +接下来在`compiler module`中配置`auto`以及`javapoet`: + +```java +apply plugin: 'java' +sourceCompatibility = JavaVersion.VERSION_1_7 +targetCompatibility = JavaVersion.VERSION_1_7 +dependencies { + compile project (':api') + compile 'com.google.auto.service:auto-service:1.0-rc2' + compile 'com.squareup:javapoet:1.7.0' +} +``` +在`api module`中也加上如下的配置: + +```java +apply plugin: 'java' +sourceCompatibility = JavaVersion.VERSION_1_7 +targetCompatibility = JavaVersion.VERSION_1_7 +``` + +接下来在`api module`中定义一个注解: +```java +package com.charon; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface AnnotationTest { + String value(); +} + +`` +然后在`compiler module`中自定义一个`Processor`: + + +```java +package com.charon; + +import com.google.auto.service.AutoService; + +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; + +@AutoService(Processor.class) +public class MyProcessor extends AbstractProcessor { + + /** + * Initializes the processor with the processing environment by + * setting the {@code processingEnv} field to the value of the + * {@code processingEnv} argument. An {@code + * IllegalStateException} will be thrown if this method is called + * more than once on the same object. + * + * @param processingEnv environment to access facilities the tool framework + * provides to the processor + * @throws IllegalStateException if this method is called more than once. + */ + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + /** + * Processes a set of annotation types on type elements originating from the prior round + * and returns whether or not these annotations are claimed by this processor. + * If true is returned, the annotations are claimed and subsequent processors will + * not be asked to process them; + * if false is returned, the annotations are unclaimed and subsequent processors may be + * asked to process them. A processor may always return the same boolean value + * or may vary the result based on chosen criteria. + * The input set will be empty if the processor supports "*" and the root elements + * have no annotations. A Processor must gracefully handle an empty set of annotations. + * @param annotations + * @param roundEnv + * @return + */ + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + System.out.println("hello processor"); return false; } + + @Override + public Set getSupportedAnnotationTypes() { + return super.getSupportedAnnotationTypes(); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + } + + ``` -SupportedAnnotationTypes 表示这个 Processor 要处理的 Annotation 名字。 -process 函数中参数 annotations 表示待处理的 Annotations,参数 env 表示当前或是之前的运行环境 -process 函数返回值表示这组 annotations 是否被这个 Processor 接受,如果接受后续子的 rocessor 不会再对这个 Annotations 进行处理 + + +注解相关API: + +- Set getElementsAnnotatedWith(Class a) +返回使用给定注释类型注释的元素。该注释可能直接出现或者被继承。只返回注释处理的此 round 中包括 的 package 元素和 type 元素、成员声明、参数或者这些元素中声明的类型参数。 +- Element:表示一个程序元素,比如包、类或者方法。每个元素都表示一个静态的语言级构造(不表示虚拟机的运行时构造)。 + +元素应该使用 equals(Object) 方法进行比较。不保证总是使用相同的对象表示某个特定的元素。 + +要实现基于 Element 对象类的操作,可以使用 visitor 或者使用 getKind() 方法的结果。使用 instanceof 确定此建模层次结构中某一对象的有效类 未必 可靠,因为一个实现可以选择让单个对象实现多个 Element 子接口。 + +- TypeElement:表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注释类型是一种接口。 + +TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。这种区别对于一般的类型是最明显的,对于这些类型,单个元素可以定义一系列完整的类型。例如,元素 java.util.Set 对应于参数化类型 java.util.Set 和 java.util.Set(以及其他许多类型),还对应于原始类型 java.util.Set。 + + + From 68df3418c8e72ce509c6393f24f0c907d601dc53 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 28 Jun 2016 01:22:45 +0800 Subject: [PATCH 043/373] format --- ...50\350\247\243\344\275\277\347\224\250.md" | 1250 ++++++----------- 1 file changed, 436 insertions(+), 814 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index e01819de..69da72c8 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -15,7 +15,7 @@ RxJava详解 ###说明 - `Annotation`的声明是通过关键字`@interface`。这个关键字会去继承`Annotation`接口。 - `Annotation`的方法定义是独特的、受限制的。 - `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 + `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、`String类型`、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 注解如果只有一个默认属性,可直接用`value()`函数。一个属性也没有则表示该`Annotation`为`Mark Annotation`。 例如: ```java @@ -23,17 +23,17 @@ public @interface UnitTest { String value(); } ``` - 在使用时可以直接使用`@UnitTest("GCD")`,`@UnitTest("GCD"`实际上就是是 @UnitTest(value="GCD)的简单写法。 + 在使用时可以直接使用`@UnitTest("GCD")`,`@UnitTest("GCD"`实际上就是是 `@UnitTest(value="GCD)`的简单写法。 -例如: -```java -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface UseCase { - public int id(); - public String description() default "no description"; -} -``` + 例如: + ```java + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface UseCase { + public int id(); + public String description() default "no description"; + } + ``` ###作用 @@ -67,34 +67,36 @@ public @interface UseCase { 元`Annotation`是指用来定义`Annotation`的`Annotation`。 -- `@Retention +- `@Retention` 保留时间,可为`RetentionPolicy.SOURCE(源码时)`、`RetentionPolicy.CLASS(编译时)`、`RetentionPolicy.RUNTIME(运行时)`,默认为`CLASS`。如果值为`RetentionPolicy.SOURCE`那大多都是`Mark Annotation`,例如:`Override`、`Deprecated`、`Suppress Warnings`。`SOURCE`表示仅存在于源码中,在`class`文件中不会包含。`CLASS`表示会在`class`文件中存在,但是运行时无法获取。`RUNTIME`表示会在`class`文件中存在,并且在运行时可以通过反射获取。 -- `@Target` +- `@Target` 用来标记可进行修饰哪些元素,例如`ElementType.TYPE`、`ElementType.METHOD`、`ElementType.CONSTRUCTOR`、`ElementType.FIELD`、`ElementType.PARAMETER`等,如果未指定则默认为可修饰所有。 -- `@Inherited` - 子类是否可以继承父类中的该注解。它所标注的`Annotation`将具有继承性。 +- `@Inherited` + 子类是否可以继承父类中的该注解。它所标注的`Annotation`将具有继承性。 例如: - ```java -java.lang.annotation.Inherited - -@Inherited -public @interface MyAnnotation { - -} -``` -```java -@MyAnnotation -public class MySuperClass { ... } -``` -```java -public class MySubClass extends MySuperClass { ... } -``` + ```java + java.lang.annotation.Inherited + + @Inherited + public @interface MyAnnotation { + + } + ``` + + ```java + @MyAnnotation + public class MySuperClass { ... } + ``` + + ```java + public class MySubClass extends MySuperClass { ... } + ``` -在这个例子中`MySubClass`类继承了`@MyAnnotation`注解,因为`MySubClass`继承了`MySuperClass`类,而`MySuperClass`类使用了`@MyAnnotation`注解。 + 在这个例子中`MySubClass`类继承了`@MyAnnotation`注解,因为`MySubClass`继承了`MySuperClass`类,而`MySuperClass`类使用了`@MyAnnotation`注解。 -- `@Documented` +- `@Documented` 是否会保存到`javadoc`文档中。 ###自定义`Annotation` @@ -147,205 +149,179 @@ public class Generation3List extends Generation2List { } ``` - - - - - - ###`Annotation`解析 -当Java源代码被编译时,编译器的一个插件annotation处理器则会处理这些annotation。处理器可以产生报告信息,或者创建附加的Java源文件或资源。如果annotation本身被加上了RententionPolicy的运行时类,则Java编译器则会将annotation的元数据存储到class文件中。然后,Java虚拟机或其他的程序可以查找这些元数据并做相应的处理。 +当`Java`源代码被编译时,编译器的一个插件`annotation`处理器则会处理这些`annotation`。 +处理器可以产生报告信息,或者创建附加的`Java`源文件或资源。 +如果`annotation`本身被加上了`RententionPolicy`的运行时类, +则`Java`编译器则会将`annotation`的元数据存储到`class`文件中。然后`Java`虚拟机或其他的程序可以查找这些元数据并做相应的处理。 -当然除了annotation处理器可以处理annotation外,我们也可以使用反射自己来处理annotation。Java SE 5有一个名为AnnotatedElement的接口,Java的反射对象类Class,Constructor,Field,Method以及Package都实现了这个接口。这个接口用来表示当前运行在Java虚拟机中的被加上了annotation的程序元素。通过这个接口可以使用反射读取annotation。AnnotatedElement接口可以访问被加上RUNTIME标记的annotation,相应的方法有getAnnotation,getAnnotations,isAnnotationPresent。由于Annotation类型被编译和存储在二进制文件中就像class一样,所以可以像查询普通的Java对象一样查询这些方法返回的Annotation。 +当然除了`annotation`处理器可以处理`annotation`外,我们也可以使用反射自己来处理`annotation`。`Java SE 5`有一个名为`AnnotatedElement`的接口, +`Java`的反射对象类`Class`,`Constructor`,`Field`,`Method`以及`Package`都实现了这个接口。 +这个接口用来表示当前运行在`Java`虚拟机中的被加上了`annotation`的程序元素。 +通过这个接口可以使用反射读取`annotation`。`AnnotatedElement`接口可以访问被加上`RUNTIME`标记的`annotation`, +相应的方法有`getAnnotation`,`getAnnotations`,`isAnnotationPresent`。 +由于`Annotation`类型被编译和存储在二进制文件中就像`class`一样, +所以可以像查询普通的`Java`对象一样查询这些方法返回的`Annotation`。 #####运行时`Annotation`解析 -该类是指`@Retention`为`RUNTIME`的`Annotation`。 -该类型的解析其实本质的使用反射。反射执行的效率是很低的 +该类是指`@Retention`为`RUNTIME`的`Annotation`。 +该类型的解析其实本质的使用反射。反射执行的效率是很低的 如果不是必要,应当尽量减少反射的使用,因为它会大大拖累你应用的执行效率。 - 类注解 -可以通过`Class`、`Method`、`Field`类来在运行时获取注解。下面是通过`Class`类获取注解的示例: - -```java -Class aClass = TheClass.class; -Annotation[] annotations = aClass.getAnnotations(); - -for(Annotation annotation : annotations){ + 可以通过`Class`、`Method`、`Field`类来在运行时获取注解。下面是通过`Class`类获取注解的示例: + + ```java + Class aClass = TheClass.class; + Annotation[] annotations = aClass.getAnnotations(); + + for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } + } + ``` + 也可以获取一个指定的注解类型: + ```java + Class aClass = TheClass.class; + Annotation annotation = aClass.getAnnotation(MyAnnotation.class); + if(annotation instanceof MyAnnotation){ MyAnnotation myAnnotation = (MyAnnotation) annotation; System.out.println("name: " + myAnnotation.name()); System.out.println("value: " + myAnnotation.value()); } -} -``` -也可以获取一个制定注解类型: -```java -Class aClass = TheClass.class; -Annotation annotation = aClass.getAnnotation(MyAnnotation.class); - -if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); -} -``` - + ``` -`JDK`提供的主要方法有: - -```java -public A getAnnotation(Class annotationType) { - ... -} - -public Annotation[] getAnnotations() { - ... -} - -public boolean isAnnotationPresent(Class annotationType) { - ... -} -``` + `JDK`提供的主要方法有: + + ```java + public A getAnnotation(Class annotationType) { + ... + } + + public Annotation[] getAnnotations() { + ... + } + + public boolean isAnnotationPresent(Class annotationType) { + ... + } + ``` - 方法注解 -下面是一个方法使用注解的例子: -```java -public class TheClass { - @MyAnnotation(name="someName", value = "Hello World") - public void doSomething(){} -} -``` - -你可以通过如下方式获取方法注解: - -```java -Method method = ... //obtain method object -Annotation[] annotations = method.getDeclaredAnnotations(); - -for(Annotation annotation : annotations){ + 下面是一个方法使用注解的例子: + ```java + public class TheClass { + @MyAnnotation(name="someName", value = "Hello World") + public void doSomething(){} + } + ``` + + 你可以通过如下方式获取方法注解: + + ```java + Method method = ... //obtain method object + Annotation[] annotations = method.getDeclaredAnnotations(); + + for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } + } + ``` + 也可以获取一个指定的方法注解,如下: + ```java + Method method = ... // obtain method object + Annotation annotation = method.getAnnotation(MyAnnotation.class); + if(annotation instanceof MyAnnotation){ MyAnnotation myAnnotation = (MyAnnotation) annotation; System.out.println("name: " + myAnnotation.name()); System.out.println("value: " + myAnnotation.value()); } -} -``` -也可以获取一个指定的方法注解,如下: -```java -Method method = ... // obtain method object -Annotation annotation = method.getAnnotation(MyAnnotation.class); - -if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); -} -``` + ``` - 参数注解 -在方法的参数中声明注解,如下: -```java -public class TheClass { - public static void doSomethingElse( - @MyAnnotation(name="aName", value="aValue") String parameter){ - } -} -``` - -可以通过`Method`对象获取到参数的注解,如下: -```java -Method method = ... //obtain method object -Annotation[][] parameterAnnotations = method.getParameterAnnotations(); -Class[] parameterTypes = method.getParameterTypes(); - -int i=0; -for(Annotation[] annotations : parameterAnnotations){ - Class parameterType = parameterTypes[i++]; - - for(Annotation annotation : annotations){ - if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("param: " + parameterType.getName()); - System.out.println("name : " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); + 在方法的参数中声明注解,如下: + ```java + public class TheClass { + public static void doSomethingElse( + @MyAnnotation(name="aName", value="aValue") String parameter){ + } } - } -} -``` -注意,`Method.getParameterAnnotations()`方法会返回一个二维的`Annotation`数组,包含每个方法参数的一个注解数组。 + ``` + + 可以通过`Method`对象获取到参数的注解,如下: + ```java + Method method = ... //obtain method object + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + Class[] parameterTypes = method.getParameterTypes(); + + int i=0; + for(Annotation[] annotations : parameterAnnotations){ + Class parameterType = parameterTypes[i++]; + + for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("param: " + parameterType.getName()); + System.out.println("name : " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } + } + } + ``` + 注意,`Method.getParameterAnnotations()`方法会返回一个二维的`Annotation`数组,包含每个方法参数的一个注解数组。 - 变量注解 -下面是一个变量使用注解的例子: -```java -public class TheClass { - - @MyAnnotation(name="someName", value = "Hello World") - public String myField = null; -} -``` - -你可以像下面这样获取变量的注解: -```java -Field field = ... //obtain field object -Annotation[] annotations = field.getDeclaredAnnotations(); - -for(Annotation annotation : annotations){ + 下面是一个变量使用注解的例子: + ```java + public class TheClass { + + @MyAnnotation(name="someName", value = "Hello World") + public String myField = null; + } + ``` + + 你可以像下面这样获取变量的注解: + ```java + Field field = ... //obtain field object + Annotation[] annotations = field.getDeclaredAnnotations(); + + for(Annotation annotation : annotations){ + if(annotation instanceof MyAnnotation){ + MyAnnotation myAnnotation = (MyAnnotation) annotation; + System.out.println("name: " + myAnnotation.name()); + System.out.println("value: " + myAnnotation.value()); + } + } + ``` + + 当然也可以获取一个指定的变量注解,如下: + ```java + Field field = ... // obtain method object + Annotation annotation = field.getAnnotation(MyAnnotation.class); + if(annotation instanceof MyAnnotation){ MyAnnotation myAnnotation = (MyAnnotation) annotation; System.out.println("name: " + myAnnotation.name()); System.out.println("value: " + myAnnotation.value()); } -} -``` - -当然也可以获取一个指定的变量注解,如下: -```java -Field field = ... // obtain method object -Annotation annotation = field.getAnnotation(MyAnnotation.class); - -if(annotation instanceof MyAnnotation){ - MyAnnotation myAnnotation = (MyAnnotation) annotation; - System.out.println("name: " + myAnnotation.name()); - System.out.println("value: " + myAnnotation.value()); -} -``` - - -一张图总结一下: - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_annotation.jpg?raw=true) - - - - - -#####编译时`Annotation`解析 - -在刚才介绍的运行时注解中,很多人肯定会说使用反射会影响性能,那有没有不影响性能的方式呢?当然有了,那就是编译时注解。在编译时会通过注解标示来动态生成一些类或者`xml`,而在运行时,这里注解是没有的,它会依靠动态生成的类来进行操作。所以它就和直接调用方法一样,当然不会有效率影响了。 - -该类值`@Retention`为`CLASS`的`Annotation`,由`APT(Annotaion Processing Tool)`自动进行解析。是在编译时注入,所以不会像反射一样影响效率问题。 - -根据sun官方的解释,APT(annotation processing tool)是一个命令行工具,它对源代码文件进行检测找出其中的annotation后,使用annotation processors来处理annotation。而annotation processors使用了一套反射API并具备对JSR175规范的支持。 - -annotation processors处理annotation的基本过程如下:首先,APT运行annotation processors根据提供的源文件中的annotation生成源代码文件和其它的文件(文件具体内容由annotation processors的编写者决定),接着APT将生成的源代码文件和提供的源文件进行编译生成类文件。 - -简单的和前面所讲的annotation实例BRFW相比,APT就像一个在编译时处理annotation的javac。而且从sun开发者的blog中看到,java1.6 beta版中已将APT的功能写入到了javac中,这样只要执行带有特定参数的javac就能达到APT的功能。 - -pt(Annotation Processing Tool)在编译时自动查找所有继承自AbstractProcessor的类,然后调用他们的process方法去处理,这样就拥有了在编译过程中执行代码的能力 - + ``` - -我们需要做的是: - -- 自定义类继承`AbstractProcessor` -- 重写`process`方法 +运行时注解示例: 相信很多人都知道`Butter Knife`。它里面就是使用了注解: ```java @@ -429,529 +405,185 @@ public @interface OnClick { @IdRes int[] value() default { View.NO_ID }; } ``` -看到了吗?是编译型的注解。这样不会影响性能。全局所以下`OnClick`,很容易找到它的处理类`ButterKnifeProccessor`: - -```java -@AutoService(Processor.class) -public final class ButterKnifeProcessor extends AbstractProcessor { - static final Id NO_ID = new Id(-1); - static final String VIEW_TYPE = "android.view.View"; - private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList"; - private static final String BITMAP_TYPE = "android.graphics.Bitmap"; - private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable"; - private static final String TYPED_ARRAY_TYPE = "android.content.res.TypedArray"; - private static final String NULLABLE_ANNOTATION_NAME = "Nullable"; - private static final String STRING_TYPE = "java.lang.String"; - private static final String LIST_TYPE = List.class.getCanonicalName(); - private static final String R = "R"; - private static final List> LISTENERS = Arrays.asList(// - OnCheckedChanged.class, // - OnClick.class, // - OnEditorAction.class, // - OnFocusChange.class, // - OnItemClick.class, // - OnItemLongClick.class, // - OnItemSelected.class, // - OnLongClick.class, // - OnPageChange.class, // - OnTextChanged.class, // - OnTouch.class // - ); -@Override public boolean process(Set elements, RoundEnvironment env) { - parseRClass(env); - - Map targetClassMap = findAndParseTargets(env); - - for (Map.Entry entry : targetClassMap.entrySet()) { - TypeElement typeElement = entry.getKey(); - BindingClass bindingClass = entry.getValue(); - - for (JavaFile javaFile : bindingClass.brewJava()) { - try { - javaFile.writeTo(filer); - } catch (IOException e) { - error(typeElement, "Unable to write view binder for type %s: %s", typeElement, - e.getMessage()); - } - } - } - - return true; - } -private Map findAndParseTargets(RoundEnvironment env) { - Map targetClassMap = new LinkedHashMap<>(); - Set erasedTargetNames = new LinkedHashSet<>(); - - // Process each @BindArray element. - for (Element element : env.getElementsAnnotatedWith(BindArray.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceArray(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindArray.class, e); - } - } - - // Process each @BindBitmap element. - for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceBitmap(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindBitmap.class, e); - } - } - - // Process each @BindBool element. - for (Element element : env.getElementsAnnotatedWith(BindBool.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceBool(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindBool.class, e); - } - } - - // Process each @BindColor element. - for (Element element : env.getElementsAnnotatedWith(BindColor.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceColor(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindColor.class, e); - } - } - - // Process each @BindDimen element. - for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceDimen(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindDimen.class, e); - } - } - - // Process each @BindDrawable element. - for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceDrawable(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindDrawable.class, e); - } - } - - // Process each @BindInt element. - for (Element element : env.getElementsAnnotatedWith(BindInt.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceInt(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindInt.class, e); - } - } - - // Process each @BindString element. - for (Element element : env.getElementsAnnotatedWith(BindString.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseResourceString(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindString.class, e); - } - } - - // Process each @BindView element. - for (Element element : env.getElementsAnnotatedWith(BindView.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseBindView(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindView.class, e); - } - } - - // Process each @BindViews element. - for (Element element : env.getElementsAnnotatedWith(BindViews.class)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseBindViews(element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - logParsingError(element, BindViews.class, e); - } - } - - // Process each annotation that corresponds to a listener. - for (Class listener : LISTENERS) { - findAndParseListener(env, listener, targetClassMap, erasedTargetNames); - } - - // Try to find a parent binder for each. - for (Map.Entry entry : targetClassMap.entrySet()) { - TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames); - if (parentType != null) { - BindingClass bindingClass = entry.getValue(); - BindingClass parentBindingClass = targetClassMap.get(parentType); - bindingClass.setParent(parentBindingClass); - } - } - - return targetClassMap; - } -``` -`findAndParseTargets()`方法中回去调用`findAndParseListener()`,那我们继续看`findAndParseListener()`方法: - -```java -private void findAndParseListener(RoundEnvironment env, - Class annotationClass, Map targetClassMap, - Set erasedTargetNames) { - for (Element element : env.getElementsAnnotatedWith(annotationClass)) { - if (!SuperficialValidation.validateElement(element)) continue; - try { - parseListenerAnnotation(annotationClass, element, targetClassMap, erasedTargetNames); - } catch (Exception e) { - StringWriter stackTrace = new StringWriter(); - e.printStackTrace(new PrintWriter(stackTrace)); - - error(element, "Unable to generate view binder for @%s.\n\n%s", - annotationClass.getSimpleName(), stackTrace.toString()); - } - } - } - - private void parseListenerAnnotation(Class annotationClass, Element element, - Map targetClassMap, Set erasedTargetNames) - throws Exception { - // This should be guarded by the annotation's @Target but it's worth a check for safe casting. - if (!(element instanceof ExecutableElement) || element.getKind() != METHOD) { - throw new IllegalStateException( - String.format("@%s annotation must be on a method.", annotationClass.getSimpleName())); - } - - ExecutableElement executableElement = (ExecutableElement) element; - TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); +看到了吗?是编译型的注解。这样不会影响性能。 - // Assemble information on the method. - Annotation annotation = element.getAnnotation(annotationClass); - Method annotationValue = annotationClass.getDeclaredMethod("value"); - if (annotationValue.getReturnType() != int[].class) { - throw new IllegalStateException( - String.format("@%s annotation value() type not int[].", annotationClass)); - } - int[] ids = (int[]) annotationValue.invoke(annotation); - String name = executableElement.getSimpleName().toString(); - boolean required = isListenerRequired(executableElement); - // Verify that the method and its containing class are accessible via generated code. - boolean hasError = isInaccessibleViaGeneratedCode(annotationClass, "methods", element); - hasError |= isBindingInWrongPackage(annotationClass, element); +一张图总结一下: - Integer duplicateId = findDuplicate(ids); - if (duplicateId != null) { - error(element, "@%s annotation for method contains duplicate ID %d. (%s.%s)", - annotationClass.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), - element.getSimpleName()); - hasError = true; - } +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_annotation.jpg?raw=true) - ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); - if (listener == null) { - throw new IllegalStateException( - String.format("No @%s defined on @%s.", ListenerClass.class.getSimpleName(), - annotationClass.getSimpleName())); - } - for (int id : ids) { - if (id == NO_ID.value) { - if (ids.length == 1) { - if (!required) { - error(element, "ID-free binding must not be annotated with @Optional. (%s.%s)", - enclosingElement.getQualifiedName(), element.getSimpleName()); - hasError = true; - } - // Verify target type is valid for a binding without an id. - String targetType = listener.targetType(); - if (!isSubtypeOfType(enclosingElement.asType(), targetType) - && !isInterface(enclosingElement.asType())) { - error(element, "@%s annotation without an ID may only be used with an object of type " - + "\"%s\" or an interface. (%s.%s)", - annotationClass.getSimpleName(), targetType, - enclosingElement.getQualifiedName(), element.getSimpleName()); - hasError = true; - } - } else { - error(element, "@%s annotation contains invalid ID %d. (%s.%s)", - annotationClass.getSimpleName(), id, enclosingElement.getQualifiedName(), - element.getSimpleName()); - hasError = true; - } - } - } - ListenerMethod method; - ListenerMethod[] methods = listener.method(); - if (methods.length > 1) { - throw new IllegalStateException(String.format("Multiple listener methods specified on @%s.", - annotationClass.getSimpleName())); - } else if (methods.length == 1) { - if (listener.callbacks() != ListenerClass.NONE.class) { - throw new IllegalStateException( - String.format("Both method() and callback() defined on @%s.", - annotationClass.getSimpleName())); - } - method = methods[0]; - } else { - Method annotationCallback = annotationClass.getDeclaredMethod("callback"); - Enum callback = (Enum) annotationCallback.invoke(annotation); - Field callbackField = callback.getDeclaringClass().getField(callback.name()); - method = callbackField.getAnnotation(ListenerMethod.class); - if (method == null) { - throw new IllegalStateException( - String.format("No @%s defined on @%s's %s.%s.", ListenerMethod.class.getSimpleName(), - annotationClass.getSimpleName(), callback.getDeclaringClass().getSimpleName(), - callback.name())); - } - } - // Verify that the method has equal to or less than the number of parameters as the listener. - List methodParameters = executableElement.getParameters(); - if (methodParameters.size() > method.parameters().length) { - error(element, "@%s methods can have at most %s parameter(s). (%s.%s)", - annotationClass.getSimpleName(), method.parameters().length, - enclosingElement.getQualifiedName(), element.getSimpleName()); - hasError = true; - } +#####编译时`Annotation`解析 - // Verify method return type matches the listener. - TypeMirror returnType = executableElement.getReturnType(); - if (returnType instanceof TypeVariable) { - TypeVariable typeVariable = (TypeVariable) returnType; - returnType = typeVariable.getUpperBound(); - } - if (!returnType.toString().equals(method.returnType())) { - error(element, "@%s methods must have a '%s' return type. (%s.%s)", - annotationClass.getSimpleName(), method.returnType(), - enclosingElement.getQualifiedName(), element.getSimpleName()); - hasError = true; - } +在刚才介绍的运行时注解中,很多人肯定会说使用反射会影响性能,那有没有不影响性能的方式呢?当然有了,那就是编译时注解。在编译时会通过注解标示来动态生成一些类或者`xml`,而在运行时,这里注解是没有的,它会依靠动态生成的类来进行操作。所以它就和直接调用方法一样,当然不会有效率影响了。 - if (hasError) { - return; - } +该类型注解值是`@Retention`为`CLASS`的`Annotation`,由`APT(Annotaion Processing Tool)`自动进行解析。是在编译时注入,所以不会像反射一样影响效率问题。 - Parameter[] parameters = Parameter.NONE; - if (!methodParameters.isEmpty()) { - parameters = new Parameter[methodParameters.size()]; - BitSet methodParameterUsed = new BitSet(methodParameters.size()); - String[] parameterTypes = method.parameters(); - for (int i = 0; i < methodParameters.size(); i++) { - VariableElement methodParameter = methodParameters.get(i); - TypeMirror methodParameterType = methodParameter.asType(); - if (methodParameterType instanceof TypeVariable) { - TypeVariable typeVariable = (TypeVariable) methodParameterType; - methodParameterType = typeVariable.getUpperBound(); - } +根据`sun`官方的解释,`APT(annotation processing tool)`是一个命令行工具, +它对源代码文件进行检测找出其中的`annotation`后,使用`annotation processors`来处理`annotation`。 +而`annotation processors`使用了一套反射`API`并具备对`JSR175`规范的支持。 - for (int j = 0; j < parameterTypes.length; j++) { - if (methodParameterUsed.get(j)) { - continue; - } - if (isSubtypeOfType(methodParameterType, parameterTypes[j]) - || isInterface(methodParameterType)) { - parameters[i] = new Parameter(j, TypeName.get(methodParameterType)); - methodParameterUsed.set(j); - break; - } - } - if (parameters[i] == null) { - StringBuilder builder = new StringBuilder(); - builder.append("Unable to match @") - .append(annotationClass.getSimpleName()) - .append(" method arguments. (") - .append(enclosingElement.getQualifiedName()) - .append('.') - .append(element.getSimpleName()) - .append(')'); - for (int j = 0; j < parameters.length; j++) { - Parameter parameter = parameters[j]; - builder.append("\n\n Parameter #") - .append(j + 1) - .append(": ") - .append(methodParameters.get(j).asType().toString()) - .append("\n "); - if (parameter == null) { - builder.append("did not match any listener parameters"); - } else { - builder.append("matched listener parameter #") - .append(parameter.getListenerPosition() + 1) - .append(": ") - .append(parameter.getType()); - } - } - builder.append("\n\nMethods may have up to ") - .append(method.parameters().length) - .append(" parameter(s):\n"); - for (String parameterType : method.parameters()) { - builder.append("\n ").append(parameterType); - } - builder.append( - "\n\nThese may be listed in any order but will be searched for from top to bottom."); - error(executableElement, builder.toString()); - return; - } - } - } +`annotation processors`处理`annotation`的基本过程如下: - MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required); - BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); - for (int id : ids) { - if (!bindingClass.addMethod(getId(id), listener, method, binding)) { - error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)", - id, enclosingElement.getQualifiedName(), element.getSimpleName()); - return; - } - } +- `APT`运行`annotation processors`根据提供的源文件中的`annotation`生成源代码文件和其它的文件(文件具体内容由`annotation processors`的编写者决定) +- 接着`APT`将生成的源代码文件和提供的源文件进行编译生成类文件。 - // Add the type-erased version to the valid binding targets set. - erasedTargetNames.add(enclosingElement); - } -``` +`APT`在编译时自动查找所有继承自`AbstractProcessor`的类,然后调用他们的`process`方法去处理,这样就拥有了在编译过程中执行代码的能力 -``` +所以我们需要做的是: +- 自定义类继承`AbstractProcessor` +- 重写`process`方法 -例如: +那我们就开始写: ```java public class Processor extends AbstractProcessor{ } ``` -我用`Android Studio`死活提示找不到`AbstractProcessor`类,但是它明明就在`jdk`中。我是这样解决的,新建一个`Moduel`,在选择类型时将该`Moduel`的类型选为`Java Library`。然后在该`Module`中创建就好了,完美解决。 -这是因为注解是`javase`中`javax`包里面的,`android.jar`默认是不包含的,所以会编译报错。新建`Java`类型的`Library`后就可以直接使用`javax`包中注解相关的类了。 +但是在`Android Studio`死活提示找不到`AbstractProcessor`类,这是因为注解是`javase`中`javax`包里面的,`android.jar`默认是不包含的,所以会编译报错. +解决方法就是新建一个`Moduel`,在选择类型时将该`Moduel`的类型选为`Java Library`。 +然后在该`Module`中创建就好了`Processor`就好了,完美解决。 好,那我们就开始写个编译时处理的`demo` : + - `Android Studio`中创建一个`Android`工程。 - 新建一个`Module`,然后选择`Java Library`类型(我的名字为`annotations`),并且让`app`依赖该`module`。 -- 在`annotations`的`module`中昔年一个注解类: - ```java -package com.charon; +- 在`annotations`的`module`中创建注解类: -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; + ```java + package com.charon; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.CLASS) + public @interface AnnotationTest { + String value() default ""; + } + ``` +- 然后在`annotations`的`module`自定义`Processor`类 -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.CLASS) -public @interface AnnotationTest { - String value() default ""; -} -``` -- 然后在`annotations`的`module`自定义`Processor`类: ```java -package com.charon; - -import java.util.Set; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; - -@SupportedAnnotationTypes("com.charon.AnnotationTest") -@SupportedSourceVersion(SourceVersion.RELEASE_7) -public class TestProcessor extends AbstractProcessor { - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - System.out.println("process"); - for (TypeElement te : annotations) { - for (Element element : roundEnv.getElementsAnnotatedWith(te)) { - AnnotationTest annotation = element.getAnnotation(AnnotationTest.class); - String value = annotation.value(); - System.out.println("type : " + value); + package com.charon; + + import java.util.Set; + + import javax.annotation.processing.AbstractProcessor; + import javax.annotation.processing.RoundEnvironment; + import javax.annotation.processing.SupportedAnnotationTypes; + import javax.annotation.processing.SupportedSourceVersion; + import javax.lang.model.SourceVersion; + import javax.lang.model.element.Element; + import javax.lang.model.element.TypeElement; + + @SupportedAnnotationTypes("com.charon.AnnotationTest") + @SupportedSourceVersion(SourceVersion.RELEASE_7) + public class TestProcessor extends AbstractProcessor { + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + System.out.println("process"); + for (TypeElement te : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(te)) { + AnnotationTest annotation = element.getAnnotation(AnnotationTest.class); + String value = annotation.value(); + System.out.println("type : " + value); + } } + return true; } - return true; } -} -``` -***注意:***`@SupportedAnnotationTypes("com.charon.AnnotationTest")`来指定要处理的注解类。`@SupportedSourceVersion(SourceVersion.RELEASE_7)`指定编译的版本。 + ``` + ***注意:***`@SupportedAnnotationTypes("com.charon.AnnotationTest")`来指定要处理的注解类。 + `@SupportedSourceVersion(SourceVersion.RELEASE_7)`指定编译的版本。这种通过注解指定编译版本和类型的方式是从`Java 1.7`才有的。 + 对于之前的版本都是通过重写`AbstractProcessor`中的方法来指定的。 - 注册处理器 - 我们自定义了`Processor`那如何才能让其生效呢?就是在`annotations`的`java`同级目录新建`resources/META-INF/services/javax.annotation.processing.Processor -```java - - java - - META-INF - - services - - javax.annotation.processing.Processor -``` -然后在`javax.annotation.processing.Processor`文件中指定自定义的处理器: -```java -com.charon.TestProcessor -``` -如果多个话就分行写。 + 我们自定义了`Processor`那如何才能让其生效呢?就是在`annotations`的`java`同级目录新建`resources/META-INF/services/javax.annotation.processing.Processor`文件 -然后我们`Build`一下(命令行执行`./gradlew build`),就能看到控制台打印出来如下的信息: -```java -:app:compileReleaseJavaWithJavac -:app:compileReleaseJavaWithJavac - is not incremental (e.g. outputs have changed, no previous execution, etc.). -process -value : haha -process -``` + ```java + - java + - META-INF + - services + - javax.annotation.processing.Processor + ``` + 然后在`javax.annotation.processing.Processor`文件中指定自定义的处理器,如: + + ```java + com.charon.TestProcessor + ``` + 如果多个话就分行写。 -注意,千万不要去`logcat`中找信息,这是编译时注解。 -主要使用`jdk1.7`,`1.8`有bug。 + 然后我们`Build`一下(命令行执行`./gradlew build`),就能看到控制台打印出来如下的信息: + ```java + :app:compileReleaseJavaWithJavac + :app:compileReleaseJavaWithJavac - is not incremental (e.g. outputs have changed, no previous execution, etc.). + process + value : haha + process + ``` + ***注意:***千万不要去`logcat`中找信息,这是编译时注解。 + ***注意:***一定要使用`jdk1.7`,`1.8`对注解的支持有`bug`。 + + +上面只是一个简单的例子,如果你想用编译时注解去做一些更高级的事情,例如自动生成一些代码,那你可能就会用到如下几个类库: -上面只是一个简单的例子,如果你想用编译时注解去做一些更高级的事情,生成一些代码,那你可能就会用到如下几个类库: - [android-apt](https://bitbucket.org/hvisser/android-apt) - [Google Auto](https://github.com/google/auto) - [Square javapoet](https://github.com/square/javapoet) -`Android Studio`原本是不支持注解处理器的, 但是用`android-apt`这个插件后, 我们就可以使用注解处理器了, 这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到APK里面去, 而且它还可以让最终编译出来的APK里面不包含注解处理器本身的代码, 因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。 - -也就是说它主要有两个目的: -- 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library -- 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用 - -那在什么情况下我们会需要使用它呢? - -当你需要引入Processors 生成的源代码到你的代码中时。例如,当你使用Dagger 2 或 AndroidAnnotaition.该插件使得Android Studio可以配置生成资源的build path,避免IDE报错。当使用 apt添加添加依赖,它将不会被包含到最终的APK里。 +这三个库分别的作用为: +- `android-apt` + `Android Studio`原本是不支持注解处理器的, 但是用`android-apt`这个插件后, 我们就可以使用注解处理器了, + 这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到`APK`里面去, 而且它还可以让最终编译出来的`APK`里面不包含注解处理器本身的代码, + 因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。 + 也就是说它主要有两个目的: + + - 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library + - 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用 + 那在什么情况下我们会需要使用它呢? + 当你需要引入`Processor`生成的源代码到你的代码中时。例如当你使用`Dagger 2`或`AndroidAnnotaition`. + 该插件使得`Android Studio`可以配置生成资源的`build path`,避免`IDE`报错。 + 当使用`apt`添加添加依赖,它将不会被包含到最终的`APK`里。 -`Google Auto`的主要作用是注解`Processor`类,并对其生成`META-INF`的配置信息,可以让你不用去写`META-INF`这些配置文件,只要在自定义的`Processor`上面加上`@AutoService(Processor.class)` -`javapoet`是一个`A Java API for generating .java source files.`可以更方便的生成代码,它可以帮助我们通过类调用的形式来生成代码。 +- `Auto` + `Google Auto`的主要作用是注解`Processor`类,并对其生成`META-INF`的配置信息, + 可以让你不用去写`META-INF`这些配置文件,只要在自定义的`Processor`上面加上`@AutoService(Processor.class)` +- `javapoet` + `javapoet`:`A Java API for generating .java source files.`可以更方便的生成代码,它可以帮助我们通过类调用的形式来生成代码。 - - -###项目结构 +###自定义编译时注解 在自定义注解时,一般来说可能会建三个`modules`: - `app module`:写一些使用注解的`android`应用逻辑。 - `api module`:定义一些可以在`app`中使用的注解。它会被`app`以及`compiler`使用。 -- `compiler module`:定义`Processor`该`module`不会被包含到应用中,它只会在构建过程中被使用,它会生成一些`java`文件,来打包进`apk`中。我们可以在该`module`中使用`auto`以及`javapoet`。 +- `compiler module`:定义`Processor`该`module`不会被包含到应用中,它只会在构建过程中被使用。在编译的过程中它会生成一些`java`文件,而这些`java`文件会被打包进`apk`中。 + 我们可以在该`module`中使用`auto`以及`javapoet`。 下面开始配置`android-apt`: -//配置在Project下的build.gradle中 +- 配置在`Project`下的`build.gradle`中 +``` buildscript { repositories { mavenCentral() @@ -963,7 +595,8 @@ buildscript { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } } -//配置到`app module`下的build.gradle中 +``` +- 配置到`app module`下的`build.gradle`中 ```java apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' @@ -974,132 +607,129 @@ dependencies { } ``` -dddddddddddd - -接下来在`compiler module`中配置`auto`以及`javapoet`: - +- 接下来在`compiler module`中配置`auto`以及`javapoet`: ```java apply plugin: 'java' sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 dependencies { - compile project (':api') - compile 'com.google.auto.service:auto-service:1.0-rc2' - compile 'com.squareup:javapoet:1.7.0' + compile project (':api') + compile 'com.google.auto.service:auto-service:1.0-rc2' + compile 'com.squareup:javapoet:1.7.0' } ``` -在`api module`中也加上如下的配置: - +- 在`api module`中也加上如下的配置: ```java apply plugin: 'java' sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 ``` -接下来在`api module`中定义一个注解: -```java -package com.charon; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.CLASS) -public @interface AnnotationTest { - String value(); -} - -`` -然后在`compiler module`中自定义一个`Processor`: - - -```java -package com.charon; - -import com.google.auto.service.AutoService; - -import java.util.Set; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.Processor; -import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.TypeElement; - -@AutoService(Processor.class) -public class MyProcessor extends AbstractProcessor { - - /** - * Initializes the processor with the processing environment by - * setting the {@code processingEnv} field to the value of the - * {@code processingEnv} argument. An {@code - * IllegalStateException} will be thrown if this method is called - * more than once on the same object. - * - * @param processingEnv environment to access facilities the tool framework - * provides to the processor - * @throws IllegalStateException if this method is called more than once. - */ - @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - super.init(processingEnv); - } - - /** - * Processes a set of annotation types on type elements originating from the prior round - * and returns whether or not these annotations are claimed by this processor. - * If true is returned, the annotations are claimed and subsequent processors will - * not be asked to process them; - * if false is returned, the annotations are unclaimed and subsequent processors may be - * asked to process them. A processor may always return the same boolean value - * or may vary the result based on chosen criteria. - * The input set will be empty if the processor supports "*" and the root elements - * have no annotations. A Processor must gracefully handle an empty set of annotations. - * @param annotations - * @param roundEnv - * @return - */ - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - System.out.println("hello processor"); - return false; - } - - @Override - public Set getSupportedAnnotationTypes() { - return super.getSupportedAnnotationTypes(); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - -} - +- 接下来在`api module`中定义一个注解 + ```java + package com.charon; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.CLASS) + public @interface AnnotationTest { + String value(); + } + ``` +- 然后在`compiler module`中自定义一个`Processor` + ```java + package com.charon; + + import com.google.auto.service.AutoService; + + import java.util.Set; + + import javax.annotation.processing.AbstractProcessor; + import javax.annotation.processing.ProcessingEnvironment; + import javax.annotation.processing.Processor; + import javax.annotation.processing.RoundEnvironment; + import javax.lang.model.SourceVersion; + import javax.lang.model.element.TypeElement; + + @AutoService(Processor.class) + public class MyProcessor extends AbstractProcessor { + /** + * Initializes the processor with the processing environment by + * setting the {@code processingEnv} field to the value of the + * {@code processingEnv} argument. An {@code + * IllegalStateException} will be thrown if this method is called + * more than once on the same object. + * + * @param processingEnv environment to access facilities the tool framework + * provides to the processor + * @throws IllegalStateException if this method is called more than once. + */ + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + /** + * Processes a set of annotation types on type elements originating from the prior round + * and returns whether or not these annotations are claimed by this processor. + * If true is returned, the annotations are claimed and subsequent processors will + * not be asked to process them; + * if false is returned, the annotations are unclaimed and subsequent processors may be + * asked to process them. A processor may always return the same boolean value + * or may vary the result based on chosen criteria. + * The input set will be empty if the processor supports "*" and the root elements + * have no annotations. A Processor must gracefully handle an empty set of annotations. + * @param annotations + * @param roundEnv + * @return + */ + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + System.out.println("hello processor"); + return false; + } + + @Override + public Set getSupportedAnnotationTypes() { + return super.getSupportedAnnotationTypes(); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + } + ``` + 这里简单的说一下这几个主要的方法: + + - `init()`:初始化操作的方法,`RoundEnvironment`会提供很多有用的工具类`Elements`、`Types`和`Filer`等。 + - `process()`:这相当于每个处理器的主函数`main()`。在该方法中去扫描、评估、处理以及生成`Java`文件。 + - `getSupportedAnnotationTypes()`:这里你必须指定,该注解器是注册给哪个注解的。 + - `getSupportedSourceVersion()`:用来指定你使用的`java`版本。通常这里会直接放回`SourceVersion.latestSupported()`即可。 + 从`idk 1.7`开始,可以使用如下注解来代替`getSupporedAnnotationTypes()`和`getSupportedSourceVersion()`方法: + ```java + @SupportedSourceVersion(SourceVersion.latestSupported()) +@SupportedAnnotationTypes({ + // 合法注解全名的集合 + }) ``` - -注解相关API: +注解相关API: - Set getElementsAnnotatedWith(Class a) -返回使用给定注释类型注释的元素。该注释可能直接出现或者被继承。只返回注释处理的此 round 中包括 的 package 元素和 type 元素、成员声明、参数或者这些元素中声明的类型参数。 + 返回使用给定注释类型注释的元素。该注释可能直接出现或者被继承。只返回注释处理的此 round 中包括 的 package 元素和 type 元素、成员声明、参数或者这些元素中声明的类型参数。 - Element:表示一个程序元素,比如包、类或者方法。每个元素都表示一个静态的语言级构造(不表示虚拟机的运行时构造)。 - -元素应该使用 equals(Object) 方法进行比较。不保证总是使用相同的对象表示某个特定的元素。 - -要实现基于 Element 对象类的操作,可以使用 visitor 或者使用 getKind() 方法的结果。使用 instanceof 确定此建模层次结构中某一对象的有效类 未必 可靠,因为一个实现可以选择让单个对象实现多个 Element 子接口。 + 元素应该使用 equals(Object) 方法进行比较。不保证总是使用相同的对象表示某个特定的元素。 + 要实现基于 Element 对象类的操作,可以使用 visitor 或者使用 getKind() 方法的结果。使用 instanceof 确定此建模层次结构中某一对象的有效类 未必 可靠,因为一个实现可以选择让单个对象实现多个 Element 子接口。 - TypeElement:表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注释类型是一种接口。 - -TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。这种区别对于一般的类型是最明显的,对于这些类型,单个元素可以定义一系列完整的类型。例如,元素 java.util.Set 对应于参数化类型 java.util.Set 和 java.util.Set(以及其他许多类型),还对应于原始类型 java.util.Set。 - - - + TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。这种区别对于一般的类型是最明显的,对于这些类型,单个元素可以定义一系列完整的类型。例如,元素 java.util.Set 对应于参数化类型 java.util.Set 和 java.util.Set(以及其他许多类型),还对应于原始类型 java.util.Set。 @@ -1116,70 +746,62 @@ dependencies { `Android`提供了很多注解来支持在方法、参数和返回值上面使用,例如: -- `@Nullable` +- `@Nullable` 可以为`null` - -- `@NonNull` - 不能为`null` -- `@StringRes` - `R.string`类型的资源。 -- `@DrawableRes` +- `@NonNull` + 不能为`null` + ```java + import android.support.annotation.NonNull; + ... + + /** Add support for inflating the tag. */ + @NonNull + @Override + public View onCreateView(String name, @NonNull Context context, + @NonNull AttributeSet attrs) { + ... + } + ... + ``` +- `@StringRes` + `R.string`类型的资源。 + ```java + import android.support.annotation.StringRes; + ... + public abstract void setTitle(@StringRes int resId); + ... + + 遇到那种你写了个`setTitle(int resId)`他确给你传`setTitle(R.drawable.xxx)`的选手,用这种方式能很好的去提示下。 + ``` +- `@DrawableRes` `Drawable`类型的资源。 -- `@ColorRes` +- `@ColorRes` `Color`类型的资源。 -- `@InterpolatorRes` +- `@InterpolatorRes` `Interpolatro`类型。 -- `@AnyRes` +- `@AnyRes` `R.`类型。 -- `@UiThread` +- `@UiThread` 从`UI thread`调用。 -- `@RequiresPermission` +- `@RequiresPermission` 来验证该方法的调用者所需要有的权限。检查一个列表中的任何一个权限可以使用`anyOf`属性。想要检查多个权限时,可以使用`allOf`属性。如下: -```java -@RequiresPermission(Manifest.permission.SET_WALLPAPER) -public abstract void setWallpaper(Bitmap bitmap) throws IOException; -``` -检查多个权限: - -```java -@RequiresPermission(allOf = { - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE}) -public static final void copyFile(String dest, String source) { - ... -} - -``` - - - -例如: - -```java -import android.support.annotation.NonNull; -... - - /** Add support for inflating the tag. */ - @NonNull - @Override - public View onCreateView(String name, @NonNull Context context, - @NonNull AttributeSet attrs) { - ... - } -... -``` - -```java -import android.support.annotation.StringRes; -... - public abstract void setTitle(@StringRes int resId); - ... - -遇到那种你写了个`setTitle(int resId)`他确给你传`setTitle(R.drawable.xxx)`的选手,用这种方式能很好的去提示下。 -``` - - + ```java + @RequiresPermission(Manifest.permission.SET_WALLPAPER) + public abstract void setWallpaper(Bitmap bitmap) throws IOException; + ``` + 检查多个权限: + + ```java + @RequiresPermission(allOf = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}) + public static final void copyFile(String dest, String source) { + ... + } + ``` + --- - 邮箱 :charon.chui@gmail.com - Good Luck! + From 8bfbb5f47a00c650f7e52bd1daf559ca3e385e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E6=98=AF=E6=9D=8E=E5=B0=8F=E5=B9=B3?= <847119601@qq.com> Date: Mon, 4 Jul 2016 14:16:08 +0800 Subject: [PATCH 044/373] =?UTF-8?q?=E5=BC=B1=E5=BC=95=E7=94=A8(WeakReferen?= =?UTF-8?q?ce)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 语句重复 --- ...4\250\343\200\201\350\231\232\345\274\225\347\224\250.md" | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" index a8d8ccfc..bb3f6485 100644 --- "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -14,8 +14,7 @@ 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 弱引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果弱引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 - 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 + - 虚引用(PhantomReference) "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用, @@ -37,4 +36,4 @@ --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 3b1ae78df30992e70baf9559fc11353f5d697fee Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 5 Jul 2016 11:04:00 +0800 Subject: [PATCH 045/373] delete the error part --- ...flater.inflate\350\257\246\350\247\243.md" | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 "Android\345\212\240\345\274\272/LayoutInflater.inflate\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/LayoutInflater.inflate\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/LayoutInflater.inflate\350\257\246\350\247\243.md" deleted file mode 100644 index 3087a5a0..00000000 --- "a/Android\345\212\240\345\274\272/LayoutInflater.inflate\350\257\246\350\247\243.md" +++ /dev/null @@ -1,94 +0,0 @@ -LayoutInflater.inflate详解 -=== - -`LayoutInflater`概述 ---- - -从`XML`文件中实例化一个布局成对应的`View`类, 它从来不会直接使用, 而是使用`getLayoutInflater()`或者`getSystemService(String)`来获得一个对应当前`context`的标准`LayoutInflater` -实例。 - -例如:     -```java -LayoutInflater inflater = (LayoutInflater)context.getSystemService - (Context.LAYOUT_INFLATER_SERVICE); -``` - -如果要在自己的`views`中通过`LayoutInflater.Factory`来创建`LayoutInflater`你可以用`cloneInContext(Context)`来克隆一个当前存在的`ViewFactory`然后再用`setFactory(LayoutInfalter.FActory)` -来设置成自己的`FActory`. - - -起因 ---- - -原来在项目中一直习惯用`inflater.inflate(R.layout.layout, null)`最近却发现会有一个黄色的警告。 十分好奇,所以决定找出原因。 - -我们发现`inflate`方法有两个: -- View inflate(int resource, ViewGroup root) -- View inflate(int resource , ViewGroup root, boolean attachToRoot) -第二个参数是指实例的布局所要放入的跟视图。 -一般我们在不需要将该布局放入跟视图的时候都会把第二个参数传为`null`,这样系统就不会去进行相应的绑定操作了,不然就蹦了。 我相信很多人都会这样理解,所以都很少用到 -第二个方法, 其实这样是错误的。 - -原因在于`android:layout_xyz`属性会在父视图中重新计算,而你用第一个方法是说需要被添加到父视图中,只不过你不知道添加到哪一个父视图中, 所以你在`xml`中定义的`LayoutParams` -就会被忽略(因为没有已知的父视图)。 - -示例 ---- - -大家肯定遇到过在`ListView`的`item`布局中设置的高度没有效果的问题。 -```java - - - -``` - -```java -public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = inflate(R.layout.item_lv_test, null); - } - return convertView; -} -``` - -如果用上面的代码会发现设置`100dp`是无效的。 - -而如果换成下面的代码就可以了。 -```java -public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = inflate(R.layout.item_lv_test, null, false); - } - return convertView; -} -``` -这里你该会想一想为什么很多需要显示`View`的方法中都有`ViewGroup`这个参数。 -所以有些人会说在跟布局中设置是无效的,要再嵌套一层布局。 这样是错误的,会造成布局层级增多,影响性能。 - - -细节 ---- - -- `setContentView()`与`LayoutInfalter.inflate()`的区别 - `setContentView()`一旦调用,就会立即显示`UI`. 而`LayoutInfalter.inflate()`这是把布局转换成对应的`View`对象,不会立即显示,只有需要的时候再显示出来。 - -- `View.inflate()`方法与`LayoutInflater.inflate()`的区别 - 直接上源码: - ```java - public static View inflate(Context context, int resource, ViewGroup root) { - LayoutInflater factory = LayoutInflater.from(context); - return factory.inflate(resource, root); - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! From 74d03a732e0dd7e90f2e378507a7154e46392731 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 7 Jul 2016 20:23:02 +0800 Subject: [PATCH 046/373] =?UTF-8?q?Update=20=E6=B3=A8=E8=A7=A3=E4=BD=BF?= =?UTF-8?q?=E7=94=A8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\350\247\243\344\275\277\347\224\250.md" | 909 ++++++++++++++++-- 1 file changed, 852 insertions(+), 57 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 69da72c8..45250d90 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -1,4 +1,4 @@ -RxJava详解 +注解使用 === ###简介 @@ -13,18 +13,18 @@ RxJava详解 `Java`语言解释器在工作时会忽略这些`annotation`,因此在`JVM`中这些`annotation`是“不起作用”的,只能通过配套的工具才能对这些`annontaion`类型的信息进行访问和处理。 ###说明 + - `Annotation`的声明是通过关键字`@interface`。这个关键字会去继承`Annotation`接口。 - `Annotation`的方法定义是独特的、受限制的。 `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、`String类型`、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 注解如果只有一个默认属性,可直接用`value()`函数。一个属性也没有则表示该`Annotation`为`Mark Annotation`。 例如: -```java -public @interface UnitTest { - String value(); -} -``` + ```java + public @interface UnitTest { + String value(); + } + ``` 在使用时可以直接使用`@UnitTest("GCD")`,`@UnitTest("GCD"`实际上就是是 `@UnitTest(value="GCD)`的简单写法。 - 例如: ```java @Target(ElementType.METHOD) @@ -507,7 +507,8 @@ public class Processor extends AbstractProcessor{ ``` ***注意:***`@SupportedAnnotationTypes("com.charon.AnnotationTest")`来指定要处理的注解类。 `@SupportedSourceVersion(SourceVersion.RELEASE_7)`指定编译的版本。这种通过注解指定编译版本和类型的方式是从`Java 1.7`才有的。 - 对于之前的版本都是通过重写`AbstractProcessor`中的方法来指定的。 + 对于之前的版本都是通过重写`AbstractProcessor`中的方法来指定的。 + - 注册处理器 我们自定义了`Processor`那如何才能让其生效呢?就是在`annotations`的`java`同级目录新建`resources/META-INF/services/javax.annotation.processing.Processor`文件 @@ -582,48 +583,75 @@ public class Processor extends AbstractProcessor{ 下面开始配置`android-apt`: -- 配置在`Project`下的`build.gradle`中 -``` -buildscript { - repositories { - mavenCentral() +- 配置到`app module`下的`build.gradle`中 + + ```java + apply plugin: 'com.android.application' + // apt + apply plugin: 'com.neenbedankt.android-apt' + + android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + + defaultConfig { + applicationId "com.charon.annotationdemo" + minSdkVersion 14 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + } + + buildscript { + repositories { + jcenter() + } + dependencies { + // 配置apt + classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' + } } + dependencies { - //替换成最新的 gradle版本 - classpath 'com.android.tools.build:gradle:1.3.0' - //替换成最新android-apt版本 - classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.4.0' + compile project(':api') + apt project(':compiler') + } -} -``` -- 配置到`app module`下的`build.gradle`中 -```java -apply plugin: 'com.android.application' -apply plugin: 'com.neenbedankt.android-apt' -... -dependencies { - compile project(':api') - apt project(':compiler') -} -``` + ``` -- 接下来在`compiler module`中配置`auto`以及`javapoet`: -```java -apply plugin: 'java' -sourceCompatibility = JavaVersion.VERSION_1_7 -targetCompatibility = JavaVersion.VERSION_1_7 -dependencies { - compile project (':api') - compile 'com.google.auto.service:auto-service:1.0-rc2' - compile 'com.squareup:javapoet:1.7.0' -} -``` +- 接下来在`compiler module`中配置`auto`以及`javapoet` + + ```java + apply plugin: 'java' + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 + dependencies { + compile project (':api') + compile 'com.google.auto.service:auto-service:1.0-rc2' + compile 'com.squareup:javapoet:1.7.0' + } + ``` - 在`api module`中也加上如下的配置: -```java -apply plugin: 'java' -sourceCompatibility = JavaVersion.VERSION_1_7 -targetCompatibility = JavaVersion.VERSION_1_7 -``` + + ```java + apply plugin: 'java' + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 + ``` - 接下来在`api module`中定义一个注解 ```java @@ -706,31 +734,795 @@ targetCompatibility = JavaVersion.VERSION_1_7 } ``` + 这里简单的说一下这几个主要的方法: - `init()`:初始化操作的方法,`RoundEnvironment`会提供很多有用的工具类`Elements`、`Types`和`Filer`等。 - `process()`:这相当于每个处理器的主函数`main()`。在该方法中去扫描、评估、处理以及生成`Java`文件。 - `getSupportedAnnotationTypes()`:这里你必须指定,该注解器是注册给哪个注解的。 - `getSupportedSourceVersion()`:用来指定你使用的`java`版本。通常这里会直接放回`SourceVersion.latestSupported()`即可。 + 从`idk 1.7`开始,可以使用如下注解来代替`getSupporedAnnotationTypes()`和`getSupportedSourceVersion()`方法: ```java @SupportedSourceVersion(SourceVersion.latestSupported()) -@SupportedAnnotationTypes({ - // 合法注解全名的集合 - }) + @SupportedAnnotationTypes({ + // 合法注解全名的集合 + }) + ``` + +注解相关`API`: + +- `Set getElementsAnnotatedWith(Class a)` + 返回使用给定注释类型注释的元素。该注释可能直接出现或者被继承。只返回注释处理的此`round`中包括的`package`元素和`type`元素、成员声明、参数或者这些元素中声明的类型参数。 +- `Element` + 表示一个程序元素,比如包、类或者方法。每个元素都表示一个静态的语言级构造(不表示虚拟机的运行时构造)。 + 元素应该使用`equals(Object)`方法进行比较。不保证总是使用相同的对象表示某个特定的元素。 + 要实现基于`Element`对象类的操作,可以使用`visitor`或者使用`getKind()`方法的结果。使用`instanceof`确定此建模层次结构中某一对象的有效类未必可靠,因为一个实现可以选择让单个对象实现多个`Element`子接口。 + +- `TypeElement` + 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注释类型是一种接口。 + 而`DeclaredType`表示一个类或接口类型,后者将成为前者的一种使用(或调用)。这种区别对于一般的类型是最明显的,对于这些类型,单个元素可以定义一系列完整的类型。 + 例如,元素`java.util.Set`对应于参数化类型`java.util.Set`和`java.util.Set`(以及其他许多类型),还对应于原始类型`java.util.Set`。 + + + +扯远了,那我们就以上面的目录结构和实现方式,通过一个具体的例子要详细说明,例子真的很难找,我在网上找到了一个大神写的,虽然从这个例子中并不能看出注解的强大之处,但是对于讲解来说还是非常有用的: + +我们要解决的问题是我们想要实现一个披萨店,该店会提供两种披萨(`Margherita`和`Calzone`)和一种甜点(`Tiramisu`)。 + +首先我们在`app module`中创建对应的接口和代码 ,如下: + +```java +public interface Meal { + public float getPrice(); +} + +public class MargheritaPizza implements Meal { + + @Override public float getPrice() { + return 6.0f; + } +} + +public class CalzonePizza implements Meal { + + @Override public float getPrice() { + return 8.5f; + } +} + +public class Tiramisu implements Meal { + + @Override public float getPrice() { + return 4.5f; + } +} +``` +如果消费者想要下单,那么它需要输入对应的物品名字: +```java +public class PizzaStore { + + public Meal order(String mealName) { + + if (mealName == null) { + throw new IllegalArgumentException("Name of the meal is null!"); + } + + if ("Margherita".equals(mealName)) { + return new MargheritaPizza(); + } + + if ("Calzone".equals(mealName)) { + return new CalzonePizza(); + } + + if ("Tiramisu".equals(mealName)) { + return new Tiramisu(); + } + + throw new IllegalArgumentException("Unknown meal '" + mealName + "'"); + } + + public static void main(String[] args) throws IOException { + PizzaStore pizzaStore = new PizzaStore(); + Meal meal = pizzaStore.order(readConsole()); + System.out.println("Bill: $" + meal.getPrice()); + } +``` +这样的话在`order()`方法中,会有很多`if`语句,并且每当我们添加一种新的披萨,我们都要添加一条新的`if`语句。但是我们可以使用注解和工厂模式,我们就可以让注解来自动生成这些在工厂中的`if`语句。我们期待的代码如下: +```java +public class PizzaStore { + + private MealFactory factory = new MealFactory(); + + public Meal order(String mealName) { + return factory.create(mealName); + } + + public static void main(String[] args) throws IOException { + PizzaStore pizzaStore = new PizzaStore(); + Meal meal = pizzaStore.order(readConsole()); + System.out.println("Bill: $" + meal.getPrice()); + } +} + +``` +`MealFactory`的实现应该如下: +```java +public class MealFactory { + + public Meal create(String id) { + if (id == null) { + throw new IllegalArgumentException("id is null!"); + } + if ("Calzone".equals(id)) { + return new CalzonePizza(); + } + + if ("Tiramisu".equals(id)) { + return new Tiramisu(); + } + + if ("Margherita".equals(id)) { + return new MargheritaPizza(); + } + + throw new IllegalArgumentException("Unknown id = " + id); + } +} +``` + +接下来我们要做的就是通过注解来生成`MealFactory`中的代码。 +想法是这样的:我们将使用同样的`type()`注解那些属于同一个工厂的类,并且用注解的`id()`做一个映射,例如从"Calzone"映射到"ClzonePizza"类。我们使用`@Factory`注解到我们的类中,如下: + +```java +@Factory( + id = "Margherita", + type = Meal.class +) +public class MargheritaPizza implements Meal { + + @Override public float getPrice() { + return 6f; + } +} +``` +```java +@Factory( + id = "Calzone", + type = Meal.class +) +public class CalzonePizza implements Meal { + + @Override public float getPrice() { + return 8.5f; + } +} +``` +```java +@Factory( + id = "Tiramisu", + type = Meal.class +) +public class Tiramisu implements Meal { + + @Override public float getPrice() { + return 4.5f; + } +} +``` + +你可能会问你自己,我们是否可以只把`@Factory`注解应用到`Meal`接口上?答案是注解是不能继承的。一个类`class X`被注解,并不意味着它的子类`class Y extends X`会自动被注解。在我们开始写处理器的代码之前,我们先规定如下一些规则: + +- 只有类可以被`@Factory`注解,因为接口或者抽象类并不能用`new`操作实例化; +- 被`@Factory`注解的类,必须至少提供一个公开的默认构造器(即没有参数的构造函数)。否者我们没法实例化一个对象。 +- 被`@Factory`注解的类必须直接或者间接的继承于`type()`指定的类型; +- 具有相同的`type`的注解类,将被聚合在一起生成一个工厂类。这个生成的类使用`Factory`后缀,例如`type = Meal.class`,将生成`MealFactory`工厂类; +- `id`只能是`String`类型,并且在同一个`type`组中必须唯一。 + +那我们接着看一下在`compiler module`中的自定义的`FactoryProcessor`类: +```java +@AutoService(Processor.class) +public class FactoryProcessor extends AbstractProcessor { + + private Types typeUtils; + private Elements elementUtils; + private Filer filer; + private Messager messager; + private Map factoryClasses = new LinkedHashMap(); + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + typeUtils = processingEnv.getTypeUtils(); + elementUtils = processingEnv.getElementUtils(); + filer = processingEnv.getFiler(); + messager = processingEnv.getMessager(); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set annotataions = new LinkedHashSet(); + // 指定支持@Factory注解 + annotataions.add(Factory.class.getCanonicalName()); + return annotataions; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + ... + } +} +``` + +在`init()`方法中,我们初始化了几个变量: + +- `Elements`: 处理`Element`的工具类 +- `Types`: 处理`TypeMirror`的工具类 +- `Filer`: 用来创建你要创建的文件 +- `Messager`:提供给注解处理器一个报告错误、警告以及提示信息的途径。 + +在注解的处理过程中会扫描所有的`java`源文件。源代码中的每一个部分都是一个特定类型的`Element`。换句话说:`Element`代表程序的元素,例如包、类或者方法等。每个`Element`代表一个构建,例如通过下面的例子我们来说明一下: +```java +package com.example; // PackageElement + +public class Foo { // TypeElement + + private int a; // VariableElement + private Foo other; // VariableElement + + public Foo () {} // ExecuteableElement + + public void setA ( // ExecuteableElement + int newA // TypeElement + ) {} +} +``` +所以在注解中我们必须要换一个角度来看待源代码,它只是一个结构化的文本,我们可以像`XML`中的`DOM`一样去解析它。 +例如现在有一个代表`public class Foo`类的`TypeElement`元素,你可以遍历它的子元素,如下: +```java +TypeElement fooClass = ... ; +for (Element e : fooClass.getEnclosedElements()){ // iterate over children + Element parent = e.getEnclosingElement(); // parent == fooClass +} +``` +正如你所见,`Element`代表的是源代码。`TypeElement`代表的是源代码中的类型元素,例如类。然而`TypeElement`并不包含类本身的信息。你可以从`TypeElement`中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过`TypeMirror`获取。你可以通过调用`elements.asType()`获取元素的`TypeMirror`。 + +接下来我们来继续实现`process()`方法: +```java +@AutoService(Processor.class) +public class FactoryProcessor extends AbstractProcessor { + + private Types typeUtils; + private Elements elementUtils; + private Filer filer; + private Messager messager; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + typeUtils = processingEnv.getTypeUtils(); + elementUtils = processingEnv.getElementUtils(); + filer = processingEnv.getFiler(); + messager = processingEnv.getMessager(); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set annotataions = new LinkedHashSet(); + annotataions.add(Factory.class.getCanonicalName()); + return annotataions; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + // roundEnv.getElementsAnnotatedWith(Factory.class))返回所有被注解了@Factory的元素的列表。 + // 你可能已经注意到,我们并没有说“所有被注解了@Factory的类的列表”,因为它真的是返回Element的列表。 + // 请记住:Element可以是类、方法、变量等。所以,接下来,我们必须检查这些Element是否是一个类 + for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) { + // Check if a class has been annotated with @Factory + if (annotatedElement.getKind() != ElementKind.CLASS) { + return false; + } + } + + return false; + } +} +``` + +在继续检查被注解`@Fractory`的类是否满足我们上面说的5条规则之前,我们将介绍一个让我们更方便继续处理的数据结构。有时候,一个问题或者解释器看起来如此简单,以至于程序员倾向于用一个面向过程方式来写整个处理器。但是你知道吗?一个注解处理器仍然是一个`Java`程序,所以我们需要使用面向对象编程、接口、设计模式,以及任何你将在其他普通`Java`程序中使用的技巧。 + +我们的`FactoryProcessor`非常简单,但是我们仍然想要把一些信息存为对象。在`FactoryAnnotatedClass`中,我们保存被注解类的数据,比如合法的类的名字,以及`@Factory`注解本身的一些信息。也就是说每一个使用了`@Factory`注解的类都对应一个`FactoryAnnotatedClass`类,所以,我们保存`TypeElement`和处理过的`@Factory`注解: + +如下: +```java +public class FactoryAnnotatedClass { + + private TypeElement annotatedClassElement; + private String qualifiedSuperClassName; + private String simpleTypeName; + private String id; + + public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException { + this.annotatedClassElement = classElement; + Factory annotation = classElement.getAnnotation(Factory.class); + id = annotation.id(); + + if (StringUtils.isEmpty(id)) { + throw new IllegalArgumentException( + String.format("id() in @%s for class %s is null or empty! that's not allowed", + Factory.class.getSimpleName(), classElement.getQualifiedName().toString())); + } + + // Get the full QualifiedTypeName + try { + Class clazz = annotation.type(); + // 返回底层阶级Java语言规范中定义的标准名称。 + qualifiedSuperClassName = clazz.getCanonicalName(); + simpleTypeName = clazz.getSimpleName(); + } catch (MirroredTypeException mte) { + DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror(); + TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement(); + qualifiedSuperClassName = classTypeElement.getQualifiedName().toString(); + simpleTypeName = classTypeElement.getSimpleName().toString(); + } + } + + /** + * 获取在{@link Factory#id()}中指定的id + * return the id + */ + public String getId() { + return id; + } + + /** + * 获取在{@link Factory#type()}指定的类型合法全名 + * + * @return qualified name + */ + public String getQualifiedFactoryGroupName() { + return qualifiedSuperClassName; + } + + + /** + * 获取在{@link Factory#type()}{@link Factory#type()}指定的类型的简单名字 + * + * @return qualified name + */ + public String getSimpleFactoryGroupName() { + return simpleTypeName; + } + + /** + * 获取被@Factory注解的原始元素 + */ + public TypeElement getTypeElement() { + return annotatedClassElement; + } +} +``` +上面我们用到了`Class`,因为这里的类型是一个`java.lang.Class`。这就意味着,他是一个真正的`Class`对象。因为注解处理是在编译`Java`源代码之前。我们需要考虑如下两种情况: + +- 这个类已经被编译:这种情况是:如果第三方`.jar`包含已编译的被`@Factory`注解`.class`文件。在这种情况下,可以通过上面的方式在`try`代码块中直接获取。 +- 这个还没有被编译:这种情况是我们尝试编译被`@Fractory`注解的源代码。这种情况下,直接获取`Class`会抛出`MirroredTypeException`异常。幸运的是,`MirroredTypeException`包含一个`TypeMirror`,它表示我们未编译类。因为我们已经知道它必定是一个类类型(我们已经在前面检查过),我们可以直接强制转换为`DeclaredType`,然后读取`TypeElement`来获取合法的名字。 + + +好了,我们现在还需要一个数据结构类`FactoryGroupedClasses`,它将简单的组合所有的`FactoryAnnotatedClasses`到一起。 +```java +public class FactoryGroupedClasses { + + private String qualifiedClassName; + + private Map itemsMap = + new LinkedHashMap(); + + public FactoryGroupedClasses(String qualifiedClassName) { + this.qualifiedClassName = qualifiedClassName; + } + + public void add(FactoryAnnotatedClass toInsert) throws IdAlreadyUsedException { + + FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId()); + if (existing != null) { + throw new IdAlreadyUsedException(existing); + } + + itemsMap.put(toInsert.getId(), toInsert); + } + + public void generateCode(Elements elementUtils, Filer filer) throws IOException { + ... + } +} ``` -注解相关API: +正如你所见,这是一个基本的`Map`,这个映射表用来映射`@Factory.id()`到`FactoryAnnotatedClass`。我们选择`Map`这个数据类型,是因为我们要确保每个`id`是唯一的,我们可以很容易通过`map`查找实现。`generateCode()`方法将被用来生成工厂类代码(将在后面讨论)。 + +我们继续实现`process()`方法。接下来我们想要检查被注解的类必须有只要一个公开的构造函数,不是抽象类,继承于特定的类型,以及是一个公开类: + +```java + +@AutoService(Processor.class) +public class FactoryProcessor extends AbstractProcessor { + + private Types typeUtils; + private Elements elementUtils; + private Filer filer; + private Messager messager; + private Map factoryClasses = + new LinkedHashMap(); + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + typeUtils = processingEnv.getTypeUtils(); + elementUtils = processingEnv.getElementUtils(); + filer = processingEnv.getFiler(); + messager = processingEnv.getMessager(); + } -- Set getElementsAnnotatedWith(Class a) - 返回使用给定注释类型注释的元素。该注释可能直接出现或者被继承。只返回注释处理的此 round 中包括 的 package 元素和 type 元素、成员声明、参数或者这些元素中声明的类型参数。 -- Element:表示一个程序元素,比如包、类或者方法。每个元素都表示一个静态的语言级构造(不表示虚拟机的运行时构造)。 - 元素应该使用 equals(Object) 方法进行比较。不保证总是使用相同的对象表示某个特定的元素。 - 要实现基于 Element 对象类的操作,可以使用 visitor 或者使用 getKind() 方法的结果。使用 instanceof 确定此建模层次结构中某一对象的有效类 未必 可靠,因为一个实现可以选择让单个对象实现多个 Element 子接口。 + @Override + public Set getSupportedAnnotationTypes() { + Set annotataions = new LinkedHashSet(); + annotataions.add(Factory.class.getCanonicalName()); + return annotataions; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + // roundEnv.getElementsAnnotatedWith(Factory.class))返回所有被注解了@Factory的元素的列表。 + // 你可能已经注意到,我们并没有说“所有被注解了@Factory的类的列表”,因为它真的是返回Element的列表。 + // 请记住:Element可以是类、方法、变量等。所以,接下来,我们必须检查这些Element是否是一个类 + for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) { + // Check if a class has been annotated with @Factory + if (annotatedElement.getKind() != ElementKind.CLASS) { + error(annotatedElement, "Only classes can be annotated with @%s", + Factory.class.getSimpleName()); + return true; // 退出处理 + } -- TypeElement:表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注释类型是一种接口。 - TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。这种区别对于一般的类型是最明显的,对于这些类型,单个元素可以定义一系列完整的类型。例如,元素 java.util.Set 对应于参数化类型 java.util.Set 和 java.util.Set(以及其他许多类型),还对应于原始类型 java.util.Set。 + // 因为我们已经知道它是ElementKind.CLASS类型,所以可以直接强制转换 + TypeElement typeElement = (TypeElement) annotatedElement; + + try { + FactoryAnnotatedClass annotatedClass = + new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException + + if (!isValidClass(annotatedClass)) { + return true; // 已经打印了错误信息,退出处理过程 + } + + // 一旦我们检查isValidClass()成功,我们将添加FactoryAnnotatedClass到对应的FactoryGroupedClasses中 + FactoryGroupedClasses factoryClass = + factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName()); + if (factoryClass == null) { + String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName(); + factoryClass = new FactoryGroupedClasses(qualifiedGroupName); + factoryClasses.put(qualifiedGroupName, factoryClass); + } + + // 如果和其他的@Factory标注的类的id相同冲突, + // 抛出IdAlreadyUsedException异常 + factoryClass.add(annotatedClass); + } catch (IllegalArgumentException e) { + // @Factory.id()为空 --> 打印错误信息 + error(typeElement, e.getMessage()); + return true; + } catch (IdAlreadyUsedException e) { + FactoryAnnotatedClass existing = e.getExisting(); + // 已经存在 + error(annotatedElement, + "Conflict: The class %s is annotated with @%s with id ='%s' but %s already uses the same id", + typeElement.getQualifiedName().toString(), Factory.class.getSimpleName(), + existing.getTypeElement().getQualifiedName().toString()); + return true; + } + } + + // 为每个工厂生成Java文件 + try { + for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { + factoryClass.generateCode(elementUtils, filer); + } + // TODO ...注意这里会遗留一个问题,我们后面再仔细说。 + } catch (IOException e) { + error(null, e.getMessage()); + } + + return true; + } + + private void error(Element e, String msg, Object... args) { + messager.printMessage( + Diagnostic.Kind.ERROR, + String.format(msg, args), + e); + } + + + private boolean isValidClass(FactoryAnnotatedClass item) { + + // 转换为TypeElement, 含有更多特定的方法 + TypeElement classElement = item.getTypeElement(); + + if (!classElement.getModifiers().contains(Modifier.PUBLIC)) { + error(classElement, "The class %s is not public.", + classElement.getQualifiedName().toString()); + return false; + } + + // 检查是否是一个抽象类 + if (classElement.getModifiers().contains(Modifier.ABSTRACT)) { + error(classElement, "The class %s is abstract. You can't annotate abstract classes with @%", + classElement.getQualifiedName().toString(), Factory.class.getSimpleName()); + return false; + } + + // 检查继承关系: 必须是@Factory.type()指定的类型子类 + TypeElement superClassElement = + elementUtils.getTypeElement(item.getQualifiedFactoryGroupName()); + if (superClassElement.getKind() == ElementKind.INTERFACE) { + // 检查接口是否实现了 + if(!classElement.getInterfaces().contains(superClassElement.asType())) { + error(classElement, "The class %s annotated with @%s must implement the interface %s", + classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), + item.getQualifiedFactoryGroupName()); + return false; + } + } else { + // 检查子类 + TypeElement currentClass = classElement; + while (true) { + TypeMirror superClassType = currentClass.getSuperclass(); + + if (superClassType.getKind() == TypeKind.NONE) { + // 到达了基本类型(java.lang.Object), 所以退出 + error(classElement, "The class %s annotated with @%s must inherit from %s", + classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), + item.getQualifiedFactoryGroupName()); + return false; + } + + if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) { + // 找到了要求的父类 + break; + } + + // 在继承树上继续向上搜寻 + currentClass = (TypeElement) typeUtils.asElement(superClassType); + } + } + + // 检查是否提供了默认公开构造函数 + for (Element enclosed : classElement.getEnclosedElements()) { + if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { + ExecutableElement constructorElement = (ExecutableElement) enclosed; + if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers() + .contains(Modifier.PUBLIC)) { + // 找到了默认构造函数 + return true; + } + } + } + + // 没有找到默认构造函数 + error(classElement, "The class %s must provide an public empty default constructor", + classElement.getQualifiedName().toString()); + return false; + } + +} +``` + +我们这里添加了`isValidClass()`方法,来检查是否我们所有的规则都被满足了: + +- 必须是公开类:`classElement.getModifiers().contains(Modifier.PUBLIC)` +- 必须是非抽象类:`classElement.getModifiers().contains(Modifier.ABSTRACT)` +- 必须是`@Factoy.type()`指定的类型的子类或者接口的实现:首先我们使用`elementUtils.getTypeElement(item.getQualifiedFactoryGroupName())`创建一个传入的`Class(@Factoy.type())`的元素。是的,你可以仅仅通过已知的合法类名来直接创建`TypeElement`(使用`TypeMirror`)。接下来我们检查它是一个接口还是一个类:`superClassElement.getKind() == ElementKind.INTERFACE`。所以我们这里有两种情况:如果是接口,就判断`classElement.getInterfaces().contains(superClassElement.asType())`;如果是类,我们就必须使用`currentClass.getSuperclass()`扫描继承层级。注意,整个检查也可以使用`typeUtils.isSubtype()`来实现。 +- 类必须有一个公开的默认构造函数:我们遍历所有的闭元素`classElement.getEnclosedElements()`,然后检查`ElementKind.CONSTRUCTOR`、`Modifier.PUBLIC`以及`constructorElement.getParameters().size() == 0`。 + + +上面都实现完成后下面就是在`FactoryGroupedClasses.generateCode()`方法中去生成`java`文件了。 +写`Java`文件,和写其他普通文件没有什么两样。使用`Filer`提供的`Writer`对象,我们可以连接字符串来写我们生成的`Java`代码。但是这样是不是非常麻烦啊?还好良心企业`Square`给我们提供了`Javapoet`,我们可以用它来非常简单的去生成`java`文件。那我们接下来就使用`javapoet`来去生成代码: + +```java +public class FactoryGroupedClasses { + + /** + * 将被添加到生成的工厂类的名字中 + */ + private static final String SUFFIX = "Factory"; + + private String qualifiedClassName; + + private Map itemsMap = + new LinkedHashMap(); + + public FactoryGroupedClasses(String qualifiedClassName) { + this.qualifiedClassName = qualifiedClassName; + } + + public void add(FactoryAnnotatedClass toInsert) throws ProcessingException { + + FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId()); + if (existing != null) { + + // Alredy existing + throw new ProcessingException(toInsert.getTypeElement(), + "Conflict: The class %s is annotated with @%s with id ='%s' but %s already uses the same id", + toInsert.getTypeElement().getQualifiedName().toString(), Factory.class.getSimpleName(), + toInsert.getId(), existing.getTypeElement().getQualifiedName().toString()); + } + + itemsMap.put(toInsert.getId(), toInsert); + } + + public void generateCode(Elements elementUtils, Filer filer) throws IOException { + TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName); + String factoryClassName = superClassName.getSimpleName() + SUFFIX; + String qualifiedFactoryClassName = qualifiedClassName + SUFFIX; + PackageElement pkg = elementUtils.getPackageOf(superClassName); + String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString(); + + MethodSpec.Builder method = MethodSpec.methodBuilder("create") + .addModifiers(Modifier.PUBLIC) + .addParameter(String.class, "id") + .returns(TypeName.get(superClassName.asType())); + + // check if id is null + method.beginControlFlow("if (id == null)") + .addStatement("throw new IllegalArgumentException($S)", "id is null!") + .endControlFlow(); + + // Generate items map + for (FactoryAnnotatedClass item : itemsMap.values()) { + method.beginControlFlow("if ($S.equals(id))", item.getId()) + .addStatement("return new $L()", item.getTypeElement().getQualifiedName().toString()) + .endControlFlow(); + } + + method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = "); + + TypeSpec typeSpec = TypeSpec.classBuilder(factoryClassName).addMethod(method.build()).build(); + + // Write file + JavaFile.builder(packageName, typeSpec).build().writeTo(filer); + } +} +``` + +到这里,我感觉完成了,运行一下,结果发现报错了: +```java +:compiler:jar UP-TO-DATE +:app:compileDebugJavaWithJavac +错误: Attempt to recreate a file for type com.charon.annotationdemo.inter.MealFactory +错误: Attempt to recreate a file for type com.charon.annotationdemo.inter.MealFactory +2 个错误 +Error:Execution failed for task ':app:compileDebugJavaWithJavac'. +> Compilation failed; see the compiler error output for details. +``` + +那我们继续来解决这个问题,这个问题是由于循环引起的,我们还要处理一个重要的事情就是循环的问题: + +注解处理可能会执行多次,官方文档中是这样介绍的: + +> Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing. + +一个简单的定义:一个处理循环是调用一个注解处理器的`process()`方法。对应到我们的工厂模式的例子中:`FactoryProcessor`被初始化一次(不是每次循环都会新建处理器对象),然而,如果生成了新的源文件`process()`能够被调用多次。听起来有点奇怪不是么?原因是这样的,这些生成的文件中也可能包含`@Factory`注解,它们还将会被`FactoryProcessor`处理。 + +例如我们的`PizzaStore`的例子中将会经过3次循环处理: + +| Round | Input | Output | +| ------------- |:-------------:| -----:| +| 1 | CalzonePizza.java +Tiramisu.java +MargheritaPizza.java +Meal.java +PizzaStore.java | MealFactory.java | +| 2 | MealFactory.java | none | +| 3 | none | none | + +会循环三次,但是第一次就会生成代码,所以上面会出现两次重复创建文件的错误。 + +解释处理循环还有另外一个原因。如果你看一下我们的`FactoryProcessor`代码你就能注意到,我们收集数据和保存它们在一个私有的域中`Map factoryClasses`。在第一轮中,我们检测到了`MagheritaPizza`, `CalzonePizza`和`Tiramisu`,然后生成了`MealFactory.java`。在第二轮中把`MealFactory`作为输入。因为在`MealFactory`中没有检测到`@Factory`注解,我们预期并没有错误,然而我们得到如下的信息: + +```java +:compiler:jar UP-TO-DATE +:app:compileDebugJavaWithJavac +错误: Attempt to recreate a file for type com.charon.annotationdemo.inter.MealFactory +错误: Attempt to recreate a file for type com.charon.annotationdemo.inter.MealFactory +2 个错误 +Error:Execution failed for task ':app:compileDebugJavaWithJavac'. +> Compilation failed; see the compiler error output for details. +``` + +这个问题是因为我们没有清除`factoryClasses`,这意味着,在第二轮的`process()`中,任然保存着第一轮的数据,并且会尝试生成在第一轮中已经生成的文件,从而导致这个错误的出现。在我们的这个场景中,我们知道只有在第一轮中检查`@Factory`注解的类,所以我们可以简单的修复这个问题,如下: +```java +@Override +public boolean process(Set annotations, RoundEnvironment roundEnv) { + try { + for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { + factoryClass.generateCode(elementUtils, filer); + } + + // 清除factoryClasses + factoryClasses.clear(); + + } catch (IOException e) { + error(null, e.getMessage()); + } + ... + return true; +} +``` + +***我们要记住注解处理过程是需要经过多轮处理的,并且你不能重载或者重新创建已经生成的源代码。*** + + + +好了,到这里就彻底完成了,运行一下,当然成功了,那生成的文件在哪里呢? + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/annotation_ato_create_file.png) + + +那既然能生成,我们该怎么去调用呢?很简单,像平常一样去用。 +```java +public class MainActivity extends AppCompatActivity { + private View view; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + view = findViewById(R.id.tv); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + MealFactory factory = new MealFactory(); + factory.create("Calzone"); + } + }); + } +} +``` + +你可能会发现报错,找不到类,你可以不用管它,直接运行,你会发现一切都`OK`的。当然如果你倒错了包你就当我没说。 + +作为`Android`开发者,当然应该很熟悉`ButterKnife`。在`ButterKnife`中使用`@InjectView`注解`View`。`ButterKnifeProcessor`生成一个`MyActivity$$ViewInjector`,但是在`ButterKnife`你不需要手动调用`new MyActivity$$ViewInjector()`来实例化一个`ButterKnife`注入的对象,而是使用`Butterknife.inject(activity)`。`ButterKnife`内部使用反射机制来实例化`MyActivity$$ViewInjector()`对象: + +```java +try { + Class injector = Class.forName(clsName + "$$ViewInjector"); +} catch (ClassNotFoundException e) { ... } +``` + +但是反射机制会稍微有些慢,我们使用注解处理来生成本地代码,会不会导致很多的反射性能的问题?的确,反射机制的性能确实是一个问题。然而,它不需要手动去创建对象,确实提高了开发者的开发速度。`ButterKnife`中有一个哈希表`HashMap`来缓存实例化过的对象。所以`MyActivity$$ViewInjector`只是使用反射机制实例化一次,第二次需要`MyActivity$$ViewInjector`的时候,就直接从哈希表中获得。 + +`FragmentArgs`非常类似于`ButterKnife`。它使用反射机制来创建对象,而不需要开发者手动来做这些。`FragmentArgs`在处理注解的时候生成一个特别的查找表类,它其实就是一种哈希表,所以整个`FragmentArgs`库只是在第一次使用的时候,执行一次反射调用,一旦整个`Class.forName()`的`Fragemnt`的参数对象被创建,后面的都是本地代码运行了。 + +好了至此例子讲完了。 + +最后讲一下按照上面的三层分类:`app`,`api`,`compiler`进行分类的好处。 +我们这么做是因为想让我们的工厂模式的例子的使用者在他们的工程中只编译注解,而包含处理器模块只是为了编译。有点晕?我们举个例子,如果我们只有一个包。如果另一个开发者想要把我们的工厂模式处理器用于他的项目中,他就必须包含`@Factory`注解和整个`FactoryProcessor`的代码(包括`FactoryAnnotatedClass`和`FactoryGroupedClasses`)到他们项目中。我非常确定的是,他并不需要在他已经编译好的项目中包含处理器相关的代码。如果你是一个`Android`的开发者,你肯定听说过`65536`个方法的限制(即在一个`.dex`文件中,只能寻址65536个方法)。如果你在`FactoryProcessor`中使用`guava`,并且把注解和处理器打包在一个包中,这样的话,`Android APK`安装包中不只是包含`FactoryProcessor`的代码,而也包含了整个`guava`的代码。`Guava`有大约20000个方法。所以分开注解和处理器是非常有意义的。 ###使用注解提高代码的检查性 @@ -799,7 +1591,10 @@ dependencies { ... } ``` - + +文中例子来自`Hannes Dorfmann`大神的`ANNOTATION PROCESSING 101`,我对其重新梳理了下,来让能正常使用,并且上面的示例已经过我实际运行: +- [Hannes Dorfmann](http://hannesdorfmann.com/annotation-processing/annotationprocessing101) + --- - 邮箱 :charon.chui@gmail.com From 0ef085baf0b6aeae0fb3f9a3a535fa4e7ed66160 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 7 Jul 2016 20:25:57 +0800 Subject: [PATCH 047/373] =?UTF-8?q?Update=20=E6=B3=A8=E8=A7=A3=E4=BD=BF?= =?UTF-8?q?=E7=94=A8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\346\263\250\350\247\243\344\275\277\347\224\250.md" | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 45250d90..06e1bf8b 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -1433,11 +1433,7 @@ Error:Execution failed for task ':app:compileDebugJavaWithJavac'. | Round | Input | Output | | ------------- |:-------------:| -----:| -| 1 | CalzonePizza.java -Tiramisu.java -MargheritaPizza.java -Meal.java -PizzaStore.java | MealFactory.java | +| 1 | CalzonePizza.java Tiramisu.java MargheritaPizza.java Meal.java PizzaStore.java | MealFactory.java | | 2 | MealFactory.java | none | | 3 | none | none | From 11a70f3a68b90a4eec228b893cd93315239d70b9 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 7 Jul 2016 21:27:00 +0800 Subject: [PATCH 048/373] update --- .../\346\263\250\350\247\243\344\275\277\347\224\250.md" | 3 +++ 1 file changed, 3 insertions(+) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index 06e1bf8b..ba6d99a9 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -1588,6 +1588,9 @@ dependencies { } ``` + +示例`Demo`已上传到`Github`:[AnnotationDemo](https://github.com/CharonChui/AnnotationDemo) + 文中例子来自`Hannes Dorfmann`大神的`ANNOTATION PROCESSING 101`,我对其重新梳理了下,来让能正常使用,并且上面的示例已经过我实际运行: - [Hannes Dorfmann](http://hannesdorfmann.com/annotation-processing/annotationprocessing101) From 47e3da105c0d648593106c7084b033454aebc952 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 12 Jul 2016 19:03:35 +0800 Subject: [PATCH 049/373] =?UTF-8?q?update=20=E6=B3=A8=E8=A7=A3=E4=BD=BF?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\350\247\243\344\275\277\347\224\250.md" | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" index ba6d99a9..7322b932 100644 --- "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" +++ "b/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" @@ -18,14 +18,16 @@ - `Annotation`的方法定义是独特的、受限制的。 `Annotation`类型的方法必须声明为无参数、无异常的。这些方法定义了`Annotation`的成员: 方法名代表成员变量名,而方法返回值代表了成员变量的类型。而且方法的返回值类型必须是基本数据类型、`Class`类型、`String类型`、枚举类型、`Annotation`类型或者由前面类型之一作为元素的一维数组。方法的后面可以使用`default`和一个默认的数值来声明成员变量的默认值,`null`不能作为成员变量的默认值,这与我们平时的使用有很大的区别。 注解如果只有一个默认属性,可直接用`value()`函数。一个属性也没有则表示该`Annotation`为`Mark Annotation`。 - 例如: + 例如: + ```java public @interface UnitTest { String value(); } ``` 在使用时可以直接使用`@UnitTest("GCD")`,`@UnitTest("GCD"`实际上就是是 `@UnitTest(value="GCD)`的简单写法。 - 例如: + 例如: + ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @@ -118,7 +120,8 @@ public class Generation3List extends Generation2List { } ``` -我们可以声明一个注解来保存这些相同的元数据。如下: +我们可以声明一个注解来保存这些相同的元数据。如下: + ```java @interface ClassPreamble { String author(); @@ -130,7 +133,8 @@ public class Generation3List extends Generation2List { String[] reviewers(); } ``` -声明完注解之后我们就可以填写一些参数来使用它,如下: +声明完注解之后我们就可以填写一些参数来使用它,如下: + ```java @ClassPreamble ( @@ -188,6 +192,7 @@ public class Generation3List extends Generation2List { } ``` 也可以获取一个指定的注解类型: + ```java Class aClass = TheClass.class; Annotation annotation = aClass.getAnnotation(MyAnnotation.class); @@ -218,6 +223,7 @@ public class Generation3List extends Generation2List { - 方法注解 下面是一个方法使用注解的例子: + ```java public class TheClass { @MyAnnotation(name="someName", value = "Hello World") @@ -240,6 +246,7 @@ public class Generation3List extends Generation2List { } ``` 也可以获取一个指定的方法注解,如下: + ```java Method method = ... // obtain method object Annotation annotation = method.getAnnotation(MyAnnotation.class); @@ -254,6 +261,7 @@ public class Generation3List extends Generation2List { - 参数注解 在方法的参数中声明注解,如下: + ```java public class TheClass { public static void doSomethingElse( @@ -262,7 +270,7 @@ public class Generation3List extends Generation2List { } ``` - 可以通过`Method`对象获取到参数的注解,如下: + 可以通过`Method`对象获取到参数的注解,如下: ```java Method method = ... //obtain method object Annotation[][] parameterAnnotations = method.getParameterAnnotations(); @@ -321,18 +329,18 @@ public class Generation3List extends Generation2List { } ``` -运行时注解示例: +记下来我们来举个栗子,运行时注解示例: 相信很多人都知道`Butter Knife`。它里面就是使用了注解: ```java - @BindView(R.id.user) EditText username; - @BindView(R.id.pass) EditText password; +@BindView(R.id.user) EditText username; +@BindView(R.id.pass) EditText password; - @BindString(R.string.login_error) String loginErrorMessage; +@BindString(R.string.login_error) String loginErrorMessage; - @OnClick(R.id.submit) void submit() { +@OnClick(R.id.submit) void submit() { // TODO call server... - } +} ``` 那我们就以`onClick`事件为例模仿着他去写一下。 @@ -366,6 +374,7 @@ public class InjectorProcessor { } } } +``` 使用: @@ -449,7 +458,7 @@ public class Processor extends AbstractProcessor{ } ``` 但是在`Android Studio`死活提示找不到`AbstractProcessor`类,这是因为注解是`javase`中`javax`包里面的,`android.jar`默认是不包含的,所以会编译报错. -解决方法就是新建一个`Moduel`,在选择类型时将该`Moduel`的类型选为`Java Library`。 +解决方法就是新建一个`Module`,在选择类型时将该`Module`的类型选为`Java Library`。 然后在该`Module`中创建就好了`Processor`就好了,完美解决。 From 086db09a9fa299a3d448b597d1b438d3b7de3314 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 14 Jul 2016 11:56:27 +0800 Subject: [PATCH 050/373] =?UTF-8?q?add=20butterknife=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\347\240\201\350\257\246\350\247\243.md" | 590 ++++++++++++++++++ 1 file changed, 590 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" new file mode 100644 index 00000000..5616c7ba --- /dev/null +++ "b/Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -0,0 +1,590 @@ +butterknife源码详解 +=== + +作为`Android`开发者,大家肯定都知道大名鼎鼎的[butterknife](https://github.com/JakeWharton/butterknife)。它大大的提高了开发效率,虽然在很早之前就开始使用它了,但是只知道是通过注解的方式实现的,却一直没有仔细的学习下大牛的代码。最近在学习运行时注解,决定今天来系统的分析下`butterknife`的实现原理。 + +如果你之前不了解`Annotation`,那强烈建议你先看[注解使用](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md). + +废多看图: + +![image](https://github.com/CharonChui/Pictures/blob/master/butterknife_sample.png?raw=true) + +从图中可以很直观的看出它的`module`结构,以及使用示例代码。 + +它的目录和我们在[注解使用](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md)这篇文章中介绍的一样,大体也是分为三个部分: + +- app : butterknife +- api : butterknife-annotations +- compiler : butterknife-compiler + +通过示例代码我们大体能预料到对应的功能实现: + +- `@BindView(R2.id.hello) Button hello;` + `BindView`注解的作用就是通过`value`指定的值然后去调用`findViewById()`来找到对应的控件,然后将该控件赋值给使用该注解的变量。 + +- `@OnClick(R2.id.hello) void sayHello() {...}` + `OnClick`注解也是通过指定的`id`来找到对应控件后,然后对其设置`onClickListener`并调用使用该注解的方法。 + +- 最后不要忘了`ButterKnife.bind(this);`该方法也是后面我们要分析的突破点。 + +当然`Butterknife`的功能是非常强大的,我们在这里只是用这两个简单的例子来进行分析说明。 + +那我们就来查看`BindView`和`Onclik`注解的源码: +```java +@Retention(CLASS) @Target(FIELD) +public @interface BindView { + /** View ID to which the field will be bound. */ + @IdRes int value(); +} +``` +作用在变量上的编译时注解。对该注解的值`value()`使用`android.support.annotation`中的`IdRes`注解,来表明该值只能是资源类型的`id`。 + +```java +@Target(METHOD) +@Retention(CLASS) +@ListenerClass( + targetType = "android.view.View", + setter = "setOnClickListener", + type = "butterknife.internal.DebouncingOnClickListener", + method = @ListenerMethod( + name = "doClick", + parameters = "android.view.View" + ) +) +public @interface OnClick { + /** View IDs to which the method will be bound. */ + @IdRes int[] value() default { View.NO_ID }; +} +``` +作用到方法上的编译时注解。我们发现该`注解还使用了`ListenerClass`注解,当然从上面的声明中可以很容易看出它的作用。 +那我们就继续简单的看一下`ListenerClass`注解的实现: + +```java +@Retention(RUNTIME) @Target(ANNOTATION_TYPE) +public @interface ListenerClass { + String targetType(); + + /** Name of the setter method on the {@linkplain #targetType() target type} for the listener. */ + String setter(); + + /** + * Name of the method on the {@linkplain #targetType() target type} to remove the listener. If + * empty {@link #setter()} will be used by default. + */ + String remover() default ""; + + /** Fully-qualified class name of the listener type. */ + String type(); + + /** Enum which declares the listener callback methods. Mutually exclusive to {@link #method()}. */ + Class> callbacks() default NONE.class; + + /** + * Method data for single-method listener callbacks. Mutually exclusive with {@link #callbacks()} + * and an error to specify more than one value. + */ + ListenerMethod[] method() default { }; + + /** Default value for {@link #callbacks()}. */ + enum NONE { } +} +``` +作用到注解类型的运行时注解。 + + +有了之前[注解使用](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md)这篇文章的基础,我们知道对于编译时注解肯定是要通过自定义`AbstractProcessor`来解析的,所以接下来我们要去`butterknife-compiler module`中找一下对应的类。通过名字我们就能很简单的找到: +```java +package butterknife.compiler; + +@AutoService(Processor.class) +public final class ButterKnifeProcessor extends AbstractProcessor { + ... +} +``` +通过`AutoService`注解我们很容易看出来`Butterknife`也使用了`Google Auto`。当然它肯定也都用了`javaopet`和`android-apt`,这里我们就不去分析了。 +其他的一些方法我们就不继续看了,我们接下来看一下具体的核心处理方法,也就是`ButterKnifeProcessor.process()`方法: +```java +@Override public boolean process(Set elements, RoundEnvironment env) { + // 查找、解析出所有的注解 + Map targetClassMap = findAndParseTargets(env); + // 将注解后要生成的相关代码信息保存到BindingClass类中 + for (Map.Entry entry : targetClassMap.entrySet()) { + TypeElement typeElement = entry.getKey(); + BindingClass bindingClass = entry.getValue(); + // 输出生成的类 + for (JavaFile javaFile : bindingClass.brewJava()) { + try { + javaFile.writeTo(filer); + } catch (IOException e) { + error(typeElement, "Unable to write view binder for type %s: %s", typeElement, + e.getMessage()); + } + } + } + + return true; + } +``` + +从`process()`方法来看,我们需要主要分析两个部分: + +- `findAndParseTargets()`:查找、解析所有的注解 +- `bindingClass.brewJava()`:生成代码 + +#####第一步:`findAndParseTargets()` + +先查看`findAndParseTargets()`方法的实现,里面解析的类型比较多,我们就以`BindView`为例进行说明: +```java +private Map findAndParseTargets(RoundEnvironment env) { + Map targetClassMap = new LinkedHashMap<>(); + Set erasedTargetNames = new LinkedHashSet<>(); + + scanForRClasses(env); + + // Process each @BindArray element. + for (Element element : env.getElementsAnnotatedWith(BindArray.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceArray(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindArray.class, e); + } + } + + // Process each @BindBitmap element. + for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceBitmap(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindBitmap.class, e); + } + } + + // Process each @BindBool element. + for (Element element : env.getElementsAnnotatedWith(BindBool.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceBool(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindBool.class, e); + } + } + + // Process each @BindColor element. + for (Element element : env.getElementsAnnotatedWith(BindColor.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceColor(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindColor.class, e); + } + } + + // Process each @BindDimen element. + for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceDimen(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindDimen.class, e); + } + } + + // Process each @BindDrawable element. + for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceDrawable(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindDrawable.class, e); + } + } + + // Process each @BindInt element. + for (Element element : env.getElementsAnnotatedWith(BindInt.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceInt(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindInt.class, e); + } + } + + // Process each @BindString element. + for (Element element : env.getElementsAnnotatedWith(BindString.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseResourceString(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindString.class, e); + } + } + + // Process each @BindView element. + for (Element element : env.getElementsAnnotatedWith(BindView.class)) { + // 检查一下合法性 + if (!SuperficialValidation.validateElement(element)) continue; + try { + // 进行解析 + parseBindView(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindView.class, e); + } + } + + // Process each @BindViews element. + for (Element element : env.getElementsAnnotatedWith(BindViews.class)) { + if (!SuperficialValidation.validateElement(element)) continue; + try { + parseBindViews(element, targetClassMap, erasedTargetNames); + } catch (Exception e) { + logParsingError(element, BindViews.class, e); + } + } + + // Process each annotation that corresponds to a listener. + for (Class listener : LISTENERS) { + findAndParseListener(env, listener, targetClassMap, erasedTargetNames); + } + + // Try to find a parent binder for each. + for (Map.Entry entry : targetClassMap.entrySet()) { + TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames); + if (parentType != null) { + BindingClass bindingClass = entry.getValue(); + BindingClass parentBindingClass = targetClassMap.get(parentType); + bindingClass.setParent(parentBindingClass); + } + } + + return targetClassMap; + } +``` + +继续看一下`parseBindView()`方法: +```java +private void parseBindView(Element element, Map targetClassMap, + Set erasedTargetNames) { + TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); + + // Start by verifying common generated code restrictions. + boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) + || isBindingInWrongPackage(BindView.class, element); + + // Verify that the target type extends from View. + TypeMirror elementType = element.asType(); + if (elementType.getKind() == TypeKind.TYPEVAR) { + TypeVariable typeVariable = (TypeVariable) elementType; + elementType = typeVariable.getUpperBound(); + } + // 必须是View类型或者接口 + if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { + error(element, "@%s fields must extend from View or be an interface. (%s.%s)", + BindView.class.getSimpleName(), enclosingElement.getQualifiedName(), + element.getSimpleName()); + hasError = true; + } + + if (hasError) { + return; + } + // 通过注解的value拿到id + // Assemble information on the field. + int id = element.getAnnotation(BindView.class).value(); + + BindingClass bindingClass = targetClassMap.get(enclosingElement); + if (bindingClass != null) { + // 之前已经绑定过该id + ViewBindings viewBindings = bindingClass.getViewBinding(getId(id)); + if (viewBindings != null && viewBindings.getFieldBinding() != null) { + FieldViewBinding existingBinding = viewBindings.getFieldBinding(); + error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", + BindView.class.getSimpleName(), id, existingBinding.getName(), + enclosingElement.getQualifiedName(), element.getSimpleName()); + return; + } + } else { + // 没有绑定过该id的话就去生成代码 + bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); + } + + String name = element.getSimpleName().toString(); + TypeName type = TypeName.get(elementType); + boolean required = isFieldRequired(element); + + FieldViewBinding binding = new FieldViewBinding(name, type, required); + // 用BindingClass添加代码 + bindingClass.addField(getId(id), binding); + + // Add the type-erased version to the valid binding targets set. + erasedTargetNames.add(enclosingElement); + } +``` +终于进入生成代码的阶段了,继续看一下`getOrCreateTargetClass()`的实现: +```java +private BindingClass getOrCreateTargetClass(Map targetClassMap, + TypeElement enclosingElement) { + BindingClass bindingClass = targetClassMap.get(enclosingElement); + if (bindingClass == null) { + TypeName targetType = TypeName.get(enclosingElement.asType()); + if (targetType instanceof ParameterizedTypeName) { + targetType = ((ParameterizedTypeName) targetType).rawType; + } + // 得到包名、类名 + String packageName = getPackageName(enclosingElement); + String className = getClassName(enclosingElement, packageName); + // 用包名、类名和_ViewBinder等拼接成要生成的类的全名,这里会有两个类:$$_ViewBinder和$$_ViewBinding + ClassName binderClassName = ClassName.get(packageName, className + "_ViewBinder"); + ClassName unbinderClassName = ClassName.get(packageName, className + "_ViewBinding"); + + boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL); + // 将要生成的类名,$$_ViewBinder和$$_ViewBinding封装给BindingClass类 + bindingClass = new BindingClass(targetType, binderClassName, unbinderClassName, isFinal); + targetClassMap.put(enclosingElement, bindingClass); + } + return bindingClass; + } +``` +继续看一下`BindingClass.addField()`: +```java +void addField(Id id, FieldViewBinding binding) { + getOrCreateViewBindings(id).setFieldBinding(binding); + } +``` +继续看`getOrCreateViewBindings()`以及`setFieldBinding()`方法: +```java +private ViewBindings getOrCreateViewBindings(Id id) { + ViewBindings viewId = viewIdMap.get(id); + if (viewId == null) { + viewId = new ViewBindings(id); + viewIdMap.put(id, viewId); + } + return viewId; + } +``` +然后看`ViewBindings.setFieldBinding()`方法: +```java +public void setFieldBinding(FieldViewBinding fieldBinding) { + if (this.fieldBinding != null) { + throw new AssertionError(); + } + this.fieldBinding = fieldBinding; + } +``` +看到这里就把`findAndParseTargets()`方法分析完了。大体总结一下就是把一些变量、参数等初始化到了`BindingClass`类中。 +也就是说上面`process()`方法中的第一步已经分析完了,下面我们来继续看第二部分. + +#####第二步:`bindingClass.brewJava()` + +继续查看`BindingClass.brewJava()`方法的实现: +```java +Collection brewJava() { + TypeSpec.Builder result = TypeSpec.classBuilder(binderClassName) + .addModifiers(PUBLIC, FINAL) + .addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, targetTypeName)); + + result.addMethod(createBindMethod(targetTypeName)); + + List files = new ArrayList<>(); + if (isGeneratingUnbinder()) { + // 生成$$_ViewBinding类 + files.add(JavaFile.builder(unbinderClassName.packageName(), createUnbinderClass()) + .addFileComment("Generated code from Butter Knife. Do not modify!") + .build() + ); + } else if (!isFinal) { + result.addMethod(createBindToTargetMethod()); + } + // 生成$$_ViewBinder类 + files.add(JavaFile.builder(binderClassName.packageName(), result.build()) + .addFileComment("Generated code from Butter Knife. Do not modify!") + .build()); + + return files; + } +``` +看到这里感觉不用再继续分析了,该方法就是使用`javaopet`来生成对应`$$_ViewBinder.java`类。 + +到这里我们已经知道在编译的过程中会去生成一个对应的`$$_ViewBinder.java`文件,该类实现了`ViewBinder`接口。它内部会去生成对应`findViewByid()`以及`setOnClickListener()`等方法的代码,它生成了该类后如何去调用呢?我们也没有发现`new $$_ViewBinder`的方法。不要忘了上面我们看到的`ButterKnife.bind(this);`。接下来我们看一下`ButterKnife.bind(this);`方法的实现: + +```java +/** + * BindView annotated fields and methods in the specified {@link Activity}. The current content + * view is used as the view root. + * + * @param target Target activity for view binding. + */ + @NonNull @UiThread + public static Unbinder bind(@NonNull Activity target) { + return getViewBinder(target).bind(Finder.ACTIVITY, target, target); + } + + /** + * BindView annotated fields and methods in the specified {@link View}. The view and its children + * are used as the view root. + * + * @param target Target view for view binding. + */ + @NonNull @UiThread + public static Unbinder bind(@NonNull View target) { + return getViewBinder(target).bind(Finder.VIEW, target, target); + } +``` +调用了`getViewBinder()`的`bind()`方法,继续看`getViewBinder()`方法: +```java +static final Map, ViewBinder> BINDERS = new LinkedHashMap<>(); +... + +@NonNull @CheckResult @UiThread + static ViewBinder getViewBinder(@NonNull Object target) { + Class targetClass = target.getClass(); + if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName()); + return findViewBinderForClass(targetClass); + } + + @NonNull @CheckResult @UiThread + private static ViewBinder findViewBinderForClass(Class cls) { + // BINDERS是一个Map集合。也就是说它内部使用Map缓存了一下,先去内存中取 + ViewBinder viewBinder = BINDERS.get(cls); + if (viewBinder != null) { + if (debug) Log.d(TAG, "HIT: Cached in view binder map."); + return viewBinder; + } + // 内存中没有缓存该类 + String clsName = cls.getName(); + // 通过类名判断下是不是系统的组件 + if (clsName.startsWith("android.") || clsName.startsWith("java.")) { + if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); + return NOP_VIEW_BINDER; + } + //noinspection TryWithIdenticalCatches Resolves to API 19+ only type. + try { + // 通过反射获取到对应通过编译时注解生成的$_ViewBinder类的实例 + Class viewBindingClass = Class.forName(clsName + "_ViewBinder"); + //noinspection unchecked + viewBinder = (ViewBinder) viewBindingClass.newInstance(); + if (debug) Log.d(TAG, "HIT: Loaded view binder class."); + } catch (ClassNotFoundException e) { + if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName()); + viewBinder = findViewBinderForClass(cls.getSuperclass()); + } catch (InstantiationException e) { + throw new RuntimeException("Unable to create view binder for " + clsName, e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to create view binder for " + clsName, e); + } + // 通过反射来操作毕竟会影响性能,所以这里通过Map缓存的方式来进行优化 + BINDERS.put(cls, viewBinder); + return viewBinder; + } + +``` + +到这里就彻底分析完了`ButterKnife.bind(this);`的实现,它其实就相当于`new`了一个`$_ViewBinder`类的实例。当然这样用起来是非常方便的,毕竟我们手动的去`new`类多不合理,虽然他里面用到了反射会影响一点点性能,但是他通过内存缓存的方式优化了,我感觉这种方式是利大于弊的。 + +那`$_ViewBinder`类里面都是什么内容呢? 我们去看一下该类的代码,但是它生成的代码在哪里呢? +![image](https://github.com/CharonChui/Pictures/blob/master/butterknife_apt_genierate_code.png?raw=true) + + +开始看一下`SimpleActivity_ViewBinder.bind()`方法: +```java +public final class SimpleActivity_ViewBinder implements ViewBinder { + @Override + public Unbinder bind(Finder finder, SimpleActivity target, Object source) { + return new SimpleActivity_ViewBinding<>(target, finder, source); + } +} +``` + +接着看`SimpleActivity_ViewBinding`类: +```java +public class SimpleActivity_ViewBinding implements Unbinder { + protected T target; + + private View view2130968578; + + private View view2130968579; + + public SimpleActivity_ViewBinding(final T target, Finder finder, Object source) { + this.target = target; + + View view; + target.title = finder.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class); + target.subtitle = finder.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class); + view = finder.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'"); + target.hello = finder.castView(view, R.id.hello, "field 'hello'", Button.class); + view2130968578 = view; + view.setOnClickListener(new DebouncingOnClickListener() { + @Override + public void doClick(View p0) { + target.sayHello(); + } + }); + view.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View p0) { + return target.sayGetOffMe(); + } + }); + view = finder.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'"); + target.listOfThings = finder.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class); + view2130968579 = view; + ((AdapterView) view).setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView p0, View p1, int p2, long p3) { + target.onItemClick(p2); + } + }); + target.footer = finder.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class); + target.headerViews = Utils.listOf( + finder.findRequiredView(source, R.id.title, "field 'headerViews'"), + finder.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), + finder.findRequiredView(source, R.id.hello, "field 'headerViews'")); + } + + @Override + public void unbind() { + T target = this.target; + if (target == null) throw new IllegalStateException("Bindings already cleared."); + + target.title = null; + target.subtitle = null; + target.hello = null; + target.listOfThings = null; + target.footer = null; + target.headerViews = null; + + view2130968578.setOnClickListener(null); + view2130968578.setOnLongClickListener(null); + view2130968578 = null; + ((AdapterView) view2130968579).setOnItemClickListener(null); + view2130968579 = null; + + this.target = null; + } +} +``` +可以看到他内部会通过`findViewByid()`等来找到对应的`View`,然后将其赋值给`target.xxxx`,所以这样就相当于把所有的控件以及事件都给初始化了,以后就可以直接使用了,通过这里也可以看到我们在使用注解的时候不要把控件或者方法声明为`private`的。 + + + +总结一下: + +- `ButterKnifeProcessor`会生成`$$_ViewBinder`类并实现了`ViewBinder`接口。 +- `$$_ViewBinder`类中包含了所有对应的代码,会通过注解去解析到`id`等,然后通过`findViewById()`等方法找到对应的控件,并且复制给调用该方法的来中的变量。这样就等同于我们直接 + 使用`View v = findViewByid(R.id.xx)`来进行初始化控件。 +- 上面虽然生成了`$$_ViewBinder`类,但是如何去调用呢? 就是在调用`ButterKnife.bind(this)`时执行,该方法会通过反射去实例化对应的`$$_ViewBinder`类,并且调用该类的`bind()`方法。 + +- `Butterknife`除了在`Butterknife.bind()`方法中使用反射之外,其他注解的处理都是通过编译时注解使用,所以不会影响效率。 +- 使用`Butterknife`是不要将变量声明为`private`类型,因为`$$_ViewBinder`类中会去直接调用变量赋值,如果声明为`private`将无法赋值。 + ```java + @BindView(R2.id.title) TextView title; + ``` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From 0ccfd1bb708800a1630be8d1b60b53ff034d6add Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 12 Aug 2016 19:17:02 +0800 Subject: [PATCH 051/373] add volley vs Retrofit --- ...13\350\275\254\345\212\250\347\224\273.md" | 20 + ...41\345\274\217\350\257\246\350\247\243.md" | 52 +- .../InstantRun\350\257\246\350\247\243.md" | 2 +- ...21\350\267\257\346\241\206\346\236\266.md" | 49 ++ ...70\345\205\263\345\267\245\345\205\267.md" | 4 +- ...56\345\244\215\345\256\236\347\216\260.md" | 782 ++++++++++++++++++ 6 files changed, 904 insertions(+), 5 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" create mode 100644 "Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" create mode 100644 "Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" diff --git "a/Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" "b/Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" new file mode 100644 index 00000000..ccbeab97 --- /dev/null +++ "b/Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" @@ -0,0 +1,20 @@ +3D旋转动画 +=== + +终于切切实实弄明白matrix那几个方法的使用了,比如preTranslate, setTranslate, postTranslate这些。以前对它们都是一知半解,以为这几个方法没什么区别,其实还是有很大不同的,最紧要是这几个方法的调用顺序对坐标变换的影响。抽象的说pre方法是向前"生长", post方法是向后"生长",具体拿个例子来说,比如一个matrix调用了下列一系列的方法: + + + +matrix.preScale(0.5f, 1); matrix.preTranslate(10, 0); matrix.postScale(0.7f, 1); matrix.postTranslate(15, 0); 则坐标变换经过的4个变换过程依次是:translate(10, 0) -> scale(0.5f, 1) -> scale(0.7f, 1) -> translate(15, 0), 所以对matrix方法的调用顺序是很重要的,不同的顺序往往会产生不同的变换效果。pre方法的调用顺序和post方法的互不影响,即以下的方法调用和前者在真实坐标变换顺序里是一致的, matrix.postScale(0.7f, 1); matrix.preScale(0.5f, 1); matrix.preTranslate(10, 0); matrix.postTranslate(15, 0); + +而matrix的set方法则会对先前的pre和post操作进行刷除,而后再设置它的值,比如下列的方法调用: + +matrix.preScale(0.5f, 1); matrix.postTranslate(10, 0); matrix.setScale(1, 0.6f); matrix.postScale(0.7f, 1); matrix.preTranslate(15, 0); 其坐标变换顺序是translate(15, 0) -> scale(1, 0.6f) -> scale(0.7f, 1). + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" index a97c07f6..2e7cf490 100644 --- "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -3,8 +3,9 @@ Android开发中的MVP模式详解 [MVC、MVP、MVVM介绍](https://github.com/CharonChui/AndroidNote/blob/master/Java%E5%9F%BA%E7%A1%80/MVC%E4%B8%8EMVP%E5%8F%8AMVVM.md) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_mvp.jpg?raw=true) -在`Android`开发中,如果不注重架构的话,`Activity`类就会变得愈发庞大。这是因为在`Android`开发中`View`和其他的线程可以共存于`Activity`内。那最大的问题是什么呢? 其实就是**`Activity`中同事存在业务逻辑和`UI`逻辑。这导致增加了单元测试和维护的成本。 +在`Android`开发中,如果不注重架构的话,`Activity`类就会变得愈发庞大。这是因为在`Android`开发中`View`和其他的线程可以共存于`Activity`内。那最大的问题是什么呢? 其实就是`Activity`中同时存在业务逻辑和`UI`逻辑。这导致增加了单元测试和维护的成本。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_is_god.png?raw=true) @@ -166,5 +167,52 @@ Android开发中的MVP模式详解 --- + +上面只是抛砖引玉。`MVP`的有点十分明显,就是代码解耦、可以让逻辑清晰,但是同样它也会有缺点,它的缺点就是项目的复杂程度会增加,项目中会多出很多类。 +之前很多人都在讨论该如何去正确的设计使用`MVP`来避免它的缺点,众说纷纭,很多人讨论的你死我活。直到`Google`发布了`MVP架构蓝图`,大家才意识到这才是规范。 + +项目地址:[android-architecture](https://github.com/googlesamples/android-architecture) +`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。 + +在它的官方介绍中是这样说的: + +> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult. + +> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools. + +> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple. + + + +已完成的示例: + +- todo-mvp/ - Basic Model-View-Presenter architecture. +- todo-mvp-loaders/ - Based on todo-mvp, fetches data using Loaders. +- todo-mvp-databinding/ - Based on todo-mvp, uses the Data Binding Library. +- todo-mvp-clean/ - Based on todo-mvp, uses concepts from Clean Architecture. +- todo-mvp-dagger/ - Based on todo-mvp, uses Dagger2 for Dependency Injection +- todo-mvp-contentproviders/ - Based on todo-mvp-loaders, fetches data using Loaders and uses Content Providers + +正在进行中的示例: + +- dev-todo-mvp-rxjava/ - Based on todo-mvp, uses RxJava for concurrency and data layer abstraction. + + +我们接下来就用`todo-mvp`来进行分析,这个应用非常简单,主要有以下几个功能: + +- 列表页:展示所有的`todo`项 +- 添加页:添加`todo`项 +- 详情页:查看`todo`项的详情 +- 统计页:查看当前所有已完成`todo`及未完成项的统计数据 + + + + + + + +--- + + - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" index ebba5262..5bb6d1b8 100644 --- "a/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" +++ "b/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" @@ -102,7 +102,7 @@ InstantRun详解 在`Instant Run`更改内容前,`Android Studio`会你的应用版本是否支持`Instant Run`并且`App Server`是否运行在对其有用的端口(内部使用了Socket)。 这样来确定你的应用是否在前台运行,并且找到`Studio`所需要的构建`ID`. -总结一下,其实就是内部会对每个`class`文件注入`instant run`相关的代码,然后自定义一个`application`内部指定自定义的`classLoader`(也就是说不使用默认的classLoader了,只要用了Instant Run就需要使用它自定义的classLoader),然后在应用程序里面开启一个服务器,`Studio`将修改的代码发送到该服务器,然后再通过自定义的`classLoader`加载代码的时候会去请求该服务器判断代码是否有更新,如果有更新就会通过委托机制加载新更新的代码然后注入到应用程序中,这样就完成了替换的操作。 +总结一下,其实就是内部会对每个`class`文件注入`instant run`相关的代码,然后自定义一个`application`内部指定自定义的`classLoader`(也就是说不使用默认的`classLoader`了,只要用了`Instant Run`就需要使用它自定义的`classLoader`),然后在应用程序里面开启一个服务器,`Studio`将修改的代码发送到该服务器,然后再通过自定义的`classLoader`加载代码的时候会去请求该服务器判断代码是否有更新,如果有更新就会通过委托机制加载新更新的代码然后注入到应用程序中,这样就完成了替换的操作。 ###热修复过程 diff --git "a/Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" "b/Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" new file mode 100644 index 00000000..e697a845 --- /dev/null +++ "b/Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" @@ -0,0 +1,49 @@ +volley-retrofit-okhttp之我们该如何选择网路框架 +=== + +说起`Volley`、`Retrofit`、`OkHttp`相信基本没有人不知道。当然这里把`OkHttp`放进来可能有些不恰当。 +因为`OkHttp`的官方介绍是`An HTTP+HTTP/2 client for Android and Java applications`。 +也就是说`OkHttp`是基于`http`协议封装的一套请求客户端。它是真正的网络请求部分, +与`HttpClient`、`HttpUrlConnection`是一样的, +但是显然它的效率非常高(说到这里顺便提一嘴,从`Android 4.4`开始`HttpUrlConnection`内部默认使用的也是`OkHttp`, +具体请参考之前的文章[HttpUrlConnection详解](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/HttpURLConnection%E8%AF%A6%E8%A7%A3.md))。 +而`Volley`、`Retrofit`是控制请求的队列、切换、解析、缓存等逻辑。所以`Volley`和`Retrofit`都可以结合`OkHttp`来使用。 + + +在`Android`开发中有很多网络请求框架,但是比较过来比较过去,最后最倾向的就是这两个: + +- `Volley`:`Google`发布的网络请求框架,专门为移动设备定制,小而美。 +- `Retrofit`:良心企业 `Square`由大神`JakeWharton`主导的开源项目,是基于`OkHttp`封装的一套`Resetful`网络请求框架。`Type-safe HTTP client for Android and Java by Square, Inc.` + + +有关`Volley`的介绍请看之前发布的文章[Volley源码分析](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/Volley%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) + + +这里就不分别介绍他俩了,直接说各自的优缺点: + +- `Retrofit`使用起来更简单。而`Volley`配置起来会稍微麻烦,因为`Volley`可以使用`HttpClient`、`HttpUrlConnection`、`OkHttp`我们需要根据自己的需求去配置。而`Retrofit`只能结合`OkHttp`使用。 + +- `Retrofit`依赖于`OkHttp`,从而会导致它的包大小会比`Volley`的大。 + +- `Volley`有很好的内存缓存管理,它在解析之前会将整个相应部分都加载到内存中,所以它对于小的网络请求非常合适,但是不支持`post`大数据,所以不适合上传文件。而`Retrofit`使用的是硬盘缓存,所以相比起从缓存这块来讲`Retrofit`可能会更慢一些。 + +- `Retrofit`依赖于`OkHttp`,而`OkHttp`自身会避免同时两次请求同一个请求。所以`Retrofit`同样会和`Volley`一样去避免重复的请求,只不过它是在网络层来处理的。 + +- `Volley`在网络请求部分默认依赖于`Apache HttpClient`。而`Apache HttpClient`从`API 23`开始已经在`Android`中被移除并废弃了。这就是为什么很多开发者会认为`Volley`已经过时了,因为`Volley`并没有迁移到新的未废弃的代码。 + +- 默认情况下`Volley`会在`DefaultRetryPolicy`中会将读取和连接的超时时间设置为`2.5s`,并且对每次请求失败或者超时都有一次自动重试。 所以对于一些服务器响应可能会超过`2s`的请求,开发者需要格外的小心下。`Retrofit`的默认超时时间是`10s`,而且它对失败或者超时的操作不会自动重试。 +- 很多开发者都会说`Retrofit`会比`Volley`更快。因为有人专门去测试过,其实这里是不严谨的。因为`Volley`可以结合使用`HttpUrlConnection`、`HttpClient`、`OkHttp`等来使用,而`Retrofit`是用`OkHttp`一起,所以如果你让`Volley`结合`OkHttp`之后再来测试你就会发现总体来说其实他们不相上下。 + + +- `Volley`实现了很完善的`Activity`声明周期管理。 + +虽然`Volley`之前也有一些问题,但是它们也都被各个大神修复。 + + +所以综合起来说使用`Volley+OKHttp`的组合是非常不错的,既可以保证速度又可以满足对缓存、重试等的处理。但是如果你是`RxJava`的使用者那你可能会更偏向于使用`Retrofit`,因为`Retrofit`可以无缝结合`RxJava`使用。目前主流的一套框架就是`Retrofit + OkHttp + RxJava + Dagger2 `,但是对使用者的要求也相对要高些。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" index 5bcbc604..9466e199 100644 --- "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" +++ "b/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" @@ -340,8 +340,8 @@ public void ProcessPeople() { - `touch .bash_profile`创建 - `open -e .bash_profile`打开 - 添加 - ``` - #Hierarchy Viewer Variable + ```java + #Hierarchy Viewer Variable export ANDROID_HVPROTO=ddm ``` - `source .bash_profile` diff --git "a/Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" "b/Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" new file mode 100644 index 00000000..2e86bef9 --- /dev/null +++ "b/Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" @@ -0,0 +1,782 @@ +热修复实现 +=== + + +现在的热修复方案已经有很多了,例如`alibaba`的[dexposed](https://github.com/alibaba/dexposed)、[AndFix](https://github.com/alibaba/AndFix)以及`jasonross`的[Nuwa](https://github.com/jasonross/Nuwa)等等。原来没有仔细去分析过也没想写这篇文章,但是之前[InstantRun详解](https://github.com/CharonChui/AndroidNote/blob/master/Android加强/InstantRun详解.md)这篇文章中介绍了`Android Studio Instant Run`的 +实现原理,这不就是活生生的一个热修复吗? 随心情久久不能平复,我们何不用这种方式来实现。 + + +方案有很多种,我就只说明下我想到的方式,也就是`Instant Run`的方式: +分拆到不同的`dex`中,然后通过`classloader`来进行加载。但是在之前`InstantRun详解`中只说到会通过内部的`server`去判断该类是否有更新,如果有的话就去从新的`dex`中加载该类,否则就从旧的`dex`中加载,但这是如何实现的呢? 怎么去从不同的`dex`中选择最新的那个来进行加载。 + +讲到这里需要先介绍一下`ClassLoader`: + +< `A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.` + + +在一般情况下,应用程序不需要创建一个全新的ClassLoader对象,而是使用当前环境已经存在的ClassLoader。因为Javad的Runtime环境在初始化时,其内部会创建一个ClassLoader对象用于加载Runtime所需的各种Java类。 +每个ClassLoader必须有一个父ClassLoader,在装载Class文件时,子ClassLoader会先请求父ClassLoader加载该Class文件,只有当其父ClassLoader找不到该Class文件时,子ClassLoader才会继续装载该类,这是一种安全机制。 + +对于Android的应用程序,本质上虽然也是用Java开发,并且使用标准的Java编译器编译出Class文件,但最终的APK文件中包含的却是dex类型的文件。dex文件是将所需的所有Class文件重新打包,打包的规则不是简单的压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是dex文件。由于dex文件是一种经过优化的Class文件,因此要加载这样特殊的Class文件就需要特殊的类装载器,这就是DexClassLoader,Android SDK中提供的DexClassLoader类就是出于这个目的。 + + +总体来说,`Android` 默认主要有三个`ClassLoader`: + +- `BootClassLoader`: 系统启动时创建, + < `Provides an explicit representation of the boot class loader. It sits at the + * head of the class loader chain and delegates requests to the VM's internal + * class loading mechanism.` + +- `PathClassLoader`: 可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk; +- `DexClassLoader`: 可以加载文件系统上的jar、dex、apk;可以从SD卡中加载未安装的apk + + +通过上面的分析知道,如果用多个`dex`的话肯定会用到`DexClassLoader`类,我们首先来看一下它的源码(这里 +插一嘴,源码可以去[googlesource](https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/dalvik/src/main/java/dalvik/system)中找): +```java +/** + * A class loader that loads classes from {@code .jar} and {@code .apk} files + * containing a {@code classes.dex} entry. This can be used to execute code not + * installed as part of an application. + * + *

This class loader requires an application-private, writable directory to + * cache optimized classes. Use {@code Context.getDir(String, int)} to create + * such a directory:

   {@code
+ *   File dexOutputDir = context.getDir("dex", 0);
+ * }
+ * + *

Do not cache optimized classes on external storage. + * External storage does not provide access controls necessary to protect your + * application from code injection attacks. + */ +public class DexClassLoader extends BaseDexClassLoader { + /** + * Creates a {@code DexClassLoader} that finds interpreted and native + * code. Interpreted classes are found in a set of DEX files contained + * in Jar or APK files. + * + *

The path lists are separated using the character specified by the + * {@code path.separator} system property, which defaults to {@code :}. + * + * @param dexPath the list of jar/apk files containing classes and + * resources, delimited by {@code File.pathSeparator}, which + * defaults to {@code ":"} on Android + * @param optimizedDirectory directory where optimized dex files + * should be written; must not be {@code null} + * @param libraryPath the list of directories containing native + * libraries, delimited by {@code File.pathSeparator}; may be + * {@code null} + * @param parent the parent class loader + */ + public DexClassLoader(String dexPath, String optimizedDirectory, + String libraryPath, ClassLoader parent) { + super(dexPath, new File(optimizedDirectory), libraryPath, parent); + } +} +``` +注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找, +因为家在都是从`loadClass()`方法中,所以我们去`ClassLoader`类中看一下`loadClass()`方法: +```java +/** + * Loads the class with the specified name. Invoking this method is + * equivalent to calling {@code loadClass(className, false)}. + *

+ * Note: In the Android reference implementation, the + * second parameter of {@link #loadClass(String, boolean)} is ignored + * anyway. + *

+ * + * @return the {@code Class} object. + * @param className + * the name of the class to look for. + * @throws ClassNotFoundException + * if the class can not be found. + */ + public Class loadClass(String className) throws ClassNotFoundException { + return loadClass(className, false); + } + + /** + * Loads the class with the specified name, optionally linking it after + * loading. The following steps are performed: + *
    + *
  1. Call {@link #findLoadedClass(String)} to determine if the requested + * class has already been loaded.
  2. + *
  3. If the class has not yet been loaded: Invoke this method on the + * parent class loader.
  4. + *
  5. If the class has still not been loaded: Call + * {@link #findClass(String)} to find the class.
  6. + *
+ *

+ * Note: In the Android reference implementation, the + * {@code resolve} parameter is ignored; classes are never linked. + *

+ * + * @return the {@code Class} object. + * @param className + * the name of the class to look for. + * @param resolve + * Indicates if the class should be resolved after loading. This + * parameter is ignored on the Android reference implementation; + * classes are not resolved. + * @throws ClassNotFoundException + * if the class can not be found. + */ + protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { + Class clazz = findLoadedClass(className); + + if (clazz == null) { + ClassNotFoundException suppressed = null; + try { + // 先检查父ClassLoader是否已经家在过该类 + clazz = parent.loadClass(className, false); + } catch (ClassNotFoundException e) { + suppressed = e; + } + + if (clazz == null) { + try { + // 调用DexClassLoader.findClass()方法。 + clazz = findClass(className); + } catch (ClassNotFoundException e) { + e.addSuppressed(suppressed); + throw e; + } + } + } + + return clazz; + } +``` +上面会调用`DexClassLoader.findClass()`方法,但是`DexClassLoader`没有实现该方法,所以去它的父类`BaseDexClassLoader`中看,接着看一下`BaseDexClassLoader`的源码: +```java +/** + * Base class for common functionality between various dex-based + * {@link ClassLoader} implementations. + */ +public class BaseDexClassLoader extends ClassLoader { + /** originally specified path (just used for {@code toString()}) */ + private final String originalPath; + /** structured lists of path elements */ + private final DexPathList pathList; + /** + * Constructs an instance. + * + * @param dexPath the list of jar/apk files containing classes and + * resources, delimited by {@code File.pathSeparator}, which + * defaults to {@code ":"} on Android + * @param optimizedDirectory directory where optimized dex files + * should be written; may be {@code null} + * @param libraryPath the list of directories containing native + * libraries, delimited by {@code File.pathSeparator}; may be + * {@code null} + * @param parent the parent class loader + */ + public BaseDexClassLoader(String dexPath, File optimizedDirectory, + String libraryPath, ClassLoader parent) { + super(parent); + this.originalPath = dexPath; + this.pathList = + new DexPathList(this, dexPath, libraryPath, optimizedDirectory); + } + @Override + protected Class findClass(String name) throws ClassNotFoundException { + // 从DexPathList中找 + Class clazz = pathList.findClass(name); + if (clazz == null) { + throw new ClassNotFoundException(name); + } + return clazz; + } + @Override + protected URL findResource(String name) { + return pathList.findResource(name); + } + @Override + protected Enumeration findResources(String name) { + return pathList.findResources(name); + } + @Override + public String findLibrary(String name) { + return pathList.findLibrary(name); + } + +``` +在`findClass()`方法中我们看到调用了`DexPathList.findClass()`方法: +```java +/** + * A pair of lists of entries, associated with a {@code ClassLoader}. + * One of the lists is a dex/resource path — typically referred + * to as a "class path" — list, and the other names directories + * containing native code libraries. Class path entries may be any of: + * a {@code .jar} or {@code .zip} file containing an optional + * top-level {@code classes.dex} file as well as arbitrary resources, + * or a plain {@code .dex} file (with no possibility of associated + * resources). + * + *

This class also contains methods to use these lists to look up + * classes and resources.

+ */ +/*package*/ final class DexPathList { + private static final String DEX_SUFFIX = ".dex"; + private static final String JAR_SUFFIX = ".jar"; + private static final String ZIP_SUFFIX = ".zip"; + private static final String APK_SUFFIX = ".apk"; + /** class definition context */ + private final ClassLoader definingContext; + /** list of dex/resource (class path) elements */ + // 把dex封装成一个数组,每个Element代表一个dex + private final Element[] dexElements; + /** list of native library directory elements */ + private final File[] nativeLibraryDirectories; + + // ..... + + /** + * Finds the named class in one of the dex files pointed at by + * this instance. This will find the one in the earliest listed + * path element. If the class is found but has not yet been + * defined, then this method will define it in the defining + * context that this instance was constructed with. + * + * @return the named class or {@code null} if the class is not + * found in any of the dex files + */ + public Class findClass(String name) { + for (Element element : dexElements) { + DexFile dex = element.dexFile; + // 遍历数组,拿到第一个就返回 + if (dex != null) { + Class clazz = dex.loadClassBinaryName(name, definingContext); + if (clazz != null) { + return clazz; + } + } + } + return null; + } +} +``` +从上面的源码中分析,我知道系统会把所有相关的`dex`维护到一个数组中,然后在加载类的时候会从该数组中的第一个元素中取,然后返回。那我们只要保证将我们热修复后的`dex`对应的`Element`放到该数组的第一个位置就可以了,这样系统就会加载我们热修复的`dex`中的类。 +所以方案出来了,只要把有问题的类修复后,放到一个单独的`dex`,然后把该`Dex`转换成对应的`Element`后再将该`Element`插入到`dexElements`数组的第一个位置就可以了。那该如何去将其插入到`dexElements`数组的第一个位置呢?-- 暴力反射。 + + + +到这里我感觉初步的思路已经有了: + +- 将补丁作为`dex`发布。 +- 通过反射修改该`dex`所对应的`Element`在数组中的位置。 + +但是我也想到肯定还会有类似下面的问题: + +- 资源文件的处理 +- 四大组件的处理 +- 清单文件的处理 + + +虽然我知道没有这么简单,但是我还是决定抱着不作不死的宗旨继续前行。 + +好了,`demo`走起来。 + + +怎么生成`dex`文件呢? +这要讲过两部分: +- `.class`-> `.jar` : `jar -cvf test.jar com/charon/instantfix_sample/MainActivity.class` +- `.jar`-> `.dex`: `dx --dex --output=target.jar test.jar` `target.jar`就是包含`.dex`的`jar`包 + + +生成好`dex`后我们为了模拟先将其放到`asset`目录下(实际开发中肯定要从接口中去下载,当然还会有一些版本号的判断等),然后就是将该`dex`转换成 + + + + + + + + + + +我的方案中采用的是MultiDex,对其进行一部分改造,具体代码: +1、添加dex文件,并执行install +/** +* 添加apk包外的dex文件 +* 自动执行install +* @param dexFile +*/ +public static void addDexFileAutoInstall(Context context, List dexFile,File optimizedDirectory) { + if (dexFile != null && !dexFile.isEmpty() &&!dexFiles.contains(dexFile)) { + dexFiles.addAll(dexFile); + LogUtil.d(TAG, "add other dexfile"); + installDexFile(context,optimizedDirectory); + } +} +2、installDexFile直接调用MultiDex 的installSecondaryDexes方法。 +/** + * 添加apk包外的dex文件, + * @param context + */ +publicstatic void installDexFile(Context context, File optimizedDirectory){ + if (checkValidZipFiles(dexFiles)) { + try { + installSecondaryDexes(context.getClassLoader(), optimizedDirectory, dexFiles); + } catch (IllegalAccessExceptione){ + e.printStackTrace(); + } catch (NoSuchFieldExceptione) { + e.printStackTrace(); + } catch (InvocationTargetExceptione){ + e.printStackTrace(); + } catch (NoSuchMethodExceptione) { + e.printStackTrace(); + } catch (IOExceptione) { + e.printStackTrace(); + } + } +} +3、将patch.dex放在所有dex最前面。 +private static voidexpandFieldArray(Object instance, String fieldName, Object[]extraElements) throws NoSuchFieldException, IllegalArgumentException, +IllegalAccessException { + Field jlrField = findField(instance, fieldName); + Object[]original = (Object[]) jlrField.get(instance); + Object[]combined = (Object[]) Array.newInstance(original.getClass().getComponentType(),original.length + extraElements.length); + // 将后来的dex放在前面,主dex放在最后。 + System.arraycopy(extraElements, 0, combined, 0, extraElements.length); + System.arraycopy(original, 0, combined, extraElements.length,original.length); + // 原始的dex合并,是将主dex放在前面,其他的dex依次放在后面。 + //System.arraycopy(original, 0, combined, 0, original.length); + //System.arraycopy(extraElements, 0, combined, original.length,extraElements.length); + jlrField.set(instance, combined); +} +到此将patch.dex放进了Element,接下来的问题就是加载Class,当加载patch.dex中类的时候,会遇到一个问题,这个问题就是QQ空间团队遇到,Classd的CLASS_ISPREVERIFIED。具体原因是dvmResolveClass这个方法对Class进行了校验。判断这个要Resolve的class是否和其引用来自一个dex。如果不是,就会遇到问题。 + + +当引用这和被引用者不在同一个dex中就会抛出异常,导致Resolve失败。QQ空间团队的方案是阻止所有的Class类打上CLASS_ISPREVERIFIED来逃过校验,这种方式其实是影响性能。 +我们的方案是和QQ团队的类似,但是和QQ空间不同的是,我们将fromUnverifiedConstant设置为true,来逃过校验,达到补丁的路径。具体怎么实现呢? +要引用Cydia Hook技术来hook Native dalvik中dvmResolveClass这个方法。有关Cydia Hook技术请参考: +官网地址:http://www.cydiasubstrate.com/ +官方教程:http://www.cydiasubstrate.com/id/38be592b-bda7-4dd2-b049-cec44ef7a73b +SDK下载地址:http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip +具体代码如下。 +//指明要hook的lib : +MSConfig(MSFilterLibrary,"/system/lib/libdvm.so") + +// 在初始化的时候进行hook +MSInitialize { + LOGD("Cydia Init"); + MSImageRef image; + //载入lib + image = MSGetImageByName("/system/lib/libdvm.so"); + if (image != NULL) { + LOGD("image is not null"); + void *dexload=MSFindSymbol(image,"dvmResolveClass"); + if(dexload != NULL) { + LOGD("dexloadis not null"); + MSHookFunction(dexload, (void*)proxyDvmResolveClass, (void**)&dvmResolveClass_Proxy); + } else{ + LOGD("errorfind dvmResolveClass"); + } + } +} +// 在初始化的时候进行hook//保留原来的地址 +ClassObject* (*dvmResolveClass_Proxy)(ClassObject* referrer, u4 classIdx, boolfromUnverifiedConstant); +// 新方法地址 +static ClassObject* proxyDvmResolveClass(ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant) { + return dvmResolveClass_Proxy(referrer, classIdx,true); +} +有人可能会担心cydia Hook性能,稳定性问题,但是据我所知,目前有些公司已经用它来实现apk加壳和脱壳防止反编译技术方案。具体可以参考http://www.gitzx.com/android-cydiasubstrate/ + + + + + + + + + + + + + + +说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报preverified错误,原来在DexPrepare.cpp,将dex转化成odex的过程中,会在DexVerify.cpp进行校验,验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。通过在所有类(Application除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex的类的引用,就可以解决这个问题。空间使用了javaassist进行编译时字节码插入。 + +所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下: + +```java +if (ClassVerifier.PREVENT_VERIFY) { + System.out.println(AntilazyLoad.class); +} +``` +其中AntilazyLoad类会被打包成单独的antilazy.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。 然后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。 所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在Application中onCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志) + +如何打包补丁包: +1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5,还有一份mapping混淆文件。 +2. 在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。 +备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex,只需要把修改过的类的class文件打包成patch dex,然后放到sdcard下,那么就会让改变的代码生效。 + + +在 Java 中,只有当两个实例的类名、包名以及加载其的 ClassLoader 都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的 ClassLoader 不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。 +同一个 Class = 相同的 ClassName + PackageName + ClassLoader +以上问题在采用动态加载功能的开发中容易出现,请注意。 + + +通过上面的分析,我们知道使用ClassLoader动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java程序,在Android程序中使用动态加载主要有两个麻烦的问题: + +Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作; +Res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些ID从Resource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上; +说到底,抛开虚拟机的差别不说,一个Android程序和标准的Java程序最大的区别就在于他们的上下文环境(Context)不同。Android中,这个环境可以给程序提供组件需要用到的功能,也可以提供一些主题、Res等资源,其实上面说到的两个问题都可以统一说是这个环境的问题,而现在的各种Android动态加载框架中,核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。 + + + + + +DexClassLoader的使用方法一般有两种: +从已安装的apk中读取dex +从apk文件中读取dex +假如有两个APK,一个是宿主APK,叫作HOST,一个是插件APK,叫作Plugin。Plugin中有一个类叫PluginClass,代码如下: +```java +public class PluginClass { + public PluginClass() { + Log.d("JG","初始化PluginClass"); + } + + public int function(int a, int b){ + return a+b; + } +} +``` +现在如果想调用插件APK中PluginClass内的方法,应该怎么办? +####从已安装的apk中读取dex + +先来看第一种方法,这种方法必须建一个Activity,在清单文件中配置Action. +```java + + + + + +``` +然后在宿主APK中如下使用: +```java +/** + * 这种方式用于从已安装的apk中读取,必须要有一个activity,且需要配置ACTION + */ +private void useDexClassLoader(){ + //创建一个意图,用来找到指定的apk + Intent intent = new Intent("com.maplejaw.plugin"); + //获得包管理器 + PackageManager pm = getPackageManager(); + List resolveinfoes = pm.queryIntentActivities(intent, 0); + if(resolveinfoes.size()==0){ + return; + } + //获得指定的activity的信息 + ActivityInfo actInfo = resolveinfoes.get(0).activityInfo; + + //获得包名 + String packageName = actInfo.packageName; + //获得apk的目录或者jar的目录 + String apkPath = actInfo.applicationInfo.sourceDir; + //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己 + //目录下的文件 + String dexOutputDir = getApplicationInfo().dataDir; + + //native代码的目录 + String libPath = actInfo.applicationInfo.nativeLibraryDir; + + //创建类加载器,把dex加载到虚拟机中 + DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, + this.getClass().getClassLoader()); + + //利用反射调用插件包内的类的方法 + + try { + Class clazz = calssLoader.loadClass(packageName+".PluginClass"); + + Object obj = clazz.newInstance(); + Class[] param = new Class[2]; + param[0] = Integer.TYPE; + param[1] = Integer.TYPE; + + Method method = clazz.getMethod("function", param); + + Integer ret = (Integer)method.invoke(obj, 12,34); + + Log.d("JG", "返回的调用结果为:" + ret); + + } catch (Exception e) { + e.printStackTrace(); + } + } +``` +我们安装完两个APK后,在宿主中就可以直接调用,调用示例如下。 + +```java +public void btnClick(View view){ + useDexClassLoader(); +} +``` +####从apk文件中读取dex + +这种方法由于并不需要安装,所以不需要通过Intent从activity中解析信息。换言之,这种方法不需要创建Activity。无需配置清单文件。我们只需要打包一个apk,然后放到SD卡中即可。 +核心代码如下: + +```java +//apk路径 +String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk"; + +private void useDexClassLoader(String path){ + + File codeDir=getDir("dex", Context.MODE_PRIVATE); + + //创建类加载器,把dex加载到虚拟机中 + DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null, + this.getClass().getClassLoader()); + + //利用反射调用插件包内的类的方法 + + try { + Class clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass"); + + Object obj = clazz.newInstance(); + Class[] param = new Class[2]; + param[0] = Integer.TYPE; + param[1] = Integer.TYPE; + + Method method = clazz.getMethod("function", param); + + Integer ret = (Integer)method.invoke(obj, 12,21); + + Log.d("JG", "返回的调用结果为: " + ret); + + } catch (Exception e) { + e.printStackTrace(); + } + +``` + + + + + + +动态加载的几个关键问题 +资源访问:无法找到某某id所对应的资源 +因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片、文本等,这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用别人的Context是无法得到自己的资源的; +解决方案一:插件中的资源在宿主程序中也预置一份; +缺点:增加了宿主apk的大小;在这种模式下,每次发布一个插件都需要将资源复制到宿主程序中,这意味着每发布一个插件都要更新一下宿主程序; +解决方案二:将插件中的资源解压出来,然后通过文件流去读取资源; +缺点:实际操作起来还是有很大难度的。首先不同资源有不同的文件流格式,比如图片、XML等,其次针对不同设备加载的资源可能是不一样的,如何选择合适的资源也是一个需要解决的问题; +实际解决方案: +Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中; + + +```java +/** Return an AssetManager instance for your application's package. */ + public abstract AssetManager getAssets(); + + /** Return a Resources instance for your application's package. */ + public abstract Resources getResources(); +``` +具体实现: +```java +protected void loadResources() { + try { + AssetManager assetManager = AssetManager.class.newInstance(); + Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); + addAssetPath.invoke(assetManager, mDexPath); + mAssetManager = assetManager; + } catch (Exception e) { + e.printStackTrace(); + } + Resources superRes = super.getResources(); + mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), + superRes.getConfiguration()); + mTheme = mResources.newTheme(); + mTheme.setTo(super.getTheme()); +} + +``` +加载资源的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射。 +addAssetPath(); + +```java +@hide + public final int addAssetPath(String path) { + + synchronized (this) { + + int res = addAssetPathNative(path); + + makeStringBlocks(mStringBlocks); + + return res; + + } + +} +``` + +Activity生命周期的管理: +反射方式和接口方式。 +反射的方式很好理解,首先通过Java的反射去获取Activity的各种生命周期方法,比如onCreate、onStart、onResume等,然后在代理Activity中去调用插件Activity对应的生命周期方法即可; +缺点:一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。 + +反射方式 +```java +@Override + +protected void onResume() { + + super.onResume(); + + Method onResume = mActivityLifecircleMethods.get("onResume"); + + if (onResume != null) { + + try { + + onResume.invoke(mRemoteActivity, new Object[] { }); + + } catch (Exception e) { + + e.printStackTrace(); + + } + + } + +} + + +@Override + +protected void onPause() { + + Method onPause = mActivityLifecircleMethods.get("onPause"); + + if (onPause != null) { + + try { + + onPause.invoke(mRemoteActivity, new Object[] { }); + + } catch (Exception e) { + + e.printStackTrace(); + + } + + } + + super.onPause(); + +} +``` + +接口方式 + +```java +public interface DLPlugin { + + public void onStart(); + + public void onRestart(); + + public void onActivityResult(int requestCode, int resultCode, Intent + + data); + + public void onResume(); + + public void onPause(); + + public void onStop(); + + public void onDestroy(); + + public void onCreate(Bundle savedInstanceState); + + public void setProxy(Activity proxyActivity, String dexPath); + + public void onSaveInstanceState(Bundle outState); + + public void onNewIntent(Intent intent); + + public void onRestoreInstanceState(Bundle savedInstanceState); + + public boolean onTouchEvent(MotionEvent event); + + public boolean onKeyUp(int keyCode, KeyEvent event); + + public void onWindowAttributesChanged(LayoutParams params); + + public void onWindowFocusChanged(boolean hasFocus); + + public void onBackPressed(); + +… + +} +``` +代理Activity中只需要按如下方式即可调用插件Activity的生命周期方法,这就完成了插件Activity的生命周期的管理;插件Activity需要实现DLPlugin接口; +```java +@Override + +protected void onStart() { + + mRemoteActivity.onStart(); + + super.onStart(); + +} + + +@Override + +protected void onRestart() { + + mRemoteActivity.onRestart(); + + super.onRestart(); + +} + + +@Override + +protected void onResume() { + + mRemoteActivity.onResume(); + + super.onResume(); + +} +``` + + + + + + + + + + + +- [Google Instant app](https://developer.android.com/topic/instant-apps/index.html) +- [微信热补丁实现](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md#rd) +- [多dex分拆](http://my.oschina.net/853294317/blog/308583) +- [QQ空间热修复方案](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a) +- [Android dex分包方案](http://my.oschina.net/853294317/blog/308583) +- [类加载器DexClassLoader](http://www.maplejaw.com/2016/05/24/Android%E6%8F%92%E4%BB%B6%E5%8C%96%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%80%EF%BC%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8DexClassLoader/) +- [基于cydia Hook在线热修复补丁方案](http://blog.csdn.net/xwl198937/article/details/49801975) +- [](http://blog.csdn.net/lmj623565791/article/details/49883661) +- [](http://tech.meituan.com/mt-android-auto-split-dex.html) +- [](http://kymjs.com/column/plugin.html) +- [](http://weishu.me/) +- [](http://www.zjutkz.net/2016/05/23/%E5%BD%93%E4%BD%A0%E5%87%86%E5%A4%87%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%A1%86%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E4%B8%80%E5%88%87/) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From dfe88780c8d6884835c78ef891abf3cc3727072c Mon Sep 17 00:00:00 2001 From: "xu.chuanren" Date: Tue, 16 Aug 2016 18:29:01 +0800 Subject: [PATCH 052/373] =?UTF-8?q?Update=20Android=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8F=8A=E7=9B=B8=E5=85=B3=E7=B1=BB=E5=BA=93?= =?UTF-8?q?.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\205\267\345\217\212\347\261\273\345\272\223.md" | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" index e2d22cdb..fc709e35 100644 --- "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" +++ "b/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" @@ -68,8 +68,19 @@ Android开发工具及类库 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lint.png?raw=true) - 进入到`Android Studio`中的具体项目中执行`./gradlew clean`后再执行`./gradlew lint && android-resource-remover --xml app/build/outputs/lint-results.xml` +9. [stetho](https://github.com/facebook/stetho) + `facebook`出品。快速查看布局、数据库、网络请求。实在不能再方便了。 +10. [RxJava](https://github.com/ReactiveX/RxJava) + 用了后你会爱上它。 +11. [Retrofilt](https://github.com/square/retrofit) + `Square`出品。大神`JakeWharton`主导出品的网络请求框架。内部结合`OkHttp`。结合`RxJava`使用非常方便。 +12. [android-architecture](https://github.com/googlesamples/android-architecture) + 放到这里可能不太合适,因为它并不是工具和类库,而是`Google`官方发布的`Android`架构示例。非常值得参考。 +13. [AndroidWiFiADB](https://github.com/pedrovgs/AndroidWiFiADB) + 还在为数据线不够用而烦恼嘛? + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From ed5355626598d3ba0beef15112c67095f704beb7 Mon Sep 17 00:00:00 2001 From: Shura Date: Sat, 20 Aug 2016 18:41:25 +0800 Subject: [PATCH 053/373] =?UTF-8?q?Update=20=E5=B8=B8=E7=94=A8=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E5=A4=A7=E5=85=A8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 稍微修改一下 --- ...44\350\241\214\345\244\247\345\205\250.md" | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git "a/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" index f0bdd905..6b2f4dfa 100644 --- "a/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ "b/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -6,9 +6,6 @@ - 进入文件夹 `cd test` - -- 显示路径 - 在需要显示的文件夹中执行 `pwd` - 创建空文件 `touch fileName` @@ -32,21 +29,23 @@ - 查看文件内容 `cat fileName` -- 清楚命令行内容 +- 清除命令行内容 `clear` -- 显示日期 +- 显示当前日期 `date` -- 显示文件 - `ls` - `ls -a`// 显示隐藏文件 - `ls -l`列出详细信息 +- 显示文件列表 + `ls`// 显示未隐藏的文件 + `ls -a`// 显示所有文件 + `ls -A`// 显示除了'.'和'.'以外所有文件 + `ls -l`// 显示文件详细信息 + `ls -R`// 递归显示目录及其子目录文件内容 - 关机 `sudo shutdown -h now`// -h 是关闭电源 now立即关机 - `sudo shutdown -r now`//重启 - `sudo shutdown -h -time 60`// 表示60分钟后关机,注意单位是分钟 + `sudo shutdown -r now`//重启 + `sudo shutdown -h -time 60`// 表示60分钟后关机,注意单位是分钟 @@ -58,4 +57,4 @@ - 邮箱 :charon.chui@gmail.com - Good Luck! - \ No newline at end of file + From 938f988fb5dbe1b7840c4e10f8add684db93e390 Mon Sep 17 00:00:00 2001 From: Shura Date: Mon, 22 Aug 2016 10:50:05 +0800 Subject: [PATCH 054/373] =?UTF-8?q?Update=20Android=E5=85=A5=E9=97=A8?= =?UTF-8?q?=E4=BB=8B=E7=BB=8D.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...45\351\227\250\344\273\213\347\273\215.md" | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git "a/Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" "b/Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" index 5db36af0..b0626692 100644 --- "a/Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" +++ "b/Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" @@ -27,7 +27,7 @@ Android入门介绍 - 硬件驱动 3. Android体系结构 - - Applications:桌面应用、打电话应用、浏览器等应用程序 + - Applications:桌面、电话、浏览器等应用程序 - Applications Framework:ActivityManager、 WindowManager、ContentProvider、ResourceManager等 - Libraries: SQLite库、SurfaceManager、WebKit、OppenGL等。 - Android运行时 @@ -50,7 +50,9 @@ Android入门介绍 - intel - AMD 2. ARM - - 摩托罗拉 + - 联发科 + - 高通 + - 海思 - 三星 6. Android项目目录结构 @@ -63,7 +65,7 @@ Android入门介绍 - anim:定义动画的XML - raw:原生文件 4. assets:资源路径,不会在R文件注册 - 5. project.properties:供Eclipse使用,读取该项目使用Android版本号。早期版本名为default.properties + 5. project.properties:供Eclipse使用,读取该项目使用Android版本号,早期版本名为default.properties 6. AndroidManifest.xml:清单文件,在软件安装的时候被读取 Android中的四大组件(Activity、ContentProvider、BroadcastReceiver、Service)都需要在该文件中注册程序所需的权限也需要在此文件中声明,例如:电话、短信、互联网、访问SD卡 7. bin:二进制文件,包括class、资源文件、dex、apk等 @@ -79,30 +81,28 @@ Android入门介绍 7. 按照main.xml文件初始化界面 简单的来说软件的安装都是两个过程 - - 拷贝apk中得一些文件到系统的某个目录 - 1. `/data/app/`目录下 - 2. 创建一个文件夹 `/data/data/com.test.helloworld/`来保存数据 - - - 在系统的注册表里面配置一些信息. `data/system/packages.xml` + - 拷贝apk中的一些文件到系统的某个目录 + 1. `/data/app/`目录下 + 2. 创建一个文件夹 `/data/data/com.test.helloworld/`来保存数据 + - 在系统的packages.xml文件(类似于Windows的注册表)中里面配置应用权限等一些信息. `/data/system/packages.xml` 8. Android安全学 Android安全学中的一个重要的设计点是在默认情况下应用程序没有权限执行对其它应用程序、操作系统或用户有害的操作。 - 这些操作包括读/写用户的隐私数据(例如联系方式或e-mail),读/写其它应用程序的文件,执行网络访问,保持设备活动,等等。 - 所以牵扯到付费或者可能与用户隐私相关的操作都要申请权限. + 这些操作包括读/写用户的隐私数据(例如联系人或e-mail),读/写其它应用程序的文件,执行网络访问,保持设备活动,等等。 + 所有牵扯到付费或者可能与用户隐私相关的操作都要申请权限。 9. 测试分类 - 单元测试(Unit test) -> 功能测试( Function test) ->集成测试(Intergation test) + 单元测试(Unit test) -> 功能测试( Function test) -> 集成测试(Intergation test) 10. Android单元测试 - AndroidManifest.xml中进行配置,导入android的junit环境 - - 编写测试类继承Android的测试父类,AndroidTestCase这个类( AndroidTestCase是为了去模拟一个手机的运行环境, - 这个类中有一个getContext方法能获取到当前测试类的应用上下文对象,所以这个方法必须要等到测试框架初始化完成后才可以去调用) - - 测试的方法名要求以小写的test开头,如不以test开头只能单独点这个方法运行,整体全部运行时没有这个方法,所有的测试方法都要抛出异常,要把异常抛给测试框架不能自己去捕获。 + - 编写测试类继承Android的测试父类,AndroidTestCase这个类( AndroidTestCase是为了去模拟一个手机的运行环境,这个类中有一个getContext方法能获取到当前测试类的应用上下文对象,所以这个方法必须要等到测试框架初始化完成后才可以去调用) + - 测试的方法名要求以小写的test开头,如不以test开头只能单独点这个方法运行,整体全部运行时没有这个方法,所有的测试方法都要抛出异常,要把异常抛给测试框架不能自己去捕获 - 注意:测试得代码也是只能在手机上跑,它是在手机上测试完之后又将信息发送到了eclipse中 + 注意:测试的代码也是只能在手机上跑,它是在手机上测试完之后又将信息发送到了eclipse中 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 94c2f38c9c4d13b434252d8b22fdc4a8c8190eb7 Mon Sep 17 00:00:00 2001 From: "xu.chuanren" Date: Mon, 5 Dec 2016 19:16:19 +0800 Subject: [PATCH 055/373] add --- ...77\347\224\250\347\256\200\344\273\213.md" | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 "Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" diff --git "a/Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" "b/Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" new file mode 100644 index 00000000..1222f1ce --- /dev/null +++ "b/Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" @@ -0,0 +1,120 @@ +adb logcat使用简介 +=== + +得有半年没写日记了,今天不太忙,抽空写一个简单的入门,因为很长时间没用了,最近用起来发现已经忘了..... + +`android`开发过程中我们经常会用到`log`,虽然平时大多用`studio`,但是如果测试发现一个`crash`时跑过来找你时你会发现用`studio`已经有点迟了。这时候就要打开`adb logcat`. +忘记怎么加参数了怎么办? 默默的打开`adb logcat --help`你会发现只要你肯努力,你肯定能把所有的事情都搞垮。 + +`adb logcat --help` + + +`Usage: logcat [options] [filterspecs]` + + +`options include:` +--- + + + - `-s` `Set default filter to silent. + Like specifying filterspec '*:s'设置默认的过滤器,我们想要输出包含"System.out"关键字的标签或信息, 就可以使用adb logcat -s System.out 命令;` + + - `-f ` `Log to file. Default to stdout 将日志输出到文件,注意这是手机里的文件,使用adb logcat -f /sdcard/log.txt` + - `-r []` `Rotate log every kbytes. (16 if unspecified). Requires -f 按照每千字节输出日志,需要与-f一起使用` + - `-n ` `Sets max number of rotated logs to , default 4 设置日志输出的最大数目` +- `-v ` `Sets the log print format, where is one of: +brief process tag thread raw time threadtime long 设置日志的输出格式, 注意只能设置一项,这里格式比较多,我们最后讲` + +- `-c` `clear (flush) the entire log and exit 清空所有的日志缓存信息` +- `-d` `dump the log and then exit (don't block) 将缓存的日志输出到屏幕上, 并且不会阻塞;` +- `-t ` `print only the most recent lines (implies -d) 输出最近的几行日志, 输出完退出, 不阻塞;` + `-g` `get the size of the log's ring buffer and exit 查看日志缓冲区信息` + `-b ` `Request alternate ring buffer, 'main', 'system', 'radio' + or 'events'. Multiple -b parameters are allowed and the + results are interleaved. The default is -b main -b system.加载一个日志缓冲区, 默认是 main` + `-B` `output the log in binary 以二进制形式输出日志` + + +`filterspecs are a series of ` +--- + + `[:priority]` 过滤项格式为标签:日志等级, 默认的日志过滤项是 ` *:I `; +``` +where is a log component tag (or * for all) and priority is: + + - V Verbose + - D Debug + - I Info + - W Warn + - E Error + - F Fatal + - S Silent (supress all output) + +'*' means '*:d' and by itself means :v + +If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS. +If no filterspec is found, filter defaults to '*:I'默认使用的是*:I级别的。 + +If not specified with -v, format is set from ANDROID_PRINTF_LOG +or defaults to "brief" +``` +例如`adb logcat *:E` 是指定只显示`error`级别的`log`, `adb logcat xxx:D *:S` 是指定只显示标签为`xxx`且`debug`级别以上的`Log`。为什么要加上`*:S`呢,`*:S`用于设置所有标记的日志优先级为`S`,这样可以确保仅输出符合条件的日志。 + +上面基本的参数都介绍完了。下面说一些常用的功能。 + +采用grep正则表达式过滤 +--- + +过滤固定字符串 : 只要命令行出现的日志都可以过滤, 不管是不是标签; + +`adb logcat | grep -E '^[VDE]/(TAG1|TAG2)'` + +`adb logcat | grep Wifi` +`adb logcat | grep -i wifi 过滤字符串忽略大小写` +`adb logcat | grep --color=auto -i myapp #设置匹配字符串颜色。更多设置请查看 grep 帮助。` + +在同时输出到屏幕和文件tee +--- + +想要把日志保存到文件,如果采用IO重定向,就无法输出到屏幕, 针对这个问题可以采用tee命令 + +`adb logcat | grep -E '^[VDE]/(TAG1|TAG2)' | tee my.log` + +`adb logcat -v time | tee a.log` + + + +持续输出日志到文件中 +--- + +`>`输出 : `>` 后面跟着要输出的日志文件, 可以将`logcat`日志输出到文件中 + +`adb logcat > test.log` //将日志保存到文件test.log + +`-d -f ` 组合命令:可以将日志保存到手机上的指定位置,对不能一直用电脑连着手机收集日志的场景非常有用。 +`adb logcat -d -f /sdcard/mylog.txt` + + + +最后说一下上面的`-v`的格式问题: +`-v `设置日志输入格式控制输出字段,默认的是`brief`格式: + +- `brief` — 显示优先级/标记和原始进程的PID (默认格式) +- `process` — 仅显示进程PID +- `tag` — 仅显示优先级/标记 +- `thread` — 仅显示进程:线程和优先级/标记 +- `raw` — 显示原始的日志信息,没有其他的元数据字段 +- `time` — 显示日期,调用时间,优先级/标记,PID +- `long` —显示所有的元数据字段并且用空行分隔消息内容 +- `adb logcat -v thread` //使用 thread 输出格式 +***注意***`-v` 选项中只能指定一种格式。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From 6a39903b7b56772cb57e2b44e517964a5ea1cbea Mon Sep 17 00:00:00 2001 From: "xu.chuanren" Date: Tue, 13 Dec 2016 18:25:17 +0800 Subject: [PATCH 056/373] add RxJava part --- ...\350\257\246\350\247\243(\344\270\212).md" | 557 +++++++++ ...\350\257\246\350\247\243(\344\270\213).md" | 218 ++++ ...\350\257\246\350\247\243(\344\270\255).md" | 1009 +++++++++++++++++ .../RxJava\350\257\246\350\247\243.md" | 9 - 4 files changed, 1784 insertions(+), 9 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" create mode 100644 "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" create mode 100644 "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" delete mode 100644 "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" new file mode 100644 index 00000000..10516ed9 --- /dev/null +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" @@ -0,0 +1,557 @@ +RxJava详解(上) +=== + +年初的时候就想学习下`RxJava`然后写一些`RxJava`的教程,无奈发现已经年底了,然而我还么有写。今天有点时间,特别是发布了`RxJava 2.0`后,我决定动笔开始。 + +现在`RxJava`变的越来越流行了,很多项目中都使用了它。特别是大神`JakeWharton`等的加入,以及`RxBinding、Retrofit、RxLifecycle`等众多项目的,然开发越来越方便,但是上手比较难,不过一旦你入门后你就会发现真是太棒了。 + +`RxJava`简介 +--- + +在介绍`RxJava`之前先说一下`Rx`。`Rx`的全称是`Reactive Extensions`,直译过来就是响应式扩展。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rx_logo.png?raw=true) + +`Rx`基于观察者模式,它是一种编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流。`ReactiveX.io`给的定义是,`Rx`是一个使用可观察数据流进行异步编程的编程接口,`ReactiveX`结合了观察者模式、迭代器模式和函数式编程的精华。`Rx`已经渗透到了各个语言中,有了`Rx`所以才有了`RxJava`、`Rx.NET`、`RxJS`、`RxSwift`、`Rx.rb`、`RxPHP`等等, + +这里先列举一下相关的官网: + +- [Rx](http://reactivex.io/) +- [RxJava](https://github.com/ReactiveX/RxJava) +- [RxAndroid](https://github.com/ReactiveX/RxAndroid) + +`RxJava`在`GitHub`上的介绍是:`a library for composing asynchronous and event-based programs by using observable sequences for the Java VM.` +翻译过来也就是一个基于事件和程序在`Java VM`上使用可观测的序列来组成异步的库。`RxJava`的本质就是一个实现异步操作的库,它的优势就是简洁,随着程序逻辑变得越来越复杂,它依然能够保持简洁。 + +其实一句话总结一下`RxJava`的作用就是:异步 + +这里可能会有人想不就是个异步吗,至于辣么矫情么?用`AsyncTask`、`Handler`甚至自定义一个`BigAsyncTask`分分钟搞定。 + +但是`RxJava`的好处是简洁。异步操作很关键的一点是程序的简洁性,因为在调度过程比较复杂的情况下,异步代码经常会既难写也难被读懂。 `Android`创造的`AsyncTask`和`Handler`其实都是为了让异步代码更加简洁。虽然`RxJava`的优势也是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁。 + + +####扩展的观察者模式 + + +`RxJava`的异步实现,是通过一种扩展的观察者模式来实现的。 + +观察者模式面向的需求是:`A`对象(观察者)对`B`对象(被观察者)的某种变化高度敏感,需要在`B`变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如`A`不需要每过`2ms`就检查一次`B`的状态),而是采用注册(`Register`)或者称为订阅(`Subscribe`)的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 `Android`开发中一个比较典型的例子是点击监听器`OnClickListener`。对设置`OnClickListener`来说,`View`是被观察者,`OnClickListener`是观察者,二者通过 `setOnClickListener()`方法达成订阅关系。订阅之后用户点击按钮的瞬间,`Android Framework`就会将点击事件发送给已经注册的`OnClickListener`。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。 + +`OnClickListener`的模式大致如下图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/btn_onclick.jpg?raw=true) + +如图所示,通过`setOnClickListener()`方法,`Button`持有`OnClickListener`的引用(这一过程没有在图上画出);当用户点击时,`Button`自动调用`OnClickListener`的`onClick()` 方法。另外,如果把这张图中的概念抽象出来(`Button` -> 被观察者、`OnClickListener` -> 观察者、`setOnClickListener()` -> 订阅,`onClick()` -> 事件),就由专用的观察者模式(例如只用于监听控件点击)转变成了通用的观察者模式。如下图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/btn_rxjava_observable.jpg?raw=true) + +而`RxJava`作为一个工具库,使用的就是通用形式的观察者模式。 + + +####`RxJava`的观察者模式 + + +`RxJava`的基本概念: + +- `Observable`(可观察者,即被观察者)、 +- `Observer`(观察者)、 +- `subscribe()`(订阅)、事件。 + `Observable`和`Observer`通过`subscribe()` 方法实现订阅关系,从而`Observable`可以在需要的时候发出事件来通知`Observer`。 + +与传统观察者模式不同,`RxJava`的事件回调方法除了普通事件`onNext()`(相当于`onClick()`/`onEvent()`)之外,还定义了两个特殊的事件:`onCompleted()`和`onError()`: +但是`RxJava`与传统的观察者设计模式有一点明显不同,那就是如果一个`Observerble`没有任何的的`Subscriber`,那么这个`Observable`是不会发出任何事件的。 + + +- `onCompleted()`: 事件队列完结。 + `RxJava`不仅把每个事件单独处理,还会把它们看做一个队列。`RxJava`规定,当不会再有新的`onNext()`发出时,需要触发`onCompleted()` 方法作为标志。 +- `onError()`: 事件队列异常。 + 在事件处理过程中出异常时,`onError()`会被触发,同时队列自动终止,不允许再有事件发出。 +- 在一个正确运行的事件序列中, `onCompleted()`和`onError()`有且只有一个,并且是事件序列中的最后一个。需要注意的是`onCompleted()`和`onError()` 二者也是互斥的,即在队列中调用了其中一个,就不应该再调用另一个。 + + +`RxJava`的观察者模式大致如下图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observer_1.jpg?raw=true) + + +####基本实现 + + +基于上面的概念, `RxJava`的基本实现主要有三点: + +- 创建`Observable` + `Observable`即被观察者,它决定什么时候触发事件以及触发怎样的事件。 `RxJava`使用`Observable.create()`方法来创建一个`Observable`,并为它定义事件触发规则。 + +- 创建`Observer`即观察者,它决定事件触发的时候将有怎样的行为。 + `RxJava`中的`Observer`接口的实现方式: + ```java + Observer observer = new Observer() { + @Override + public void onNext(String s) { + Log.d(tag, "Item: " + s); + } + + @Override + public void onCompleted() { + Log.d(tag, "Completed!"); + } + + @Override + public void onError(Throwable e) { + Log.d(tag, "Error!"); + } + }; + ``` + 除了`Observer`接口之外,`RxJava`还内置了一个实现了`Observer`的抽象类:`Subscriber`。`Subscriber`对`Observer`接口进行了一些扩展,但他们的基本使用方式是完全一样的。 + + ```java + Subscriber subscriber = new Subscriber() { + @Override + public void onNext(String s) { + Log.d(tag, "Item: " + s); + } + + @Override + public void onCompleted() { + Log.d(tag, "Completed!"); + } + + @Override + public void onError(Throwable e) { + Log.d(tag, "Error!"); + } + }; + ``` + 不仅基本使用方式一样,实质上,在`RxJava`的`subscribe()`过程中,`Observer`也总是会先被转换成一个`Subscriber`再使用。所以如果你只想使用基本功能,选择`Observer`和 `Subscriber`是完全一样的。它们的区别对于使用者来说主要有两点: + + - `onStart()`: 这是`Subscriber`增加的方法。它会在`subscribe()`刚开始而事件还未发送之前被调用,可以用于做一些准备工作,例如数据的清零或重置。这是一个可选方法,默认情况下它的实现为空。需要注意的是,如果对准备工作的线程有要求(例如弹出一个显示进度的对话框,这必须在主线程执行), `onStart()`就不适用了,因为它总是在`subscribe()` 所发生的线程被调用,而不能指定线程。要在指定的线程来做准备工作,可以使用`doOnSubscribe()`方法,具体可以在后面的文中看到。 + - `unsubscribe`(): 这是`Subscriber`所实现的另一个接口`Subscription`的方法,用于取消订阅。在这个方法被调用后,`Subscriber` 将不再接收事件。一般在这个方法调用前,可以使用`isUnsubscribed()`先判断一下状态。 `unsubscribe()`这个方法很重要,因为在`subscribe()`之后,`Observable`会持有 `Subscriber`的引用,这个引用如果不能及时被释放,将有内存泄露的风险。所以最好保持一个原则:要在不再使用的时候尽快在合适的地方(例如`onPause()、onStop()`等方法中)调用`unsubscribe()`来解除引用关系,以避免内存泄露的发生。 + 所以后续讲解时我们有时会用`Subscriber`来代替`Observer`。 + +- 调用`subscribe()`方法(订阅) + + 创建了一个`Observable`和`Observer`之后,再用`subscribe()`方法将它们联结起来,整条链子就可以工作了。代码形式很简单: + + ```java + observable.subscribe(observer); + // 或者: + observable.subscribe(subscriber); + ``` + + > 有人可能会注意到,`subscribe()`这个方法有点怪:它看起来是`observalbe`订阅了`observer/subscriber`而不是`observer/subscriber`订阅了`observalbe`,这看起来就像『杂志订阅了读者』一样颠倒了对象关系。这让人读起来有点别扭,不过如果把`API`设计成`observer.subscribe(observable)/subscriber.subscribe(observable)` ,虽然更加符合思维逻辑,但对流式`API`的设计就造成影响了,比较起来明显是得不偿失的。 + + +`RxJava`入门示例 +--- + +一个`Observable`可以发出零个或者多个事件,知道结束或者出错。每发出一个事件,就会调用它的`Subscriber`的`onNext`方法,最后调用`Subscriber.onComplete()`或者`Subscriber.onError()`结束。 + +####`Hello World` + + +``` +compile 'io.reactivex:rxandroid:1.2.1' +// Because RxAndroid releases are few and far between, it is recommended you also +// explicitly depend on RxJava's latest version for bug fixes and new features. +compile 'io.reactivex:rxjava:1.2.3' +``` + +```java +// 创建被观察者、数据源 +Observable observable = Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // 可以看到,这里传入了一个 OnSubscribe 对象作为参数。OnSubscribe 会被存储在返回的 Observable 对象中,它的作用相当于一个计划表,当 Observable + //被订阅的时候,OnSubscribe 的 call() 方法会自动被调用,事件序列就会依照设定依次触发(对于上面的代码,就是观察者Subscriber 将会被调用三次 onNext() 和一次 + // onCompleted())。这样,由被观察者调用了观察者的回调方法,就实现了由被观察者向观察者的事件传递,即观察者模式。 + Log.i("@@@", "call"); + subscriber.onNext("Hello "); + subscriber.onNext("World !"); + subscriber.onCompleted(); + } +}); +// 创建观察者 +Subscriber subscriber = new Subscriber() { + @Override + public void onCompleted() { + Log.i("@@@", "onCompleted"); + } + + @Override + public void onError(Throwable e) { + Log.i("@@@", "onError"); + } + + @Override + public void onNext(String s) { + Log.i("@@@", "onNext : " + s); + } +}; +// 关联或者叫订阅更合适。 +observable.subscribe(subscriber); +``` + +一旦`subscriber`订阅了`observable`,`observable`就会调用`subscriber`对象的`onNext`和`onComplete`方法,`subscriber`就会打印出`Hello World`. + + `Observable.subscribe(Subscriber)`的内部实现是这样的(仅核心代码): + +```java +// 注意:这不是`subscribe()`的源码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的核心代码。 +public Subscription subscribe(Subscriber subscriber) { + subscriber.onStart(); + onSubscribe.call(subscriber); + return subscriber; +} +``` + +可以看到`subscriber()`做了3件事: + +- 调用`Subscriber.onStart()`。这个方法在前面已经介绍过,是一个可选的准备方法。 +- 调用`Observable`中的`onSubscribe.call(Subscriber)`。在这里,事件发送的逻辑开始运行。从这也可以看出,在`RxJava`中,`Observable` 并不是在创建的时候就立即开始发送事件,而是在它被订阅的时候,即当`subscribe()`方法执行的时候。 +- 将传入的`Subscriber`作为`Subscription`返回。这是为了方便`unsubscribe()`. + +整个过程中对象间的关系如下图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observable_list.gif?raw=true) + + +讲到这里很多人肯定会骂傻`X`,这TM简洁你妹啊...,这里只是个入门`Hello World`,真正的简洁等你看完全部介绍后就明白了。 + +`RxJava`内置了很多简化创建`Observable`对象的函数,比如`Observable.just()`就是用来创建只发出一个事件就结束的`Observable`对象,上面创建`Observable`对象的代码可以简化为一行 + +```java +Observable observable = Observable.just("Hello ", "World !"); +``` + +接下来看看如何简化`Subscriber`,上面的例子中,我们其实并不关心`onComplete()`和`onError`,我们只需要在`onNext`的时候做一些处理,这时候就可以使用`Action1`类。 + +```java +Action1 action1 = new Action1() { + @Override + public void call(String s) { + Log.i("@@@", "call : " + s); + } +}; +``` + +`Observable.subscribe()`方法有一个重载版本,接受三个`Action1`类型的参数 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_subscribe_params.png?raw=true) + +所以上面的代码最终可以写成这样: + +```java +Observable.just("Hello ", "World !").subscribe(new Action1() { + @Override + public void call(String s) { + Log.i("@@@", "call : " + s); + } +}); +``` + +这里顺便多提一些`subscribe()`的多个`Action`参数: +```java +Action1 onNextAction = new Action1() { + // onNext() + @Override + public void call(String s) { + Log.d(tag, s); + } +}; +Action1 onErrorAction = new Action1() { + // onError() + @Override + public void call(Throwable throwable) { + // Error handling + } +}; +Action0 onCompletedAction = new Action0() { + // onCompleted() + @Override + public void call() { + Log.d(tag, "completed"); + } +}; + +observable.subscribe(onNextAction, onErrorAction, onCompletedAction); +``` + +简单解释一下这段代码中出现的`Action1`和`Action0`。`Action0`是`RxJava`的一个接口,它只有一个方法`call()`,这个方法是无参无返回值的;由于`onCompleted()` 方法也是无参无返回值的,因此`Action0`可以被当成一个包装对象,将`onCompleted()`的内容打包起来将自己作为一个参数传入`subscribe()`以实现不完整定义的回调。这样其实也可以看做将 `onCompleted()`方法作为参数传进了`subscribe()`,相当于其他某些语言中的『闭包』。`Action1`也是一个接口,它同样只有一个方法`call(T param)`,这个方法也无返回值,但有一个参数;与`Action0`同理,由于`onNext(T obj)`和`onError(Throwable error)`也是单参数无返回值的,因此`Action1`可以将`onNext(obj)`和`onError(error)`打包起来传入`subscribe()`以实现不完整定义的回调。事实上,虽然`Action0`和`Action1`在`API`中使用最广泛,但`RxJava`是提供了多个`ActionX`形式的接口(例如`Action2`, `Action3`)的,它们可以被用以包装不同的无返回值的方法。 + + +假设我们的`Observable`是第三方提供的,它提供了大量的用户数据给我们,而我们需要从用户数据中筛选部分有用的信息,那我们该怎么办呢? +从`Observable`中去修改肯定是不现实的?那从`Subscriber`中进行修改呢? 这样好像是可以完成的。但是这种方式并不好,因为我们希望`Subscriber`越轻量越好,因为很有可能我们需要 +在主线程中去执行`Subscriber`。另外,根据响应式函数编程的概念,`Subscribers`更应该做的事情是`响应`,响应`Observable`发出的事件,而不是去修改。 +那该怎么办呢? 这就要用到下面的部分要讲的操作符。 + +操作符(`Operators`) +--- + +`RxJava`提供了对事件序列进行变换的支持,这是它的核心功能之一.所谓变换,就是将事件序列中的对象或整个序列进行加工处理,转换成不同的事件或事件序列。 +操作符就是为了解决对`Observable`对象的变换的问题,操作符用于在`Observable`和最终的`Subscriber`之间修改`Observable`发出的事件。`RxJava`提供了很多很有用的操作符。 +比如`map`操作符,就是用来把把一个事件转换为另一个事件的。 + +####`map` + + +`Returns an Observable that applies a specified function to each item emitted by the source Observable and emits the results of these function applications.` + +```java +Observable just = Observable.just("Hello ", "World !"); +Observable map = just.map(new Func1() { + @Override + public String call(String s) { + return s + "@@@"; + } +}); +map.subscribe(new Action1() { + @Override + public void call(String s) { + Log.i("@@@", s); + } +}); +``` +上面的代码打印出的结果是: +``` +12-12 15:51:22.184 472-472/com.charon.rxjavastudydemo I/@@@: Hello @@@ +12-12 15:51:22.184 472-472/com.charon.rxjavastudydemo I/@@@: World !@@@ +``` + +`map()`操作符就是用于变换`Observable`对象的,`map`操作符返回一个`Observable`对象,这样就可以实现链式调用,在一个`Observable`对象上多次使用map操作符,最终将最简洁的数据传递给`Subscriber`对象。 + +`map`操作符更有趣的一点是它不必返回Observable对象返回的类型,你可以使用`map`操作符返回一个发出新的数据类型的`Observable`对象。 +比如上面的例子中,`Subscriber`并不关心返回的字符串,而是想要字符串的`hash`值。 + + +```java +Observable just = Observable.just("Hello ", "World !"); +Observable map = just.map(new Func1() { + @Override + public Integer call(String s) { + return s.hashCode(); + } +}); +map.subscribe(new Action1() { + @Override + public void call(Integer integer) { + Log.i("@@@", "" + integer); + } +}); +``` + +上面部分的打印结果是: +``` +12-12 15:54:35.515 8521-8521/com.charon.rxjavastudydemo I/@@@: -2137068114 +12-12 15:54:35.516 8521-8521/com.charon.rxjavastudydemo I/@@@: -1105126669 +``` + +`map()`的示意图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_map.jpg?raw=true) + +通过上面的部分我们可以得知: + +- `Observable`和`Subscriber`可以做任何事情 + `Observable`可以是一个数据库查询,`Subscriber`用来显示查询结果;`Observable`可以是屏幕上的点击事件,`Subscriber`用来响应点击事件;`Observable`可以是一个网络请求,`Subscriber`用来显示请求结果。 + +- `Observable`和`Subscriber`是独立于中间的变换过程的。 + 在`Observable`和`Subscriber`中间 可以增减任何数量的`map`。整个系统是高度可组合的,操作数据是一个很简单的过程。 + + +####`flatmap` + + +`Returns an Observable that emits items based on applying a function that you supply to each item emitted + by the source Observable, where that function returns an Observable, and then merging those resulting + Observables and emitting the results of this merger.` + +`flatMap()`是一个很有用但非常难理解的变换,首先假设这么一种需求:假设有一个数据结构『学生』,现在需要打印出一组学生的名字。实现方式很简单: + +```java +Student[] students = ...; +Subscriber subscriber = new Subscriber() { + @Override + public void onNext(String name) { + Log.d(tag, name); + } + ... +}; +Observable.from(students) + .map(new Func1() { + @Override + public String call(Student student) { + return student.getName(); + } + }) + .subscribe(subscriber); +``` + +很简单。那么再假设:如果要打印出每个学生所需要修的所有课程的名称呢?(需求的区别在于,每个学生只有一个名字,但却有多个课程)首先可以这样实现: + +```java +Student[] students = ...; +Subscriber subscriber = new Subscriber() { + @Override + public void onNext(Student student) { + List courses = student.getCourses(); + for (int i = 0; i < courses.size(); i++) { + Course course = courses.get(i); + Log.d(tag, course.getName()); + } + } + ... +}; +Observable.from(students) + .subscribe(subscriber); +``` + +依然很简单。那么如果我不想在`Subscriber`中使用`for`循环,而是希望`Subscriber`中直接传入单个的`Course`对象呢(这对于代码复用很重要),用`map()`显然是不行的,因为`map()` 是一对一的转化,而我现在的要求是一对多的转化。那怎么才能把一个`Student`转化成多个`Course`呢? + +这个时候,就需要用`flatMap()`了: +```java +Student[] students = ...; +Subscriber subscriber = new Subscriber() { + @Override + public void onNext(Course course) { + Log.d(tag, course.getName()); + } + ... +}; +Observable.from(students) + .flatMap(new Func1>() { + @Override + public Observable call(Student student) { + return Observable.from(student.getCourses()); + } + }) + .subscribe(subscriber); +``` + +`map`与`flatmap`在功能上是一致的,它也是把传入的参数转化之后返回另一个对象。区别在于`flatmap`是通过中间`Observable`来进行,而`map`是直接执行.`flatMap()`中返回的是个 `Observable`对象,并且这个`Observable`对象并不是被直接发送到了`Subscriber`的回调方法中。 + +`flatMap()`的原理是这样的: + +- 使用传入的事件对象创建一个`Observable`对象 +- 并不发送这个`Observable`而是将它激活,于是它开始发送事件 +- 每一个创建出来的`Observable`发送的事件,都被汇入同一个`Observable`,而这个`Observable`负责将这些事件统一交给`Subscriber` 的回调方法。 + +这三个步骤,把事件拆成了两级,通过一组新创建的`Observable`将初始的对象『铺平』之后通过统一路径分发了下去。而这个『铺平』就是`flatMap()`所谓的`flat`。 + +`flatMap()`就是根据你的规则,将`Observable`转换之后再发射出去,注意最后的顺序很可能是错乱的,如果要保证顺序的一致性,要使用`concatMap()` +由于可以在嵌套的`Observable`中添加异步代码`flatMap()`也常用于嵌套的异步操作,例如嵌套的网络请求`(Retrofit + RxJava)` + +`flatMap()`示意图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_flatmap.jpg?raw=true) + + +####`throttleFirst()` + + +在每次事件触发后的一定时间间隔内丢弃新的事件。常用作去抖动过滤。 +例如按钮的点击监听器: +```java +RxView.clickEvents(button); // RxBinding 代码` + .throttleFirst(500, TimeUnit.MILLISECONDS) // 设置防抖间隔为 500ms + .subscribe(subscriber); // 妈妈再也不怕我的用户手抖点开两个重复的界面啦。 +``` + +####`from` + + +`convert various other objects and data types into Observables` + +`from()`接收一个集合作为输入,然后每次输出一个元素给`subscriber`. + +```java +List s = Arrays.asList("Java", "Android", "Ruby", "Ios", "Swift"); +Observable.from(s).subscribe(new Action1() { + @Override + public void call(String s) { + Log.i("@@@", s); + } +}); +``` + +####`filter` + + +返回满足过滤条件的数据。 + +```java +Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9}) + .filter(new Func1() { + @Override + public Boolean call(Integer integer) { + return integer < 5; + } + }) + .subscribe(new Action1() { + @Override + public void call(Integer integer) { + Log.i("@@@", "integer=" + integer); //1,2,3,4 + } + }); +``` + +####`timer` + + +`Timer`会在指定时间后发射一个数字0,该操作符运行在`Computation Scheduler`。 + +```java +Observable.timer(3, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1() { + @Override + public void call(Long aLong) { + Log.i("@@@", "aLong=" + aLong); // 延时3s + } + }); +``` + +####`interval` + + +创建一个按固定时间间隔发射整数序列的`Observable`. +`interval`默认在`computation`调度器上执行。 + +```java +Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) + .subscribe(new Action1() { + @Override + public void call(Long aLong) { + Log.i("@@@", "aLong=" + aLong); //从0递增,间隔1s 0,1,2,3.... + } + }); +``` + +####`Repeat` + + +重复执行 + +等等...就不继续介绍了,到时候查下文档就好了。 + +是不是感觉没什么乱用,那就继续看下一部分吧。 + + +参考: + +- [RxJava Wiki](https://github.com/ReactiveX/RxJava/wiki) + +- [Grokking RxJava, Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) + +- [NotRxJava](https://yarikx.github.io/NotRxJava/) + +- [When Not to Use RxJava](http://tomstechnicalblog.blogspot.hk/2016/07/when-not-to-use-rxjava.html) + +- [给 Android 开发者的 RxJava 详解](http://gank.io/post/560e15be2dca930e00da1083) + +- [Google Agera 从入门到放弃](http://blog.chengyunfeng.com/?p=984) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" new file mode 100644 index 00000000..68697fa8 --- /dev/null +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" @@ -0,0 +1,218 @@ +RxJava详解(下) +=== + +变换的原理`lift()` +--- + +这些变换虽然功能各有不同,但实质上都是针对事件序列的处理和再发送。而在`RxJava`的内部,它们是基于同一个基础的变换方法:`lift()`。 + +首先看一下`lift()` 的内部实现(仅核心代码): + +```java +// 注意:这不是 lift() 的源码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的核心代码。 +public Observable lift(Operator operator) { + return Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + Subscriber newSubscriber = operator.call(subscriber); + newSubscriber.onStart(); + onSubscribe.call(newSubscriber); + } + }); +} +``` + +这段代码很有意思:它生成了一个新的`Observable`并返回,而且创建新`Observable`所用的参数`OnSubscribe的回调方法`call()`中的实现竟然看起来和前面讲过的`Observable.subscribe()`一样!然而它们并不一样哟~不一样的地方关键就在于第二行`onSubscribe.call(subscriber)`中的`onSubscribe` 所指代的对象不同(高能预警:接下来的几句话可能会导致身体的严重不适) + +- `subscribe()`中这句话的`onSubscribe`指的是`Observable`中的`onSubscribe`对象,这个没有问题,但是`lift()`之后的情况就复杂了点。 +- 当含有`lift()`时: + - `lift()`创建了一个`Observable`后,加上之前的原始`Observable`,已经有两个`Observable`了; + - 而同样地,新`Observable`里的新`OnSubscribe`加上之前的原始`Observable`中的原始`OnSubscribe`,也就有了两个`OnSubscribe`; + - 当用户调用经过`lift()`后的`Observable`的`subscribe()`的时候,使用的是`lift()`所返回的新的`Observable`,于是它所触发的`onSubscribe.call(subscriber)`,也是用的新`Observable`中的新`OnSubscribe`,即在`lift()`中生成的那个`OnSubscribe`; + - 而这个新`OnSubscribe`的`call()`方法中的`onSubscribe`,就是指的原始`Observable`中的原始`OnSubscribe`,在这个`call()`方法里,新`OnSubscribe`利用`operator.call(subscriber)`生成了一个新的`Subscriber`(`Operator`就是在这里,通过自己的`call()`方法将新`Subscriber`和原始`Subscriber` 进行关联,并插入自己的『变换』代码以实现变换),然后利用这个新`Subscriber`向原始`Observable`进行订阅。 + 这样就实现了`lift()`过程,有点像一种代理机制,通过事件拦截和处理实现事件序列的变换。 + +精简掉细节的话,也可以这么说:在`Observable`执行了`lift(Operator)`方法之后,会返回一个新的`Observable`,这个新的`Observable`会像一个代理一样,负责接收原始的`Observable` 发出的事件,并在处理后发送给`Subscriber`。 + +如果你更喜欢具象思维,可以看图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_lift.gif?raw=true) + +举一个具体的`Operator`的实现。下面这是一个将事件中的`Integer`对象转换成`String`的例子,仅供参考: + +```java +observable.lift(new Observable.Operator() { + @Override + public Subscriber call(final Subscriber subscriber) { + // 将事件序列中的 Integer 对象转换为 String 对象 + return new Subscriber() { + @Override + public void onNext(Integer integer) { + subscriber.onNext("" + integer); + } + + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + }; + } +}); +``` +> 讲述`lift()`的原理只是为了让你更好地了解`RxJava` ,从而可以更好地使用它。然而不管你是否理解了`lift()`的原理,`RxJava`都不建议开发者自定义`Operator`来直接使用`lift()`,而是建议尽量使用已有的`lift()`包装方法(如`map()、flatMap()`等)进行组合来实现需求,因为直接使用`lift()`非常容易发生一些难以发现的错误。 + + +线程控制`Scheduler` +--- + +在不指定线程的情况下,`RxJava`遵循的是线程不变的原则,即在哪个线程调用`subscribe()`方法就在哪个线程生产事件;在哪个线程生产事件,就在哪个线程消费事件。也就是说事件的发出和消费都是在同一个线程的。观察者模式本身的目的就是『后台处理,前台回调』的异步机制,因此异步对于`RxJava`是至关重要的。而要实现异步,则需要用到`RxJava`的另一个概念:`Scheduler`。 + + +####`Scheduler`简介 + + +在`RxJava`中,`Scheduler`相当于线程控制器,`RxJava`通过它来指定每一段代码应该运行在什么样的线程。`RxJava`已经内置了几个`Scheduler`,它们已经适合大多数的使用场景: + +- `Schedulers.immediate()`: 直接在当前线程运行,相当于不指定线程。这是默认的`Scheduler`。 +- `Schedulers.newThread()`: 总是启用新线程,并在新线程执行操作。 +- `Schedulers.io()`: I/O 操作(读写文件、读写数据库、网络信息交互等)所使用的`Scheduler`。行为模式和`newThread()`差不多,区别在于`io()` 的内部实现是是用一个无数量上限的线程池,可以重用空闲的线程,因此多数情况下`io()`比`newThread()`更有效率。不要把计算工作放在`io()`中,可以避免创建不必要的线程。 +- `Schedulers.computation()`: 计算所使用的`Scheduler`。这个计算指的是`CPU`密集型计算,即不会被`I/O`等操作限制性能的操作,例如图形的计算。这个`Scheduler` 使用的固定的线程池,大小为`CPU`核数。不要把`I/O`操作放在`computation()`中,否则`I/O`操作的等待时间会浪费`CPU`。 +- 另外,`Android`还有一个专用的`AndroidSchedulers.mainThread()`,它指定的操作将在`Android`主线程运行。 + + +有了这几个`Scheduler`,就可以使用`subscribeOn()`和`observeOn()`两个方法来对线程进行控制了。`subscribeOn()`指定`subscribe()`所发生的线程,即`Observable.OnSubscribe()`被激活时所处的线程或者叫做事件产生的线程。`observeOn()`指定`Subscriber`所运行在的线程或者叫做事件消费的线程。 + +```java +Observable.just("Hello ", "World !") + .subscribeOn(Schedulers.io()) // 指定 subscribe() 发生在 IO 线程,可以理解成数据的获取是在io线程 + .observeOn(AndroidSchedulers.mainThread())// 指定 Subscriber 的回调发生在主线程,可以理解成数据的消费时在主线程 + .subscribe(new Action1() { + @Override + public void call(String s) { + Log.i("@@@", s); + } + }); +``` + +上面这段代码中,`subscribeOn(Schedulers.io())`的指定会让创建的事件的内容`Hello `、`World !`将会在`IO`线程发出;而由于`observeOn(AndroidScheculers.mainThread())` 的指定,因此`subscriber()`方法设置后的回调中内容的打印将发生在主线程中。事实上,这种在`subscribe()`之前写上两句`subscribeOn(Scheduler.io())`和`observeOn(AndroidSchedulers.mainThread())`的使用方式非常常见,它适用于多数的***后台线程取数据,主线程显示***的程序策略。 + +####`Scheduler`的原理 + +`RxJava`的`Scheduler API`很方便,也很神奇(加了一句话就把线程切换了,怎么做到的?而且 subscribe() 不是最外层直接调用的方法吗,它竟然也能被指定线程?)。然而 Scheduler 的原理需要放在后面讲,因为它的原理是以下一节《变换》的原理作为基础的。 + +好吧这一节其实我屁也没说,只是为了让你安心,让你知道我不是忘了讲原理,而是把它放在了更合适的地方。 + + +能不能多切换几次线程?答案是:能。因为`observeOn()`指定的是`Subscriber`的线程,而这个`Subscriber`并不是(严格说应该为『不一定是』,但这里不妨理解为『不是』)`subscribe()` 参数中的`Subscriber`,而是`observeOn()`执行时的当前`Observable`所对应的`Subscriber`,即它的直接下级`Subscriber`。换句话说`observeOn()` 指定的是它之后的操作所在的线程。因此如果有多次切换线程的需求,只要在每个想要切换线程的位置调用一次`observeOn()`即可。 + +上代码: + +```java +Observable.just(1, 2, 3, 4) // IO 线程,由 subscribeOn() 指定 + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.newThread()) + .map(mapOperator) // 新线程,由 observeOn() 指定 + .observeOn(Schedulers.io()) + .map(mapOperator2) // IO 线程,由 observeOn() 指定 + .observeOn(AndroidSchedulers.mainThread) + .subscribe(subscriber); // Android 主线程,由 observeOn() 指定 +``` +如上,通过`observeOn()`的多次调用,程序实现了线程的多次切换。 +不过,不同于`observeOn()`,`subscribeOn()`的位置放在哪里都可以,但它是只能调用一次的。 + +其实,`subscribeOn()`和`observeOn()`的内部实现,也是用的`lift()`。 + +具体看图(不同颜色的箭头表示不同的线程,`subscribeOn()`原理图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_subscribeon.jpg?raw=true) + +`observeOn()`原理图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observeon.jpg?raw=true) + +从图中可以看出,`subscribeOn()`和`observeOn()`都做了线程切换的工作(图中的`schedule...`部位)。不同的是,`subscribeOn()`的线程切换发生在`OnSubscribe`中,即在它通知上一级 `OnSubscribe`时,这时事件还没有开始发送,因此`subscribeOn()`的线程控制可以从事件发出的开端就造成影响;而`observeOn()`的线程切换则发生在它内建的`Subscriber`中,即发生在它即将给下一级`Subscriber`发送事件时,因此`observeOn()`控制的是它后面的线程。 + +最后,我用一张图来解释当多个`subscribeOn()`和`observeOn()`混合使用时,线程调度是怎么发生的(由于图中对象较多,相对于上面的图对结构做了一些简化调整): + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observable_list.jpg?raw=true) + +图中共有5处含有对事件的操作。由图中可以看出,①和②两处受第一个`subscribeOn()`影响,运行在红色线程;③和④处受第一个`observeOn()`的影响,运行在绿色线程;⑤处受第二个 `onserveOn()`影响,运行在紫色线程;而第二个`subscribeOn()`,由于在通知过程中线程就被第一个`subscribeOn()` 截断,因此对整个流程并没有任何影响。这里也就回答了前面的问题:当使用了多个`subscribeOn()`的时候,只有第一个`subscribeOn()`起作用。 + +在前面讲`Subscriber`的时候,提到过`Subscriber`的`onStart()`可以用作流程开始前的初始化。然而`onStart()`由于在`subscribe()`发生时就被调用了,因此不能指定线程,而是只能执行在`subscribe()`被调用时的线程。这就导致如果`onStart()`中含有对线程有要求的代码(例如在界面上显示一个`ProgressBar`,这必须在主线程执行),将会有线程非法的风险,因为有时你无法预测`subscribe()`将会在什么线程执行。 + +而与`Subscriber.onStart()`相对应的,有一个方法`Observable.doOnSubscribe()`。它和`Subscriber.onStart()`同样是在`subscribe()`调用后而且在事件发送前执行,但区别在于它可以指定线程。默认情况下,`doOnSubscribe()`执行在`subscribe()`发生的线程;而如果在`doOnSubscribe()`之后有`subscribeOn()`的话,它将执行在离它最近的`subscribeOn()`所指定的线程。 + +示例代码: + +```java +Observable.create(onSubscribe) + .subscribeOn(Schedulers.io()) + .doOnSubscribe(new Action0() { + @Override + public void call() { + progressBar.setVisibility(View.VISIBLE); // 需要在主线程执行 + } + }) + .subscribeOn(AndroidSchedulers.mainThread()) // 指定主线程 + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(subscriber); +``` + +如上,在`doOnSubscribe()`的后面跟一个`subscribeOn()`,就能指定准备工作的线程了。 + +####总结 + + +`RxJava`辣么好,难道他就没有缺点吗?当然有那就是使用越来越多的订阅,内存开销也会变得很大,稍不留神就会出现内存溢出的情况。我们可以用`RxJava`实现基本任何功能,但是你并不能这么做,你要明白什么时候需要用它,而什么时候没必要用它,不要一味的把功能都有`RxJava`来实现。 + +至于上面提到的`2.0`版本,有关它的区别请见: +[What's different in 2.0](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0) + + +Agera +--- + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/agera.png?raw=true) + + +之前`Google`发布`agera`,它在`Github`上的介绍是:`Reactive Programming for Android` + +[agera](https://github.com/google/agera) + +既然是响应式编程框架,其核心思想肯定和`RxJava`也是类似的。 +`Agera`中核心也是关于事件流(数据流)的处理,可以转换数据、可以指定数据操作函数在那个线程执行。 +而`Agera`和`RxJava`不一样的地方在于,`Agera`提供了一个新的事件响应和数据请求的模型,被称之为`Push event, pull data`。也就是一个事件发生了,会通过回调来主动告诉你,你关心的事件发生了。然后你需要主动的去获取数据,根据获取到的数据做一些操作。而`RxJava`在事件发生的时候,已经带有数据了。为了支持 `Push event pull data`模型,`Agera`中有个新的概念 — `Repository`。`Repository`翻译过来也就是数据仓库,是用来提供数据的,当你收到事件的时候,通过`Repository`来获取需要的数据。 这样做的好处是,把事件分发和数据处理的逻辑给分开了,事件分发做的事情比较少,事件分发就比较高效,当你收到事件后,根据当前的状态如果发现这个时候,数据已经无效了,则你根本不用请求数据,这样数据也就不用去处理和转换了。这样可以避免无用的数据计算,而有用的数据计算也可以在你需要的时候才去计算。 +同样,在多线程环境下,使用`push event pull data`模型,可能当你收到事件通知的时候,你只能获取到最新的数据,无法去获取历史数据了。设计就是这样考虑的,因为在`Android`开发中大部分的数据都是用来更新`UI`的,这个时候你根本不需要旧的数据了,只要最新的就够了。 + +`Agera`相对比较简单,并且是一个专业为`Android`平台打造的响应式编程框架。但是如果你熟悉了`RxJava`你会发现其用起来会有点不舒服,特别是还要主动的获取数据略感不爽(虽然提高了事件分发的效率)。 + +总之,现在看来`RxJava`支持的比较全面,但是会略显笨重,而`Agera`是一个专门为`Android`平台设计的响应式编程框架,比较轻巧,当然支持的并不是很全面。可以根据自己的喜好来选择. + +但是从个人观点来看`Agera`比起来`RxJava`还是相差几个版本。当然这是我的片面之词,有关更多信息请看[Question: what's the relation to RxJava?](https://github.com/google/agera/issues/20) + + +参考: + +- [RxJava Wiki](https://github.com/ReactiveX/RxJava/wiki) + +- [Grokking RxJava, Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) + +- [NotRxJava](https://yarikx.github.io/NotRxJava/) + +- [When Not to Use RxJava](http://tomstechnicalblog.blogspot.hk/2016/07/when-not-to-use-rxjava.html) + +- [给 Android 开发者的 RxJava 详解](http://gank.io/post/560e15be2dca930e00da1083) + +- [Google Agera 从入门到放弃](http://blog.chengyunfeng.com/?p=984) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" new file mode 100644 index 00000000..597436f3 --- /dev/null +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" @@ -0,0 +1,1009 @@ +RxJava详解(中) +=== + + +说好的简洁呢? +--- + +上面这一部分,又是介绍、又是`Hello World`、又是数据变换,但是你会发现然而并没有什么卵用。说好的简洁一点也木有体现出来。 + +下面我们会通过一个简单的例子来进行说明。现在我们有这样一个需求: + +> 有一个服务提供了一些`API`来搜索整个网络上的符合查询关键字的所有猫的图片。 每个图片包含一个可爱程度的参数(一个整数值表示其可爱程度)。 我们的任务就是下载所有猫的列表并选择最可爱的那个,把它的图片保存到本地。 + +####`Model`和`API` + + +下面是猫的数据结构`Cat`: + +```java +public class Cat implements Comparable{ + Bitmap image; + int cuteness; + + @Override + public int compareTo(Cat another) { + return Integer.compare(cuteness, another.cuteness); + } +} +``` + +我们的`API`会调用`cat-sdk.jar`中堵塞式的接口。 +```java +public interface Api { + List queryCats(String query); + Uri store(Cat cat); +} +``` + +看起来很清晰吧?当然了,我们继续写客户端的业务逻辑. +```java +public class CatsHelper { + + Api api; + + public Uri saveTheCutestCat(String query){ + List cats = api.queryCats(query); + Cat cutest = findCutest(cats); + Uri savedUri = api.store(cutest); + return savedUri; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +通俗易懂、简单明了,非常酷的代码。主要的函数`saveTheCutestCat()`只包含了3个方法。使用这些方法然后等待方法执行完并接受返回值就可以了。 + +非常简单、有效。接下来我们看一下这种方式的其他优点。 + +####组合 + + +可以看到我们的`saveTheCutestCat`由其他三个函数调用所组成的。我们通过函数来把一个大功能分割为每个容易理解的小功能。通过函数调用来组合使用这些小功能。使用和理解起来都相当简单 + +####异常传递 + + +另外一个使用函数的好处就是方便处理异常。每个函数都可以通过抛出异常来结束运行。该异常可以在抛出异常的函数里面处理,也可以在调用该函数的外面处理,所以我们无需每次都处理每个异常,我们可以在一个地方处理所有可能抛出的异常。 + + +```java +try{ + List cats = api.queryCats(query); + Cat cutest = findCutest(cats); + Uri savedUri = api.store(cutest); + return savedUri; +} catch (Exception e) { + e.printStackTrace() + return someDefaultValue; +} +``` + +这样,我们就可以处理这三个函数中所抛出的任何异常了。如果没有`try catch`语句,我们也可以把异常继续传递下去。 + + +向异步粗发 +--- + +但是,现实世界中我们往往没法等待。有些时候你没法只使用阻塞调用。在`Android`中你需要处理各种异步操作。 +就那`Android`的`OnClickListener`接口来说吧,如果你需要处理一个`View`的点击事件,你必须提供一个该`Listener` 的实现来处理用户的点击事件。下面来看看如何处理异步调用。 + + +####异步网络调用 + + +假设我们的`cats-sdk.jar`使用了异步调用的`API`来访问网络资源, +这样我们的新`API`接口就变为这样了: + +```java +public interface Api { + interface CatsQueryCallback { + void onCatListReceived(List cats); + void onError(Exception e); + } + + + void queryCats(String query, CatsQueryCallback catsQueryCallback); + + Uri store(Cat cat); +} +``` +这样我们查询猫的操作就变为异步的了, 通过`CatsQueryCallback`回调接口来结束查询的数据和处理异常情况。 +我们的业务逻辑也需要跟着改变一下: + +```java +public class CatsHelper { + + public interface CutestCatCallback { + void onCutestCatSaved(Uri uri); + void onQueryFailed(Exception e); + } + + Api api; + + public void saveTheCutestCat(String query, CutestCatCallback cutestCatCallback){ + api.queryCats(query, new Api.CatsQueryCallback() { + @Override + public void onCatListReceived(List cats) { + Cat cutest = findCutest(cats); + Uri savedUri = api.store(cutest); + cutestCatCallback.onCutestCatSaved(savedUri); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onQueryFailed(e); + } + }); + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +我们没法让`saveTheCutestCat`函数返回一个值了, 我们需要一个回调接口来异步的处理结果。 +这里我们再进一步,使用两个异步操作来实现我们的功能,例如我们需要使用异步`IO`来写文件。 + +```java +public interface Api { + interface CatsQueryCallback { + void onCatListReceived(List cats); + void onQueryFailed(Exception e); + } + + interface StoreCallback{ + void onCatStored(Uri uri); + void onStoreFailed(Exception e); + } + + + void queryCats(String query, CatsQueryCallback catsQueryCallback); + + void store(Cat cat, StoreCallback storeCallback); +} +``` + +我们的`helper`会变成: + +```java +public class CatsHelper { + + public interface CutestCatCallback { + void onCutestCatSaved(Uri uri); + void onError(Exception e); + } + + Api api; + + public void saveTheCutestCat(String query, CutestCatCallback cutestCatCallback){ + api.queryCats(query, new Api.CatsQueryCallback() { + @Override + public void onCatListReceived(List cats) { + Cat cutest = findCutest(cats); + api.store(cutest, new Api.StoreCallback() { + @Override + public void onCatStored(Uri uri) { + cutestCatCallback.onCutestCatSaved(uri); + } + + @Override + public void onStoreFailed(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + @Override + public void onQueryFailed(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +现在我们再来看看这部分代码?还是之前那样简单暴力?现在有太多的干扰代码、匿名类,这简直是太恐怖了,但是他们的业务逻辑其实是一样的,都是查询猫的列表数据,然后找出最可爱的猫并保存它的图片。 + +上面说好的组合功能没有了,现在你没法像阻塞操作一样来组合调用每个功能了,异步操作中,每次你都必须通过回调接口来手工的处理结果。 + +上面说好的异常处理也没有了,异步代码中的异常不会自动传递,我们需要手动的去重新传递。(`onStoreFailed()`和`onQueryFailed()`就是干这事的) + + +####结果? + + +然后呢?我们可以怎么做?我们能不能使用无回调的模式?我们试着修复一下。 + + +####奔向更好的世界 +####通用的回调 + + +如果我们仔细的观察下回调接口,我们会发现它们的共性: + +- 它们都有一个分发结果的方法(`onCutestCatSaved()`,`onCatListReceived()`,`onCatStored()`) +- 它们中的绝大部分都有一个处理操作过程中异常的方法(`onError()`, `onQueryFailed()`, `onStoreFailed()`) + + +所以我们可以创建一个通用的回调来取代它们。但是我们无法修改`api`的调用结构,我们只能创建一个包裹层的调用。 + +我们通用的回调如下: + +```java +public interface Callback { + void onResult(T result); + void onError(Exception e); +} +``` + +我们创建一个`ApiWrapper`类来改变我们调用的结构: + +```java +public class ApiWrapper { + Api api; + + public void queryCats(String query, Callback> catsCallback){ + api.queryCats(query, new Api.CatsQueryCallback() { + @Override + public void onCatListReceived(List cats) { + catsCallback.onResult(cats); + } + + @Override + public void onQueryFailed(Exception e) { + catsCallback.onError(e); + } + }); + } + + public void store(Cat cat, Callback uriCallback){ + api.store(cat, new Api.StoreCallback() { + @Override + public void onCatStored(Uri uri) { + uriCallback.onResult(uri); + } + + @Override + public void onStoreFailed(Exception e) { + uriCallback.onError(e); + } + }); + } +} +``` +这样通过新的回调我们可以减少一次处理结果和异常的逻辑。 +最终,我们的`CatsHelper`如下: +```java +public class CatsHelper{ + + ApiWrapper apiWrapper; + + public void saveTheCutestCat(String query, Callback cutestCatCallback){ + apiWrapper.queryCats(query, new Callback>() { + @Override + public void onResult(List cats) { + Cat cutest = findCutest(cats); + apiWrapper.store(cutest, cutestCatCallback); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +好了,现在比之前的代码稍微简单点了。但是我们能不能做的更好? 当然可以! + +####保持参数和回调的分离性 + + + +看看这些新的异步操作(`queryCats`,` store`和`saveTheCutestCat`)。这些函数都有同样的模式。使用一些参数来调用这些函数`(query,cat)`,同时还有一个回调接口作为参数。甚至,所有的异步操作都带有一些常规参数和一个额外的回调接口参数。如果我们把他们分离开会如何,让每个异步操作只有一些常规参数而把返回一个临时的对象来操作回调接口。 +下面来试试看看这种方式能否有效。 +如果我们返回一个临时的对象作为异步操作的回调接口处理方式,我们需要先定义这个对象。由于对象遵守通用的行为(有一个回调接口参数),我们定义一个能用于所有操作的对象。我们称之为`AsyncJob`。 + +> 注意: 我非常想把这个名字称之为`AsyncTask`。但是由于`Android`系统已经有个`AsyncTask`类了, 为了避免混淆,所以就用`AsyncJob`了。 + +该对象如下: +```java +public abstract class AsyncJob { + public abstract void start(Callback callback); +} +``` + +`start()`函数有个`Callback`回调接口参数,并开始执行该操作。 +`ApiWrapper`修改为: +```java +public class ApiWrapper { + Api api; + + public AsyncJob> queryCats(String query) { + return new AsyncJob>() { + @Override + public void start(Callback> catsCallback) { + api.queryCats(query, new Api.CatsQueryCallback() { + @Override + public void onCatListReceived(List cats) { + catsCallback.onResult(cats); + } + + @Override + public void onQueryFailed(Exception e) { + catsCallback.onError(e); + } + }); + } + }; + } + + public AsyncJob store(Cat cat) { + return new AsyncJob() { + @Override + public void start(Callback uriCallback) { + api.store(cat, new Api.StoreCallback() { + @Override + public void onCatStored(Uri uri) { + uriCallback.onResult(uri); + } + + @Override + public void onStoreFailed(Exception e) { + uriCallback.onError(e); + } + }); + } + }; + } +} +``` +目前看起来还不错。现在可以使用`AsyncJob.start()`来启动每个操作了。接下来我们修改`CatsHelper`类: + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + return new AsyncJob() { + @Override + public void start(Callback cutestCatCallback) { + apiWrapper.queryCats(query) + .start(new Callback>() { + @Override + public void onResult(List cats) { + Cat cutest = findCutest(cats); + apiWrapper.store(cutest) + .start(new Callback() { + @Override + public void onResult(Uri result) { + cutestCatCallback.onResult(result); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + }; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +看起来比前面一个版本更加复杂啊,这样有啥好处啊? +这里其实我们返回的是一个`AsyncJob`对象,该对象和客户端代码组合使用,这样在`Activity`或者`Fragment`客户端代码中就可以操作这个返回的对象了。 +代码虽然目前看起来比较复杂,下面我们就来改进一下。 + +####分解 + + +下面是流程图: +```java + (async) (sync) (async) +query ===========> List -------------> Cat ==========> Uri + queryCats findCutest store +``` + +为了让代码具有可读性,我们把这个流程分解为每个操作。同时我们再进一步假设,如果一个操作是异步的,则每个调用该异步操作的函数也是异步的。例如:如果查询猫是个异步操作,则找到最可爱的猫操作也是异步的。 + +因此,我们可以使用`AsyncJob`来把这些操作分解为一些小的操作中。 + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + AsyncJob> catsListAsyncJob = apiWrapper.queryCats(query); + AsyncJob cutestCatAsyncJob = new AsyncJob() { + @Override + public void start(Callback callback) { + catsListAsyncJob.start(new Callback>() { + @Override + public void onResult(List result) { + callback.onResult(findCutest(result)); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } + }; + + AsyncJob storedUriAsyncJob = new AsyncJob() { + @Override + public void start(Callback cutestCatCallback) { + cutestCatAsyncJob.start(new Callback() { + @Override + public void onResult(Cat cutest) { + apiWrapper.store(cutest) + .start(new Callback() { + @Override + public void onResult(Uri result) { + cutestCatCallback.onResult(result); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + }; + return storedUriAsyncJob; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +虽然代码量多了,但是看起来更加清晰了。 嵌套的回调函数没那么多层级了,异步操作的名字也更容易理解了(`catsListAsyncJob`,`cutestCatAsyncJob`, `storedUriAsyncJob`)。 +看起来还不错,但是还可以更好。 + +####简单的映射 + + +先来看看我们创建 AsyncJob cutestCatAsyncJob 的代码: + +```java +AsyncJob cutestCatAsyncJob = new AsyncJob() { + @Override + public void start(Callback callback) { + catsListAsyncJob.start(new Callback>() { + @Override + public void onResult(List result) { + callback.onResult(findCutest(result)); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } + }; +``` + +这 16 行代码中,只有一行代码是我们的业务逻辑代码: +```java +findCutest(result) +``` +其他的代码只是为了启动`AsyncJob`并接收结果和处理异常的干扰代码。 但是这些代码是通用的,我们可以把他们放到其他地方来让我们更加专注业务逻辑代码。 +那么如何实现呢?需要做两件事情: +- 通过`AsyncJob`获取需要转换的结果 +- 转换的函数 + +但是由于`Java`的限制,无法把函数作为参数,所以需要用一个接口(或者类)并在里面定义一个转换函数: + +```java +public interface Func { + R call(T t); +} +``` +灰常简单。 有两个泛型类型定义,`T`代表参数的类型;`R`代表返回值的类型。 + +当我们把`AsyncJob`的结果转换为其他类型的时候,我们需要把一个结果值映射为另外一种类型,这个操作我们称之为`map`。 把该函数定义到`AsyncJob`类中比较方便,这样就可以通过`this`来访问`AsyncJob`对象了。 + +```java +public abstract class AsyncJob { + public abstract void start(Callback callback); + + public AsyncJob map(Func func){ + final AsyncJob source = this; + return new AsyncJob() { + @Override + public void start(Callback callback) { + source.start(new Callback() { + @Override + public void onResult(T result) { + R mapped = func.call(result); + callback.onResult(mapped); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } + }; + } +} +``` + +看起来不错, 现在的`CatsHelper`如下: + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + AsyncJob> catsListAsyncJob = apiWrapper.queryCats(query); + AsyncJob cutestCatAsyncJob = catsListAsyncJob.map(new Func, Cat>() { + @Override + public Cat call(List cats) { + return findCutest(cats); + } + }); + + AsyncJob storedUriAsyncJob = new AsyncJob() { + @Override + public void start(Callback cutestCatCallback) { + cutestCatAsyncJob.start(new Callback() { + @Override + public void onResult(Cat cutest) { + apiWrapper.store(cutest) + .start(new Callback() { + @Override + public void onResult(Uri result) { + cutestCatCallback.onResult(result); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + }; + return storedUriAsyncJob; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +新创建的`AsyncJob cutestCatAsyncJob()`的代码只有6行,并且只有一层嵌套。 + + +####高级映射 + + +但是`AsyncJob storedUriAsyncJob()`看起来还是非常糟糕。 这里也能使用映射吗? 下面就来试试吧! + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + AsyncJob> catsListAsyncJob = apiWrapper.queryCats(query); + AsyncJob cutestCatAsyncJob = catsListAsyncJob.map(new Func, Cat>() { + @Override + public Cat call(List cats) { + return findCutest(cats); + } + }); + + AsyncJob storedUriAsyncJob = cutestCatAsyncJob.map(new Func() { + @Override + public Uri call(Cat cat) { + return apiWrapper.store(cat); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 将会导致无法编译 + // Incompatible types: + // Required: Uri + // Found: AsyncJob + } + }); + return storedUriAsyncJob; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` +哎。。。 看起来没这么简单啊, 下面修复返回的类型再试一次: + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + AsyncJob> catsListAsyncJob = apiWrapper.queryCats(query); + AsyncJob cutestCatAsyncJob = catsListAsyncJob.map(new Func, Cat>() { + @Override + public Cat call(List cats) { + return findCutest(cats); + } + }); + + AsyncJob> storedUriAsyncJob = cutestCatAsyncJob.map(new Func>() { + @Override + public AsyncJob call(Cat cat) { + return apiWrapper.store(cat); + } + }); + return storedUriAsyncJob; + //^^^^^^^^^^^^^^^^^^^^^^^ 将会导致无法编译 + // Incompatible types: + // Required: AsyncJob + // Found: AsyncJob> + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +这里我们只能拿到`AsyncJob` 。看来还需要更进一步。我们需要压缩一层`AsyncJob`,把两个异步操作当做一个单一的异步操作来对待。 +现在我们需要一个参数为`AsyncJob`的`map`转换操作而不是`R`。该操作类似于`map`,但是该操作会把嵌套的`AsyncJob`压缩为`flatten`一层`AsyncJob`. 我们称之为`flatMap`,实现如下: + +```java +public abstract class AsyncJob { + public abstract void start(Callback callback); + + public AsyncJob map(Func func){ + final AsyncJob source = this; + return new AsyncJob() { + @Override + public void start(Callback callback) { + source.start(new Callback() { + @Override + public void onResult(T result) { + R mapped = func.call(result); + callback.onResult(mapped); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } + }; + } + + public AsyncJob flatMap(Func> func){ + final AsyncJob source = this; + return new AsyncJob() { + @Override + public void start(Callback callback) { + source.start(new Callback() { + @Override + public void onResult(T result) { + AsyncJob mapped = func.call(result); + mapped.start(new Callback() { + @Override + public void onResult(R result) { + callback.onResult(result); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } + }; + } +} +``` + +看起来有很多干扰代码,但是还好这些代码在客户端代码中并不会出现。 现在我们的`CatsHelper`如下: + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + AsyncJob> catsListAsyncJob = apiWrapper.queryCats(query); + AsyncJob cutestCatAsyncJob = catsListAsyncJob.map(new Func, Cat>() { + @Override + public Cat call(List cats) { + return findCutest(cats); + } + }); + + AsyncJob storedUriAsyncJob = cutestCatAsyncJob.flatMap(new Func>() { + @Override + public AsyncJob call(Cat cat) { + return apiWrapper.store(cat); + } + }); + return storedUriAsyncJob; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +如果把匿名类修改为`Java 8`的`lambdas`表达式(逻辑是一样的,只是让代码看起来更清晰点)就很容易发现了。 + +```java +public class CatsHelper { + + ApiWrapper apiWrapper; + + public AsyncJob saveTheCutestCat(String query) { + AsyncJob> catsListAsyncJob = apiWrapper.queryCats(query); + AsyncJob cutestCatAsyncJob = catsListAsyncJob.map(cats -> findCutest(cats)); + AsyncJob storedUriAsyncJob = cutestCatAsyncJob.flatMap(cat -> apiWrapper.store(cat)); + return storedUriAsyncJob; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +这样看起来是不是就很清晰了。 这个代码和刚刚开头的阻塞式代码是不是非常相似: + +```java +public class CatsHelper { + + Api api; + + public Uri saveTheCutestCat(String query){ + List cats = api.queryCats(query); + Cat cutest = findCutest(cats); + Uri savedUri = api.store(cutest); + return savedUri; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +现在他们不仅逻辑是一样的,语义上也是一样的。 太棒了! +同时我们还可以使用组合操作,现在把两个异步操作组合一起并返还另外一个异步操作。 +异常处理也会传递到最终的回调接口中。 +下面来看看`RxJava`吧。 +你没必要把上面代码应用到您的项目中去, 这些简单的、线程不安全的代码只是 `RxJava`的一部分。 +只有一些名字上的不同: + +- `AsyncJob`等同于`Observable`,不仅仅可以返回一个结果,还可以返回一系列的结果,当然也可能没有结果返回。 +- `Callback`等同于`Observer`,除了`onNext(T t)`, `onError(Throwable t)`以外,还有一个`onCompleted()`函数,该函数在结束继续返回结果的时候通知`Observable`。 +- `abstract void start(Callback callback)`和`Subscription subscribe(final Observer observer)`类似,返回一个`Subscription`,如果你不再需要后面的结果了,可以取消该任务。 + +下面是`RxJava`版本的代码: + +```java +public class ApiWrapper { + Api api; + + public Observable> queryCats(final String query) { + return Observable.create(new Observable.OnSubscribe>() { + @Override + public void call(final Subscriber> subscriber) { + api.queryCats(query, new Api.CatsQueryCallback() { + @Override + public void onCatListReceived(List cats) { + subscriber.onNext(cats); + } + + @Override + public void onQueryFailed(Exception e) { + subscriber.onError(e); + } + }); + } + }); + } + + public Observable store(final Cat cat) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + api.store(cat, new Api.StoreCallback() { + @Override + public void onCatStored(Uri uri) { + subscriber.onNext(uri); + } + + @Override + public void onStoreFailed(Exception e) { + subscriber.onError(e); + } + }); + } + }); + } +} + +public class CatsHelper { + + ApiWrapper apiWrapper; + + public Observable saveTheCutestCat(String query) { + Observable> catsListObservable = apiWrapper.queryCats(query); + Observable cutestCatObservable = catsListObservable.map(new Func1, Cat>() { + @Override + public Cat call(List cats) { + return CatsHelper.this.findCutest(cats); + } + }); + Observable storedUriObservable = cutestCatObservable.flatMap(new Func1>() { + @Override + public Observable call(Cat cat) { + return apiWrapper.store(cat); + } + }); + return storedUriObservable; + } + + private Cat findCutest(List cats) { + return Collections.max(cats); + } +} +``` + +把 Observable 替换为 AsyncJob 后 他们的代码是一样的。 + +####结论 + + +通过简单的转换操作,我们可以把异步操作抽象出来。这种抽象的结果可以像操作简单的阻塞函数一样来操作异步操作并组合异步操作。这样我们就可以摆脱层层嵌套的回调接口了,并且不用手工的去处理每次异步操作的异常。 + + +上面这个例子非常好,建议多看几遍,加深理解,可能把这个例子放在这里并不太好,把它放到开始讲之前可能更容易理解,但是我觉得,介绍完概念、使用方法和基本的操作符后,我们可能并不能理解操作符的原理和作用。之前看完操作符原理后迷迷糊糊的状态再来看这个例子会豁然开朗。 + +这里也感谢牛逼的作者[Yaroslav](https://github.com/yarikx)(也是`RxAndroid`项目的一个重要参与者)能用这么牛逼的例子,讲解的如此透彻。 + + +如果嫌上面的代码麻烦,可以通过下面的例子看: + +假设有这样一个需求:界面上有一个自定义的视图`imageCollectorView`,它的作用是显示多张图片,并能使用`addImage(Bitmap)` 方法来任意增加显示的图片。现在需要程序将一个给出的目录数组`File[] folders`中每个目录下的`png`图片都加载出来并显示在`imageCollectorView`中。需要注意的是,由于读取图片的这一过程较为耗时,需要放在后台执行,而图片的显示则必须在`UI`线程执行。常用的实现方式有多种,我这里贴出其中一种: + + +```java +new Thread() { + @Override + public void run() { + super.run(); + for (File folder : folders) { + File[] files = folder.listFiles(); + for (File file : files) { + if (file.getName().endsWith(".png")) { + final Bitmap bitmap = getBitmapFromFile(file); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + imageCollectorView.addImage(bitmap); + } + }); + } + } + } + } +}.start(); +``` + +而如果使用 RxJava ,实现方式是这样的: + +```java +Observable.from(folders) + .flatMap(new Func1>() { + @Override + public Observable call(File file) { + return Observable.from(file.listFiles()); + } + }) + .filter(new Func1() { + @Override + public Boolean call(File file) { + return file.getName().endsWith(".png"); + } + }) + .map(new Func1() { + @Override + public Bitmap call(File file) { + return getBitmapFromFile(file); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1() { + @Override + public void call(Bitmap bitmap) { + imageCollectorView.addImage(bitmap); + } + }); +``` + +那位说话了:『你这代码明明变多了啊!简洁个毛啊!』大兄弟你消消气,我说的是逻辑的简洁,不是单纯的代码量少(逻辑简洁才是提升读写代码速度的必杀技对不?)。观察一下你会发现, `RxJava`的这个实现,是一条从上到下的链式调用,没有任何嵌套,这在逻辑的简洁性上是具有优势的。当需求变得复杂时,这种优势将更加明显(试想如果还要求只选取前`10`张图片,常规方式要怎么办?如果有更多这样那样的要求呢?再试想,在这一大堆需求实现完两个月之后需要改功能,当你翻回这里看到自己当初写下的那一片迷之缩进,你能保证自己将迅速看懂,而不是对着代码重新捋一遍思路?)。 + + +参考: + +- [RxJava Wiki](https://github.com/ReactiveX/RxJava/wiki) + +- [Grokking RxJava, Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) + +- [NotRxJava](https://yarikx.github.io/NotRxJava/) + +- [When Not to Use RxJava](http://tomstechnicalblog.blogspot.hk/2016/07/when-not-to-use-rxjava.html) + +- [给 Android 开发者的 RxJava 详解](http://gank.io/post/560e15be2dca930e00da1083) + +- [Google Agera 从入门到放弃](http://blog.chengyunfeng.com/?p=984) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" deleted file mode 100644 index 6b48dbd7..00000000 --- "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243.md" +++ /dev/null @@ -1,9 +0,0 @@ -RxJava详解 -=== - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file From c5eec13ef22529f1f8170d502fee780f8dbaa605 Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 13 Dec 2016 18:34:52 +0800 Subject: [PATCH 057/373] add RxJava part --- .../RxJava\350\257\246\350\247\243(\344\270\212).md" | 6 +----- .../RxJava\350\257\246\350\247\243(\344\270\213).md" | 7 ------- .../RxJava\350\257\246\350\247\243(\344\270\255).md" | 8 +------- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" index 10516ed9..fee88bbf 100644 --- "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" @@ -534,19 +534,15 @@ Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) 是不是感觉没什么乱用,那就继续看下一部分吧。 +更多内容请看下一篇文章[RxJava详解(下)](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/RxJava%E8%AF%A6%E8%A7%A3(%E4%B8%AD).md) 参考: - [RxJava Wiki](https://github.com/ReactiveX/RxJava/wiki) - - [Grokking RxJava, Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) - - [NotRxJava](https://yarikx.github.io/NotRxJava/) - - [When Not to Use RxJava](http://tomstechnicalblog.blogspot.hk/2016/07/when-not-to-use-rxjava.html) - - [给 Android 开发者的 RxJava 详解](http://gank.io/post/560e15be2dca930e00da1083) - - [Google Agera 从入门到放弃](http://blog.chengyunfeng.com/?p=984) diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" index 68697fa8..68988270 100644 --- "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" @@ -199,19 +199,12 @@ Agera 参考: - [RxJava Wiki](https://github.com/ReactiveX/RxJava/wiki) - - [Grokking RxJava, Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) - - [NotRxJava](https://yarikx.github.io/NotRxJava/) - - [When Not to Use RxJava](http://tomstechnicalblog.blogspot.hk/2016/07/when-not-to-use-rxjava.html) - - [给 Android 开发者的 RxJava 详解](http://gank.io/post/560e15be2dca930e00da1083) - - [Google Agera 从入门到放弃](http://blog.chengyunfeng.com/?p=984) - - --- - 邮箱 :charon.chui@gmail.com diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" index 597436f3..84a947fb 100644 --- "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" +++ "b/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" @@ -986,23 +986,17 @@ Observable.from(folders) 那位说话了:『你这代码明明变多了啊!简洁个毛啊!』大兄弟你消消气,我说的是逻辑的简洁,不是单纯的代码量少(逻辑简洁才是提升读写代码速度的必杀技对不?)。观察一下你会发现, `RxJava`的这个实现,是一条从上到下的链式调用,没有任何嵌套,这在逻辑的简洁性上是具有优势的。当需求变得复杂时,这种优势将更加明显(试想如果还要求只选取前`10`张图片,常规方式要怎么办?如果有更多这样那样的要求呢?再试想,在这一大堆需求实现完两个月之后需要改功能,当你翻回这里看到自己当初写下的那一片迷之缩进,你能保证自己将迅速看懂,而不是对着代码重新捋一遍思路?)。 +更多内容请看下一篇文章[RxJava详解(下)](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/RxJava%E8%AF%A6%E8%A7%A3(%E4%B8%8B).md) 参考: - [RxJava Wiki](https://github.com/ReactiveX/RxJava/wiki) - - [Grokking RxJava, Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) - - [NotRxJava](https://yarikx.github.io/NotRxJava/) - - [When Not to Use RxJava](http://tomstechnicalblog.blogspot.hk/2016/07/when-not-to-use-rxjava.html) - - [给 Android 开发者的 RxJava 详解](http://gank.io/post/560e15be2dca930e00da1083) - - [Google Agera 从入门到放弃](http://blog.chengyunfeng.com/?p=984) - - --- - 邮箱 :charon.chui@gmail.com From 103b115245b6c6e9c1005c2f43c877f6033b8f0f Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 13 Dec 2016 19:14:59 +0800 Subject: [PATCH 058/373] add retrofit part --- .../Retrofit\350\257\246\350\247\243.md" | 23 +++++++++++++++++++ ...00\345\217\221\347\273\204\345\220\210.md" | 14 +++++++++++ 2 files changed, 37 insertions(+) create mode 100644 "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" create mode 100644 "Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" new file mode 100644 index 00000000..56c77286 --- /dev/null +++ "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" @@ -0,0 +1,23 @@ +Retrofit详解 +=== + +之前写过一篇文章[volley-retrofit-okhttp之我们该如何选择网路框架](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/volley-retrofit-okhttp%E4%B9%8B%E6%88%91%E4%BB%AC%E8%AF%A5%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E7%BD%91%E8%B7%AF%E6%A1%86%E6%9E%B6.md)来分析`Volley`与`Retrofit`之间的区别。之前一直用`Volley`比较多。但是随着`Rx`系列的走红,目前越来越多的项目使用`RxJava+Retrofit`这一黄金组合。而且`Retrofit`使用注解的方式比较方便,今天简单的来学习记录下。 + +- 有关更多`Volley`的知识请查看[Volley源码分析](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/Volley%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) +- 有关注解更多的知识请查看[注解使用](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md) +- 有关更多[RxJava]的介绍请查看[Rx详解系列](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/RxJava%E8%AF%A6%E8%A7%A3(%E4%B8%8A).md) + +简介 +--- + +[Retrofit](http://square.github.io/retrofit/) + +> A type-safe HTTP client for Android and Java + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" "b/Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" new file mode 100644 index 00000000..a753091f --- /dev/null +++ "b/Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" @@ -0,0 +1,14 @@ +目前流行的开发组合 +=== + + +目前主流的一套框架就是`Retrofit + RxJava + RxBinding + Dagger2` + +架构上面目前是`MVP`居多。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 99debc9d67e93c8fafdbe543184b85409649111e Mon Sep 17 00:00:00 2001 From: Charon Date: Wed, 14 Dec 2016 20:01:21 +0800 Subject: [PATCH 059/373] add retrofit part --- ...\350\257\246\350\247\243(\344\270\212).md" | 376 +++++++++++ ...\350\257\246\350\247\243(\344\270\213).md" | 637 ++++++++++++++++++ .../Retrofit\350\257\246\350\247\243.md" | 23 - 3 files changed, 1013 insertions(+), 23 deletions(-) create mode 100644 "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" create mode 100644 "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" delete mode 100644 "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" new file mode 100644 index 00000000..10159855 --- /dev/null +++ "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" @@ -0,0 +1,376 @@ +Retrofit详解(上) +=== + +之前写过一篇文章[volley-retrofit-okhttp之我们该如何选择网路框架](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/volley-retrofit-okhttp%E4%B9%8B%E6%88%91%E4%BB%AC%E8%AF%A5%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E7%BD%91%E8%B7%AF%E6%A1%86%E6%9E%B6.md)来分析`Volley`与`Retrofit`之间的区别。之前一直用`Volley`比较多。但是随着`Rx`系列的走红,目前越来越多的项目使用`RxJava+Retrofit`这一黄金组合。而且`Retrofit`使用注解的方式比较方便以及`2.x`版本的提示让`Retrofit`更加完善,今天简单的来学习记录下。 + +- 有关更多`Volley`的知识请查看[Volley源码分析](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/Volley%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) +- 有关注解更多的知识请查看[注解使用](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md) +- 有关更多[RxJava]的介绍请查看[Rx详解系列](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/RxJava%E8%AF%A6%E8%A7%A3(%E4%B8%8A).md) + +简介 +--- + +[Retrofit](http://square.github.io/retrofit/) + +> A type-safe HTTP client for Android and Java + +`type-safe`是什么鬼? +类型安全代码指访问被授权可以访问的内存位置。例如,类型安全代码不能从其他对象的私有字段读取值。它只从定义完善的允许方式访问类型才能读取。 + +使用 +--- + +`Gradle`中集成: + +```java +compile 'com.squareup.retrofit2:retrofit:2.1.0' +``` +`Retrofit 2`底层默认使用自家兄弟`OKHttp`作为网络层,并且在它上面进行构建。所以不需要在想`1.x`版本那样在 +项目中显式的定义`OkHttp`依赖。所以`Retrofit`与`OkHttp`的关系是后者专注与网络请求的高效优化,而前者专注于接口的封装和调用管理等。 + +![image](https://github.com/CharonChui/Pictures/blob/master/retrofit_okhttp_relation.jpg?raw=true) + +当然你还需要在清单文件中添加网络请求的权限: + +``` + +``` + +我们就以`Github`获取个人仓库的`api`来进行举例测试: +```java +https://api.github.com/users/{user}/repos +``` + +- `Retrofit`会将你的`api`封装成`Java`接口 +```java +public interface GitHubService { + @GET("users/{user}/repos") + Call> listRepos(@Path("user") String user); +} +``` + +- `Retrofit`类会生成一个`GitHubService`接口的实现类: + +```java +Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .build(); + +GitHubService service = retrofit.create(GitHubService.class); +``` + +- 从创建的`GithubService`类返回的每个`Call`对象调用后都可以创建一个同步或异步的网络请求: + +```java +Call> repos = service.listRepos("CharonChui"); +``` + +- 上面返回的`Call`其实并不是真正的数据结果,它更像一条指令,你需要执行它: + +```java +// 同步调用 +List data = repos.execute(); + +// 异步调用 +repos.enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + List data = response.body(); + Log.i("@@@", "data size : " + (data == null ? "null" : data.size() + "")); + } + + @Override + public void onFailure(Call> call, Throwable t) { + + } +}); +``` + +那如何取消请求呢? + +```java +repos.cancel(); +``` + +上面这一部分代码,你要是拷贝运行后是运行不了的。 +当然了,因为木有`Repo`对象。但是添加`Repo`对象也是运行不了的。会报错。 + +```java +Process: com.charon.retrofitdemo, PID: 7229 +java.lang.RuntimeException: Unable to start activity ComponentInfo{com.charon.retrofitdemo/com.charon.retrofitdemo.MainActivity}: java.lang.IllegalArgumentException: Unable to create converter for java.util.List + for method GitHubService.listRepos + at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2281) + at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2331) + at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3974) + at android.app.ActivityThread.access$1100(ActivityThread.java:143) + at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1250) + at android.os.Handler.dispatchMessage(Handler.java:102) + at android.os.Looper.loop(Looper.java:136) + at android.app.ActivityThread.main(ActivityThread.java:5291) + at java.lang.reflect.Method.invokeNative(Native Method) + at java.lang.reflect.Method.invoke(Method.java:515) + at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:849) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:665) + at dalvik.system.NativeStart.main(Native Method) +Caused by: java.lang.IllegalArgumentException: Unable to create converter for java.util.List + for method GitHubService.listRepos + at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:720) + at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:706) + at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:167) + at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:166) + at retrofit2.Retrofit$1.invoke(Retrofit.java:145) + at $Proxy0.listRepos(Native Method) + at com.charon.retrofitdemo.MainActivity$override.onCreate(MainActivity.java:29) + at com.charon.retrofitdemo.MainActivity$override.access$dispatch(MainActivity.java) + at com.charon.retrofitdemo.MainActivity.onCreate(MainActivity.java:0) + at android.app.Activity.performCreate(Activity.java:5304) + at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1090) + at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2245) + ... 12 more +Caused by: java.lang.IllegalArgumentException: Could not locate ResponseBody converter for java.util.List. + Tried: +``` + +这是什么鬼?难道官方给的示例代码有问题? 从`log`上面我们能看出来是返回的数据结构不匹配导致的,返回的是`ResponseBody`的转换器无法转换为`List`。 + + +`Retrofit`是一个将`API`接口转换成回调对象的类,默认情况下`Retrofit`会根绝平台提供一些默认的配置,但是它是支持配置的。 + +Converters(解析数据) +--- + +默认情况下,`Retrofit`只能将`HTTP`响应体反序列化为`OkHttp`的`ResponseBody`类型。并且通过`@Body`也只能接受`RequestBody`类型。 + +可以通过添加转换器来支持其他类型。它提供了6中类似的序列化类库来方便进行使用: + +- Gson: com.squareup.retrofit2:converter-gson +- Jackson: com.squareup.retrofit2:converter-jackson +- Moshi: com.squareup.retrofit2:converter-moshi +- Protobuf: com.squareup.retrofit2:converter-protobuf +- Wire: com.squareup.retrofit2:converter-wire +- Simple XML: com.squareup.retrofit2:converter-simplexml +- Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars + + +我们这里通过`Gson`为例,讲解一下如何使用,首先需要在`gradle`文件中配置对应的支持模块。 + +```java +compile 'com.squareup.retrofit2:retrofit:2.1.0' +compile 'com.squareup.retrofit2:converter-gson:2.1.0' +``` + +下面是一个通过使用`GsonConverterFactory`类来指定`GitHubService`接口使用`Gson`来解析结果的配置。 + +```java +Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com") + .addConverterFactory(GsonConverterFactory.create()) + .build(); + +GitHubService service = retrofit.create(GitHubService.class); +``` + +经过这些改造,就可以了,贴一下完整的代码: +```java +public interface GitHubService { + @GET("users/{user}/repos") + Call> listRepos(@Path("user") String user); +} + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + GitHubService service = retrofit.create(GitHubService.class); + + Call> call= service.listRepos("CharonChui"); + + call.enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + List body = response.body(); + Log.i("@@@", "call " + body.size()); + } + + @Override + public void onFailure(Call> call, Throwable t) { + + } + }); + } +} +``` + +运行上面的代码,`log`会打印出`12-14 14:43:38.900 21509-21509/com.charon.retrofitdemo I/@@@: call 26` + + +到这里,我们入门的`Hello World`就完成了。 + +`Retrofit`支持的协议包括`GET/POST/PUT/DELETE/HEAD/PATCH`,当然你也可以直接用`HTTP` 来自定义请求。这些协议均以注解的形式进行配置,比如我们已经见过`GET`的用法. + +我们发现在`Retrofit`创建的时候需要传入一个`baseUrl(https://api.github.com/)`,在`GitHubService`中会通过注解设置`@GET("users/{user}/repos")`,请求的完整`Url`就是通过`baseUrl`与注解的`value`也就是`path`路径结合起来组成。虽然提供了多种规则,但是统一使用下面这一种是最好的。 +```java +通过注解的value指定的是相对路径,baseUrl是目录形式: +path = "users/CharonChui/repos",baseUrl = "https://api.github.com/" +Url = "https://api.github.com/users/CharonChui/repos" +``` + +上面介绍的例子中使用的是`@Path`。 + +`@Query`及`@QueryMap` +--- + +假设我们有一个分页查询的功能: +```java +@GET("/list") +Call list(@Query("page") int page); +``` + +这就相当于`https://api.github.com/list?page=1`这种。`Query`其实就是`Url`中`?`后面的`k-v`。而`QueryMap`就是多个查询条件。 + + +`@Field`及`@FieldMap` +--- + +用`Post`的场景相对比较多,绝大多数的服务端接口要做加密、校验等。所以使用`Post`提交表单的场景就会很多。 + +```java +@FormUrlEncoded +@POST("user/edit") +Call updateUser(@Field("first_name") String first, @Field("last_name") String last); +``` + +当然`FieldMap`就是多个的版本了。 + +`@Part`及`@PartMap` +--- + +上传文件时使用。 +```java +public interface FileUploadService { +@Multipart +@PUT("user/photo") +Call updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description); +} +``` + +`@Headers` + +可以通过`@Headers`来设置静态的请求头 + +```java +@Headers({ + "Accept: application/vnd.github.v3.full+json", + "User-Agent: Retrofit-Sample-App" +}) +@GET("users/{username}") +Call getUser(@Path("username") String username); +``` + +请求头信息可以通过`@Header`注解来动态更新 + +```java +@GET("user") +Call getUser(@Header("Authorization") String authorization) +``` +如果传的值是`null`,该请求头将会被忽略,否则将会使用该值得`toString`。 + + +`RxJava`+`Retrofit` +--- + +`Retrofit`如何与`RxJava`结合使用呢? + +- 添加依赖 + +```java +compile 'com.squareup.retrofit2:retrofit:2.1.0' +compile 'com.squareup.retrofit2:converter-gson:2.1.0'// 支持gson +compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'// 支持rxjava + +// rxjava part +compile 'io.reactivex:rxandroid:1.2.1' +compile 'io.reactivex:rxjava:1.2.3' +``` + +- 修改`Retrofit`的配置,让其支持`RxJava` + +```java +Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava + .build(); +``` + +- 修改`GitHubService`,将返回值改为`Observable`,而不是`Call`。 + +```java +public interface GitHubService { + @GET("users/{user}/repos") + Observable> listRepos(@Path("user") String user); +} +``` + +- 执行部分 +```java +GitHubService service = retrofit.create(GitHubService.class); + +service.listRepos("CharonChui") + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber>() { + @Override + public void onCompleted() { + Log.i("@@@", "onCompleted"); + } + + @Override + public void onError(Throwable e) { + Log.i("@@@", "onError : " + e.toString()); + } + + @Override + public void onNext(List repos) { + Log.i("@@@", "onNext : " + repos.size()); + Toast.makeText(MainActivity.this, "size : " + repos.size(), Toast.LENGTH_SHORT).show(); + } + }); +``` + + +`RxJava`+`Retrofit`形式的时候,`Retrofit`把请求封装进`Observable`在请求结束后调用 `onNext()`或在请求失败后调用`onError()`。 + + +`Proguard`配置 +--- + +如果项目中使用了`Proguard`,你需要添加如下配置: +```java +# Platform calls Class.forName on types which do not exist on Android to determine platform. +-dontnote retrofit2.Platform +# Platform used when running on RoboVM on iOS. Will not be used at runtime. +-dontnote retrofit2.Platform$IOS$MainThreadExecutor +# Platform used when running on Java 8 VMs. Will not be used at runtime. +-dontwarn retrofit2.Platform$Java8 +# Retain generic type information for use by reflection by converters and adapters. +-keepattributes Signature +# Retain declared checked exceptions for use by a Proxy instance. +-keepattributes Exceptions +``` + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" new file mode 100644 index 00000000..4392bc62 --- /dev/null +++ "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" @@ -0,0 +1,637 @@ +Retrofit详解(下) +=== + + +上一篇文件介绍了`Retrofit`的基本使用,接下来我们通过从源码的角度分析一下`Retrofit`的实现。 + +首先看一下它的基本使用方法: + +```java +// 1 +Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build(); +// 2 +GitHubService gitHubService = retrofit.create(GitHubService.class); + +// 3 +Call> call = gitHubService.listRepos("CharonChui"); + +// 4 +call.enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + List data = response.body(); + Log.i("@@@", "data size : " + (data == null ? "null" : data.size() + "")); + } + + @Override + public void onFailure(Call> call, Throwable t) { + + } +}); +``` + +我把上面主要分为4个部分,接下来逐一分析: + +1. 创建`Retrofit`并进行配置。 +--- + +```java +Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build(); +``` + +简单的一句话,确埋藏了很多。 + +这是典型的***外观模式*** + +就想平时我们写的下载模块,作为一个公共的模块,我们可以对外提供一个`DownloadManager`供外界使用,而对于里面的实现我们完全可以闭门造车。 + +具体`baseUrl()`、`addConverterFactory()`方法里面的具体实现就不去看了,比较简单。当然这里也用到了***工厂设计模式***。 + +2. 创建对应的服务类 +--- + +```java +GitHubService gitHubService = retrofit.create(GitHubService.class); +``` + +这一部分是如何实现的呢?我们看一下`retrofit.create()`方法的实现: + +```java +public T create(final Class service) { + Utils.validateServiceInterface(service); + if (validateEagerly) { + eagerlyValidateMethods(service); + } + return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[] { service }, + new InvocationHandler() { + private final Platform platform = Platform.get(); + + @Override public Object invoke(Object proxy, Method method, Object... args) + throws Throwable { + // If the method is a method from Object then defer to normal invocation. + if (method.getDeclaringClass() == Object.class) { + return method.invoke(this, args); + } + if (platform.isDefaultMethod(method)) { + return platform.invokeDefaultMethod(method, service, proxy, args); + } + // 1 + ServiceMethod serviceMethod = loadServiceMethod(method); + // 2 + OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); + // 3 + return serviceMethod.callAdapter.adapt(okHttpCall); + } + }); + } +``` + +看到`Proxy.newProxyInstance()`发现使用了***动态代理*** + +- `loadServiceMethod()` +实现如下: + +```java +ServiceMethod loadServiceMethod(Method method) { + ServiceMethod result; + synchronized (serviceMethodCache) { + result = serviceMethodCache.get(method); + if (result == null) { + // build + result = new ServiceMethod.Builder(this, method).build(); + serviceMethodCache.put(method, result); + } + } + return result; + } +``` + +会通过缓存的方式来获取一个`ServiceMethod`类。通过缓存来保证同一个`API`的同一个方法只会创建一次。 +有关`ServiceMethod`类的文档介绍是: +```java +Adapts an invocation of an interface method into an HTTP call. +``` +大体翻译一下就是将一个请求接口的方法转换到`Http Call`中调用。 + +而上面第一次使用的时候会通过`new ServiceMethod.Builder(this, method).build()`创建,那我们看一下它的实现: + +```java +public ServiceMethod build() { + callAdapter = createCallAdapter(); + responseType = callAdapter.responseType(); + if (responseType == Response.class || responseType == okhttp3.Response.class) { + throw methodError("'" + + Utils.getRawType(responseType).getName() + + "' is not a valid response body type. Did you mean ResponseBody?"); + } + responseConverter = createResponseConverter(); + + for (Annotation annotation : methodAnnotations) { + parseMethodAnnotation(annotation); + } + + if (httpMethod == null) { + throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.)."); + } + + if (!hasBody) { + if (isMultipart) { + throw methodError( + "Multipart can only be specified on HTTP methods with request body (e.g., @POST)."); + } + if (isFormEncoded) { + throw methodError("FormUrlEncoded can only be specified on HTTP methods with " + + "request body (e.g., @POST)."); + } + } + + int parameterCount = parameterAnnotationsArray.length; + parameterHandlers = new ParameterHandler[parameterCount]; + for (int p = 0; p < parameterCount; p++) { + Type parameterType = parameterTypes[p]; + if (Utils.hasUnresolvableType(parameterType)) { + throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", + parameterType); + } + + Annotation[] parameterAnnotations = parameterAnnotationsArray[p]; + if (parameterAnnotations == null) { + throw parameterError(p, "No Retrofit annotation found."); + } + + parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); + } + + if (relativeUrl == null && !gotUrl) { + throw methodError("Missing either @%s URL or @Url parameter.", httpMethod); + } + if (!isFormEncoded && !isMultipart && !hasBody && gotBody) { + throw methodError("Non-body HTTP method cannot contain @Body."); + } + if (isFormEncoded && !gotField) { + throw methodError("Form-encoded method must contain at least one @Field."); + } + if (isMultipart && !gotPart) { + throw methodError("Multipart method must contain at least one @Part."); + } + // 创建`ServiceMethod()`对象 + return new ServiceMethod<>(this); + } +``` + +而`ServiceMethod`的构造函数如下: +```java +ServiceMethod(Builder builder) { + this.callFactory = builder.retrofit.callFactory(); + this.callAdapter = builder.callAdapter; + this.baseUrl = builder.retrofit.baseUrl(); + this.responseConverter = builder.responseConverter; + this.httpMethod = builder.httpMethod; + this.relativeUrl = builder.relativeUrl; + this.headers = builder.headers; + this.contentType = builder.contentType; + this.hasBody = builder.hasBody; + this.isFormEncoded = builder.isFormEncoded; + this.isMultipart = builder.isMultipart; + this.parameterHandlers = builder.parameterHandlers; + } +``` + +看到吗? 这一部分我们应该都稍微有点印象,因为在上一篇文章介绍使用方法的时候,基本会用到这里。 + + +- 创建`OkHttpCall` + +接下来会创建`OkHttpCall`,而`OkHttpCall`是`Call`的子类,那`Call`是什么鬼? + +文档中对`Call`类的介绍如下: +```java +/** + * An invocation of a Retrofit method that sends a request to a webserver and returns a response. + * Each call yields its own HTTP request and response pair. Use {@link #clone} to make multiple + * calls with the same parameters to the same webserver; this may be used to implement polling or + * to retry a failed call. + * + *

Calls may be executed synchronously with {@link #execute}, or asynchronously with {@link + * #enqueue}. In either case the call can be canceled at any time with {@link #cancel}. A call that + * is busy writing its request or reading its response may receive a {@link IOException}; this is + * working as designed. + * + * @param Successful response body type. + */ +``` + +`Retrofit`底层默认使用`OkHttp`,所以当然要创建`OkHttpCall`了。 + +- `serviceMethod.callAdapter.adapt(okHttpCall)` + +这个`CallApdater`是什么鬼? + +```java +/** + * Adapts a {@link Call} into the type of {@code T}. Instances are created by {@linkplain Factory a + * factory} which is {@linkplain Retrofit.Builder#addCallAdapterFactory(Factory) installed} into + * the {@link Retrofit} instance. + */ +``` + +可以很简单的看出来这是一个结果类型转换的类。 + +而它里面的`adapt()`方法的作用呢? + +```java +/** + * Returns an instance of {@code T} which delegates to {@code call}. + *

+ * For example, given an instance for a hypothetical utility, {@code Async}, this instance would + * return a new {@code Async} which invoked {@code call} when run. + *


+   * @Override
+   * public <R> Async<R> adapt(final Call<R> call) {
+   *   return Async.create(new Callable<Response<R>>() {
+   *     @Override
+   *     public Response<R> call() throws Exception {
+   *       return call.execute();
+   *     }
+   *   });
+   * }
+   * 
+ */ + T adapt(Call call); +``` + +所以分析到这里我们基本明白了`retrofit.create()`方法的作用,就是:将请求接口的服务类转换成`Call`,然后将`Call`的结果转换成实体类。 + +3. 调用方法,得到`Call`对象 +--- + +```java +Call> call = gitHubService.listRepos("CharonChui"); +``` + +这个就不分析了,就是在`ServiceMethod`中返回的`Call`。 + +4. 调用`Call.enqueue()` +--- + +在上面分析了,这个`Call`其实是`OkHttpCall`,那我们来看一下`OkHttpCall.enqueue()`方法的实现: + +```java +@Override public void enqueue(final Callback callback) { + if (callback == null) throw new NullPointerException("callback == null"); + + okhttp3.Call call; + Throwable failure; + + synchronized (this) { + if (executed) throw new IllegalStateException("Already executed."); + executed = true; + + call = rawCall; + failure = creationFailure; + if (call == null && failure == null) { + try { + // 调用createRawCall()方法创建Call + call = rawCall = createRawCall(); + } catch (Throwable t) { + failure = creationFailure = t; + } + } + } + + if (failure != null) { + callback.onFailure(this, failure); + return; + } + + if (canceled) { + call.cancel(); + } + // 加入到队列,执行网络请求,注意这里的Call是okhttp3.Call + call.enqueue(new okhttp3.Callback() { + @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) + throws IOException { + Response response; + try { + // 解析结果 + response = parseResponse(rawResponse); + } catch (Throwable e) { + callFailure(e); + return; + } + callSuccess(response); + } + + @Override public void onFailure(okhttp3.Call call, IOException e) { + try { + callback.onFailure(OkHttpCall.this, e); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + private void callFailure(Throwable e) { + try { + callback.onFailure(OkHttpCall.this, e); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + private void callSuccess(Response response) { + try { + callback.onResponse(OkHttpCall.this, response); + } catch (Throwable t) { + t.printStackTrace(); + } + } + }); + } +``` + +上面的部分也主要分为三部分: + +- 创建Call +- 执行网络请求 +- 解析结果 + +#### 第一部分:创建`Call` + +我们分别进行分析,首先是`createRawCall()`方法的实现: +```java +private okhttp3.Call createRawCall() throws IOException { + Request request = serviceMethod.toRequest(args); + // 调用Factory.newCall方法 + okhttp3.Call call = serviceMethod.callFactory.newCall(request); + if (call == null) { + throw new NullPointerException("Call.Factory returned null."); + } + return call; +} +``` + +然后看一下`Factory.newCall()`方法: +```java +interface Factory { + Call newCall(Request request); + } +``` + +它的实现类是`OkHttpClient`类中的`newCall()`方法,并且创建`RealCall`对象: +```java +@Override public Call newCall(Request request) { + return new RealCall(this, request); + } +``` + +#### 第二部分:执行网络请求 + +这一部分是在`call.enqueue()`方法中执行的,上面我们分析了创建的`Call`最终是`RealCall`类,所以这里直接到看`RealCall.enqueue()`方法: + +```java +@Override public void enqueue(Callback responseCallback) { + enqueue(responseCallback, false); + } + + void enqueue(Callback responseCallback, boolean forWebSocket) { + synchronized (this) { + if (executed) throw new IllegalStateException("Already Executed"); + executed = true; + } + + // 调用OkHttpClient中的Dispatcher中的enqueue方法 + client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket)); + } +``` + +我们接着看`client.dispatcher().enqueue()`方法: + +```java +synchronized void enqueue(AsyncCall call) { + // maxRequests的个数是64; + if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { + runningAsyncCalls.add(call); + // 线程池执行 + executorService().execute(call); + } else { + readyAsyncCalls.add(call); + } + } +``` + +而这个参数`AsyncCall`是什么鬼? 它是`RealCall`的内部类,它里面的`execute()`方法是如何实现的? 只要找到该方法的实现就算是完成了。 + +首先看一下`AsyncCall`的声明和构造: +```java +final class AsyncCall extends NamedRunnable { + private final Callback responseCallback; + private final boolean forWebSocket; + + private AsyncCall(Callback responseCallback, boolean forWebSocket) { + super("OkHttp %s", redactedUrl().toString()); + this.responseCallback = responseCallback; + this.forWebSocket = forWebSocket; + } + ... + } +``` + +`NamedRunnable`是`Runnable`的实现类。我们看一下它的`execute()`方法: +```java +@Override protected void execute() { + boolean signalledCallback = false; + try { + // 获取请求结果 + Response response = getResponseWithInterceptorChain(forWebSocket); + if (canceled) { + signalledCallback = true; + responseCallback.onFailure(RealCall.this, new IOException("Canceled")); + } else { + signalledCallback = true; + // 将响应结果设置给之前构造函数传递回来的回调 + responseCallback.onResponse(RealCall.this, response); + } + } catch (IOException e) { + if (signalledCallback) { + // Do not signal the callback twice! + Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e); + } else { + responseCallback.onFailure(RealCall.this, e); + } + } finally { + client.dispatcher().finished(this); + } + } +``` + +接着看一下`RealCall.getResponseWithInterceptorChain()`: + +```java +private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { + Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket); + return chain.proceed(originalRequest); + } + + class ApplicationInterceptorChain implements Interceptor.Chain { + private final int index; + private final Request request; + private final boolean forWebSocket; + + ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) { + this.index = index; + this.request = request; + this.forWebSocket = forWebSocket; + } + + @Override public Connection connection() { + return null; + } + + @Override public Request request() { + return request; + } + + @Override public Response proceed(Request request) throws IOException { + // If there's another interceptor in the chain, call that. + if (index < client.interceptors().size()) { + Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket); + Interceptor interceptor = client.interceptors().get(index); + Response interceptedResponse = interceptor.intercept(chain); + + if (interceptedResponse == null) { + throw new NullPointerException("application interceptor " + interceptor + + " returned null"); + } + + return interceptedResponse; + } + + // No more interceptors. Do HTTP. + return getResponse(request, forWebSocket); + } + } +``` + +又会调用`getResponse()`方法: +```java +/** + * Performs the request and returns the response. May return null if this call was canceled. + */ + Response getResponse(Request request, boolean forWebSocket) throws IOException { + // Copy body metadata to the appropriate request headers. + RequestBody body = request.body(); + if (body != null) { + Request.Builder requestBuilder = request.newBuilder(); + + MediaType contentType = body.contentType(); + if (contentType != null) { + requestBuilder.header("Content-Type", contentType.toString()); + } + + long contentLength = body.contentLength(); + if (contentLength != -1) { + requestBuilder.header("Content-Length", Long.toString(contentLength)); + requestBuilder.removeHeader("Transfer-Encoding"); + } else { + requestBuilder.header("Transfer-Encoding", "chunked"); + requestBuilder.removeHeader("Content-Length"); + } + + request = requestBuilder.build(); + } + + // Create the initial HTTP engine. Retries and redirects need new engine for each attempt. + engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null); + + int followUpCount = 0; + while (true) { + if (canceled) { + engine.releaseStreamAllocation(); + throw new IOException("Canceled"); + } + + boolean releaseConnection = true; + try { + engine.sendRequest(); + engine.readResponse(); + releaseConnection = false; + } catch (RequestException e) { + // The attempt to interpret the request failed. Give up. + throw e.getCause(); + } catch (RouteException e) { + // The attempt to connect via a route failed. The request will not have been sent. + HttpEngine retryEngine = engine.recover(e.getLastConnectException(), true, null); + if (retryEngine != null) { + releaseConnection = false; + engine = retryEngine; + continue; + } + // Give up; recovery is not possible. + throw e.getLastConnectException(); + } catch (IOException e) { + // An attempt to communicate with a server failed. The request may have been sent. + HttpEngine retryEngine = engine.recover(e, false, null); + if (retryEngine != null) { + releaseConnection = false; + engine = retryEngine; + continue; + } + + // Give up; recovery is not possible. + throw e; + } finally { + // We're throwing an unchecked exception. Release any resources. + if (releaseConnection) { + StreamAllocation streamAllocation = engine.close(); + streamAllocation.release(); + } + } + + Response response = engine.getResponse(); + Request followUp = engine.followUpRequest(); + + if (followUp == null) { + if (!forWebSocket) { + engine.releaseStreamAllocation(); + } + return response; + } + + StreamAllocation streamAllocation = engine.close(); + + if (++followUpCount > MAX_FOLLOW_UPS) { + streamAllocation.release(); + throw new ProtocolException("Too many follow-up requests: " + followUpCount); + } + + if (!engine.sameConnection(followUp.url())) { + streamAllocation.release(); + streamAllocation = null; + } else if (streamAllocation.stream() != null) { + throw new IllegalStateException("Closing the body of " + response + + " didn't close its backing stream. Bad interceptor?"); + } + + request = followUp; + engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null, + response); + } + } +``` + +完了没有? 完了.... + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" deleted file mode 100644 index 56c77286..00000000 --- "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243.md" +++ /dev/null @@ -1,23 +0,0 @@ -Retrofit详解 -=== - -之前写过一篇文章[volley-retrofit-okhttp之我们该如何选择网路框架](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/volley-retrofit-okhttp%E4%B9%8B%E6%88%91%E4%BB%AC%E8%AF%A5%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E7%BD%91%E8%B7%AF%E6%A1%86%E6%9E%B6.md)来分析`Volley`与`Retrofit`之间的区别。之前一直用`Volley`比较多。但是随着`Rx`系列的走红,目前越来越多的项目使用`RxJava+Retrofit`这一黄金组合。而且`Retrofit`使用注解的方式比较方便,今天简单的来学习记录下。 - -- 有关更多`Volley`的知识请查看[Volley源码分析](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/Volley%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) -- 有关注解更多的知识请查看[注解使用](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md) -- 有关更多[RxJava]的介绍请查看[Rx详解系列](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/RxJava%E8%AF%A6%E8%A7%A3(%E4%B8%8A).md) - -简介 ---- - -[Retrofit](http://square.github.io/retrofit/) - -> A type-safe HTTP client for Android and Java - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! From 01375f3b1fcf3fa00ba09dce22671a3906c24ca8 Mon Sep 17 00:00:00 2001 From: Charon Date: Fri, 16 Dec 2016 13:01:25 +0800 Subject: [PATCH 060/373] update retrofit part --- ...\350\257\246\350\247\243(\344\270\212).md" | 178 ++++---- ...\350\257\246\350\247\243(\344\270\213).md" | 385 ++++++++++-------- 2 files changed, 300 insertions(+), 263 deletions(-) diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" index 10159855..d428cf24 100644 --- "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" +++ "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" @@ -42,55 +42,55 @@ https://api.github.com/users/{user}/repos ``` - `Retrofit`会将你的`api`封装成`Java`接口 -```java -public interface GitHubService { - @GET("users/{user}/repos") - Call> listRepos(@Path("user") String user); -} -``` + ```java + public interface GitHubService { + @GET("users/{user}/repos") + Call> listRepos(@Path("user") String user); + } + ``` - `Retrofit`类会生成一个`GitHubService`接口的实现类: -```java -Retrofit retrofit = new Retrofit.Builder() - .baseUrl("https://api.github.com/") - .build(); - -GitHubService service = retrofit.create(GitHubService.class); -``` + ```java + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .build(); + + GitHubService service = retrofit.create(GitHubService.class); + ``` - 从创建的`GithubService`类返回的每个`Call`对象调用后都可以创建一个同步或异步的网络请求: -```java -Call> repos = service.listRepos("CharonChui"); -``` + ```java + Call> repos = service.listRepos("CharonChui"); + ``` - 上面返回的`Call`其实并不是真正的数据结果,它更像一条指令,你需要执行它: -```java -// 同步调用 -List data = repos.execute(); - -// 异步调用 -repos.enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - List data = response.body(); - Log.i("@@@", "data size : " + (data == null ? "null" : data.size() + "")); - } - - @Override - public void onFailure(Call> call, Throwable t) { - - } -}); -``` + ```java + // 同步调用 + List data = repos.execute(); + + // 异步调用 + repos.enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + List data = response.body(); + Log.i("@@@", "data size : " + (data == null ? "null" : data.size() + "")); + } + + @Override + public void onFailure(Call> call, Throwable t) { + + } + }); + ``` -那如何取消请求呢? + 那如何取消请求呢? -```java -repos.cancel(); -``` + ```java + repos.cancel(); + ``` 上面这一部分代码,你要是拷贝运行后是运行不了的。 当然了,因为木有`Repo`对象。但是添加`Repo`对象也是运行不了的。会报错。 @@ -126,7 +126,7 @@ Caused by: java.lang.IllegalArgumentException: Unable to create converter for ja at android.app.Activity.performCreate(Activity.java:5304) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1090) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2245) - ... 12 more + ... 12 more Caused by: java.lang.IllegalArgumentException: Could not locate ResponseBody converter for java.util.List. Tried: ``` @@ -230,7 +230,7 @@ Url = "https://api.github.com/users/CharonChui/repos" 假设我们有一个分页查询的功能: ```java -@GET("/list") +GET("/list") Call list(@Query("page") int page); ``` @@ -263,6 +263,7 @@ Call updateUser(@Part("photo") RequestBody photo, @Part("description") Req ``` `@Headers` +--- 可以通过`@Headers`来设置静态的请求头 @@ -291,63 +292,64 @@ Call getUser(@Header("Authorization") String authorization) - 添加依赖 -```java -compile 'com.squareup.retrofit2:retrofit:2.1.0' -compile 'com.squareup.retrofit2:converter-gson:2.1.0'// 支持gson -compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'// 支持rxjava - -// rxjava part -compile 'io.reactivex:rxandroid:1.2.1' -compile 'io.reactivex:rxjava:1.2.3' -``` + ```java + compile 'com.squareup.retrofit2:retrofit:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.1.0'// 支持gson + compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'// 支持rxjava + + // rxjava part + compile 'io.reactivex:rxandroid:1.2.1' + compile 'io.reactivex:rxjava:1.2.3' + ``` - 修改`Retrofit`的配置,让其支持`RxJava` -```java -Retrofit retrofit = new Retrofit.Builder() - .baseUrl("https://api.github.com/") - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava - .build(); -``` + ```java + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava + .build(); + ``` - 修改`GitHubService`,将返回值改为`Observable`,而不是`Call`。 -```java -public interface GitHubService { - @GET("users/{user}/repos") - Observable> listRepos(@Path("user") String user); -} -``` + ```java + public interface GitHubService { + @GET("users/{user}/repos") + Observable> listRepos(@Path("user") String user); + } + ``` - 执行部分 -```java -GitHubService service = retrofit.create(GitHubService.class); - -service.listRepos("CharonChui") - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Subscriber>() { - @Override - public void onCompleted() { - Log.i("@@@", "onCompleted"); - } - - @Override - public void onError(Throwable e) { - Log.i("@@@", "onError : " + e.toString()); - } - - @Override - public void onNext(List repos) { - Log.i("@@@", "onNext : " + repos.size()); - Toast.makeText(MainActivity.this, "size : " + repos.size(), Toast.LENGTH_SHORT).show(); - } - }); -``` - -`RxJava`+`Retrofit`形式的时候,`Retrofit`把请求封装进`Observable`在请求结束后调用 `onNext()`或在请求失败后调用`onError()`。 + ```java + GitHubService service = retrofit.create(GitHubService.class); + + service.listRepos("CharonChui") + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber>() { + @Override + public void onCompleted() { + Log.i("@@@", "onCompleted"); + } + + @Override + public void onError(Throwable e) { + Log.i("@@@", "onError : " + e.toString()); + } + + @Override + public void onNext(List repos) { + Log.i("@@@", "onNext : " + repos.size()); + Toast.makeText(MainActivity.this, "size : " + repos.size(), Toast.LENGTH_SHORT).show(); + } + }); + ``` + + + `RxJava`+`Retrofit`形式的时候,`Retrofit`把请求封装进`Observable`在请求结束后调用 `onNext()`或在请求失败后调用`onError()`。 `Proguard`配置 diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" index 4392bc62..de30bfd9 100644 --- "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" +++ "b/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" @@ -45,9 +45,9 @@ Retrofit retrofit = new Retrofit.Builder() .build(); ``` -简单的一句话,确埋藏了很多。 +简单的一句话,却埋藏了很多。 -这是典型的***外观模式*** +这是典型的***建造者模式、外观模式*** 就想平时我们写的下载模块,作为一个公共的模块,我们可以对外提供一个`DownloadManager`供外界使用,而对于里面的实现我们完全可以闭门造车。 @@ -70,204 +70,227 @@ public T create(final Class service) { } return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[] { service }, new InvocationHandler() { + // 这里的Platform主要是为了检测当前的运行平台,是java还是android,会根据当前的平台来返回默认的CallAdapter private final Platform platform = Platform.get(); @Override public Object invoke(Object proxy, Method method, Object... args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { + // 代理调用 return method.invoke(this, args); } if (platform.isDefaultMethod(method)) { return platform.invokeDefaultMethod(method, service, proxy, args); } - // 1 + // 1,根据动态代理的方法去生成ServiceMethod这里动态代理的方法就是listRepos方法 ServiceMethod serviceMethod = loadServiceMethod(method); - // 2 + // 2,根绝ServiceMethod和参数去生成OkHttpCall,这里args是CharonChui OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); - // 3 + // 3, serviceMethod去进行处理并返回Call对象,拿到这个Call对象才能去执行网络请求。 return serviceMethod.callAdapter.adapt(okHttpCall); } }); } ``` -看到`Proxy.newProxyInstance()`发现使用了***动态代理*** +看到`Proxy.newProxyInstance()`就明白了,这里使用了***动态代理***。简单的说动态代理是在你要调用某个`Class`的方法前或后,插入你想要执行的代码。那这里要代理的是什么方法? `Call> call = gitHubService.listRepos("CharonChui");`,这里就是`listRepos()`方法。 就是说在调用`listRepos()`方法时会被动态代理所拦截,然后执行`Proxy.newProxyInstance()`里面的`InvocationHandler.invoke()`中的部分。 而`invoke()`方法的三个参数分别是啥? 分别是`Object proxy`: 代理对象,`Method method`:调用的方法,就是`listRepos()`方法,`Object... args`:方法的参数,这里是`CharonChui`。 -- `loadServiceMethod()` -实现如下: - -```java -ServiceMethod loadServiceMethod(Method method) { - ServiceMethod result; - synchronized (serviceMethodCache) { - result = serviceMethodCache.get(method); - if (result == null) { - // build - result = new ServiceMethod.Builder(this, method).build(); - serviceMethodCache.put(method, result); - } - } - return result; - } -``` - -会通过缓存的方式来获取一个`ServiceMethod`类。通过缓存来保证同一个`API`的同一个方法只会创建一次。 -有关`ServiceMethod`类的文档介绍是: -```java -Adapts an invocation of an interface method into an HTTP call. -``` -大体翻译一下就是将一个请求接口的方法转换到`Http Call`中调用。 - -而上面第一次使用的时候会通过`new ServiceMethod.Builder(this, method).build()`创建,那我们看一下它的实现: - -```java -public ServiceMethod build() { - callAdapter = createCallAdapter(); - responseType = callAdapter.responseType(); - if (responseType == Response.class || responseType == okhttp3.Response.class) { - throw methodError("'" - + Utils.getRawType(responseType).getName() - + "' is not a valid response body type. Did you mean ResponseBody?"); - } - responseConverter = createResponseConverter(); - - for (Annotation annotation : methodAnnotations) { - parseMethodAnnotation(annotation); - } +有关动态代理介绍可以看[张孝祥老师的java1.5高新技术系列中的动态代理]() - if (httpMethod == null) { - throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.)."); - } - - if (!hasBody) { - if (isMultipart) { - throw methodError( - "Multipart can only be specified on HTTP methods with request body (e.g., @POST)."); - } - if (isFormEncoded) { - throw methodError("FormUrlEncoded can only be specified on HTTP methods with " - + "request body (e.g., @POST)."); - } - } - - int parameterCount = parameterAnnotationsArray.length; - parameterHandlers = new ParameterHandler[parameterCount]; - for (int p = 0; p < parameterCount; p++) { - Type parameterType = parameterTypes[p]; - if (Utils.hasUnresolvableType(parameterType)) { - throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", - parameterType); - } - - Annotation[] parameterAnnotations = parameterAnnotationsArray[p]; - if (parameterAnnotations == null) { - throw parameterError(p, "No Retrofit annotation found."); - } - - parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); - } - - if (relativeUrl == null && !gotUrl) { - throw methodError("Missing either @%s URL or @Url parameter.", httpMethod); - } - if (!isFormEncoded && !isMultipart && !hasBody && gotBody) { - throw methodError("Non-body HTTP method cannot contain @Body."); - } - if (isFormEncoded && !gotField) { - throw methodError("Form-encoded method must contain at least one @Field."); - } - if (isMultipart && !gotPart) { - throw methodError("Multipart method must contain at least one @Part."); - } - // 创建`ServiceMethod()`对象 - return new ServiceMethod<>(this); - } -``` +这里就不仔细介绍动态代理了,上面的代码中又分为三部分: -而`ServiceMethod`的构造函数如下: -```java -ServiceMethod(Builder builder) { - this.callFactory = builder.retrofit.callFactory(); - this.callAdapter = builder.callAdapter; - this.baseUrl = builder.retrofit.baseUrl(); - this.responseConverter = builder.responseConverter; - this.httpMethod = builder.httpMethod; - this.relativeUrl = builder.relativeUrl; - this.headers = builder.headers; - this.contentType = builder.contentType; - this.hasBody = builder.hasBody; - this.isFormEncoded = builder.isFormEncoded; - this.isMultipart = builder.isMultipart; - this.parameterHandlers = builder.parameterHandlers; - } -``` +- `loadServiceMethod()` +- `new OkHttpCall()` +- `serviceMethod.callAdapter.adapt()` -看到吗? 这一部分我们应该都稍微有点印象,因为在上一篇文章介绍使用方法的时候,基本会用到这里。 +我们这里分别来进行分析。 +- `loadServiceMethod()` + 实现如下: + + ```java + ServiceMethod loadServiceMethod(Method method) { + ServiceMethod result; + synchronized (serviceMethodCache) { + // ServiceMethod包含了请求的所有相关数据,以及获取请求的request和把请求结果转换成java对象,所以相比较而言较重,用缓存来提高效率。 + result = serviceMethodCache.get(method); + if (result == null) { + // build + result = new ServiceMethod.Builder(this, method).build(); + serviceMethodCache.put(method, result); + } + } + return result; + } + ``` + + 会通过缓存的方式来获取一个`ServiceMethod`类。通过缓存来保证同一个`API`的同一个方法只会创建一次。 + 有关`ServiceMethod`类的文档介绍是: + ```java + Adapts an invocation of an interface method into an HTTP call. + ``` + 大体翻译一下就是将一个请求接口的方法转换到`Http Call`中调用。 + + 而上面第一次使用的时候会通过`new ServiceMethod.Builder(this, method).build()`创建,那我们看一下它的实现: + + ```java + public ServiceMethod build() { + // 创建CallAdapter用来代理Call + callAdapter = createCallAdapter(); + responseType = callAdapter.responseType(); + if (responseType == Response.class || responseType == okhttp3.Response.class) { + throw methodError("'" + + Utils.getRawType(responseType).getName() + + "' is not a valid response body type. Did you mean ResponseBody?"); + } + // responseConverter用来解析结果将json等返回结果解析成java对象 + responseConverter = createResponseConverter(); + + for (Annotation annotation : methodAnnotations) { + parseMethodAnnotation(annotation); + } + + if (httpMethod == null) { + throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.)."); + } + + if (!hasBody) { + if (isMultipart) { + throw methodError( + "Multipart can only be specified on HTTP methods with request body (e.g., @POST)."); + } + if (isFormEncoded) { + throw methodError("FormUrlEncoded can only be specified on HTTP methods with " + + "request body (e.g., @POST)."); + } + } + + int parameterCount = parameterAnnotationsArray.length; + parameterHandlers = new ParameterHandler[parameterCount]; + for (int p = 0; p < parameterCount; p++) { + Type parameterType = parameterTypes[p]; + if (Utils.hasUnresolvableType(parameterType)) { + throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", + parameterType); + } + // 解析对应method的注解,这里是listRepos方法的注解。 + Annotation[] parameterAnnotations = parameterAnnotationsArray[p]; + if (parameterAnnotations == null) { + throw parameterError(p, "No Retrofit annotation found."); + } + // 通过注解和参数类型,解析并赋值到parameterHandlers中 + parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); + } + + if (relativeUrl == null && !gotUrl) { + throw methodError("Missing either @%s URL or @Url parameter.", httpMethod); + } + if (!isFormEncoded && !isMultipart && !hasBody && gotBody) { + throw methodError("Non-body HTTP method cannot contain @Body."); + } + if (isFormEncoded && !gotField) { + throw methodError("Form-encoded method must contain at least one @Field."); + } + if (isMultipart && !gotPart) { + throw methodError("Multipart method must contain at least one @Part."); + } + // 创建`ServiceMethod()`对象 + return new ServiceMethod<>(this); + } + ``` + 总起来说,就是创建`CallAdapter`、`responseConverter`、解析注解、设置参数,然后创建`ServiceMethod`对象。 + + 而`ServiceMethod`的构造函数如下: + ```java + ServiceMethod(Builder builder) { + this.callFactory = builder.retrofit.callFactory(); + this.callAdapter = builder.callAdapter; + this.baseUrl = builder.retrofit.baseUrl(); + this.responseConverter = builder.responseConverter; + this.httpMethod = builder.httpMethod; + this.relativeUrl = builder.relativeUrl; + this.headers = builder.headers; + this.contentType = builder.contentType; + this.hasBody = builder.hasBody; + this.isFormEncoded = builder.isFormEncoded; + this.isMultipart = builder.isMultipart; + this.parameterHandlers = builder.parameterHandlers; + } + ``` + + 看到吗? 这一部分我们应该都稍微有点印象,因为在上一篇文章介绍使用方法的时候,基本会用到这里。 + +至于这里的`CallAdapter`、`ResponseConverter`、`Headers`、`ParamterHandlers`等这里就不分析了,最后我们再简单介绍下。 - 创建`OkHttpCall` -接下来会创建`OkHttpCall`,而`OkHttpCall`是`Call`的子类,那`Call`是什么鬼? - -文档中对`Call`类的介绍如下: -```java -/** - * An invocation of a Retrofit method that sends a request to a webserver and returns a response. - * Each call yields its own HTTP request and response pair. Use {@link #clone} to make multiple - * calls with the same parameters to the same webserver; this may be used to implement polling or - * to retry a failed call. - * - *

Calls may be executed synchronously with {@link #execute}, or asynchronously with {@link - * #enqueue}. In either case the call can be canceled at any time with {@link #cancel}. A call that - * is busy writing its request or reading its response may receive a {@link IOException}; this is - * working as designed. - * - * @param Successful response body type. - */ -``` - -`Retrofit`底层默认使用`OkHttp`,所以当然要创建`OkHttpCall`了。 - -- `serviceMethod.callAdapter.adapt(okHttpCall)` - -这个`CallApdater`是什么鬼? - -```java -/** - * Adapts a {@link Call} into the type of {@code T}. Instances are created by {@linkplain Factory a - * factory} which is {@linkplain Retrofit.Builder#addCallAdapterFactory(Factory) installed} into - * the {@link Retrofit} instance. - */ -``` - -可以很简单的看出来这是一个结果类型转换的类。 - -而它里面的`adapt()`方法的作用呢? - -```java -/** - * Returns an instance of {@code T} which delegates to {@code call}. - *

- * For example, given an instance for a hypothetical utility, {@code Async}, this instance would - * return a new {@code Async} which invoked {@code call} when run. - *


-   * @Override
-   * public <R> Async<R> adapt(final Call<R> call) {
-   *   return Async.create(new Callable<Response<R>>() {
-   *     @Override
-   *     public Response<R> call() throws Exception {
-   *       return call.execute();
-   *     }
-   *   });
-   * }
-   * 
- */ - T adapt(Call call); -``` - -所以分析到这里我们基本明白了`retrofit.create()`方法的作用,就是:将请求接口的服务类转换成`Call`,然后将`Call`的结果转换成实体类。 - + 接下来会创建`OkHttpCall`,而`OkHttpCall`是`Call`的子类,那`Call`是什么鬼? 它是具体的网络请求类。 + + 文档中对`Call`类的介绍如下: + ```java + /** + * An invocation of a Retrofit method that sends a request to a webserver and returns a response. + * Each call yields its own HTTP request and response pair. Use {@link #clone} to make multiple + * calls with the same parameters to the same webserver; this may be used to implement polling or + * to retry a failed call. + * + *

Calls may be executed synchronously with {@link #execute}, or asynchronously with {@link + * #enqueue}. In either case the call can be canceled at any time with {@link #cancel}. A call that + * is busy writing its request or reading its response may receive a {@link IOException}; this is + * working as designed. + * + * @param Successful response body type. + */ + public interface Call extends Cloneable { + // 这里我特地把这句话放上,Call集成了Cloneable接口。 + // 每一个 call 对象实例只能被用一次,所以说 request 和 response 都是一一对应的。你其实可以通过 Clone 方法来创建一个一模一样的实例,这个开销是很小的。比如说:你可以在每次决定发请求前 clone 一个之前的实例。 + .... + } + ``` + + `Retrofit`底层默认使用`OkHttp`,所以当然要创建`OkHttpCall`了。 + + - `serviceMethod.callAdapter.adapt(okHttpCall)` + + 这个`CallApdater`是什么鬼? + + ```java + /** + * Adapts a {@link Call} into the type of {@code T}. Instances are created by {@linkplain Factory a + * factory} which is {@linkplain Retrofit.Builder#addCallAdapterFactory(Factory) installed} into + * the {@link Retrofit} instance. + */ + ``` + + 可以很简单的看出来这是一个结果类型转换的类。就是`Call`的适配器,作用就是创建/转换`Call`对象,把`Call`转换成预期的格式。`CallAdatper`创建是通过`CallAdapter.factory`工厂类进行的。`DefaultCallAdapter`为`Retrofit2`自带默认`Call`转换器,用来生成`OKHTTP`的`call`请求调用。 + + + 而它里面的`adapt()`方法的作用呢? + + ```java + /** + * Returns an instance of {@code T} which delegates to {@code call}. + *

+ * For example, given an instance for a hypothetical utility, {@code Async}, this instance would + * return a new {@code Async} which invoked {@code call} when run. + *


+	   * @Override
+	   * public <R> Async<R> adapt(final Call<R> call) {
+	   *   return Async.create(new Callable<Response<R>>() {
+	   *     @Override
+	   *     public Response<R> call() throws Exception {
+	   *       return call.execute();
+	   *     }
+	   *   });
+	   * }
+	   * 
+ */ + T adapt(Call call); + ``` + + 所以分析到这里我们基本明白了`retrofit.create()`方法的作用,就是将请求接口的服务类转换成`Call`,然后将`Call`的结果转换成实体类。 + 3. 调用方法,得到`Call`对象 --- @@ -313,13 +336,13 @@ Call> call = gitHubService.listRepos("CharonChui"); if (canceled) { call.cancel(); } - // 加入到队列,执行网络请求,注意这里的Call是okhttp3.Call + // 把请求任务加入到okhttp的请求队列中,执行网络请求,注意这里的Call是okhttp3.Call call.enqueue(new okhttp3.Callback() { @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) throws IOException { Response response; try { - // 解析结果 + // 解析结果,该方法内部会将OkHttp中Request的执行结果转换成对应的Java对象。 response = parseResponse(rawResponse); } catch (Throwable e) { callFailure(e); @@ -366,6 +389,7 @@ Call> call = gitHubService.listRepos("CharonChui"); 我们分别进行分析,首先是`createRawCall()`方法的实现: ```java private okhttp3.Call createRawCall() throws IOException { + // ServiceMethod.toRequest()方法的作用是将ServiceMethod中的网络请求相关的数据转换成一个OkHttp的网络请求所需要的Request对象。因为之前分析过所有Retrofit解析的网络请求相关的数据都是在ServiceMethod中 Request request = serviceMethod.toRequest(args); // 调用Factory.newCall方法 okhttp3.Call call = serviceMethod.callFactory.newCall(request); @@ -628,7 +652,18 @@ private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IO 完了没有? 完了.... +上面只是简单的分析了下大体的调用流程和主要的类,但是好像并没有什么乱用,因为没有具体的去分析里面各部分的实现,如果都分析下来内容太多了。这里就不仔细看了,大体总结一下。 + +通过上面的分析,最终的网络请求是在`OkHttp`的`Call`中去执行,也就是说`Retrofit`其实是将一个`Java`接口通过注解等方式来解析参数等然后转换成一个请求交给`OkHttp`去执行,然后将执行结果进行解析转换暴露给上层调用者。 +而这一切是如何实现的呢? +`Retrofit`非常巧妙的用注解来描述一个`HTTP`请求,将一个`HTTP`请求抽象成一个`Java`接口,然后用了`动态代理`的方式,动态的将这个接口的注解转换成一个`HTTP`请求,然后再将这个`Http`请求交给`OkHttp`执行。 + +动态代理用的太妙,而它的过程中也使用了大量的工厂模式,这里就不分析了。 + +参考: +- [simple-http-retrofit-2](https://realm.io/news/droidcon-jake-wharton-simple-http-retrofit-2) +- [Retrofit API](http://square.github.io/retrofit/2.x/retrofit/) --- From 650eb2a1a6778c64c18ef6438a43c22ed15a9088 Mon Sep 17 00:00:00 2001 From: Charon Date: Fri, 16 Dec 2016 17:41:57 +0800 Subject: [PATCH 061/373] change directory --- ...13\350\275\254\345\212\250\347\224\273.md" | 0 .../ART\344\270\216Dalvik.md" | 0 ...57\345\212\250\350\277\207\347\250\213.md" | 0 ...07\347\250\213\350\257\246\350\247\243.md" | 1168 ++-- ...23\347\232\204\346\223\215\344\275\234.md" | 0 ...06\345\217\221\350\257\246\350\247\243.md" | 1430 ++--- ...03\351\231\220\347\263\273\347\273\237.md" | 0 ...350\241\214ndk\345\274\200\345\217\221.md" | 0 ...1\253\230Build\351\200\237\345\272\246.md" | 0 ...70\350\275\275\345\217\215\351\246\210.md" | 0 ...41\345\274\217\350\257\246\350\247\243.md" | 0 ...24\347\224\250\345\217\221\345\270\203.md" | 0 ...71\345\272\224\345\212\237\350\203\275.md" | 0 ...41\345\274\217\350\257\246\350\247\243.md" | 0 ...67\345\217\212\347\261\273\345\272\223.md" | 172 +- .../ApplicationId vs PackageName.md | 0 .../AsyncTask\350\257\246\350\247\243.md" | 1864 +++--- ...11\345\205\250\351\227\256\351\242\230.md" | 96 +- ...72\346\225\260\346\215\256\345\214\205.md" | 0 ...21\345\256\232\345\237\237\345\220\215.md" | 64 +- .../Gradle\344\270\223\351\242\230.md" | 0 ...04\351\234\262\345\210\206\346\236\220.md" | 206 +- ...ttpURLConnection\344\270\216HttpClient.md" | 28 +- ...pURLConnection\350\257\246\350\247\243.md" | 0 .../InstantRun\350\257\246\350\247\243.md" | 0 ...se\346\227\266\346\212\245\351\224\231.md" | 0 ...20\347\240\201\345\210\206\346\236\220.md" | 0 ...05\345\255\230\345\210\206\346\236\220.md" | 60 +- ...217\212Android\345\221\275\344\273\244.md" | 66 +- ...46\344\271\240\346\211\213\345\206\214.md" | 0 ...MaterialDesign\344\275\277\347\224\250.md" | 0 .../RecyclerView\344\270\223\351\242\230.md" | 0 ...\350\257\246\350\247\243(\344\270\212).md" | 0 ...\350\257\246\350\247\243(\344\270\213).md" | 0 ...\350\257\246\350\247\243(\344\270\212).md" | 0 ...\350\257\246\350\247\243(\344\270\213).md" | 0 ...\350\257\246\350\247\243(\344\270\255).md" | 0 ...20\347\240\201\345\210\206\346\236\220.md" | 5188 ++++++++--------- ...07\347\250\213\350\257\246\350\247\243.md" | 3150 +++++----- ...20\347\240\201\345\210\206\346\236\220.md" | 0 .../Zipalign\344\274\230\345\214\226.md" | 0 ...20\347\240\201\350\257\246\350\247\243.md" | 0 ...21\350\267\257\346\241\206\346\236\266.md" | 0 ...53\346\215\267\346\226\271\345\274\217.md" | 0 ...5\210\260Maven\344\273\223\345\272\223.md" | 0 ...70\351\251\273\345\206\205\345\255\230.md" | 0 ...71\346\241\210\350\257\246\350\247\243.md" | 0 ...03\345\261\200\344\274\230\345\214\226.md" | 304 +- ...47\350\203\275\344\274\230\345\214\226.md" | 84 +- ...70\345\205\263\345\267\245\345\205\267.md" | 0 ...50\350\247\243\344\275\277\347\224\250.md" | 0 ...56\345\244\215\345\256\236\347\216\260.md" | 0 ...00\345\217\221\347\273\204\345\220\210.md" | 0 ...44\271\211View\350\257\246\350\247\243.md" | 0 ...05\345\256\271\346\200\273\347\273\223.md" | 244 +- ...43\344\270\216\347\241\254\350\247\243.md" | 0 ...50\347\224\273\346\200\247\350\203\275.md" | 0 ...(\347\254\254\344\270\200\345\274\271).md" | 216 +- ...(\347\254\254\344\270\203\345\274\271).md" | 826 +-- ...(\347\254\254\344\270\211\345\274\271).md" | 106 +- ...(\347\254\254\344\272\214\345\274\271).md" | 128 +- ...(\347\254\254\344\272\224\345\274\271).md" | 368 +- ...(\347\254\254\345\205\255\345\274\271).md" | 88 +- ...(\347\254\254\345\233\233\345\274\271).md" | 258 +- ...45\351\227\250\344\273\213\347\273\215.md" | 216 +- .../Android\345\212\250\347\224\273.md" | 0 ...344\273\266\344\271\213ContentProvider.md" | 620 +- ...273\204\344\273\266\344\271\213Service.md" | 432 +- ...00\351\235\242\350\257\225\351\242\230.md" | 1244 ++-- ...26\347\240\201\350\247\204\350\214\203.md" | 0 .../Ant\346\211\223\345\214\205.md" | 76 +- .../Bitmap\344\274\230\345\214\226.md" | 272 +- .../DLNA\347\256\200\344\273\213.md" | 0 .../Fragment\344\270\223\351\242\230.md" | 560 +- ...me\351\224\256\347\233\221\345\220\254.md" | 0 ...45\222\214Post\350\257\267\346\261\202.md" | 0 ...55\350\250\200\345\237\272\347\241\200.md" | 932 +-- .../JNI\345\237\272\347\241\200.md" | 818 +-- .../ListView\344\270\223\351\242\230.md" | 240 +- .../Parcelable\345\217\212Serializable.md" | 0 .../PopupWindow\347\273\206\350\212\202.md" | 172 +- ...60\347\232\204\351\227\256\351\242\230.md" | 0 .../Scroller\347\256\200\344\273\213.md" | 106 +- .../ScrollingTabs.md | 330 +- .../Selector\344\275\277\347\224\250.md" | 212 +- .../SlidingMenu.md | 432 +- ...ng\346\240\274\345\274\217\345\214\226.md" | 0 ...54\347\201\257\346\225\210\346\236\234.md" | 142 +- .../WebView\346\200\273\347\273\223.md" | 0 ...3\345\260\217\351\203\250\344\273\266).md" | 318 +- ...66\346\200\201\347\233\221\345\220\254.md" | 0 .../XmlPullParser.md | 158 +- ...77\347\224\250\347\256\200\344\273\213.md" | 0 ...13\211\345\210\267\346\226\260ListView.md" | 602 +- ...43\347\240\201\346\267\267\346\267\206.md" | 226 +- ...7\220\206\345\231\250(ActivityManager).md" | 0 ...04\344\273\266\346\240\267\345\274\217.md" | 310 +- ...05\345\255\230\346\263\204\346\274\217.md" | 516 +- ...55\347\202\271\344\270\213\350\275\275.md" | 0 ...24\347\224\250\347\250\213\345\272\217.md" | 212 +- ...17\345\271\225\351\200\202\351\205\215.md" | 0 ...56\347\232\204\345\210\267\346\226\260.md" | 130 +- ...24\347\224\250\345\256\211\350\243\205.md" | 106 +- ...og\347\232\204\347\256\241\347\220\206.md" | 102 +- ...70\347\232\204\345\244\204\347\220\206.md" | 112 +- ...17\345\267\245\345\205\267\347\261\273.md" | 206 +- ...13\346\234\272\346\221\207\346\231\203.md" | 0 .../\346\220\234\347\264\242\346\241\206.md" | 228 +- ...60\346\215\256\345\255\230\345\202\250.md" | 648 +- ...07\344\273\266\344\270\212\344\274\240.md" | 0 ...60\346\217\220\347\244\272\346\241\206.md" | 636 +- ...54\345\217\212\345\275\225\351\237\263.md" | 0 .../\346\250\252\345\220\221ListView.md" | 0 ...7\346\215\242Activity(GestureDetector).md" | 274 +- .../\347\227\205\346\257\222.md" | 290 +- ...06\345\244\247\346\235\202\347\203\251.md" | 0 ...55\346\216\245\346\224\266\350\200\205.md" | 0 ...75\345\222\214\345\210\206\344\272\253.md" | 150 +- ...253\226\347\235\200\347\232\204Seekbar.md" | 158 +- ...0\207\252\345\256\232\344\271\211Toast.md" | 370 +- ...32\344\271\211\346\216\247\344\273\266.md" | 330 +- ...01\346\240\217\351\200\232\347\237\245.md" | 256 +- ...32\344\271\211\350\203\214\346\231\257.md" | 0 ...4\275\215\347\275\256(LocationManager).md" | 0 ...00\351\224\256\346\270\205\347\220\206.md" | 182 +- ...05\347\232\204\347\250\213\345\272\217.md" | 0 ...30\345\202\250\347\251\272\351\227\264.md" | 0 ...26\350\201\224\347\263\273\344\272\272.md" | 110 +- ...\210\267logcat\346\227\245\345\277\227.md" | 0 ...11\347\247\215\346\226\271\345\274\217.md" | 58 +- ...0\206\345\221\230(DevicePoliceManager).md" | 182 +- ...43\351\224\201\347\233\221\345\220\254.md" | 0 ...12\344\274\240\346\225\260\346\215\256.md" | 86 +- ...56\345\272\246\350\260\203\350\212\202.md" | 0 ...65\350\257\235\350\256\260\345\275\225.md" | 276 +- "JS\345\237\272\347\241\200/DOM.md" | 234 - ...13\345\217\212\345\205\245\351\227\250.md" | 430 -- "JS\345\237\272\347\241\200/README.md" | 6 - "JS\345\237\272\347\241\200/Window.md" | 135 - .../\345\257\271\350\261\241.md" | 89 - .../Base64\345\212\240\345\257\206.md" | 0 .../Git\345\221\275\344\273\244.md" | 0 ...37\347\220\206\345\210\206\346\236\220.md" | 0 ...36\346\224\266\346\234\272\345\210\266.md" | 0 ...00\351\235\242\350\257\225\351\242\230.md" | 3800 ++++++------ .../MD5\345\212\240\345\257\206.md" | 78 +- .../MVC\344\270\216MVP\345\217\212MVVM.md" | 0 ...17\345\206\231\350\275\254\346\215\242.md" | 0 ...77\347\224\250\346\225\231\347\250\213.md" | 102 +- .../hashCode\344\270\216equals.md" | 0 ...14Synchronized\345\214\272\345\210\253.md" | 0 ...211\221\346\214\207Offer(\344\270\212).md" | 0 ...211\221\346\214\207Offer(\344\270\213).md" | 0 ...36\347\216\260\346\226\271\345\274\217.md" | 50 +- .../\345\215\225\351\223\276\350\241\250.md" | 0 ...12\346\234\211\345\272\217\346\200\247.md" | 0 ...44\350\241\214\345\244\247\345\205\250.md" | 120 +- ...01\350\231\232\345\274\225\347\224\250.md" | 78 +- ...06\345\217\212\350\247\243\345\257\206.md" | 0 .../\346\255\273\351\224\201.md" | 0 ...05\346\266\210\350\264\271\350\200\205.md" | 0 .../\347\256\227\346\263\225.md" | 124 +- ...40\347\232\204\345\216\237\347\220\206.md" | 0 ...05\345\256\271\346\200\273\347\273\223.md" | 430 +- ...16\347\232\204\346\227\245\346\234\237.md" | 68 +- "Java\345\237\272\347\241\200/.DS_Store" | Bin 8196 -> 0 bytes 166 files changed, 17500 insertions(+), 18394 deletions(-) rename "Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" => "AndroidAdavancedPart/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" (100%) rename "Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" => "AndroidAdavancedPart/ART\344\270\216Dalvik.md" (100%) rename "Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" => "AndroidAdavancedPart/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" (100%) rename "Android\345\212\240\345\274\272/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" => "AndroidAdavancedPart/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" (97%) rename "Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" => "AndroidAdavancedPart/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" (100%) rename "Android\345\212\240\345\274\272/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" => "AndroidAdavancedPart/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" (97%) rename "Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" => "AndroidAdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" (100%) rename "Android\345\212\240\345\274\272/AndroidStudio\344\270\255\350\277\233\350\241\214ndk\345\274\200\345\217\221.md" => "AndroidAdavancedPart/AndroidStudio\344\270\255\350\277\233\350\241\214ndk\345\274\200\345\217\221.md" (100%) rename "Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" => "AndroidAdavancedPart/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" (100%) rename "Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" => "AndroidAdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" (100%) rename "Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" => "AndroidAdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/Android\345\272\224\347\224\250\345\217\221\345\270\203.md" => "AndroidAdavancedPart/Android\345\272\224\347\224\250\345\217\221\345\270\203.md" (100%) rename "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" => "AndroidAdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" (100%) rename "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" => "AndroidAdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" => "AndroidAdavancedPart/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" (98%) rename "Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" => AndroidAdavancedPart/ApplicationId vs PackageName.md (100%) rename "Android\345\212\240\345\274\272/AsyncTask\350\257\246\350\247\243.md" => "AndroidAdavancedPart/AsyncTask\350\257\246\350\247\243.md" (97%) rename "Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" => "AndroidAdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" (98%) rename "Android\345\212\240\345\274\272/Fiddler\346\212\223\345\217\226Android\346\211\213\346\234\272\346\225\260\346\215\256\345\214\205.md" => "AndroidAdavancedPart/Fiddler\346\212\223\345\217\226Android\346\211\213\346\234\272\346\225\260\346\215\256\345\214\205.md" (100%) rename "Android\345\212\240\345\274\272/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" => "AndroidAdavancedPart/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" (97%) rename "Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" => "AndroidAdavancedPart/Gradle\344\270\223\351\242\230.md" (100%) rename "Android\345\212\240\345\274\272/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" => "AndroidAdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" (97%) rename "Android\345\212\240\345\274\272/HttpURLConnection\344\270\216HttpClient.md" => "AndroidAdavancedPart/HttpURLConnection\344\270\216HttpClient.md" (97%) rename "Android\345\212\240\345\274\272/HttpURLConnection\350\257\246\350\247\243.md" => "AndroidAdavancedPart/HttpURLConnection\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" => "AndroidAdavancedPart/InstantRun\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" => "AndroidAdavancedPart/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" (100%) rename "Android\345\212\240\345\274\272/ListView\346\272\220\347\240\201\345\210\206\346\236\220.md" => "AndroidAdavancedPart/ListView\346\272\220\347\240\201\345\210\206\346\236\220.md" (100%) rename "Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" => "AndroidAdavancedPart/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" (98%) rename "Android\345\212\240\345\274\272/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" => "AndroidAdavancedPart/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" (97%) rename "Android\345\212\240\345\274\272/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" => "AndroidAdavancedPart/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" (100%) rename "Android\345\212\240\345\274\272/MaterialDesign\344\275\277\347\224\250.md" => "AndroidAdavancedPart/MaterialDesign\344\275\277\347\224\250.md" (100%) rename "Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" => "AndroidAdavancedPart/RecyclerView\344\270\223\351\242\230.md" (100%) rename "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" => "AndroidAdavancedPart/Retrofit\350\257\246\350\247\243(\344\270\212).md" (100%) rename "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" => "AndroidAdavancedPart/Retrofit\350\257\246\350\247\243(\344\270\213).md" (100%) rename "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" => "AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\212).md" (100%) rename "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" => "AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\213).md" (100%) rename "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" => "AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\255).md" (100%) rename "Android\345\212\240\345\274\272/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" => "AndroidAdavancedPart/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" (97%) rename "Android\345\212\240\345\274\272/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" => "AndroidAdavancedPart/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" (97%) rename "Android\345\212\240\345\274\272/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" => "AndroidAdavancedPart/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" (100%) rename "Android\345\212\240\345\274\272/Zipalign\344\274\230\345\214\226.md" => "AndroidAdavancedPart/Zipalign\344\274\230\345\214\226.md" (100%) rename "Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" => "AndroidAdavancedPart/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" => "AndroidAdavancedPart/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" (100%) rename "Android\345\212\240\345\274\272/\345\210\233\345\273\272\345\277\253\346\215\267\346\226\271\345\274\217.md" => "AndroidAdavancedPart/\345\210\233\345\273\272\345\277\253\346\215\267\346\226\271\345\274\217.md" (100%) rename "Android\345\212\240\345\274\272/\345\217\221\345\270\203library\345\210\260Maven\344\273\223\345\272\223.md" => "AndroidAdavancedPart/\345\217\221\345\270\203library\345\210\260Maven\344\273\223\345\272\223.md" (100%) rename "Android\345\212\240\345\274\272/\345\246\202\344\275\225\350\256\251Service\345\270\270\351\251\273\345\206\205\345\255\230.md" => "AndroidAdavancedPart/\345\246\202\344\275\225\350\256\251Service\345\270\270\351\251\273\345\206\205\345\255\230.md" (100%) rename "Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" => "AndroidAdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/\345\270\203\345\261\200\344\274\230\345\214\226.md" => "AndroidAdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" (97%) rename "Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226.md" => "AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226.md" (98%) rename "Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" => "AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" (100%) rename "Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" => "AndroidAdavancedPart/\346\263\250\350\247\243\344\275\277\347\224\250.md" (100%) rename "Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" => "AndroidAdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" (100%) rename "Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" => "AndroidAdavancedPart/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" (100%) rename "Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" => "AndroidAdavancedPart/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" => "AndroidAdavancedPart/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" (98%) rename "Android\345\212\240\345\274\272/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" => "AndroidAdavancedPart/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" (100%) rename "Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" => "AndroidAdavancedPart/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" (100%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" (99%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" (97%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" (98%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" (98%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" (97%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" (98%) rename "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" => "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" (98%) rename "Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" => "AndroidBasicPart/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" (98%) rename "Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" => "AndroidBasicPart/Android\345\212\250\347\224\273.md" (100%) rename "Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" => "AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" (97%) rename "Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" => "AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" (97%) rename "Android\345\237\272\347\241\200/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" => "AndroidBasicPart/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" (98%) rename "Android\345\237\272\347\241\200/Android\347\274\226\347\240\201\350\247\204\350\214\203.md" => "AndroidBasicPart/Android\347\274\226\347\240\201\350\247\204\350\214\203.md" (100%) rename "Android\345\237\272\347\241\200/Ant\346\211\223\345\214\205.md" => "AndroidBasicPart/Ant\346\211\223\345\214\205.md" (98%) rename "Android\345\237\272\347\241\200/Bitmap\344\274\230\345\214\226.md" => "AndroidBasicPart/Bitmap\344\274\230\345\214\226.md" (97%) rename "Android\345\237\272\347\241\200/DLNA\347\256\200\344\273\213.md" => "AndroidBasicPart/DLNA\347\256\200\344\273\213.md" (100%) rename "Android\345\237\272\347\241\200/Fragment\344\270\223\351\242\230.md" => "AndroidBasicPart/Fragment\344\270\223\351\242\230.md" (97%) rename "Android\345\237\272\347\241\200/Home\351\224\256\347\233\221\345\220\254.md" => "AndroidBasicPart/Home\351\224\256\347\233\221\345\220\254.md" (100%) rename "Android\345\237\272\347\241\200/HttpClient\346\211\247\350\241\214Get\345\222\214Post\350\257\267\346\261\202.md" => "AndroidBasicPart/HttpClient\346\211\247\350\241\214Get\345\222\214Post\350\257\267\346\261\202.md" (100%) rename "Android\345\237\272\347\241\200/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" => "AndroidBasicPart/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" (97%) rename "Android\345\237\272\347\241\200/JNI\345\237\272\347\241\200.md" => "AndroidBasicPart/JNI\345\237\272\347\241\200.md" (97%) rename "Android\345\237\272\347\241\200/ListView\344\270\223\351\242\230.md" => "AndroidBasicPart/ListView\344\270\223\351\242\230.md" (97%) rename "Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" => "AndroidBasicPart/Parcelable\345\217\212Serializable.md" (100%) rename "Android\345\237\272\347\241\200/PopupWindow\347\273\206\350\212\202.md" => "AndroidBasicPart/PopupWindow\347\273\206\350\212\202.md" (97%) rename "Android\345\237\272\347\241\200/SDK Manager\346\227\240\346\263\225\346\233\264\346\226\260\347\232\204\351\227\256\351\242\230.md" => "AndroidBasicPart/SDK Manager\346\227\240\346\263\225\346\233\264\346\226\260\347\232\204\351\227\256\351\242\230.md" (100%) rename "Android\345\237\272\347\241\200/Scroller\347\256\200\344\273\213.md" => "AndroidBasicPart/Scroller\347\256\200\344\273\213.md" (98%) rename "Android\345\237\272\347\241\200/ScrollingTabs.md" => AndroidBasicPart/ScrollingTabs.md (96%) rename "Android\345\237\272\347\241\200/Selector\344\275\277\347\224\250.md" => "AndroidBasicPart/Selector\344\275\277\347\224\250.md" (98%) rename "Android\345\237\272\347\241\200/SlidingMenu.md" => AndroidBasicPart/SlidingMenu.md (96%) rename "Android\345\237\272\347\241\200/String\346\240\274\345\274\217\345\214\226.md" => "AndroidBasicPart/String\346\240\274\345\274\217\345\214\226.md" (100%) rename "Android\345\237\272\347\241\200/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" => "AndroidBasicPart/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" (96%) rename "Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" => "AndroidBasicPart/WebView\346\200\273\347\273\223.md" (100%) rename "Android\345\237\272\347\241\200/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" => "AndroidBasicPart/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" (97%) rename "Android\345\237\272\347\241\200/Wifi\347\212\266\346\200\201\347\233\221\345\220\254.md" => "AndroidBasicPart/Wifi\347\212\266\346\200\201\347\233\221\345\220\254.md" (100%) rename "Android\345\237\272\347\241\200/XmlPullParser.md" => AndroidBasicPart/XmlPullParser.md (97%) rename "Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" => "AndroidBasicPart/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" (100%) rename "Android\345\237\272\347\241\200/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" => "AndroidBasicPart/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" (97%) rename "Android\345\237\272\347\241\200/\344\273\243\347\240\201\346\267\267\346\267\206.md" => "AndroidBasicPart/\344\273\243\347\240\201\346\267\267\346\267\206.md" (97%) rename "Android\345\237\272\347\241\200/\344\273\273\345\212\241\347\256\241\347\220\206\345\231\250(ActivityManager).md" => "AndroidBasicPart/\344\273\273\345\212\241\347\256\241\347\220\206\345\231\250(ActivityManager).md" (100%) rename "Android\345\237\272\347\241\200/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" => "AndroidBasicPart/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" (97%) rename "Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" => "AndroidBasicPart/\345\206\205\345\255\230\346\263\204\346\274\217.md" (97%) rename "Android\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213\346\226\255\347\202\271\344\270\213\350\275\275.md" => "AndroidBasicPart/\345\244\232\347\272\277\347\250\213\346\226\255\347\202\271\344\270\213\350\275\275.md" (100%) rename "Android\345\237\272\347\241\200/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" => "AndroidBasicPart/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" (96%) rename "Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" => "AndroidBasicPart/\345\261\217\345\271\225\351\200\202\351\205\215.md" (100%) rename "Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" => "AndroidBasicPart/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" (97%) rename "Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\256\211\350\243\205.md" => "AndroidBasicPart/\345\272\224\347\224\250\345\256\211\350\243\205.md" (97%) rename "Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" => "AndroidBasicPart/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" (95%) rename "Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" => "AndroidBasicPart/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" (97%) rename "Android\345\237\272\347\241\200/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" => "AndroidBasicPart/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" (97%) rename "Android\345\237\272\347\241\200/\346\211\213\346\234\272\346\221\207\346\231\203.md" => "AndroidBasicPart/\346\211\213\346\234\272\346\221\207\346\231\203.md" (100%) rename "Android\345\237\272\347\241\200/\346\220\234\347\264\242\346\241\206.md" => "AndroidBasicPart/\346\220\234\347\264\242\346\241\206.md" (97%) rename "Android\345\237\272\347\241\200/\346\225\260\346\215\256\345\255\230\345\202\250.md" => "AndroidBasicPart/\346\225\260\346\215\256\345\255\230\345\202\250.md" (96%) rename "Android\345\237\272\347\241\200/\346\226\207\344\273\266\344\270\212\344\274\240.md" => "AndroidBasicPart/\346\226\207\344\273\266\344\270\212\344\274\240.md" (100%) rename "Android\345\237\272\347\241\200/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" => "AndroidBasicPart/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" (97%) rename "Android\345\237\272\347\241\200/\346\235\245\347\224\265\347\233\221\345\220\254\345\217\212\345\275\225\351\237\263.md" => "AndroidBasicPart/\346\235\245\347\224\265\347\233\221\345\220\254\345\217\212\345\275\225\351\237\263.md" (100%) rename "Android\345\237\272\347\241\200/\346\250\252\345\220\221ListView.md" => "AndroidBasicPart/\346\250\252\345\220\221ListView.md" (100%) rename "Android\345\237\272\347\241\200/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" => "AndroidBasicPart/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" (97%) rename "Android\345\237\272\347\241\200/\347\227\205\346\257\222.md" => "AndroidBasicPart/\347\227\205\346\257\222.md" (97%) rename "Android\345\237\272\347\241\200/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" => "AndroidBasicPart/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" (100%) rename "Android\345\237\272\347\241\200/\347\237\255\344\277\241\345\271\277\346\222\255\346\216\245\346\224\266\350\200\205.md" => "AndroidBasicPart/\347\237\255\344\277\241\345\271\277\346\222\255\346\216\245\346\224\266\350\200\205.md" (100%) rename "Android\345\237\272\347\241\200/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" => "AndroidBasicPart/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" (97%) rename "Android\345\237\272\347\241\200/\347\253\226\347\235\200\347\232\204Seekbar.md" => "AndroidBasicPart/\347\253\226\347\235\200\347\232\204Seekbar.md" (96%) rename "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211Toast.md" => "AndroidBasicPart/\350\207\252\345\256\232\344\271\211Toast.md" (97%) rename "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" => "AndroidBasicPart/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" (97%) rename "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" => "AndroidBasicPart/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" (98%) rename "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\350\203\214\346\231\257.md" => "AndroidBasicPart/\350\207\252\345\256\232\344\271\211\350\203\214\346\231\257.md" (100%) rename "Android\345\237\272\347\241\200/\350\216\267\345\217\226\344\275\215\347\275\256(LocationManager).md" => "AndroidBasicPart/\350\216\267\345\217\226\344\275\215\347\275\256(LocationManager).md" (100%) rename "Android\345\237\272\347\241\200/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" => "AndroidBasicPart/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" (97%) rename "Android\345\237\272\347\241\200/\350\216\267\345\217\226\346\211\213\346\234\272\344\270\255\346\211\200\346\234\211\345\256\211\350\243\205\347\232\204\347\250\213\345\272\217.md" => "AndroidBasicPart/\350\216\267\345\217\226\346\211\213\346\234\272\344\270\255\346\211\200\346\234\211\345\256\211\350\243\205\347\232\204\347\250\213\345\272\217.md" (100%) rename "Android\345\237\272\347\241\200/\350\216\267\345\217\226\346\211\213\346\234\272\345\217\212SD\345\215\241\345\217\257\347\224\250\345\255\230\345\202\250\347\251\272\351\227\264.md" => "AndroidBasicPart/\350\216\267\345\217\226\346\211\213\346\234\272\345\217\212SD\345\215\241\345\217\257\347\224\250\345\255\230\345\202\250\347\251\272\351\227\264.md" (100%) rename "Android\345\237\272\347\241\200/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" => "AndroidBasicPart/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" (97%) rename "Android\345\237\272\347\241\200/\350\257\273\345\217\226\347\224\250\346\210\267logcat\346\227\245\345\277\227.md" => "AndroidBasicPart/\350\257\273\345\217\226\347\224\250\346\210\267logcat\346\227\245\345\277\227.md" (100%) rename "Android\345\237\272\347\241\200/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" => "AndroidBasicPart/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" (98%) rename "Android\345\237\272\347\241\200/\350\266\205\347\272\247\347\256\241\347\220\206\345\221\230(DevicePoliceManager).md" => "AndroidBasicPart/\350\266\205\347\272\247\347\256\241\347\220\206\345\221\230(DevicePoliceManager).md" (97%) rename "Android\345\237\272\347\241\200/\351\224\201\345\261\217\344\273\245\345\217\212\350\247\243\351\224\201\347\233\221\345\220\254.md" => "AndroidBasicPart/\351\224\201\345\261\217\344\273\245\345\217\212\350\247\243\351\224\201\347\233\221\345\220\254.md" (100%) rename "Android\345\237\272\347\241\200/\351\233\266\346\235\203\351\231\220\344\270\212\344\274\240\346\225\260\346\215\256.md" => "AndroidBasicPart/\351\233\266\346\235\203\351\231\220\344\270\212\344\274\240\346\225\260\346\215\256.md" (97%) rename "Android\345\237\272\347\241\200/\351\237\263\351\207\217\345\217\212\345\261\217\345\271\225\344\272\256\345\272\246\350\260\203\350\212\202.md" => "AndroidBasicPart/\351\237\263\351\207\217\345\217\212\345\261\217\345\271\225\344\272\256\345\272\246\350\260\203\350\212\202.md" (100%) rename "Android\345\237\272\347\241\200/\351\273\221\345\220\215\345\215\225\346\214\202\346\226\255\347\224\265\350\257\235\345\217\212\345\210\240\351\231\244\347\224\265\350\257\235\350\256\260\345\275\225.md" => "AndroidBasicPart/\351\273\221\345\220\215\345\215\225\346\214\202\346\226\255\347\224\265\350\257\235\345\217\212\345\210\240\351\231\244\347\224\265\350\257\235\350\256\260\345\275\225.md" (97%) delete mode 100644 "JS\345\237\272\347\241\200/DOM.md" delete mode 100644 "JS\345\237\272\347\241\200/JS\345\237\272\347\241\200\347\256\200\344\273\213\345\217\212\345\205\245\351\227\250.md" delete mode 100644 "JS\345\237\272\347\241\200/README.md" delete mode 100644 "JS\345\237\272\347\241\200/Window.md" delete mode 100644 "JS\345\237\272\347\241\200/\345\257\271\350\261\241.md" rename "Java\345\237\272\347\241\200/Base64\345\212\240\345\257\206.md" => "JavaBasicKnowledge/Base64\345\212\240\345\257\206.md" (100%) rename "Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" => "JavaBasicKnowledge/Git\345\221\275\344\273\244.md" (100%) rename "Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" => "JavaBasicKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" (100%) rename "Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" => "JavaBasicKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" (100%) rename "Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" => "JavaBasicKnowledge/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" (98%) rename "Java\345\237\272\347\241\200/MD5\345\212\240\345\257\206.md" => "JavaBasicKnowledge/MD5\345\212\240\345\257\206.md" (97%) rename "Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" => "JavaBasicKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" (100%) rename "Java\345\237\272\347\241\200/RMB\345\244\247\345\260\217\345\206\231\350\275\254\346\215\242.md" => "JavaBasicKnowledge/RMB\345\244\247\345\260\217\345\206\231\350\275\254\346\215\242.md" (100%) rename "Java\345\237\272\347\241\200/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" => "JavaBasicKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" (96%) rename "Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" => "JavaBasicKnowledge/hashCode\344\270\216equals.md" (100%) rename "Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" => "JavaBasicKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" (100%) rename "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" => "JavaBasicKnowledge/\345\211\221\346\214\207Offer(\344\270\212).md" (100%) rename "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" => "JavaBasicKnowledge/\345\211\221\346\214\207Offer(\344\270\213).md" (100%) rename "Java\345\237\272\347\241\200/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" => "JavaBasicKnowledge/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" (95%) rename "Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" => "JavaBasicKnowledge/\345\215\225\351\223\276\350\241\250.md" (100%) rename "Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" => "JavaBasicKnowledge/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" (100%) rename "Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" => "JavaBasicKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" (95%) rename "Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" => "JavaBasicKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" (99%) rename "Java\345\237\272\347\241\200/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" => "JavaBasicKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" (100%) rename "Java\345\237\272\347\241\200/\346\255\273\351\224\201.md" => "JavaBasicKnowledge/\346\255\273\351\224\201.md" (100%) rename "Java\345\237\272\347\241\200/\347\224\237\344\272\247\350\200\205\346\266\210\350\264\271\350\200\205.md" => "JavaBasicKnowledge/\347\224\237\344\272\247\350\200\205\346\266\210\350\264\271\350\200\205.md" (100%) rename "Java\345\237\272\347\241\200/\347\256\227\346\263\225.md" => "JavaBasicKnowledge/\347\256\227\346\263\225.md" (97%) rename "Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" => "JavaBasicKnowledge/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" (100%) rename "Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" => "JavaBasicKnowledge/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" (98%) rename "Java\345\237\272\347\241\200/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" => "JavaBasicKnowledge/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" (96%) delete mode 100644 "Java\345\237\272\347\241\200/.DS_Store" diff --git "a/Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" "b/AndroidAdavancedPart/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" similarity index 100% rename from "Android\345\212\240\345\274\272/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" rename to "AndroidAdavancedPart/3D\346\227\213\350\275\254\345\212\250\347\224\273.md" diff --git "a/Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" "b/AndroidAdavancedPart/ART\344\270\216Dalvik.md" similarity index 100% rename from "Android\345\212\240\345\274\272/ART\344\270\216Dalvik.md" rename to "AndroidAdavancedPart/ART\344\270\216Dalvik.md" diff --git "a/Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/AndroidAdavancedPart/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" rename to "AndroidAdavancedPart/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" diff --git "a/Android\345\212\240\345\274\272/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" similarity index 97% rename from "Android\345\212\240\345\274\272/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" index e2528846..1eee7bf4 100644 --- "a/Android\345\212\240\345\274\272/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" +++ "b/AndroidAdavancedPart/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" @@ -1,585 +1,585 @@ -Activity界面绘制过程详解 -=== - -设置界面首先就是`Activity.setContentView()`方法:我们先看一下他的源码: -```java -/** - * Set the activity content from a layout resource. The resource will be - * inflated, adding all top-level views to the activity. - * - * @param layoutResID Resource ID to be inflated. - * - * @see #setContentView(android.view.View) - * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) - */ -public void setContentView(int layoutResID) { - getWindow().setContentView(layoutResID); - initWindowDecorActionBar(); -} -``` -`getWindow()`就是获取该页面的窗体`Window`,该类是一个抽象类,他有一个唯一的子类`PhoneWindow`. -```java -/** - * Retrieve the current {@link android.view.Window} for the activity. - * This can be used to directly access parts of the Window API that - * are not available through Activity/Screen. - * - * @return Window The current window, or null if the activity is not - * visual. - */ -public Window getWindow() { - return mWindow; -} -``` -接下来,我们看一下`PhoneWindow.setContentView()`方法。(简单的理解是`PhoneWindow`把`DectorView(`FrameLayout的子类)`进行包装,将它作为窗口的根`View`) -```java -@Override -public void setContentView(int layoutResID) { - - // mContentParent 是当前window显示内容的父布局,它只能是mDecor或mDecor的子View,如果mContentParent为null,说明是第一次调用setContentView方法 - // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window - // decor, when theme attributes and the like are crystalized. Do not check the feature - // before this happens. - if (mContentParent == null) { - // 内部会创建mContentParent, 如果mDecor为null就创建,然后如果mContentParent为null,就根据当前要显示的主题去添加对应的布局, - // 并把该布局中id为content的ViewGroup赋值给mContentParent。 - installDecor(); - } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { - // 多次调用setContentView,可能是第二次、第三次,先把之前的页面内容移除 - mContentParent.removeAllViews(); - } - - if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { - final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, - getContext()); - transitionTo(newScene); - } else { - // 把布局inflate后添加到mContentParent中 - mLayoutInflater.inflate(layoutResID, mContentParent); - } - final Callback cb = getCallback(); - if (cb != null && !isDestroyed()) { - cb.onContentChanged(); - } -} -``` - -上面简单的说明了installDecor()的作用,这里我们在源码中仔细说明一下, 通过这个源码 -我们知道设置主题要在setContentView之前去调用,如用代码设置`requestWindowFeature()`设置主题时要在`setContentView()`之前设置才有用. -```java -private void installDecor() { - if (mDecor == null) { - // 内部就是new 一个 DecorView - mDecor = generateDecor(); - mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); - mDecor.setIsRootNamespace(true); - if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { - mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); - } - } - if (mContentParent == null) { - // 根据当前主题去选择对应的布局文件,然后把布局中的id=content(一般为FrameLayout)部分赋值给mContentParent. - mContentParent = generateLayout(mDecor); - - // Set up decor part of UI to ignore fitsSystemWindows if appropriate. - mDecor.makeOptionalFitsSystemWindows(); - - final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( - R.id.decor_content_parent); - - if (decorContentParent != null) { - mDecorContentParent = decorContentParent; - mDecorContentParent.setWindowCallback(getCallback()); - if (mDecorContentParent.getTitle() == null) { - mDecorContentParent.setWindowTitle(mTitle); - } - - final int localFeatures = getLocalFeatures(); - for (int i = 0; i < FEATURE_MAX; i++) { - if ((localFeatures & (1 << i)) != 0) { - mDecorContentParent.initFeature(i); - } - } - - mDecorContentParent.setUiOptions(mUiOptions); - - if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 || - (mIconRes != 0 && !mDecorContentParent.hasIcon())) { - mDecorContentParent.setIcon(mIconRes); - } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 && - mIconRes == 0 && !mDecorContentParent.hasIcon()) { - mDecorContentParent.setIcon( - getContext().getPackageManager().getDefaultActivityIcon()); - mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; - } - if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 || - (mLogoRes != 0 && !mDecorContentParent.hasLogo())) { - mDecorContentParent.setLogo(mLogoRes); - } - - // Invalidate if the panel menu hasn't been created before this. - // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu - // being called in the middle of onCreate or similar. - // A pending invalidation will typically be resolved before the posted message - // would run normally in order to satisfy instance state restoration. - PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); - if (!isDestroyed() && (st == null || st.menu == null)) { - invalidatePanelMenu(FEATURE_ACTION_BAR); - } - } else { - mTitleView = (TextView)findViewById(R.id.title); - if (mTitleView != null) { - mTitleView.setLayoutDirection(mDecor.getLayoutDirection()); - if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { - View titleContainer = findViewById( - R.id.title_container); - if (titleContainer != null) { - titleContainer.setVisibility(View.GONE); - } else { - mTitleView.setVisibility(View.GONE); - } - if (mContentParent instanceof FrameLayout) { - ((FrameLayout)mContentParent).setForeground(null); - } - } else { - mTitleView.setText(mTitle); - } - } - } - - if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) { - mDecor.setBackgroundFallback(mBackgroundFallbackResource); - } - - // Only inflate or create a new TransitionManager if the caller hasn't - // already set a custom one. - if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) { - if (mTransitionManager == null) { - final int transitionRes = getWindowStyle().getResourceId( - R.styleable.Window_windowContentTransitionManager, - 0); - if (transitionRes != 0) { - final TransitionInflater inflater = TransitionInflater.from(getContext()); - mTransitionManager = inflater.inflateTransitionManager(transitionRes, - mContentParent); - } else { - mTransitionManager = new TransitionManager(); - } - } - - mEnterTransition = getTransition(mEnterTransition, null, - R.styleable.Window_windowEnterTransition); - mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION, - R.styleable.Window_windowReturnTransition); - mExitTransition = getTransition(mExitTransition, null, - R.styleable.Window_windowExitTransition); - mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION, - R.styleable.Window_windowReenterTransition); - mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null, - R.styleable.Window_windowSharedElementEnterTransition); - mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition, - USE_DEFAULT_TRANSITION, - R.styleable.Window_windowSharedElementReturnTransition); - mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null, - R.styleable.Window_windowSharedElementExitTransition); - mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition, - USE_DEFAULT_TRANSITION, - R.styleable.Window_windowSharedElementReenterTransition); - if (mAllowEnterTransitionOverlap == null) { - mAllowEnterTransitionOverlap = getWindowStyle().getBoolean( - R.styleable.Window_windowAllowEnterTransitionOverlap, true); - } - if (mAllowReturnTransitionOverlap == null) { - mAllowReturnTransitionOverlap = getWindowStyle().getBoolean( - R.styleable.Window_windowAllowReturnTransitionOverlap, true); - } - if (mBackgroundFadeDurationMillis < 0) { - mBackgroundFadeDurationMillis = getWindowStyle().getInteger( - R.styleable.Window_windowTransitionBackgroundFadeDuration, - DEFAULT_BACKGROUND_FADE_DURATION_MS); - } - if (mSharedElementsUseOverlay == null) { - mSharedElementsUseOverlay = getWindowStyle().getBoolean( - R.styleable.Window_windowSharedElementsUseOverlay, true); - } - } - } -} -``` - -我们先看一下`generateLayout`的源码: -```java -protected ViewGroup generateLayout(DecorView decor) { - // Apply data from current theme. - - TypedArray a = getWindowStyle(); - - if (false) { - System.out.println("From style:"); - String s = "Attrs:"; - for (int i = 0; i < R.styleable.Window.length; i++) { - s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "=" - + a.getString(i); - } - System.out.println(s); - } - - mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); - int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) - & (~getForcedWindowFlags()); - if (mIsFloating) { - setLayout(WRAP_CONTENT, WRAP_CONTENT); - setFlags(0, flagsToUpdate); - } else { - setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); - } - - if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { - requestFeature(FEATURE_NO_TITLE); - } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { - // Don't allow an action bar if there is no title. - requestFeature(FEATURE_ACTION_BAR); - } - - if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) { - requestFeature(FEATURE_ACTION_BAR_OVERLAY); - } - - if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) { - requestFeature(FEATURE_ACTION_MODE_OVERLAY); - } - - if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) { - requestFeature(FEATURE_SWIPE_TO_DISMISS); - } - - if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { - setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags())); - } - - if (a.getBoolean(R.styleable.Window_windowTranslucentStatus, - false)) { - setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS - & (~getForcedWindowFlags())); - } - - if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation, - false)) { - setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION - & (~getForcedWindowFlags())); - } - - if (a.getBoolean(R.styleable.Window_windowOverscan, false)) { - setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN&(~getForcedWindowFlags())); - } - - if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { - setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags())); - } - - if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch, - getContext().getApplicationInfo().targetSdkVersion - >= android.os.Build.VERSION_CODES.HONEYCOMB)) { - setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH&(~getForcedWindowFlags())); - } - - a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor); - a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor); - if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) { - if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue(); - a.getValue(R.styleable.Window_windowFixedWidthMajor, - mFixedWidthMajor); - } - if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) { - if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue(); - a.getValue(R.styleable.Window_windowFixedWidthMinor, - mFixedWidthMinor); - } - if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) { - if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue(); - a.getValue(R.styleable.Window_windowFixedHeightMajor, - mFixedHeightMajor); - } - if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) { - if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue(); - a.getValue(R.styleable.Window_windowFixedHeightMinor, - mFixedHeightMinor); - } - if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) { - requestFeature(FEATURE_CONTENT_TRANSITIONS); - } - if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) { - requestFeature(FEATURE_ACTIVITY_TRANSITIONS); - } - - final WindowManager windowService = (WindowManager) getContext().getSystemService( - Context.WINDOW_SERVICE); - if (windowService != null) { - final Display display = windowService.getDefaultDisplay(); - final boolean shouldUseBottomOutset = - display.getDisplayId() == Display.DEFAULT_DISPLAY - || (getForcedWindowFlags() & FLAG_FULLSCREEN) != 0; - if (shouldUseBottomOutset && a.hasValue(R.styleable.Window_windowOutsetBottom)) { - if (mOutsetBottom == null) mOutsetBottom = new TypedValue(); - a.getValue(R.styleable.Window_windowOutsetBottom, - mOutsetBottom); - } - } - - final Context context = getContext(); - final int targetSdk = context.getApplicationInfo().targetSdkVersion; - final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB; - final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; - final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP; - final boolean targetHcNeedsOptions = context.getResources().getBoolean( - R.bool.target_honeycomb_needs_options_menu); - final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE); - - if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) { - addFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY); - } else { - clearFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY); - } - - // Non-floating windows on high end devices must put up decor beneath the system bars and - // therefore must know about visibility changes of those. - if (!mIsFloating && ActivityManager.isHighEndGfx()) { - if (!targetPreL && a.getBoolean( - R.styleable.Window_windowDrawsSystemBarBackgrounds, - false)) { - setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags()); - } - } - if (!mForcedStatusBarColor) { - mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000); - } - if (!mForcedNavigationBarColor) { - mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000); - } - - if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion - >= android.os.Build.VERSION_CODES.HONEYCOMB) { - if (a.getBoolean( - R.styleable.Window_windowCloseOnTouchOutside, - false)) { - setCloseOnTouchOutsideIfNotSet(true); - } - } - - WindowManager.LayoutParams params = getAttributes(); - - if (!hasSoftInputMode()) { - params.softInputMode = a.getInt( - R.styleable.Window_windowSoftInputMode, - params.softInputMode); - } - - if (a.getBoolean(R.styleable.Window_backgroundDimEnabled, - mIsFloating)) { - /* All dialogs should have the window dimmed */ - if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) { - params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; - } - if (!haveDimAmount()) { - params.dimAmount = a.getFloat( - android.R.styleable.Window_backgroundDimAmount, 0.5f); - } - } - - if (params.windowAnimations == 0) { - params.windowAnimations = a.getResourceId( - R.styleable.Window_windowAnimationStyle, 0); - } - - // The rest are only done if this window is not embedded; otherwise, - // the values are inherited from our container. - if (getContainer() == null) { - if (mBackgroundDrawable == null) { - if (mBackgroundResource == 0) { - mBackgroundResource = a.getResourceId( - R.styleable.Window_windowBackground, 0); - } - if (mFrameResource == 0) { - mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0); - } - mBackgroundFallbackResource = a.getResourceId( - R.styleable.Window_windowBackgroundFallback, 0); - if (false) { - System.out.println("Background: " - + Integer.toHexString(mBackgroundResource) + " Frame: " - + Integer.toHexString(mFrameResource)); - } - } - mElevation = a.getDimension(R.styleable.Window_windowElevation, 0); - mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false); - mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT); - } - - // 上面的那一块都是对Activity中设置的主题进行判断。下面就是根据不同的主题,选择不同的布局文件。 - // Inflate the window decor. - - int layoutResource; - int features = getLocalFeatures(); - // System.out.println("Features: 0x" + Integer.toHexString(features)); - if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { - layoutResource = R.layout.screen_swipe_dismiss; - } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { - if (mIsFloating) { - TypedValue res = new TypedValue(); - getContext().getTheme().resolveAttribute( - R.attr.dialogTitleIconsDecorLayout, res, true); - layoutResource = res.resourceId; - } else { - layoutResource = R.layout.screen_title_icons; - } - // XXX Remove this once action bar supports these features. - removeFeature(FEATURE_ACTION_BAR); - // System.out.println("Title Icons!"); - } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 - && (features & (1 << FEATURE_ACTION_BAR)) == 0) { - // Special case for a window with only a progress bar (and title). - // XXX Need to have a no-title version of embedded windows. - layoutResource = R.layout.screen_progress; - // System.out.println("Progress!"); - } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { - // Special case for a window with a custom title. - // If the window is floating, we need a dialog layout - if (mIsFloating) { - TypedValue res = new TypedValue(); - getContext().getTheme().resolveAttribute( - R.attr.dialogCustomTitleDecorLayout, res, true); - layoutResource = res.resourceId; - } else { - layoutResource = R.layout.screen_custom_title; - } - // XXX Remove this once action bar supports these features. - removeFeature(FEATURE_ACTION_BAR); - } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { - // If no other features and not embedded, only need a title. - // If the window is floating, we need a dialog layout - if (mIsFloating) { - TypedValue res = new TypedValue(); - getContext().getTheme().resolveAttribute( - R.attr.dialogTitleDecorLayout, res, true); - layoutResource = res.resourceId; - } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { - layoutResource = a.getResourceId( - R.styleable.Window_windowActionBarFullscreenDecorLayout, - R.layout.screen_action_bar); - } else { - layoutResource = R.layout.screen_title; - } - // System.out.println("Title!"); - } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { - layoutResource = R.layout.screen_simple_overlay_action_mode; - } else { - // Embedded, so no decoration is needed. - layoutResource = R.layout.screen_simple; - // System.out.println("Simple!"); - } - - mDecor.startChanging(); - - // inflate对应的布局文件,并添加到mDecor中。 - View in = mLayoutInflater.inflate(layoutResource, null); - decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - mContentRoot = (ViewGroup) in; - - // 找到布局中android:id="@android:id/content"。的ViewGroup赋值给contentParent,一般是FrameLayout - ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); - if (contentParent == null) { - throw new RuntimeException("Window couldn't find content container view"); - } - - if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { - ProgressBar progress = getCircularProgressBar(false); - if (progress != null) { - progress.setIndeterminate(true); - } - } - - if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { - registerSwipeCallbacks(); - } - - // Remaining setup -- of background and title -- that only applies - // to top-level windows. - if (getContainer() == null) { - final Drawable background; - if (mBackgroundResource != 0) { - background = getContext().getDrawable(mBackgroundResource); - } else { - background = mBackgroundDrawable; - } - mDecor.setWindowBackground(background); - - final Drawable frame; - if (mFrameResource != 0) { - frame = getContext().getDrawable(mFrameResource); - } else { - frame = null; - } - mDecor.setWindowFrame(frame); - - mDecor.setElevation(mElevation); - mDecor.setClipToOutline(mClipToOutline); - - if (mTitle != null) { - setTitle(mTitle); - } - - if (mTitleColor == 0) { - mTitleColor = mTextColor; - } - setTitleColor(mTitleColor); - } - - mDecor.finishChanging(); - - return contentParent; -} -``` -上面看到有很多相应主题的布局文件,我们就以典型的`R.layout.screen_title`为例看一下。 -```xml - - - - // 标题 - - - - // 页面内容 - - -``` -看到这里基本差不多了,我们就以第一次调用`setContentView`方法为例总结一下整体的流程。 -- 第一次调用`setContentView()`方法时会去创建一个`DecorView`,这就是整个窗口的根布局。 -- 接着回去根据我们设置的对应主题,来加载与之对应的布局文件并将其添加到`DecorView`中,然后找到该布局中`id=content`的`ViewGroup`赋值给`mContentParent`。 -- 将我们要设置的`Activity`对应的布局添加到`mContentParent`中。 - ---- - -- 邮箱 :charon.chui@gmail.com +Activity界面绘制过程详解 +=== + +设置界面首先就是`Activity.setContentView()`方法:我们先看一下他的源码: +```java +/** + * Set the activity content from a layout resource. The resource will be + * inflated, adding all top-level views to the activity. + * + * @param layoutResID Resource ID to be inflated. + * + * @see #setContentView(android.view.View) + * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) + */ +public void setContentView(int layoutResID) { + getWindow().setContentView(layoutResID); + initWindowDecorActionBar(); +} +``` +`getWindow()`就是获取该页面的窗体`Window`,该类是一个抽象类,他有一个唯一的子类`PhoneWindow`. +```java +/** + * Retrieve the current {@link android.view.Window} for the activity. + * This can be used to directly access parts of the Window API that + * are not available through Activity/Screen. + * + * @return Window The current window, or null if the activity is not + * visual. + */ +public Window getWindow() { + return mWindow; +} +``` +接下来,我们看一下`PhoneWindow.setContentView()`方法。(简单的理解是`PhoneWindow`把`DectorView(`FrameLayout的子类)`进行包装,将它作为窗口的根`View`) +```java +@Override +public void setContentView(int layoutResID) { + + // mContentParent 是当前window显示内容的父布局,它只能是mDecor或mDecor的子View,如果mContentParent为null,说明是第一次调用setContentView方法 + // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window + // decor, when theme attributes and the like are crystalized. Do not check the feature + // before this happens. + if (mContentParent == null) { + // 内部会创建mContentParent, 如果mDecor为null就创建,然后如果mContentParent为null,就根据当前要显示的主题去添加对应的布局, + // 并把该布局中id为content的ViewGroup赋值给mContentParent。 + installDecor(); + } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + // 多次调用setContentView,可能是第二次、第三次,先把之前的页面内容移除 + mContentParent.removeAllViews(); + } + + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, + getContext()); + transitionTo(newScene); + } else { + // 把布局inflate后添加到mContentParent中 + mLayoutInflater.inflate(layoutResID, mContentParent); + } + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } +} +``` + +上面简单的说明了installDecor()的作用,这里我们在源码中仔细说明一下, 通过这个源码 +我们知道设置主题要在setContentView之前去调用,如用代码设置`requestWindowFeature()`设置主题时要在`setContentView()`之前设置才有用. +```java +private void installDecor() { + if (mDecor == null) { + // 内部就是new 一个 DecorView + mDecor = generateDecor(); + mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + mDecor.setIsRootNamespace(true); + if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { + mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); + } + } + if (mContentParent == null) { + // 根据当前主题去选择对应的布局文件,然后把布局中的id=content(一般为FrameLayout)部分赋值给mContentParent. + mContentParent = generateLayout(mDecor); + + // Set up decor part of UI to ignore fitsSystemWindows if appropriate. + mDecor.makeOptionalFitsSystemWindows(); + + final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( + R.id.decor_content_parent); + + if (decorContentParent != null) { + mDecorContentParent = decorContentParent; + mDecorContentParent.setWindowCallback(getCallback()); + if (mDecorContentParent.getTitle() == null) { + mDecorContentParent.setWindowTitle(mTitle); + } + + final int localFeatures = getLocalFeatures(); + for (int i = 0; i < FEATURE_MAX; i++) { + if ((localFeatures & (1 << i)) != 0) { + mDecorContentParent.initFeature(i); + } + } + + mDecorContentParent.setUiOptions(mUiOptions); + + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 || + (mIconRes != 0 && !mDecorContentParent.hasIcon())) { + mDecorContentParent.setIcon(mIconRes); + } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 && + mIconRes == 0 && !mDecorContentParent.hasIcon()) { + mDecorContentParent.setIcon( + getContext().getPackageManager().getDefaultActivityIcon()); + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; + } + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 || + (mLogoRes != 0 && !mDecorContentParent.hasLogo())) { + mDecorContentParent.setLogo(mLogoRes); + } + + // Invalidate if the panel menu hasn't been created before this. + // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu + // being called in the middle of onCreate or similar. + // A pending invalidation will typically be resolved before the posted message + // would run normally in order to satisfy instance state restoration. + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (!isDestroyed() && (st == null || st.menu == null)) { + invalidatePanelMenu(FEATURE_ACTION_BAR); + } + } else { + mTitleView = (TextView)findViewById(R.id.title); + if (mTitleView != null) { + mTitleView.setLayoutDirection(mDecor.getLayoutDirection()); + if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { + View titleContainer = findViewById( + R.id.title_container); + if (titleContainer != null) { + titleContainer.setVisibility(View.GONE); + } else { + mTitleView.setVisibility(View.GONE); + } + if (mContentParent instanceof FrameLayout) { + ((FrameLayout)mContentParent).setForeground(null); + } + } else { + mTitleView.setText(mTitle); + } + } + } + + if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) { + mDecor.setBackgroundFallback(mBackgroundFallbackResource); + } + + // Only inflate or create a new TransitionManager if the caller hasn't + // already set a custom one. + if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) { + if (mTransitionManager == null) { + final int transitionRes = getWindowStyle().getResourceId( + R.styleable.Window_windowContentTransitionManager, + 0); + if (transitionRes != 0) { + final TransitionInflater inflater = TransitionInflater.from(getContext()); + mTransitionManager = inflater.inflateTransitionManager(transitionRes, + mContentParent); + } else { + mTransitionManager = new TransitionManager(); + } + } + + mEnterTransition = getTransition(mEnterTransition, null, + R.styleable.Window_windowEnterTransition); + mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION, + R.styleable.Window_windowReturnTransition); + mExitTransition = getTransition(mExitTransition, null, + R.styleable.Window_windowExitTransition); + mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION, + R.styleable.Window_windowReenterTransition); + mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null, + R.styleable.Window_windowSharedElementEnterTransition); + mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition, + USE_DEFAULT_TRANSITION, + R.styleable.Window_windowSharedElementReturnTransition); + mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null, + R.styleable.Window_windowSharedElementExitTransition); + mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition, + USE_DEFAULT_TRANSITION, + R.styleable.Window_windowSharedElementReenterTransition); + if (mAllowEnterTransitionOverlap == null) { + mAllowEnterTransitionOverlap = getWindowStyle().getBoolean( + R.styleable.Window_windowAllowEnterTransitionOverlap, true); + } + if (mAllowReturnTransitionOverlap == null) { + mAllowReturnTransitionOverlap = getWindowStyle().getBoolean( + R.styleable.Window_windowAllowReturnTransitionOverlap, true); + } + if (mBackgroundFadeDurationMillis < 0) { + mBackgroundFadeDurationMillis = getWindowStyle().getInteger( + R.styleable.Window_windowTransitionBackgroundFadeDuration, + DEFAULT_BACKGROUND_FADE_DURATION_MS); + } + if (mSharedElementsUseOverlay == null) { + mSharedElementsUseOverlay = getWindowStyle().getBoolean( + R.styleable.Window_windowSharedElementsUseOverlay, true); + } + } + } +} +``` + +我们先看一下`generateLayout`的源码: +```java +protected ViewGroup generateLayout(DecorView decor) { + // Apply data from current theme. + + TypedArray a = getWindowStyle(); + + if (false) { + System.out.println("From style:"); + String s = "Attrs:"; + for (int i = 0; i < R.styleable.Window.length; i++) { + s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "=" + + a.getString(i); + } + System.out.println(s); + } + + mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); + int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) + & (~getForcedWindowFlags()); + if (mIsFloating) { + setLayout(WRAP_CONTENT, WRAP_CONTENT); + setFlags(0, flagsToUpdate); + } else { + setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); + } + + if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { + requestFeature(FEATURE_NO_TITLE); + } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { + // Don't allow an action bar if there is no title. + requestFeature(FEATURE_ACTION_BAR); + } + + if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) { + requestFeature(FEATURE_ACTION_BAR_OVERLAY); + } + + if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) { + requestFeature(FEATURE_ACTION_MODE_OVERLAY); + } + + if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) { + requestFeature(FEATURE_SWIPE_TO_DISMISS); + } + + if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { + setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowTranslucentStatus, + false)) { + setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS + & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation, + false)) { + setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION + & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowOverscan, false)) { + setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN&(~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { + setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch, + getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB)) { + setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH&(~getForcedWindowFlags())); + } + + a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor); + a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor); + if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) { + if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedWidthMajor, + mFixedWidthMajor); + } + if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) { + if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedWidthMinor, + mFixedWidthMinor); + } + if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) { + if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedHeightMajor, + mFixedHeightMajor); + } + if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) { + if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedHeightMinor, + mFixedHeightMinor); + } + if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) { + requestFeature(FEATURE_CONTENT_TRANSITIONS); + } + if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) { + requestFeature(FEATURE_ACTIVITY_TRANSITIONS); + } + + final WindowManager windowService = (WindowManager) getContext().getSystemService( + Context.WINDOW_SERVICE); + if (windowService != null) { + final Display display = windowService.getDefaultDisplay(); + final boolean shouldUseBottomOutset = + display.getDisplayId() == Display.DEFAULT_DISPLAY + || (getForcedWindowFlags() & FLAG_FULLSCREEN) != 0; + if (shouldUseBottomOutset && a.hasValue(R.styleable.Window_windowOutsetBottom)) { + if (mOutsetBottom == null) mOutsetBottom = new TypedValue(); + a.getValue(R.styleable.Window_windowOutsetBottom, + mOutsetBottom); + } + } + + final Context context = getContext(); + final int targetSdk = context.getApplicationInfo().targetSdkVersion; + final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB; + final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; + final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP; + final boolean targetHcNeedsOptions = context.getResources().getBoolean( + R.bool.target_honeycomb_needs_options_menu); + final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE); + + if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) { + addFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY); + } else { + clearFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY); + } + + // Non-floating windows on high end devices must put up decor beneath the system bars and + // therefore must know about visibility changes of those. + if (!mIsFloating && ActivityManager.isHighEndGfx()) { + if (!targetPreL && a.getBoolean( + R.styleable.Window_windowDrawsSystemBarBackgrounds, + false)) { + setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags()); + } + } + if (!mForcedStatusBarColor) { + mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000); + } + if (!mForcedNavigationBarColor) { + mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000); + } + + if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB) { + if (a.getBoolean( + R.styleable.Window_windowCloseOnTouchOutside, + false)) { + setCloseOnTouchOutsideIfNotSet(true); + } + } + + WindowManager.LayoutParams params = getAttributes(); + + if (!hasSoftInputMode()) { + params.softInputMode = a.getInt( + R.styleable.Window_windowSoftInputMode, + params.softInputMode); + } + + if (a.getBoolean(R.styleable.Window_backgroundDimEnabled, + mIsFloating)) { + /* All dialogs should have the window dimmed */ + if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) { + params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; + } + if (!haveDimAmount()) { + params.dimAmount = a.getFloat( + android.R.styleable.Window_backgroundDimAmount, 0.5f); + } + } + + if (params.windowAnimations == 0) { + params.windowAnimations = a.getResourceId( + R.styleable.Window_windowAnimationStyle, 0); + } + + // The rest are only done if this window is not embedded; otherwise, + // the values are inherited from our container. + if (getContainer() == null) { + if (mBackgroundDrawable == null) { + if (mBackgroundResource == 0) { + mBackgroundResource = a.getResourceId( + R.styleable.Window_windowBackground, 0); + } + if (mFrameResource == 0) { + mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0); + } + mBackgroundFallbackResource = a.getResourceId( + R.styleable.Window_windowBackgroundFallback, 0); + if (false) { + System.out.println("Background: " + + Integer.toHexString(mBackgroundResource) + " Frame: " + + Integer.toHexString(mFrameResource)); + } + } + mElevation = a.getDimension(R.styleable.Window_windowElevation, 0); + mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false); + mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT); + } + + // 上面的那一块都是对Activity中设置的主题进行判断。下面就是根据不同的主题,选择不同的布局文件。 + // Inflate the window decor. + + int layoutResource; + int features = getLocalFeatures(); + // System.out.println("Features: 0x" + Integer.toHexString(features)); + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { + layoutResource = R.layout.screen_swipe_dismiss; + } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogTitleIconsDecorLayout, res, true); + layoutResource = res.resourceId; + } else { + layoutResource = R.layout.screen_title_icons; + } + // XXX Remove this once action bar supports these features. + removeFeature(FEATURE_ACTION_BAR); + // System.out.println("Title Icons!"); + } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 + && (features & (1 << FEATURE_ACTION_BAR)) == 0) { + // Special case for a window with only a progress bar (and title). + // XXX Need to have a no-title version of embedded windows. + layoutResource = R.layout.screen_progress; + // System.out.println("Progress!"); + } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { + // Special case for a window with a custom title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogCustomTitleDecorLayout, res, true); + layoutResource = res.resourceId; + } else { + layoutResource = R.layout.screen_custom_title; + } + // XXX Remove this once action bar supports these features. + removeFeature(FEATURE_ACTION_BAR); + } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { + // If no other features and not embedded, only need a title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogTitleDecorLayout, res, true); + layoutResource = res.resourceId; + } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { + layoutResource = a.getResourceId( + R.styleable.Window_windowActionBarFullscreenDecorLayout, + R.layout.screen_action_bar); + } else { + layoutResource = R.layout.screen_title; + } + // System.out.println("Title!"); + } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { + layoutResource = R.layout.screen_simple_overlay_action_mode; + } else { + // Embedded, so no decoration is needed. + layoutResource = R.layout.screen_simple; + // System.out.println("Simple!"); + } + + mDecor.startChanging(); + + // inflate对应的布局文件,并添加到mDecor中。 + View in = mLayoutInflater.inflate(layoutResource, null); + decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mContentRoot = (ViewGroup) in; + + // 找到布局中android:id="@android:id/content"。的ViewGroup赋值给contentParent,一般是FrameLayout + ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); + if (contentParent == null) { + throw new RuntimeException("Window couldn't find content container view"); + } + + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + ProgressBar progress = getCircularProgressBar(false); + if (progress != null) { + progress.setIndeterminate(true); + } + } + + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { + registerSwipeCallbacks(); + } + + // Remaining setup -- of background and title -- that only applies + // to top-level windows. + if (getContainer() == null) { + final Drawable background; + if (mBackgroundResource != 0) { + background = getContext().getDrawable(mBackgroundResource); + } else { + background = mBackgroundDrawable; + } + mDecor.setWindowBackground(background); + + final Drawable frame; + if (mFrameResource != 0) { + frame = getContext().getDrawable(mFrameResource); + } else { + frame = null; + } + mDecor.setWindowFrame(frame); + + mDecor.setElevation(mElevation); + mDecor.setClipToOutline(mClipToOutline); + + if (mTitle != null) { + setTitle(mTitle); + } + + if (mTitleColor == 0) { + mTitleColor = mTextColor; + } + setTitleColor(mTitleColor); + } + + mDecor.finishChanging(); + + return contentParent; +} +``` +上面看到有很多相应主题的布局文件,我们就以典型的`R.layout.screen_title`为例看一下。 +```xml + + + + // 标题 + + + + // 页面内容 + + +``` +看到这里基本差不多了,我们就以第一次调用`setContentView`方法为例总结一下整体的流程。 +- 第一次调用`setContentView()`方法时会去创建一个`DecorView`,这就是整个窗口的根布局。 +- 接着回去根据我们设置的对应主题,来加载与之对应的布局文件并将其添加到`DecorView`中,然后找到该布局中`id=content`的`ViewGroup`赋值给`mContentParent`。 +- 将我们要设置的`Activity`对应的布局添加到`mContentParent`中。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" "b/AndroidAdavancedPart/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" rename to "AndroidAdavancedPart/Android Studio\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\346\223\215\344\275\234.md" diff --git "a/Android\345\212\240\345\274\272/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" similarity index 97% rename from "Android\345\212\240\345\274\272/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" index 2f9fedec..66fb66eb 100644 --- "a/Android\345\212\240\345\274\272/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" +++ "b/AndroidAdavancedPart/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" @@ -1,716 +1,716 @@ -Android Touch事件分发详解 -=== - -先说一些基本的知识,方便后面分析源码时能更好理解。 -- 所有`Touch`事件都被封装成`MotionEvent`对象,包括`Touch`的位置、历史记录、第几个手指等. - -- 事件类型分为`ACTION_DOWN`,`ACTION_UP`,`ACTION_MOVE`,`ACTION_POINTER_DOWN`,`ACTION_POINTER_UP`,`ACTION_CANCEL`, 每个 -一个完整的事件以`ACTION_DOWN`开始`ACTION_UP`结束,并且`ACTION_CANCEL`只能由代码引起.一般对于`CANCEL`的处理和`UP`的相同。 -`CANCEL`的一个简单例子:手指在移动的过程中突然移动到了边界外,那么这时`ACTION_UP`事件了,所以这是的`CANCEL`和`UP`的处理是一致的。 - -- 事件的处理分别为`dispatchTouchEveent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件. - -- 事件从`Activity.dispatchTouchEveent()`开始传递,只要没有停止拦截,就会从最上层(`ViewGroup`)开始一直往下传递,子`View`通过`onTouchEvent()`消费事件。(隧道式向下分发). - -- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件,那么该事件会反序网上传递(从该`View`传递给自己的`ViewGroup`,然后再传给更上层的`ViewGroup`直至传递给`Activity.onTouchEvent()`). -(冒泡式向上处理). - -- 如果`View`没有消费`ACTION_DOWN`事件,之后其他的`MOVE`、`UP`等事件都不会传递过来. - -- 事件由父`View(ViewGroup)`传递给子`View`,`ViewGroup`可以通过`onInterceptTouchEvent()`方法对事件进行拦截,停止其往下传递,如果拦截(返回`true`)后该事件 -会直接走到该`ViewGroup`中的`onTouchEvent()`中,不会再往下传递给子`View`.如果从`DOWN`开始,之后的`MOVE`、`UP`都会直接在该`ViewGroup.onTouchEvent()`中进行处理。 -如果子`View`之前在处理某个事件,但是后续被`ViewGroup`拦截,那么子`View`会接收到`ACTION_CANCEL`. - -- `OnTouchListener`优先于`onTouchEvent()`对事件进行消费。 - -- `TouchTarget`是保存手指点击区域属性的一个类,手指的所有移动过程都会被它记录下来, 包含被`touch`的`View`。 - -废话不多说,直接上源码,源码妥妥的是最新版5.0: -我们先从`Activity.dispatchTouchEveent()`说起: - -```java -/** - * Called to process touch screen events. You can override this to - * intercept all touch screen events before they are dispatched to the - * window. Be sure to call this implementation for touch screen events - * that should be handled normally. - * - * @param ev The touch screen event. - * - * @return boolean Return true if this event was consumed. - */ -public boolean dispatchTouchEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - onUserInteraction(); - } - if (getWindow().superDispatchTouchEvent(ev)) { - return true; - } - return onTouchEvent(ev); -} -``` - -代码一看能感觉出来`DOWN`事件比较特殊。我们继续走到`onUserInteraction()`代码中. -```java -/** - * Called whenever a key, touch, or trackball event is dispatched to the - * activity. Implement this method if you wish to know that the user has - * interacted with the device in some way while your activity is running. - * This callback and {@link #onUserLeaveHint} are intended to help - * activities manage status bar notifications intelligently; specifically, - * for helping activities determine the proper time to cancel a notfication. - * - *

All calls to your activity's {@link #onUserLeaveHint} callback will - * be accompanied by calls to {@link #onUserInteraction}. This - * ensures that your activity will be told of relevant user activity such - * as pulling down the notification pane and touching an item there. - * - *

Note that this callback will be invoked for the touch down action - * that begins a touch gesture, but may not be invoked for the touch-moved - * and touch-up actions that follow. - * - * @see #onUserLeaveHint() - */ -public void onUserInteraction() { -} -``` -但是该方法是空方法,没有具体实现。 我们往下看`getWindow().superDispatchTouchEvent(ev)`. -`getWindow()`获取到当前`Window`对象,表示顶层窗口,管理界面的显示和事件的响应;每个Activity 均会创建一个PhoneWindow对象, -是Activity和整个View系统交互的接口,但是该类是一个抽象类。 -从文档中可以看到`The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window. `, -所以我们找到`PhoneWindow`类,查看它的`superDispatchTouchEvent()`方法。 -```java -@Override -public boolean superDispatchTouchEvent(MotionEvent event) { - return mDecor.superDispatchTouchEvent(event); -} -``` -该方法又是调用了`mDecor.superDispatchTouchEvent(event)`, `mDecor`是什么呢? 从名字中我们大概也能猜出来是当前窗口最顶层的`DecorView`, -`Window`界面的最顶层的`View`对象。 -```java -// This is the top-level view of the window, containing the window decor. -private DecorView mDecor; -``` -讲到这里不妨就提一下`DecorView`. -```java -private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { - ... -} -``` -它集成子`FrameLayout`所有很多时候我们在用布局工具查看的时候发现`Activity`的布局`FrameLayout`的。就是这个原因。 -好了,我们接着看`DecorView`中的`superDispatchTouchEvent()`方法。 -```java -public boolean superDispatchTouchEvent(MotionEvent event) { - return super.dispatchTouchEvent(event); -} -``` -是调用了`super.dispatchTouchEveent()`,而`DecorView`的父类是`FrameLayout`所以我们找到`FrameLayout.dispatchTouchEveent()`. -我们看到`FrameLayout`中没有重写`dispatchTouchEveent()`方法,所以我们再找到`FrameLayout`的父类`ViewGroup`.看`ViewGroup.dispatchTouchEveent()`实现。 -新大陆浮现了... -```java -/** - * {@inheritDoc} - */ -@Override -public boolean dispatchTouchEvent(MotionEvent ev) { - - // Consistency verifier for debugging purposes.是调试使用的,我们不用管这里了。 - if (mInputEventConsistencyVerifier != null) { - mInputEventConsistencyVerifier.onTouchEvent(ev, 1); - } - - boolean handled = false; - // onFilterTouchEventForSecurity()用安全机制来过滤触摸事件,true为不过滤分发下去,false则销毁掉该事件。 - // 方法具体实现是去判断是否被其它窗口遮挡住了,如果遮挡住就要过滤掉该事件。 - if (onFilterTouchEventForSecurity(ev)) { - // 没有被其它窗口遮住 - final int action = ev.getAction(); - final int actionMasked = action & MotionEvent.ACTION_MASK; - - // 下面这一块注释说的很清楚了,就是在`Down`的时候把所有的状态都重置,作为一个新事件的开始。 - // Handle an initial down. - if (actionMasked == MotionEvent.ACTION_DOWN) { - // Throw away all previous state when starting a new touch gesture. - // The framework may have dropped the up or cancel event for the previous gesture - // due to an app switch, ANR, or some other state change. - cancelAndClearTouchTargets(ev); - resetTouchState(); - // 如果是`Down`,那么`mFirstTouchTarget`到这里肯定是`null`.因为是新一系列手势的开始。 - // `mFirstTouchTarget`是处理第一个事件的目标。 - } - - // 检查是否拦截该事件(如果`onInterceptTouchEvent()`返回true就拦截该事件) - // Check for interception. - final boolean intercepted; - if (actionMasked == MotionEvent.ACTION_DOWN - || mFirstTouchTarget != null) { - // 标记事件不允许被拦截, 默认是`false`, 该值可以通过`requestDisallowInterceptTouchEvent(true)`方法来设置, - // 通知父`View`不要拦截该`View`上的事件。 - final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; - if (!disallowIntercept) { - // 判断该`ViewGroup`是否要拦截该事件。`onInterceptTouchEvent()`方法默认返回`false`即不拦截。 - intercepted = onInterceptTouchEvent(ev); - ev.setAction(action); // restore action in case it was changed - } else { - // 子`View`通知父`View`不要拦截。这样就不会走到上面`onInterceptTouchEvent()`方法中了, - // 所以父`View`就不会拦截该事件。 - intercepted = false; - } - } else { - // 注释比较清楚了,就是没有目标来处理该事件,而且也不是一个新的事件`Down`事件(新事件的开始), - // 我们应该拦截下他。 - // There are no touch targets and this action is not an initial down - // so this view group continues to intercept touches. - intercepted = true; - } - - // Check for cancelation.检查当前是否是`Cancel`事件或者是有`Cancel`标记。 - final boolean canceled = resetCancelNextUpFlag(this) - || actionMasked == MotionEvent.ACTION_CANCEL; - - // Update list of touch targets for pointer down, if needed. 这行代码为是否需要将当前的触摸事件分发给多个子`View`, - // 默认为`true`,分发给多个`View`(比如几个子`View`位置重叠)。默认是true - final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; - - // 保存当前要分发给的目标 - TouchTarget newTouchTarget = null; - boolean alreadyDispatchedToNewTouchTarget = false; - - // 如果没取消也不拦截,进入方法内部 - if (!canceled && !intercepted) { - - // 下面这部分代码的意思其实就是找到该事件位置下的`View`(可见或者是在动画中的View), 并且与`pointID`关联。 - if (actionMasked == MotionEvent.ACTION_DOWN - || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) - || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { - final int actionIndex = ev.getActionIndex(); // always 0 for down - final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) - : TouchTarget.ALL_POINTER_IDS; - - // Clean up earlier touch targets for this pointer id in case they - // have become out of sync. - removePointersFromTouchTargets(idBitsToAssign); - - final int childrenCount = mChildrenCount; - if (newTouchTarget == null && childrenCount != 0) { - final float x = ev.getX(actionIndex); - final float y = ev.getY(actionIndex); - // Find a child that can receive the event. - // Scan children from front to back. - final ArrayList preorderedList = buildOrderedChildList(); - final boolean customOrder = preorderedList == null - && isChildrenDrawingOrderEnabled(); - // 遍历找子`View`进行分发了。 - final View[] children = mChildren; - for (int i = childrenCount - 1; i >= 0; i--) { - final int childIndex = customOrder - ? getChildDrawingOrder(childrenCount, i) : i; - final View child = (preorderedList == null) - ? children[childIndex] : preorderedList.get(childIndex); - - // `canViewReceivePointerEvents()`方法会去判断这个`View`是否可见或者在播放动画, - // 只有这两种情况下可以接受事件的分发 - - // `isTransformedTouchPointInView`判断这个事件的坐标值是否在该`View`内。 - if (!canViewReceivePointerEvents(child) - || !isTransformedTouchPointInView(x, y, child, null)) { - continue; - } - - // 找到该`View`对应的在`mFristTouchTarget`中的存储的目标, 判断这个`View`可能已经不是之前`mFristTouchTarget`中的`View`了。 - // 如果找不到就返回null, 这种情况是用于多点触摸, 比如在同一个`View`上按下了多跟手指。 - newTouchTarget = getTouchTarget(child); - if (newTouchTarget != null) { - // Child View已经接受了这个事件了 - // Child is already receiving touch within its bounds. - // Give it the new pointer in addition to the ones it is handling. - newTouchTarget.pointerIdBits |= idBitsToAssign; - // 找到该View了,不用再循环找了 - break; - } - - resetCancelNextUpFlag(child); - // 如果上面没有break,只有newTouchTarget为null,说明上面我们找到的Child View和之前的肯定不是同一个了, - // 是新增的, 比如多点触摸的时候,一个手指按在了这个`View`上,另一个手指按在了另一个`View`上。 - // 这时候我们就看child是否分发该事件。dispatchTransformedTouchEvent如果child为null,就直接该ViewGroup出来事件 - // 如果child不为null,就调用child.dispatchTouchEvent - if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { - // 如果这个Child View能分发,那我们就要把之前存储的值改变成现在的Child View。 - // Child wants to receive touch within its bounds. - mLastTouchDownTime = ev.getDownTime(); - if (preorderedList != null) { - // childIndex points into presorted list, find original index - for (int j = 0; j < childrenCount; j++) { - if (children[childIndex] == mChildren[j]) { - mLastTouchDownIndex = j; - break; - } - } - } else { - mLastTouchDownIndex = childIndex; - } - mLastTouchDownX = ev.getX(); - mLastTouchDownY = ev.getY(); - // 赋值成现在的Child View对应的值,并且会把`mFirstTouchTarget`也改成该值(mFristTouchTarget`与`newTouchTarget`是一样的)。 - newTouchTarget = addTouchTarget(child, idBitsToAssign); - // 分发给子`View`了,不用再继续循环了 - alreadyDispatchedToNewTouchTarget = true; - break; - } - } - if (preorderedList != null) preorderedList.clear(); - } - - // `newTouchTarget == null`就是没有找到新的可以分发该事件的子`View`,那我们只能用上一次的分发对象了。 - if (newTouchTarget == null && mFirstTouchTarget != null) { - // Did not find a child to receive the event. - // Assign the pointer to the least recently added target. - newTouchTarget = mFirstTouchTarget; - while (newTouchTarget.next != null) { - newTouchTarget = newTouchTarget.next; - } - newTouchTarget.pointerIdBits |= idBitsToAssign; - } - } - } - - // DOWN事件在上面会去找touch target - // Dispatch to touch targets. - if (mFirstTouchTarget == null) { - // dispatchTransformedTouchEvent方法中如果child为null,那么就调用super.dispatchTouchEvent(transformedEvent);否则调用child.dispatchTouchEvent(transformedEvent)。 - // `super.dispatchTouchEvent()`也就是说,此时`Viewgroup`处理`touch`消息跟普通`view`一致。普通`View`类内部会调用`onTouchEvent()`方法 - // No touch targets so treat this as an ordinary view. 自己处理 - handled = dispatchTransformedTouchEvent(ev, canceled, null, - TouchTarget.ALL_POINTER_IDS); - } else { - // 分发 - // Dispatch to touch targets, excluding the new touch target if we already - // dispatched to it. Cancel touch targets if necessary. - TouchTarget predecessor = null; - TouchTarget target = mFirstTouchTarget; - while (target != null) { - final TouchTarget next = target.next; - // 找到了新的子`View`,并且这个是新加的对象,上面已经处理过了。 - if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { - handled = true; - } else { - // 否则都调用dispatchTransformedTouchEvent处理,传递给child - final boolean cancelChild = resetCancelNextUpFlag(target.child) - || intercepted; - - // 正常分发 - if (dispatchTransformedTouchEvent(ev, cancelChild, - target.child, target.pointerIdBits)) { - handled = true; - } - - // 如果是onInterceptTouchEvent返回true就会遍历mFirstTouchTarget全部给销毁,这就是为什么onInterceptTouchEvent返回true,之后所有的时间都不会再继续分发的了。 - if (cancelChild) { - if (predecessor == null) { - mFirstTouchTarget = next; - } else { - predecessor.next = next; - } - target.recycle(); - target = next; - continue; - } - } - predecessor = target; - target = next; - } - } - - // Update list of touch targets for pointer up or cancel, if needed. - if (canceled - || actionMasked == MotionEvent.ACTION_UP - || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { - resetTouchState(); - } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { - // 当某个手指抬起的时候,清除他相关的数据。 - final int actionIndex = ev.getActionIndex(); - final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); - removePointersFromTouchTargets(idBitsToRemove); - } - } - - if (!handled && mInputEventConsistencyVerifier != null) { - mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); - } - return handled; -} -``` - -接下来还要说说`dispatchTransformedTouchEvent()`方法,虽然上面也说了大体功能,但是看一下源码能说明另一个问题: -```java -/** - * Transforms a motion event into the coordinate space of a particular child view, - * filters out irrelevant pointer ids, and overrides its action if necessary. - * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. - */ -private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, - View child, int desiredPointerIdBits) { - final boolean handled; - - // Canceling motions is a special case. We don't need to perform any transformations - // or filtering. The important part is the action, not the contents. - final int oldAction = event.getAction(); - - // 这就是为什么时间被拦截之后,之前处理过该事件的`View`会收到`CANCEL`. - if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { - event.setAction(MotionEvent.ACTION_CANCEL); - if (child == null) { - handled = super.dispatchTouchEvent(event); - } else { - // 子`View`去处理,如果子`View`仍然是`ViewGroup`那还是同样的处理,如果子`View`是普通`View`,普通`View`的`dispatchTouchEveent()`会调用`onTouchEvent()`. - handled = child.dispatchTouchEvent(event); - } - event.setAction(oldAction); - return handled; - } - - // Calculate the number of pointers to deliver. - final int oldPointerIdBits = event.getPointerIdBits(); - final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; - - // If for some reason we ended up in an inconsistent state where it looks like we - // might produce a motion event with no pointers in it, then drop the event. - if (newPointerIdBits == 0) { - return false; - } - - // If the number of pointers is the same and we don't need to perform any fancy - // irreversible transformations, then we can reuse the motion event for this - // dispatch as long as we are careful to revert any changes we make. - // Otherwise we need to make a copy. - final MotionEvent transformedEvent; - if (newPointerIdBits == oldPointerIdBits) { - if (child == null || child.hasIdentityMatrix()) { - if (child == null) { - handled = super.dispatchTouchEvent(event); - } else { - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - event.offsetLocation(offsetX, offsetY); - - handled = child.dispatchTouchEvent(event); - - event.offsetLocation(-offsetX, -offsetY); - } - return handled; - } - transformedEvent = MotionEvent.obtain(event); - } else { - transformedEvent = event.split(newPointerIdBits); - } - - // Perform any necessary transformations and dispatch. - if (child == null) { - handled = super.dispatchTouchEvent(transformedEvent); - } else { - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - transformedEvent.offsetLocation(offsetX, offsetY); - if (! child.hasIdentityMatrix()) { - transformedEvent.transform(child.getInverseMatrix()); - } - - handled = child.dispatchTouchEvent(transformedEvent); - } - - // Done. - transformedEvent.recycle(); - return handled; -} -``` - - -上面讲了`ViewGroup`的`dispatchTouchEveent()`有些地方会调用`super.dispatchTouchEveent()`,而`ViewGroup`的父类就是`View`,接下来我们看一下`View.dispatchTouchEveent()`方法: -```java -/** - * Pass the touch screen motion event down to the target view, or this - * view if it is the target. - * - * @param event The motion event to be dispatched. - * @return True if the event was handled by the view, false otherwise. - */ -public boolean dispatchTouchEvent(MotionEvent event) { - boolean result = false; - // 调试用 - if (mInputEventConsistencyVerifier != null) { - mInputEventConsistencyVerifier.onTouchEvent(event, 0); - } - - final int actionMasked = event.getActionMasked(); - if (actionMasked == MotionEvent.ACTION_DOWN) { - // Defensive cleanup for new gesture - stopNestedScroll(); - } - - // 判断该`View`是否被其它`View`遮盖住。 - if (onFilterTouchEventForSecurity(event)) { - //noinspection SimplifiableIfStatement - ListenerInfo li = mListenerInfo; - if (li != null && li.mOnTouchListener != null - && (mViewFlags & ENABLED_MASK) == ENABLED - && li.mOnTouchListener.onTouch(this, event)) { - // 先执行`listener`. - result = true; - } - - if (!result && onTouchEvent(event)) { - // 执行`onTouchEvent()`. - result = true; - } - } - - if (!result && mInputEventConsistencyVerifier != null) { - mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); - } - - // Clean up after nested scrolls if this is the end of a gesture; - // also cancel it if we tried an ACTION_DOWN but we didn't want the rest - // of the gesture. - if (actionMasked == MotionEvent.ACTION_UP || - actionMasked == MotionEvent.ACTION_CANCEL || - (actionMasked == MotionEvent.ACTION_DOWN && !result)) { - stopNestedScroll(); - } - - return result; -} -``` - -通过上面的分析我们看到`View.dispatchTouchEvent()`里面会调用到`onTouchEvent()`来消耗事件。那么`onTouchEvent()`是如何处理的呢?下面我们看一下 -`View.onTouchEvent()`源码: -```java -/** - * Implement this method to handle touch screen motion events. - *

- * If this method is used to detect click actions, it is recommended that - * the actions be performed by implementing and calling - * {@link #performClick()}. This will ensure consistent system behavior, - * including: - *

    - *
  • obeying click sound preferences - *
  • dispatching OnClickListener calls - *
  • handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when - * accessibility features are enabled - *
- * - * @param event The motion event. - * @return True if the event was handled, false otherwise. - */ -public boolean onTouchEvent(MotionEvent event) { - final float x = event.getX(); - final float y = event.getY(); - final int viewFlags = mViewFlags; - - // 对disable按钮的处理,注释说的比较明白,一个disable但是clickable的view仍然会消耗事件,只是不响应而已。 - if ((viewFlags & ENABLED_MASK) == DISABLED) { - if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { - setPressed(false); - } - // A disabled view that is clickable still consumes the touch - // events, it just doesn't respond to them. - return (((viewFlags & CLICKABLE) == CLICKABLE || - (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); - } - - // 关于TouchDelegate,文档中是这样说的The delegate to handle touch events that are physically in this view - // but should be handled by another view. 就是说如果两个View, View2在View1中,View1比较大,如果我们想点击 - // View1的时候,让View2去响应点击事件,这时候就需要使用TouchDelegate来设置。 - // 简单的理解就是如果这个View有自己的时间委托处理人,就交给委托人处理。 - if (mTouchDelegate != null) { - if (mTouchDelegate.onTouchEvent(event)) { - return true; - } - } - - if (((viewFlags & CLICKABLE) == CLICKABLE || - (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { - // 这个View可点击 - switch (event.getAction()) { - case MotionEvent.ACTION_UP: - // 最好先看DOWN后再看MOVE最后看UP。 - // PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动. - // PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作. - boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; - if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { - // 处理点击或长按事件 - // take focus if we don't have it already and we should in - // touch mode. - boolean focusTaken = false; - if (isFocusable() && isFocusableInTouchMode() && !isFocused()) - // 如果现在还没获取到焦点,就再获取一次焦点 - focusTaken = requestFocus(); - } - - // 在前面`DOWN`事件的时候会延迟显示`View`的`pressed`状态,用户可能在我们还没有显示按下状态效果时就不按了.我们还是得在进行实际的点击操作时,让用户看到效果。 - if (prepressed) { - // The button is being released before we actually - // showed it as pressed. Make it show the pressed - // state now (before scheduling the click) to ensure - // the user sees it. - setPressed(true, x, y); - } - - - if (!mHasPerformedLongPress) { - // 判断不是长按 - - // This is a tap, so remove the longpress check - removeLongPressCallback(); - - // Only perform take click actions if we were in the pressed state - if (!focusTaken) { - // Use a Runnable and post this rather than calling - // performClick directly. This lets other visual state - // of the view update before click actions start. - if (mPerformClick == null) { - mPerformClick = new PerformClick(); - } - // PerformClick就是个Runnable,里面执行performClick()方法。performClick()方法中怎么执行呢?我们在后面再说。 - if (!post(mPerformClick)) { - performClick(); - } - } - } - - if (mUnsetPressedState == null) { - mUnsetPressedState = new UnsetPressedState(); - } - // 取消按下状态,UnsetPressedState也是个Runnable,里面执行setPressed(false) - if (prepressed) { - postDelayed(mUnsetPressedState, - ViewConfiguration.getPressedStateDuration()); - } else if (!post(mUnsetPressedState)) { - // If the post failed, unpress right now - mUnsetPressedState.run(); - } - - removeTapCallback(); - } - break; - - case MotionEvent.ACTION_DOWN: - mHasPerformedLongPress = false; - // performButtonActionOnTouchDown()处理鼠标右键菜单,有些View显示右键菜单就直接弹菜单.一般设备用不到鼠标,所以返回false。 - if (performButtonActionOnTouchDown(event)) { - break; - } - - // Walk up the hierarchy to determine if we're inside a scrolling container. - boolean isInScrollingContainer = isInScrollingContainer(); - - // For views inside a scrolling container, delay the pressed feedback for - // a short period in case this is a scroll. - // 就是遍历下View层级,判断这个View是不是在一个能scroll的View中。 - if (isInScrollingContainer) { - // 因为用户可能是点击或者是滚动,所以我们不能立马判断,先给用户设置一个要点击的事件。 - mPrivateFlags |= PFLAG_PREPRESSED; - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - mPendingCheckForTap.x = event.getX(); - mPendingCheckForTap.y = event.getY(); - // 发送一个延时的操作,用于判断用户到底是点击还是滚动。其实就是在tapTimeout中如果用户没有滚动,那就是点击了。 - postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); - } else { - // 设置成点击状态 - // Not inside a scrolling container, so show the feedback right away - setPressed(true, x, y); - // 检查是否是长按,就是过一段时间后如果还在按住,那就是长按了。长按的时间是ViewConfiguration.getLongPressTimeout() - // 也就是500毫秒 - checkForLongClick(0); - } - break; - - case MotionEvent.ACTION_CANCEL: - // 取消按下状态,移动点击消息,移动长按消息。 - setPressed(false); - removeTapCallback(); - removeLongPressCallback(); - break; - - case MotionEvent.ACTION_MOVE: - drawableHotspotChanged(x, y); - - // Be lenient about moving outside of buttons, 检查是否移动到View外面了。 - if (!pointInView(x, y, mTouchSlop)) { - // 移动到区域外面去了,就要取消点击。 - // Outside button - removeTapCallback(); - if ((mPrivateFlags & PFLAG_PRESSED) != 0) { - // Remove any future long press/tap checks - removeLongPressCallback(); - - setPressed(false); - } - } - break; - } - - return true; - } - - return false; -} -``` - - -上面讲了`Touch`事件的分发和处理,随便说一下点击事件: -我们平时使用的时候都知道给`View`设置点击事件是`setOnClickListener()` -```java -/** - * Register a callback to be invoked when this view is clicked. If this view is not - * clickable, it becomes clickable. - * - * @param l The callback that will run - * - * @see #setClickable(boolean) - */ -public void setOnClickListener(OnClickListener l) { - if (!isClickable()) { - setClickable(true); - } - // `getListenerInfo()`就是判断成员变量`mListenerInfo`是否是null,不是就返回,是的话就初始化一个。 - getListenerInfo().mOnClickListener = l; -} -``` - -那什么地方会调用`mListenerInfo.mOnClickListener`呢? -```java -/** - * Call this view's OnClickListener, if it is defined. Performs all normal - * actions associated with clicking: reporting accessibility event, playing - * a sound, etc. - * - * @return True there was an assigned OnClickListener that was called, false - * otherwise is returned. - */ -public boolean performClick() { - final boolean result; - final ListenerInfo li = mListenerInfo; - if (li != null && li.mOnClickListener != null) { - playSoundEffect(SoundEffectConstants.CLICK); - li.mOnClickListener.onClick(this); - result = true; - } else { - result = false; - } - - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); - return result; -} -``` - -讲到这里就明白了。`onTouchEvent()`中的`ACTION_UP`中会调用`performClick()`方法。 - - -到这里,就全部分析完了,这一块还是比较麻烦的,中间查了很多资料,有些地方自己可能也理解的不太对,如果有哪里理解的不对的地方,还请大家指出来。谢谢。 - ---- - -- 邮箱 :charon.chui@gmail.com +Android Touch事件分发详解 +=== + +先说一些基本的知识,方便后面分析源码时能更好理解。 +- 所有`Touch`事件都被封装成`MotionEvent`对象,包括`Touch`的位置、历史记录、第几个手指等. + +- 事件类型分为`ACTION_DOWN`,`ACTION_UP`,`ACTION_MOVE`,`ACTION_POINTER_DOWN`,`ACTION_POINTER_UP`,`ACTION_CANCEL`, 每个 +一个完整的事件以`ACTION_DOWN`开始`ACTION_UP`结束,并且`ACTION_CANCEL`只能由代码引起.一般对于`CANCEL`的处理和`UP`的相同。 +`CANCEL`的一个简单例子:手指在移动的过程中突然移动到了边界外,那么这时`ACTION_UP`事件了,所以这是的`CANCEL`和`UP`的处理是一致的。 + +- 事件的处理分别为`dispatchTouchEveent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件. + +- 事件从`Activity.dispatchTouchEveent()`开始传递,只要没有停止拦截,就会从最上层(`ViewGroup`)开始一直往下传递,子`View`通过`onTouchEvent()`消费事件。(隧道式向下分发). + +- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件,那么该事件会反序网上传递(从该`View`传递给自己的`ViewGroup`,然后再传给更上层的`ViewGroup`直至传递给`Activity.onTouchEvent()`). +(冒泡式向上处理). + +- 如果`View`没有消费`ACTION_DOWN`事件,之后其他的`MOVE`、`UP`等事件都不会传递过来. + +- 事件由父`View(ViewGroup)`传递给子`View`,`ViewGroup`可以通过`onInterceptTouchEvent()`方法对事件进行拦截,停止其往下传递,如果拦截(返回`true`)后该事件 +会直接走到该`ViewGroup`中的`onTouchEvent()`中,不会再往下传递给子`View`.如果从`DOWN`开始,之后的`MOVE`、`UP`都会直接在该`ViewGroup.onTouchEvent()`中进行处理。 +如果子`View`之前在处理某个事件,但是后续被`ViewGroup`拦截,那么子`View`会接收到`ACTION_CANCEL`. + +- `OnTouchListener`优先于`onTouchEvent()`对事件进行消费。 + +- `TouchTarget`是保存手指点击区域属性的一个类,手指的所有移动过程都会被它记录下来, 包含被`touch`的`View`。 + +废话不多说,直接上源码,源码妥妥的是最新版5.0: +我们先从`Activity.dispatchTouchEveent()`说起: + +```java +/** + * Called to process touch screen events. You can override this to + * intercept all touch screen events before they are dispatched to the + * window. Be sure to call this implementation for touch screen events + * that should be handled normally. + * + * @param ev The touch screen event. + * + * @return boolean Return true if this event was consumed. + */ +public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + onUserInteraction(); + } + if (getWindow().superDispatchTouchEvent(ev)) { + return true; + } + return onTouchEvent(ev); +} +``` + +代码一看能感觉出来`DOWN`事件比较特殊。我们继续走到`onUserInteraction()`代码中. +```java +/** + * Called whenever a key, touch, or trackball event is dispatched to the + * activity. Implement this method if you wish to know that the user has + * interacted with the device in some way while your activity is running. + * This callback and {@link #onUserLeaveHint} are intended to help + * activities manage status bar notifications intelligently; specifically, + * for helping activities determine the proper time to cancel a notfication. + * + *

All calls to your activity's {@link #onUserLeaveHint} callback will + * be accompanied by calls to {@link #onUserInteraction}. This + * ensures that your activity will be told of relevant user activity such + * as pulling down the notification pane and touching an item there. + * + *

Note that this callback will be invoked for the touch down action + * that begins a touch gesture, but may not be invoked for the touch-moved + * and touch-up actions that follow. + * + * @see #onUserLeaveHint() + */ +public void onUserInteraction() { +} +``` +但是该方法是空方法,没有具体实现。 我们往下看`getWindow().superDispatchTouchEvent(ev)`. +`getWindow()`获取到当前`Window`对象,表示顶层窗口,管理界面的显示和事件的响应;每个Activity 均会创建一个PhoneWindow对象, +是Activity和整个View系统交互的接口,但是该类是一个抽象类。 +从文档中可以看到`The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window. `, +所以我们找到`PhoneWindow`类,查看它的`superDispatchTouchEvent()`方法。 +```java +@Override +public boolean superDispatchTouchEvent(MotionEvent event) { + return mDecor.superDispatchTouchEvent(event); +} +``` +该方法又是调用了`mDecor.superDispatchTouchEvent(event)`, `mDecor`是什么呢? 从名字中我们大概也能猜出来是当前窗口最顶层的`DecorView`, +`Window`界面的最顶层的`View`对象。 +```java +// This is the top-level view of the window, containing the window decor. +private DecorView mDecor; +``` +讲到这里不妨就提一下`DecorView`. +```java +private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { + ... +} +``` +它集成子`FrameLayout`所有很多时候我们在用布局工具查看的时候发现`Activity`的布局`FrameLayout`的。就是这个原因。 +好了,我们接着看`DecorView`中的`superDispatchTouchEvent()`方法。 +```java +public boolean superDispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); +} +``` +是调用了`super.dispatchTouchEveent()`,而`DecorView`的父类是`FrameLayout`所以我们找到`FrameLayout.dispatchTouchEveent()`. +我们看到`FrameLayout`中没有重写`dispatchTouchEveent()`方法,所以我们再找到`FrameLayout`的父类`ViewGroup`.看`ViewGroup.dispatchTouchEveent()`实现。 +新大陆浮现了... +```java +/** + * {@inheritDoc} + */ +@Override +public boolean dispatchTouchEvent(MotionEvent ev) { + + // Consistency verifier for debugging purposes.是调试使用的,我们不用管这里了。 + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(ev, 1); + } + + boolean handled = false; + // onFilterTouchEventForSecurity()用安全机制来过滤触摸事件,true为不过滤分发下去,false则销毁掉该事件。 + // 方法具体实现是去判断是否被其它窗口遮挡住了,如果遮挡住就要过滤掉该事件。 + if (onFilterTouchEventForSecurity(ev)) { + // 没有被其它窗口遮住 + final int action = ev.getAction(); + final int actionMasked = action & MotionEvent.ACTION_MASK; + + // 下面这一块注释说的很清楚了,就是在`Down`的时候把所有的状态都重置,作为一个新事件的开始。 + // Handle an initial down. + if (actionMasked == MotionEvent.ACTION_DOWN) { + // Throw away all previous state when starting a new touch gesture. + // The framework may have dropped the up or cancel event for the previous gesture + // due to an app switch, ANR, or some other state change. + cancelAndClearTouchTargets(ev); + resetTouchState(); + // 如果是`Down`,那么`mFirstTouchTarget`到这里肯定是`null`.因为是新一系列手势的开始。 + // `mFirstTouchTarget`是处理第一个事件的目标。 + } + + // 检查是否拦截该事件(如果`onInterceptTouchEvent()`返回true就拦截该事件) + // Check for interception. + final boolean intercepted; + if (actionMasked == MotionEvent.ACTION_DOWN + || mFirstTouchTarget != null) { + // 标记事件不允许被拦截, 默认是`false`, 该值可以通过`requestDisallowInterceptTouchEvent(true)`方法来设置, + // 通知父`View`不要拦截该`View`上的事件。 + final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; + if (!disallowIntercept) { + // 判断该`ViewGroup`是否要拦截该事件。`onInterceptTouchEvent()`方法默认返回`false`即不拦截。 + intercepted = onInterceptTouchEvent(ev); + ev.setAction(action); // restore action in case it was changed + } else { + // 子`View`通知父`View`不要拦截。这样就不会走到上面`onInterceptTouchEvent()`方法中了, + // 所以父`View`就不会拦截该事件。 + intercepted = false; + } + } else { + // 注释比较清楚了,就是没有目标来处理该事件,而且也不是一个新的事件`Down`事件(新事件的开始), + // 我们应该拦截下他。 + // There are no touch targets and this action is not an initial down + // so this view group continues to intercept touches. + intercepted = true; + } + + // Check for cancelation.检查当前是否是`Cancel`事件或者是有`Cancel`标记。 + final boolean canceled = resetCancelNextUpFlag(this) + || actionMasked == MotionEvent.ACTION_CANCEL; + + // Update list of touch targets for pointer down, if needed. 这行代码为是否需要将当前的触摸事件分发给多个子`View`, + // 默认为`true`,分发给多个`View`(比如几个子`View`位置重叠)。默认是true + final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; + + // 保存当前要分发给的目标 + TouchTarget newTouchTarget = null; + boolean alreadyDispatchedToNewTouchTarget = false; + + // 如果没取消也不拦截,进入方法内部 + if (!canceled && !intercepted) { + + // 下面这部分代码的意思其实就是找到该事件位置下的`View`(可见或者是在动画中的View), 并且与`pointID`关联。 + if (actionMasked == MotionEvent.ACTION_DOWN + || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) + || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + final int actionIndex = ev.getActionIndex(); // always 0 for down + final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) + : TouchTarget.ALL_POINTER_IDS; + + // Clean up earlier touch targets for this pointer id in case they + // have become out of sync. + removePointersFromTouchTargets(idBitsToAssign); + + final int childrenCount = mChildrenCount; + if (newTouchTarget == null && childrenCount != 0) { + final float x = ev.getX(actionIndex); + final float y = ev.getY(actionIndex); + // Find a child that can receive the event. + // Scan children from front to back. + final ArrayList preorderedList = buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + // 遍历找子`View`进行分发了。 + final View[] children = mChildren; + for (int i = childrenCount - 1; i >= 0; i--) { + final int childIndex = customOrder + ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); + + // `canViewReceivePointerEvents()`方法会去判断这个`View`是否可见或者在播放动画, + // 只有这两种情况下可以接受事件的分发 + + // `isTransformedTouchPointInView`判断这个事件的坐标值是否在该`View`内。 + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { + continue; + } + + // 找到该`View`对应的在`mFristTouchTarget`中的存储的目标, 判断这个`View`可能已经不是之前`mFristTouchTarget`中的`View`了。 + // 如果找不到就返回null, 这种情况是用于多点触摸, 比如在同一个`View`上按下了多跟手指。 + newTouchTarget = getTouchTarget(child); + if (newTouchTarget != null) { + // Child View已经接受了这个事件了 + // Child is already receiving touch within its bounds. + // Give it the new pointer in addition to the ones it is handling. + newTouchTarget.pointerIdBits |= idBitsToAssign; + // 找到该View了,不用再循环找了 + break; + } + + resetCancelNextUpFlag(child); + // 如果上面没有break,只有newTouchTarget为null,说明上面我们找到的Child View和之前的肯定不是同一个了, + // 是新增的, 比如多点触摸的时候,一个手指按在了这个`View`上,另一个手指按在了另一个`View`上。 + // 这时候我们就看child是否分发该事件。dispatchTransformedTouchEvent如果child为null,就直接该ViewGroup出来事件 + // 如果child不为null,就调用child.dispatchTouchEvent + if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { + // 如果这个Child View能分发,那我们就要把之前存储的值改变成现在的Child View。 + // Child wants to receive touch within its bounds. + mLastTouchDownTime = ev.getDownTime(); + if (preorderedList != null) { + // childIndex points into presorted list, find original index + for (int j = 0; j < childrenCount; j++) { + if (children[childIndex] == mChildren[j]) { + mLastTouchDownIndex = j; + break; + } + } + } else { + mLastTouchDownIndex = childIndex; + } + mLastTouchDownX = ev.getX(); + mLastTouchDownY = ev.getY(); + // 赋值成现在的Child View对应的值,并且会把`mFirstTouchTarget`也改成该值(mFristTouchTarget`与`newTouchTarget`是一样的)。 + newTouchTarget = addTouchTarget(child, idBitsToAssign); + // 分发给子`View`了,不用再继续循环了 + alreadyDispatchedToNewTouchTarget = true; + break; + } + } + if (preorderedList != null) preorderedList.clear(); + } + + // `newTouchTarget == null`就是没有找到新的可以分发该事件的子`View`,那我们只能用上一次的分发对象了。 + if (newTouchTarget == null && mFirstTouchTarget != null) { + // Did not find a child to receive the event. + // Assign the pointer to the least recently added target. + newTouchTarget = mFirstTouchTarget; + while (newTouchTarget.next != null) { + newTouchTarget = newTouchTarget.next; + } + newTouchTarget.pointerIdBits |= idBitsToAssign; + } + } + } + + // DOWN事件在上面会去找touch target + // Dispatch to touch targets. + if (mFirstTouchTarget == null) { + // dispatchTransformedTouchEvent方法中如果child为null,那么就调用super.dispatchTouchEvent(transformedEvent);否则调用child.dispatchTouchEvent(transformedEvent)。 + // `super.dispatchTouchEvent()`也就是说,此时`Viewgroup`处理`touch`消息跟普通`view`一致。普通`View`类内部会调用`onTouchEvent()`方法 + // No touch targets so treat this as an ordinary view. 自己处理 + handled = dispatchTransformedTouchEvent(ev, canceled, null, + TouchTarget.ALL_POINTER_IDS); + } else { + // 分发 + // Dispatch to touch targets, excluding the new touch target if we already + // dispatched to it. Cancel touch targets if necessary. + TouchTarget predecessor = null; + TouchTarget target = mFirstTouchTarget; + while (target != null) { + final TouchTarget next = target.next; + // 找到了新的子`View`,并且这个是新加的对象,上面已经处理过了。 + if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { + handled = true; + } else { + // 否则都调用dispatchTransformedTouchEvent处理,传递给child + final boolean cancelChild = resetCancelNextUpFlag(target.child) + || intercepted; + + // 正常分发 + if (dispatchTransformedTouchEvent(ev, cancelChild, + target.child, target.pointerIdBits)) { + handled = true; + } + + // 如果是onInterceptTouchEvent返回true就会遍历mFirstTouchTarget全部给销毁,这就是为什么onInterceptTouchEvent返回true,之后所有的时间都不会再继续分发的了。 + if (cancelChild) { + if (predecessor == null) { + mFirstTouchTarget = next; + } else { + predecessor.next = next; + } + target.recycle(); + target = next; + continue; + } + } + predecessor = target; + target = next; + } + } + + // Update list of touch targets for pointer up or cancel, if needed. + if (canceled + || actionMasked == MotionEvent.ACTION_UP + || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + resetTouchState(); + } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { + // 当某个手指抬起的时候,清除他相关的数据。 + final int actionIndex = ev.getActionIndex(); + final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); + removePointersFromTouchTargets(idBitsToRemove); + } + } + + if (!handled && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); + } + return handled; +} +``` + +接下来还要说说`dispatchTransformedTouchEvent()`方法,虽然上面也说了大体功能,但是看一下源码能说明另一个问题: +```java +/** + * Transforms a motion event into the coordinate space of a particular child view, + * filters out irrelevant pointer ids, and overrides its action if necessary. + * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. + */ +private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, + View child, int desiredPointerIdBits) { + final boolean handled; + + // Canceling motions is a special case. We don't need to perform any transformations + // or filtering. The important part is the action, not the contents. + final int oldAction = event.getAction(); + + // 这就是为什么时间被拦截之后,之前处理过该事件的`View`会收到`CANCEL`. + if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { + event.setAction(MotionEvent.ACTION_CANCEL); + if (child == null) { + handled = super.dispatchTouchEvent(event); + } else { + // 子`View`去处理,如果子`View`仍然是`ViewGroup`那还是同样的处理,如果子`View`是普通`View`,普通`View`的`dispatchTouchEveent()`会调用`onTouchEvent()`. + handled = child.dispatchTouchEvent(event); + } + event.setAction(oldAction); + return handled; + } + + // Calculate the number of pointers to deliver. + final int oldPointerIdBits = event.getPointerIdBits(); + final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; + + // If for some reason we ended up in an inconsistent state where it looks like we + // might produce a motion event with no pointers in it, then drop the event. + if (newPointerIdBits == 0) { + return false; + } + + // If the number of pointers is the same and we don't need to perform any fancy + // irreversible transformations, then we can reuse the motion event for this + // dispatch as long as we are careful to revert any changes we make. + // Otherwise we need to make a copy. + final MotionEvent transformedEvent; + if (newPointerIdBits == oldPointerIdBits) { + if (child == null || child.hasIdentityMatrix()) { + if (child == null) { + handled = super.dispatchTouchEvent(event); + } else { + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + event.offsetLocation(offsetX, offsetY); + + handled = child.dispatchTouchEvent(event); + + event.offsetLocation(-offsetX, -offsetY); + } + return handled; + } + transformedEvent = MotionEvent.obtain(event); + } else { + transformedEvent = event.split(newPointerIdBits); + } + + // Perform any necessary transformations and dispatch. + if (child == null) { + handled = super.dispatchTouchEvent(transformedEvent); + } else { + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + transformedEvent.offsetLocation(offsetX, offsetY); + if (! child.hasIdentityMatrix()) { + transformedEvent.transform(child.getInverseMatrix()); + } + + handled = child.dispatchTouchEvent(transformedEvent); + } + + // Done. + transformedEvent.recycle(); + return handled; +} +``` + + +上面讲了`ViewGroup`的`dispatchTouchEveent()`有些地方会调用`super.dispatchTouchEveent()`,而`ViewGroup`的父类就是`View`,接下来我们看一下`View.dispatchTouchEveent()`方法: +```java +/** + * Pass the touch screen motion event down to the target view, or this + * view if it is the target. + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + */ +public boolean dispatchTouchEvent(MotionEvent event) { + boolean result = false; + // 调试用 + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(event, 0); + } + + final int actionMasked = event.getActionMasked(); + if (actionMasked == MotionEvent.ACTION_DOWN) { + // Defensive cleanup for new gesture + stopNestedScroll(); + } + + // 判断该`View`是否被其它`View`遮盖住。 + if (onFilterTouchEventForSecurity(event)) { + //noinspection SimplifiableIfStatement + ListenerInfo li = mListenerInfo; + if (li != null && li.mOnTouchListener != null + && (mViewFlags & ENABLED_MASK) == ENABLED + && li.mOnTouchListener.onTouch(this, event)) { + // 先执行`listener`. + result = true; + } + + if (!result && onTouchEvent(event)) { + // 执行`onTouchEvent()`. + result = true; + } + } + + if (!result && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + + // Clean up after nested scrolls if this is the end of a gesture; + // also cancel it if we tried an ACTION_DOWN but we didn't want the rest + // of the gesture. + if (actionMasked == MotionEvent.ACTION_UP || + actionMasked == MotionEvent.ACTION_CANCEL || + (actionMasked == MotionEvent.ACTION_DOWN && !result)) { + stopNestedScroll(); + } + + return result; +} +``` + +通过上面的分析我们看到`View.dispatchTouchEvent()`里面会调用到`onTouchEvent()`来消耗事件。那么`onTouchEvent()`是如何处理的呢?下面我们看一下 +`View.onTouchEvent()`源码: +```java +/** + * Implement this method to handle touch screen motion events. + *

+ * If this method is used to detect click actions, it is recommended that + * the actions be performed by implementing and calling + * {@link #performClick()}. This will ensure consistent system behavior, + * including: + *

    + *
  • obeying click sound preferences + *
  • dispatching OnClickListener calls + *
  • handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when + * accessibility features are enabled + *
+ * + * @param event The motion event. + * @return True if the event was handled, false otherwise. + */ +public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + final int viewFlags = mViewFlags; + + // 对disable按钮的处理,注释说的比较明白,一个disable但是clickable的view仍然会消耗事件,只是不响应而已。 + if ((viewFlags & ENABLED_MASK) == DISABLED) { + if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { + setPressed(false); + } + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (((viewFlags & CLICKABLE) == CLICKABLE || + (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); + } + + // 关于TouchDelegate,文档中是这样说的The delegate to handle touch events that are physically in this view + // but should be handled by another view. 就是说如果两个View, View2在View1中,View1比较大,如果我们想点击 + // View1的时候,让View2去响应点击事件,这时候就需要使用TouchDelegate来设置。 + // 简单的理解就是如果这个View有自己的时间委托处理人,就交给委托人处理。 + if (mTouchDelegate != null) { + if (mTouchDelegate.onTouchEvent(event)) { + return true; + } + } + + if (((viewFlags & CLICKABLE) == CLICKABLE || + (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { + // 这个View可点击 + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + // 最好先看DOWN后再看MOVE最后看UP。 + // PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动. + // PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作. + boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; + if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { + // 处理点击或长按事件 + // take focus if we don't have it already and we should in + // touch mode. + boolean focusTaken = false; + if (isFocusable() && isFocusableInTouchMode() && !isFocused()) + // 如果现在还没获取到焦点,就再获取一次焦点 + focusTaken = requestFocus(); + } + + // 在前面`DOWN`事件的时候会延迟显示`View`的`pressed`状态,用户可能在我们还没有显示按下状态效果时就不按了.我们还是得在进行实际的点击操作时,让用户看到效果。 + if (prepressed) { + // The button is being released before we actually + // showed it as pressed. Make it show the pressed + // state now (before scheduling the click) to ensure + // the user sees it. + setPressed(true, x, y); + } + + + if (!mHasPerformedLongPress) { + // 判断不是长按 + + // This is a tap, so remove the longpress check + removeLongPressCallback(); + + // Only perform take click actions if we were in the pressed state + if (!focusTaken) { + // Use a Runnable and post this rather than calling + // performClick directly. This lets other visual state + // of the view update before click actions start. + if (mPerformClick == null) { + mPerformClick = new PerformClick(); + } + // PerformClick就是个Runnable,里面执行performClick()方法。performClick()方法中怎么执行呢?我们在后面再说。 + if (!post(mPerformClick)) { + performClick(); + } + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(); + } + // 取消按下状态,UnsetPressedState也是个Runnable,里面执行setPressed(false) + if (prepressed) { + postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(); + } + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + // performButtonActionOnTouchDown()处理鼠标右键菜单,有些View显示右键菜单就直接弹菜单.一般设备用不到鼠标,所以返回false。 + if (performButtonActionOnTouchDown(event)) { + break; + } + + // Walk up the hierarchy to determine if we're inside a scrolling container. + boolean isInScrollingContainer = isInScrollingContainer(); + + // For views inside a scrolling container, delay the pressed feedback for + // a short period in case this is a scroll. + // 就是遍历下View层级,判断这个View是不是在一个能scroll的View中。 + if (isInScrollingContainer) { + // 因为用户可能是点击或者是滚动,所以我们不能立马判断,先给用户设置一个要点击的事件。 + mPrivateFlags |= PFLAG_PREPRESSED; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + // 发送一个延时的操作,用于判断用户到底是点击还是滚动。其实就是在tapTimeout中如果用户没有滚动,那就是点击了。 + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } else { + // 设置成点击状态 + // Not inside a scrolling container, so show the feedback right away + setPressed(true, x, y); + // 检查是否是长按,就是过一段时间后如果还在按住,那就是长按了。长按的时间是ViewConfiguration.getLongPressTimeout() + // 也就是500毫秒 + checkForLongClick(0); + } + break; + + case MotionEvent.ACTION_CANCEL: + // 取消按下状态,移动点击消息,移动长按消息。 + setPressed(false); + removeTapCallback(); + removeLongPressCallback(); + break; + + case MotionEvent.ACTION_MOVE: + drawableHotspotChanged(x, y); + + // Be lenient about moving outside of buttons, 检查是否移动到View外面了。 + if (!pointInView(x, y, mTouchSlop)) { + // 移动到区域外面去了,就要取消点击。 + // Outside button + removeTapCallback(); + if ((mPrivateFlags & PFLAG_PRESSED) != 0) { + // Remove any future long press/tap checks + removeLongPressCallback(); + + setPressed(false); + } + } + break; + } + + return true; + } + + return false; +} +``` + + +上面讲了`Touch`事件的分发和处理,随便说一下点击事件: +我们平时使用的时候都知道给`View`设置点击事件是`setOnClickListener()` +```java +/** + * Register a callback to be invoked when this view is clicked. If this view is not + * clickable, it becomes clickable. + * + * @param l The callback that will run + * + * @see #setClickable(boolean) + */ +public void setOnClickListener(OnClickListener l) { + if (!isClickable()) { + setClickable(true); + } + // `getListenerInfo()`就是判断成员变量`mListenerInfo`是否是null,不是就返回,是的话就初始化一个。 + getListenerInfo().mOnClickListener = l; +} +``` + +那什么地方会调用`mListenerInfo.mOnClickListener`呢? +```java +/** + * Call this view's OnClickListener, if it is defined. Performs all normal + * actions associated with clicking: reporting accessibility event, playing + * a sound, etc. + * + * @return True there was an assigned OnClickListener that was called, false + * otherwise is returned. + */ +public boolean performClick() { + final boolean result; + final ListenerInfo li = mListenerInfo; + if (li != null && li.mOnClickListener != null) { + playSoundEffect(SoundEffectConstants.CLICK); + li.mOnClickListener.onClick(this); + result = true; + } else { + result = false; + } + + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); + return result; +} +``` + +讲到这里就明白了。`onTouchEvent()`中的`ACTION_UP`中会调用`performClick()`方法。 + + +到这里,就全部分析完了,这一块还是比较麻烦的,中间查了很多资料,有些地方自己可能也理解的不太对,如果有哪里理解的不对的地方,还请大家指出来。谢谢。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" "b/AndroidAdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" rename to "AndroidAdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" diff --git "a/Android\345\212\240\345\274\272/AndroidStudio\344\270\255\350\277\233\350\241\214ndk\345\274\200\345\217\221.md" "b/AndroidAdavancedPart/AndroidStudio\344\270\255\350\277\233\350\241\214ndk\345\274\200\345\217\221.md" similarity index 100% rename from "Android\345\212\240\345\274\272/AndroidStudio\344\270\255\350\277\233\350\241\214ndk\345\274\200\345\217\221.md" rename to "AndroidAdavancedPart/AndroidStudio\344\270\255\350\277\233\350\241\214ndk\345\274\200\345\217\221.md" diff --git "a/Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" "b/AndroidAdavancedPart/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" similarity index 100% rename from "Android\345\212\240\345\274\272/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" rename to "AndroidAdavancedPart/AndroidStudio\346\217\220\351\253\230Build\351\200\237\345\272\246.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" "b/AndroidAdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" rename to "AndroidAdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\272\224\347\224\250\345\217\221\345\270\203.md" "b/AndroidAdavancedPart/Android\345\272\224\347\224\250\345\217\221\345\270\203.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android\345\272\224\347\224\250\345\217\221\345\270\203.md" rename to "AndroidAdavancedPart/Android\345\272\224\347\224\250\345\217\221\345\270\203.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" "b/AndroidAdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" rename to "AndroidAdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" "b/AndroidAdavancedPart/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" similarity index 98% rename from "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" rename to "AndroidAdavancedPart/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" index fc709e35..9806157a 100644 --- "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" +++ "b/AndroidAdavancedPart/Android\345\274\200\345\217\221\345\267\245\345\205\267\345\217\212\347\261\273\345\272\223.md" @@ -1,86 +1,86 @@ -Android开发工具及类库 -=== - -在项目开发过程中,总有一些必要的工具和类库。下面就简单介绍下我常用的一些(还在用`Eclipse`的请无视)。 - -1. [volley](https://android.googlesource.com/platform/frameworks/volley) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/volley.png?raw=true) -在`Google I/0 2013`中发布了`Volley`.`Volley`是`Android`平台上的网络通信库,能使网络通信更快,更简单,更健壮。 -这是`Volley`名称的由来:`a burst or emission of many things or a large amount at once`.`Volley`特别适合数据量不大但是通信频繁的场景。 -`Github`上面已经有大神做了镜像,使用更方便有木有。[Volley On Github](https://github.com/mcxiaoke/android-volley) - -2. [Gson](https://code.google.com/p/google-gson/) -`Json`转换神器。 - -3. [GsonFormat](https://github.com/zzz40500/GsonFormat) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/GsonFormat.gif?raw=true) -既然用了`Gson`怎么能少了该神器呢? - -4. [android-butterknife-zelezny](https://github.com/avast/android-butterknife-zelezny) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/zelezny_animated.gif?raw=true) -使用[butterknife](https://github.com/JakeWharton/butterknife)制作的`Android Studio/IDEA`插件。非常方便有木有。 - -5. [android-selector-chapek](https://github.com/inmite/android-selector-chapek) -`selector`写起来是不是很麻烦?以后让`UI`规范化命名,然后就没有然后了。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/select_folder.png?raw=true) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/select_option.png?raw=true) -接下来你就会在`drawable`目录发现对应的`selector`文件。 - -6. [leakcanary](https://github.com/square/leakcanary) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/screenshot.png?raw=true) -内存泄漏你怕不怕? - -7. [fresco](https://github.com/facebook/fresco) -怎么能少了对图片的处理呢?`Fracebook`出品。更快、更强、更方便。 - -8. [android-resource-remover](https://github.com/KeepSafe/android-resource-remover) - 开发过程中可能会经常遇到需求的变更,时间长了,项目中的无用资源就会越来越多。 虽然在`Gradle`中支持相应的配置来去除无用资源: - - ```xml - buildTypes { - debug { - minifyEnabled false - zipAlignEnabled false - shrinkResources false - } - - release { - zipAlignEnabled true - // remove unused resources - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release - } - } - ``` - - 但是这只是在打包的时候不会打进去无用的资源,但是这些资源还是会在工程中。 - 那我们怎么能快速的移除掉这些无用资源呢?答案也很简单,就是使用`lint`检查出无用的资源后用工具删除,这个工具就是`android-resource-remover`。 - 因为它是一个`python`脚本,所以如果不懂`python`的话使用起来会比较麻烦,下面就介绍一下具体的使用方法: - - 下载并安装`Python 2.x`版本 - 去[Python](https://www.python.org/)下载后即可,这里要下载2.x版本,因为3.x版本对语法做了很多改动,可能会不兼容,下载完成后安装就可。安装完成后将安装路径加入到`Path`中。如`D:\Python;`。 - - 安装`android-resource-remover` - 在命令行输入下面的命令`pip install android-resource-remover`。 这里有些电脑可能会提示错误,是因为没有安装`pip`导致的,具体可以看[pip](https://pip.pypa.io/en/latest/installing.html)找到安装的方法。上面介绍了要下载`get-pip.py`后执行`python get-pip.py`就能安装了。 - - 将`D:\Python\Scripts`添加到`Path`中。 - - 将`lint`命令添加到`Path`中,`D:\android-sdk-windows\tools`. - - 在`Studio`右侧的`Gradle`窗口中执行`lint`任务。 这样就会在`app/build/outputs`下生成`lint-results.xml`文件。下一步清理的时候需要使用`lint-results.xml`文件。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lint.png?raw=true) - - 进入到`Android Studio`中的具体项目中执行`./gradlew clean`后再执行`./gradlew lint && android-resource-remover --xml app/build/outputs/lint-results.xml` - -9. [stetho](https://github.com/facebook/stetho) - `facebook`出品。快速查看布局、数据库、网络请求。实在不能再方便了。 -10. [RxJava](https://github.com/ReactiveX/RxJava) - 用了后你会爱上它。 -11. [Retrofilt](https://github.com/square/retrofit) - `Square`出品。大神`JakeWharton`主导出品的网络请求框架。内部结合`OkHttp`。结合`RxJava`使用非常方便。 -12. [android-architecture](https://github.com/googlesamples/android-architecture) - 放到这里可能不太合适,因为它并不是工具和类库,而是`Google`官方发布的`Android`架构示例。非常值得参考。 -13. [AndroidWiFiADB](https://github.com/pedrovgs/AndroidWiFiADB) - 还在为数据线不够用而烦恼嘛? - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +Android开发工具及类库 +=== + +在项目开发过程中,总有一些必要的工具和类库。下面就简单介绍下我常用的一些(还在用`Eclipse`的请无视)。 + +1. [volley](https://android.googlesource.com/platform/frameworks/volley) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/volley.png?raw=true) +在`Google I/0 2013`中发布了`Volley`.`Volley`是`Android`平台上的网络通信库,能使网络通信更快,更简单,更健壮。 +这是`Volley`名称的由来:`a burst or emission of many things or a large amount at once`.`Volley`特别适合数据量不大但是通信频繁的场景。 +`Github`上面已经有大神做了镜像,使用更方便有木有。[Volley On Github](https://github.com/mcxiaoke/android-volley) + +2. [Gson](https://code.google.com/p/google-gson/) +`Json`转换神器。 + +3. [GsonFormat](https://github.com/zzz40500/GsonFormat) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/GsonFormat.gif?raw=true) +既然用了`Gson`怎么能少了该神器呢? + +4. [android-butterknife-zelezny](https://github.com/avast/android-butterknife-zelezny) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/zelezny_animated.gif?raw=true) +使用[butterknife](https://github.com/JakeWharton/butterknife)制作的`Android Studio/IDEA`插件。非常方便有木有。 + +5. [android-selector-chapek](https://github.com/inmite/android-selector-chapek) +`selector`写起来是不是很麻烦?以后让`UI`规范化命名,然后就没有然后了。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/select_folder.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/select_option.png?raw=true) +接下来你就会在`drawable`目录发现对应的`selector`文件。 + +6. [leakcanary](https://github.com/square/leakcanary) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/screenshot.png?raw=true) +内存泄漏你怕不怕? + +7. [fresco](https://github.com/facebook/fresco) +怎么能少了对图片的处理呢?`Fracebook`出品。更快、更强、更方便。 + +8. [android-resource-remover](https://github.com/KeepSafe/android-resource-remover) + 开发过程中可能会经常遇到需求的变更,时间长了,项目中的无用资源就会越来越多。 虽然在`Gradle`中支持相应的配置来去除无用资源: + + ```xml + buildTypes { + debug { + minifyEnabled false + zipAlignEnabled false + shrinkResources false + } + + release { + zipAlignEnabled true + // remove unused resources + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + ``` + + 但是这只是在打包的时候不会打进去无用的资源,但是这些资源还是会在工程中。 + 那我们怎么能快速的移除掉这些无用资源呢?答案也很简单,就是使用`lint`检查出无用的资源后用工具删除,这个工具就是`android-resource-remover`。 + 因为它是一个`python`脚本,所以如果不懂`python`的话使用起来会比较麻烦,下面就介绍一下具体的使用方法: + - 下载并安装`Python 2.x`版本 + 去[Python](https://www.python.org/)下载后即可,这里要下载2.x版本,因为3.x版本对语法做了很多改动,可能会不兼容,下载完成后安装就可。安装完成后将安装路径加入到`Path`中。如`D:\Python;`。 + - 安装`android-resource-remover` + 在命令行输入下面的命令`pip install android-resource-remover`。 这里有些电脑可能会提示错误,是因为没有安装`pip`导致的,具体可以看[pip](https://pip.pypa.io/en/latest/installing.html)找到安装的方法。上面介绍了要下载`get-pip.py`后执行`python get-pip.py`就能安装了。 + - 将`D:\Python\Scripts`添加到`Path`中。 + - 将`lint`命令添加到`Path`中,`D:\android-sdk-windows\tools`. + - 在`Studio`右侧的`Gradle`窗口中执行`lint`任务。 这样就会在`app/build/outputs`下生成`lint-results.xml`文件。下一步清理的时候需要使用`lint-results.xml`文件。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lint.png?raw=true) + - 进入到`Android Studio`中的具体项目中执行`./gradlew clean`后再执行`./gradlew lint && android-resource-remover --xml app/build/outputs/lint-results.xml` + +9. [stetho](https://github.com/facebook/stetho) + `facebook`出品。快速查看布局、数据库、网络请求。实在不能再方便了。 +10. [RxJava](https://github.com/ReactiveX/RxJava) + 用了后你会爱上它。 +11. [Retrofilt](https://github.com/square/retrofit) + `Square`出品。大神`JakeWharton`主导出品的网络请求框架。内部结合`OkHttp`。结合`RxJava`使用非常方便。 +12. [android-architecture](https://github.com/googlesamples/android-architecture) + 放到这里可能不太合适,因为它并不是工具和类库,而是`Google`官方发布的`Android`架构示例。非常值得参考。 +13. [AndroidWiFiADB](https://github.com/pedrovgs/AndroidWiFiADB) + 还在为数据线不够用而烦恼嘛? + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" b/AndroidAdavancedPart/ApplicationId vs PackageName.md similarity index 100% rename from "Android\345\212\240\345\274\272/ApplicationId vs PackageName.md" rename to AndroidAdavancedPart/ApplicationId vs PackageName.md diff --git "a/Android\345\212\240\345\274\272/AsyncTask\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/AsyncTask\350\257\246\350\247\243.md" similarity index 97% rename from "Android\345\212\240\345\274\272/AsyncTask\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/AsyncTask\350\257\246\350\247\243.md" index 90382e54..6550cc67 100644 --- "a/Android\345\212\240\345\274\272/AsyncTask\350\257\246\350\247\243.md" +++ "b/AndroidAdavancedPart/AsyncTask\350\257\246\350\247\243.md" @@ -1,933 +1,933 @@ -AsyncTask详解 -=== - -`AsyncTask`简单的说其实就是`Handler`和`Thread`的结合,就想下面自己写的`MyAsyncTask`一样,这就是它的基本远离,当然它并不止这么简单。 - -- 经典版异步任务 - -```java -public abstract class MyAsyncTask { - private Handler handler = new Handler(){ - public void handleMessage(android.os.Message msg) { - onPostExecute(); - }; - }; - - /** - * 后台任务执行之前 提示用户的界面操作. - */ - public abstract void onPreExecute(); - - /** - * 后台任务执行之后 更新界面的操作. - */ - public abstract void onPostExecute(); - - /** - * 在后台执行的一个耗时的操作. - */ - public abstract void doInBackground(); - - - public void execute(){ - //1. 耗时任务执行之前通知界面更新 - onPreExecute(); - new Thread(){ - public void run() { - doInBackground(); - handler.sendEmptyMessage(0); - }; - }.start(); - - } -} -``` - -- AsyncTask - -```java -new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - blackNumberInfos = dao.findByPage(startIndex, maxNumber); - return null; - } - @Override - protected void onPreExecute() { - loading.setVisibility(View.VISIBLE); - super.onPreExecute(); - } - @Override - protected void onPostExecute(Void result) { - loading.setVisibility(View.INVISIBLE); - if (adapter == null) {// 第一次加载数据 数据适配器还不存在 - adapter = new CallSmsAdapter(); - lv_callsms_safe.setAdapter(adapter); - } else {// 有新的数据被添加进来. - adapter.notifyDataSetChanged();// 通知数据适配器 数据变化了. - } - super.onPostExecute(result); - } - }.execute(); -类的构造方法中接收三个参数,这里我们不用参数就都给它传Void,new出来AsyncTask类之后然后重写这三个方法, -最后别忘了执行execute方法,其实它的内部和我们写的经典版的异步任务相同,也是里面写了一个在新的线程中去执行耗时的操作, -然后用handler发送Message对象,主线程收到这个Message之后去执行onPostExecute中的内容。 - - -//AsyncTask ,params 异步任务执行(doBackgroud方法)需要的参数这个参数的实参可以由execute()方法的参数传入, -// Progess 执行的进度,result是(doBackground方法)执行后的结果 -new AsyncTask() { - ProgressDialog pd; - @Override - protected Boolean doInBackground(String... params) { //这里返回的就是执行的接口,这个返回的结果会传递给onPostExecute的参数 - try { - String filename = params[0];//得到execute传入的参数 - File file = new File(Environment.getExternalStorageDirectory(),filename); - FileOutputStream fos = new FileOutputStream(file); - SmsUtils.backUp(getApplicationContext(), fos, new BackUpStatusListener() { - public void onBackUpProcess(int process) { - pd.setProgress(process); - } - - public void beforeBackup(int max) { - pd.setMax(max); - } - }); - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - @Override - protected void onPreExecute() { - pd = new ProgressDialog(AtoolsActivity.this); - pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - pd.setMessage("正在备份短信"); - pd.show(); - super.onPreExecute(); - } - @Override - protected void onPostExecute(Boolean result) { - pd.dismiss(); - if(result){ - Toast.makeText(getApplicationContext(), "备份成功", 0).show(); - }else{ - Toast.makeText(getApplicationContext(), "备份失败", 0).show(); - } - super.onPostExecute(result); - } - - }.execute("backup.xml"); //这里传入的参数就是doInBackgound中的参数,会传入到doInBackground中 - -ProgressDialog有个方法 -incrementProgressBy(int num);方法,这个方法能够让进度条自动增加,如果参数为1就是进度条累加1。 - -可以给ProgressDialog添加一个监听dismiss的监听器。pd.setOnDismisListener(DismisListener listener);让其在取消显示后做什么事 -``` - -经过上面两部分,我们会发现`AsyncTask`太好了,他帮我们封装了`Handler`和`Thread`,当然他内部肯定会有线程池的管理,所以以后我们在开发中对于耗时的操作可以都用`AsyncTask`来搞定的。其实这种做法是错误的。今天发现公司项目中的网络请求都是用`AsyncTask`来做的(刚换的工作)。这样会有严重的问题。 - -`AsyncTask`存在的问题: - -- `AsyncTask`虽然有`cancel`方法,但是一旦执行了`doInBackground`方法,就算调用取消方法,也会执行完`doInBackground`方法中的内容才会停止。 -- 串行还是并行的问题。 - 在`1.6`之前,`AsyncTask`是串行执行任务的。`1.6`的时候开始采用线程池并行处理。但是从`3.0`开始为了解决`AsyncTask`的并发问题,`AsyncTask`又采用一个现成来串行执行任务。(串行啊,每个任务10秒,五个任务,最后一个就要到50秒的时候才执行完) -- 线程池的问题。 - - -先从源码的角度分析下: -打开源码后先看下他的注释,注释把我们所关心的内容说的很明白了。`AsyncTask`并不是设计来处理耗时操作的,耗时的上限最多为几秒钟。 - -``` -AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and - publish results on the UI thread without having to manipulate threads and/or handlers. -AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading - framework. ***AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep - threads running for long periods of time, it is highly recommended you use the various APIs provided by the - java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask. *** - -There are a few threading rules that must be followed for this class to work properly: - - The AsyncTask class must be loaded on the UI thread. This is done automatically as of - android.os.Build.VERSION_CODES.JELLY_BEAN. - - The task instance must be created on the UI thread. - - execute must be invoked on the UI thread. - - Do not call onPreExecute(), onPostExecute, doInBackground, onProgressUpdate manually. - - The task can be executed only once (an exception will be thrown if a second execution is attempted.) Memory - observability - - When first introduced, AsyncTasks were executed serially on a single background thread. Starting with -android.os.Build.VERSION_CODES.DONUT, this was changed to a pool of threads allowing multiple tasks to operate -in parallel. Starting with android.os.Build.VERSION_CODES.HONEYCOMB, tasks are executed on a single thread to -avoid common application errors caused by parallel execution. -If you truly want parallel execution, you can invoke executeOnExecutor(java.util.concurrent.Executor, Object[]) with - THREAD_POOL_EXECUTOR. -``` - -拿到源码我们应该从哪里入手: 使用的时候我们都是 `new AsyncTask<>.execute()`所以我们可以先从构造方法和`execute`方法入手: - -```java -/** - * Creates a new asynchronous task. This constructor must be invoked on the UI thread. - */ -public AsyncTask() { - // 初始化mWorker - mWorker = new WorkerRunnable() { - public Result call() throws Exception { - // 修改该变量值 - mTaskInvoked.set(true); - - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - // 熟悉的doInBackground方法,并且返回该方法的返回值。 - //noinspection unchecked - return postResult(doInBackground(mParams)); - } - }; - // 初始化mFuture并且将mWorker作为参数。这个FutureTask是什么...我也不知道,放狗查了一下。FutureTask是一种可以取消的异步的计算任务实现了Runnable接口, - // 它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。其实就是FutureTask就是个子线程,会去执行mWorker回调中的耗时的操作 - // 然后在执行完后执行done回调方法。 - mFuture = new FutureTask(mWorker) { - @Override - protected void done() { - try { - // 执行完成后的操作 - postResultIfNotInvoked(get()); - } catch (InterruptedException e) { - android.util.Log.w(LOG_TAG, e); - } catch (ExecutionException e) { - throw new RuntimeException("An error occured while executing doInBackground()", - e.getCause()); - } catch (CancellationException e) { - postResultIfNotInvoked(null); - } - } - }; -} -``` - -`WorkerRunnable`是`Callable`接口的抽象实现类: - -```java -private static abstract class WorkerRunnable implements Callable { - Params[] mParams; -} -``` - -下面上`postResultIfNotInvoked()`源码: -```java -private void postResultIfNotInvoked(Result result) { - final boolean wasTaskInvoked = mTaskInvoked.get(); - if (!wasTaskInvoked) { - postResult(result); - } -} -// 通过Handler和Message将结果发布出去 -private Result postResult(Result result) { - @SuppressWarnings("unchecked") - // 调用getHandler去发送Message - Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, - new AsyncTaskResult(this, result)); - message.sendToTarget(); - return result; -} -``` - -那我们再看一下`getHandler()`方法得到的是哪个`Handler`: -```java -private static Handler getHandler() { - synchronized (AsyncTask.class) { - if (sHandler == null) { - sHandler = new InternalHandler(); - } - return sHandler; - } -} -``` -那接下来再看一下`InternalHandler`的实现: -```java -private static class InternalHandler extends Handler { - public InternalHandler() { - super(Looper.getMainLooper()); - } - - @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) - @Override - public void handleMessage(Message msg) { - AsyncTaskResult result = (AsyncTaskResult) msg.obj; - switch (msg.what) { - case MESSAGE_POST_RESULT: - // There is only one result - result.mTask.finish(result.mData[0]); - break; - case MESSAGE_POST_PROGRESS: - result.mTask.onProgressUpdate(result.mData); - break; - } - } -} -``` -我们看到如果判断消息类型为`MESSAGE_POST_RESULT`时,回去执行`finish()`方法,接着看一下`result.mTask.finish()`方法的源码: -```java -private void finish(Result result) { - if (isCancelled()) { - // 如果被取消了就执行onCancelled方法,这就是为什么虽然AsyncTask可以取消,但是doInBackground方法还是会执行完的原因。 - onCancelled(result); - } else { - // 没被取消就执行oPostExecute方法 - onPostExecute(result); - } - mStatus = Status.FINISHED; -} -``` - -到这里我们会发现已经分析完了`doInBackground`方法执行完后的一系列操作。那`onPreExecute`方法是在哪里? - -好了,接着看`execute()`方法 : -```java -public final AsyncTask execute(Params... params) { - return executeOnExecutor(sDefaultExecutor, params); -} -``` -里面调用了`executeOnExecutor()`,我们看一下`executeOnExecutor()`方法: -```java -public final AsyncTask executeOnExecutor(Executor exec, - Params... params) { - if (mStatus != Status.PENDING) { - switch (mStatus) { - case RUNNING: - throw new IllegalStateException("Cannot execute task:" - + " the task is already running."); - case FINISHED: - throw new IllegalStateException("Cannot execute task:" - + " the task has already been executed " - + "(a task can be executed only once)"); - } - } - - mStatus = Status.RUNNING; - // 看到我们熟悉的onPreExecute()方法。 - onPreExecute(); - // 将参数设置给mWorker变量 - mWorker.mParams = params; - // 执行了Executor的execute方法并用mFuture为参数,这个exec就是上面的sDefaultExecutor - exec.execute(mFuture); - - return this; -} -``` -我们看一下`sDefaultExecutor`是什么: -```java -/** - * An {@link Executor} that executes tasks one at a time in serial - * order. This serialization is global to a particular process. - */ -public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); - -private static final int MESSAGE_POST_RESULT = 0x1; -private static final int MESSAGE_POST_PROGRESS = 0x2; - -private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; -``` -从上面的部分能够看出`sDefaultExecutor`是一个`SerialExecutor`对象,好了,接下来看一下`SerialExecutor`类: -```java -private static class SerialExecutor implements Executor { - // 用一个队列来管理所有的runnable。offer是把要执行的添加进来,在scheduleNext中取出来去执行。 - final ArrayDeque mTasks = new ArrayDeque(); - Runnable mActive; - - public synchronized void execute(final Runnable r) { - // 终于找到了sDefaultExecutor.execute()所真正执行的部分。 - mTasks.offer(new Runnable() { - public void run() { - try { - // 就是mFuture的run方法,他会去调用mWorker.call方法,这样就会执行doInBackground方法,执行完后会把返回值用Handler发送出去 - r.run(); - } finally { - scheduleNext(); - } - } - }); - if (mActive == null) { - scheduleNext(); - } - } - - protected synchronized void scheduleNext() { - if ((mActive = mTasks.poll()) != null) { - // 去取队列中的runnable去执行,这个mActive其实就是mFuture对象。 - THREAD_POOL_EXECUTOR.execute(mActive); - } - } -} -``` - -所以从`SerialExecutor`中我们能看到这就是为什么会串行的去执行了。因为他只会取队列的第一个去执行,其他的都在队列中等待。 - -但是这里`THREAD_POOL_EXECUTOR`是什么呢?     -```java -/** - * An {@link Executor} that can be used to execute tasks in parallel. - */ -public static final Executor THREAD_POOL_EXECUTOR - = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, - TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); -``` -静态常量,也就是所不管你用多少个`AsyncTask`都会用这同一个线程池。 - -接着我们重点看一下`THREAD_POOL_EXECUTOR.execute(mActive)`: -因为`mActive`就是`mFuture = new FutureTask(mWorker)`。所以在执行`execute`方法时会执行`FutureTask`的`run`方法: -```java -public void run() { - if (state != NEW || - !UNSAFE.compareAndSwapObject(this, runnerOffset, - null, Thread.currentThread())) - return; - try { - Callable c = callable; - if (c != null && state == NEW) { - V result; - boolean ran; - try { - // 他会去调用 Callable的call()方法,而上面传入的Callable参数是mWorker。所以这里就会调用mWorker的call方法。 - // 通过这里就和之前我们讲的doInBackground方法联系上了. - result = c.call(); - ran = true; - } catch (Throwable ex) { - result = null; - ran = false; - setException(ex); - } - if (ran) - set(result); - } - } finally { - // runner must be non-null until state is settled to - // prevent concurrent calls to run() - runner = null; - // state must be re-read after nulling runner to prevent - // leaked interrupts - int s = state; - if (s >= INTERRUPTING) - handlePossibleCancellationInterrupt(s); - } -} -``` - -到这里就分析完了。 - -下面把完整的代码粘贴上(5.1.1): -```java -public abstract class AsyncTask { - private static final String LOG_TAG = "AsyncTask"; - - private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); - private static final int CORE_POOL_SIZE = CPU_COUNT + 1; - private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; - private static final int KEEP_ALIVE = 1; - - private static final ThreadFactory sThreadFactory = new ThreadFactory() { - private final AtomicInteger mCount = new AtomicInteger(1); - - public Thread newThread(Runnable r) { - return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); - } - }; - - private static final BlockingQueue sPoolWorkQueue = - new LinkedBlockingQueue(128); - - /** - * An {@link Executor} that can be used to execute tasks in parallel. - */ - public static final Executor THREAD_POOL_EXECUTOR - = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, - TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); - - /** - * An {@link Executor} that executes tasks one at a time in serial - * order. This serialization is global to a particular process. - */ - public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); - - private static final int MESSAGE_POST_RESULT = 0x1; - private static final int MESSAGE_POST_PROGRESS = 0x2; - - private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; - private static InternalHandler sHandler; - - private final WorkerRunnable mWorker; - private final FutureTask mFuture; - - private volatile Status mStatus = Status.PENDING; - - private final AtomicBoolean mCancelled = new AtomicBoolean(); - private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); - - private static class SerialExecutor implements Executor { - final ArrayDeque mTasks = new ArrayDeque(); - Runnable mActive; - - public synchronized void execute(final Runnable r) { - mTasks.offer(new Runnable() { - public void run() { - try { - r.run(); - } finally { - scheduleNext(); - } - } - }); - if (mActive == null) { - scheduleNext(); - } - } - - protected synchronized void scheduleNext() { - if ((mActive = mTasks.poll()) != null) { - THREAD_POOL_EXECUTOR.execute(mActive); - } - } - } - - /** - * Indicates the current status of the task. Each status will be set only once - * during the lifetime of a task. - */ - public enum Status { - /** - * Indicates that the task has not been executed yet. - */ - PENDING, - /** - * Indicates that the task is running. - */ - RUNNING, - /** - * Indicates that {@link AsyncTask#onPostExecute} has finished. - */ - FINISHED, - } - - private static Handler getHandler() { - synchronized (AsyncTask.class) { - if (sHandler == null) { - sHandler = new InternalHandler(); - } - return sHandler; - } - } - - /** @hide */ - public static void setDefaultExecutor(Executor exec) { - sDefaultExecutor = exec; - } - - /** - * Creates a new asynchronous task. This constructor must be invoked on the UI thread. - */ - public AsyncTask() { - mWorker = new WorkerRunnable() { - public Result call() throws Exception { - mTaskInvoked.set(true); - - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - //noinspection unchecked - return postResult(doInBackground(mParams)); - } - }; - - mFuture = new FutureTask(mWorker) { - @Override - protected void done() { - try { - postResultIfNotInvoked(get()); - } catch (InterruptedException e) { - android.util.Log.w(LOG_TAG, e); - } catch (ExecutionException e) { - throw new RuntimeException("An error occured while executing doInBackground()", - e.getCause()); - } catch (CancellationException e) { - postResultIfNotInvoked(null); - } - } - }; - } - - private void postResultIfNotInvoked(Result result) { - final boolean wasTaskInvoked = mTaskInvoked.get(); - if (!wasTaskInvoked) { - postResult(result); - } - } - - private Result postResult(Result result) { - @SuppressWarnings("unchecked") - Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, - new AsyncTaskResult(this, result)); - message.sendToTarget(); - return result; - } - - /** - * Returns the current status of this task. - * - * @return The current status. - */ - public final Status getStatus() { - return mStatus; - } - - /** - * Override this method to perform a computation on a background thread. The - * specified parameters are the parameters passed to {@link #execute} - * by the caller of this task. - * - * This method can call {@link #publishProgress} to publish updates - * on the UI thread. - * - * @param params The parameters of the task. - * - * @return A result, defined by the subclass of this task. - * - * @see #onPreExecute() - * @see #onPostExecute - * @see #publishProgress - */ - protected abstract Result doInBackground(Params... params); - - /** - * Runs on the UI thread before {@link #doInBackground}. - * - * @see #onPostExecute - * @see #doInBackground - */ - protected void onPreExecute() { - } - - /** - *

Runs on the UI thread after {@link #doInBackground}. The - * specified result is the value returned by {@link #doInBackground}.

- * - *

This method won't be invoked if the task was cancelled.

- * - * @param result The result of the operation computed by {@link #doInBackground}. - * - * @see #onPreExecute - * @see #doInBackground - * @see #onCancelled(Object) - */ - @SuppressWarnings({"UnusedDeclaration"}) - protected void onPostExecute(Result result) { - } - - /** - * Runs on the UI thread after {@link #publishProgress} is invoked. - * The specified values are the values passed to {@link #publishProgress}. - * - * @param values The values indicating progress. - * - * @see #publishProgress - * @see #doInBackground - */ - @SuppressWarnings({"UnusedDeclaration"}) - protected void onProgressUpdate(Progress... values) { - } - - /** - *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and - * {@link #doInBackground(Object[])} has finished.

- * - *

The default implementation simply invokes {@link #onCancelled()} and - * ignores the result. If you write your own implementation, do not call - * super.onCancelled(result).

- * - * @param result The result, if any, computed in - * {@link #doInBackground(Object[])}, can be null - * - * @see #cancel(boolean) - * @see #isCancelled() - */ - @SuppressWarnings({"UnusedParameters"}) - protected void onCancelled(Result result) { - onCancelled(); - } - - /** - *

Applications should preferably override {@link #onCancelled(Object)}. - * This method is invoked by the default implementation of - * {@link #onCancelled(Object)}.

- * - *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and - * {@link #doInBackground(Object[])} has finished.

- * - * @see #onCancelled(Object) - * @see #cancel(boolean) - * @see #isCancelled() - */ - protected void onCancelled() { - } - - /** - * Returns true if this task was cancelled before it completed - * normally. If you are calling {@link #cancel(boolean)} on the task, - * the value returned by this method should be checked periodically from - * {@link #doInBackground(Object[])} to end the task as soon as possible. - * - * @return true if task was cancelled before it completed - * - * @see #cancel(boolean) - */ - public final boolean isCancelled() { - return mCancelled.get(); - } - - /** - *

Attempts to cancel execution of this task. This attempt will - * fail if the task has already completed, already been cancelled, - * or could not be cancelled for some other reason. If successful, - * and this task has not started when cancel is called, - * this task should never run. If the task has already started, - * then the mayInterruptIfRunning parameter determines - * whether the thread executing this task should be interrupted in - * an attempt to stop the task.

- * - *

Calling this method will result in {@link #onCancelled(Object)} being - * invoked on the UI thread after {@link #doInBackground(Object[])} - * returns. Calling this method guarantees that {@link #onPostExecute(Object)} - * is never invoked. After invoking this method, you should check the - * value returned by {@link #isCancelled()} periodically from - * {@link #doInBackground(Object[])} to finish the task as early as - * possible.

- * - * @param mayInterruptIfRunning true if the thread executing this - * task should be interrupted; otherwise, in-progress tasks are allowed - * to complete. - * - * @return false if the task could not be cancelled, - * typically because it has already completed normally; - * true otherwise - * - * @see #isCancelled() - * @see #onCancelled(Object) - */ - public final boolean cancel(boolean mayInterruptIfRunning) { - mCancelled.set(true); - return mFuture.cancel(mayInterruptIfRunning); - } - - /** - * Waits if necessary for the computation to complete, and then - * retrieves its result. - * - * @return The computed result. - * - * @throws CancellationException If the computation was cancelled. - * @throws ExecutionException If the computation threw an exception. - * @throws InterruptedException If the current thread was interrupted - * while waiting. - */ - public final Result get() throws InterruptedException, ExecutionException { - return mFuture.get(); - } - - /** - * Waits if necessary for at most the given time for the computation - * to complete, and then retrieves its result. - * - * @param timeout Time to wait before cancelling the operation. - * @param unit The time unit for the timeout. - * - * @return The computed result. - * - * @throws CancellationException If the computation was cancelled. - * @throws ExecutionException If the computation threw an exception. - * @throws InterruptedException If the current thread was interrupted - * while waiting. - * @throws TimeoutException If the wait timed out. - */ - public final Result get(long timeout, TimeUnit unit) throws InterruptedException, - ExecutionException, TimeoutException { - return mFuture.get(timeout, unit); - } - - /** - * Executes the task with the specified parameters. The task returns - * itself (this) so that the caller can keep a reference to it. - * - *

Note: this function schedules the task on a queue for a single background - * thread or pool of threads depending on the platform version. When first - * introduced, AsyncTasks were executed serially on a single background thread. - * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed - * to a pool of threads allowing multiple tasks to operate in parallel. Starting - * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being - * executed on a single thread to avoid common application errors caused - * by parallel execution. If you truly want parallel execution, you can use - * the {@link #executeOnExecutor} version of this method - * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings - * on its use. - * - *

This method must be invoked on the UI thread. - * - * @param params The parameters of the task. - * - * @return This instance of AsyncTask. - * - * @throws IllegalStateException If {@link #getStatus()} returns either - * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. - * - * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) - * @see #execute(Runnable) - */ - public final AsyncTask execute(Params... params) { - return executeOnExecutor(sDefaultExecutor, params); - // 这里就多插一嘴了。 sDefaultExecutor在上面我们分析过了,就是一个队列来保证串行进行。从3.0开始都是这样。 - // 那在1.6到3.0之间是怎么并行执行的呢? 按照下面的方式改就可以了 - // return executeOnExecutor(THREAD_POOL_EXECUTOR, params); - // 就是将sDefaultExecutor改成THREAD_POOL_EXECUTOR, THREAD_POOL_EXECUTOR就是线程池。 - // new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); - // 原来的CORE_POOL_SIZE是5, KEEP_ALIVE是10, MAXIMUM_POOL_SIZE是128 - // 也就是说可以同时执行的线程是5个,如果超过5个后,超过的部分就会放到缓存队列中,如果超过了128那就挂了 - } - - /** - * Executes the task with the specified parameters. The task returns - * itself (this) so that the caller can keep a reference to it. - * - *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to - * allow multiple tasks to run in parallel on a pool of threads managed by - * AsyncTask, however you can also use your own {@link Executor} for custom - * behavior. - * - *

Warning: Allowing multiple tasks to run in parallel from - * a thread pool is generally not what one wants, because the order - * of their operation is not defined. For example, if these tasks are used - * to modify any state in common (such as writing a file due to a button click), - * there are no guarantees on the order of the modifications. - * Without careful work it is possible in rare cases for the newer version - * of the data to be over-written by an older one, leading to obscure data - * loss and stability issues. Such changes are best - * executed in serial; to guarantee such work is serialized regardless of - * platform version you can use this function with {@link #SERIAL_EXECUTOR}. - * - *

This method must be invoked on the UI thread. - * - * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a - * convenient process-wide thread pool for tasks that are loosely coupled. - * @param params The parameters of the task. - * - * @return This instance of AsyncTask. - * - * @throws IllegalStateException If {@link #getStatus()} returns either - * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. - * - * @see #execute(Object[]) - */ - public final AsyncTask executeOnExecutor(Executor exec, - Params... params) { - if (mStatus != Status.PENDING) { - switch (mStatus) { - case RUNNING: - throw new IllegalStateException("Cannot execute task:" - + " the task is already running."); - case FINISHED: - throw new IllegalStateException("Cannot execute task:" - + " the task has already been executed " - + "(a task can be executed only once)"); - } - } - - mStatus = Status.RUNNING; - - onPreExecute(); - - mWorker.mParams = params; - exec.execute(mFuture); - - return this; - } - - /** - * Convenience version of {@link #execute(Object...)} for use with - * a simple Runnable object. See {@link #execute(Object[])} for more - * information on the order of execution. - * - * @see #execute(Object[]) - * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) - */ - public static void execute(Runnable runnable) { - sDefaultExecutor.execute(runnable); - } - - /** - * This method can be invoked from {@link #doInBackground} to - * publish updates on the UI thread while the background computation is - * still running. Each call to this method will trigger the execution of - * {@link #onProgressUpdate} on the UI thread. - * - * {@link #onProgressUpdate} will not be called if the task has been - * canceled. - * - * @param values The progress values to update the UI with. - * - * @see #onProgressUpdate - * @see #doInBackground - */ - protected final void publishProgress(Progress... values) { - if (!isCancelled()) { - getHandler().obtainMessage(MESSAGE_POST_PROGRESS, - new AsyncTaskResult(this, values)).sendToTarget(); - } - } - - private void finish(Result result) { - if (isCancelled()) { - onCancelled(result); - } else { - onPostExecute(result); - } - mStatus = Status.FINISHED; - } - - private static class InternalHandler extends Handler { - public InternalHandler() { - super(Looper.getMainLooper()); - } - - @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) - @Override - public void handleMessage(Message msg) { - AsyncTaskResult result = (AsyncTaskResult) msg.obj; - switch (msg.what) { - case MESSAGE_POST_RESULT: - // There is only one result - result.mTask.finish(result.mData[0]); - break; - case MESSAGE_POST_PROGRESS: - result.mTask.onProgressUpdate(result.mData); - break; - } - } - } - - private static abstract class WorkerRunnable implements Callable { - Params[] mParams; - } - - @SuppressWarnings({"RawUseOfParameterizedType"}) - private static class AsyncTaskResult { - final AsyncTask mTask; - final Data[] mData; - - AsyncTaskResult(AsyncTask task, Data... data) { - mTask = task; - mData = data; - } - } -} - -``` - ---- - -- 邮箱 :charon.chui@gmail.com +AsyncTask详解 +=== + +`AsyncTask`简单的说其实就是`Handler`和`Thread`的结合,就想下面自己写的`MyAsyncTask`一样,这就是它的基本远离,当然它并不止这么简单。 + +- 经典版异步任务 + +```java +public abstract class MyAsyncTask { + private Handler handler = new Handler(){ + public void handleMessage(android.os.Message msg) { + onPostExecute(); + }; + }; + + /** + * 后台任务执行之前 提示用户的界面操作. + */ + public abstract void onPreExecute(); + + /** + * 后台任务执行之后 更新界面的操作. + */ + public abstract void onPostExecute(); + + /** + * 在后台执行的一个耗时的操作. + */ + public abstract void doInBackground(); + + + public void execute(){ + //1. 耗时任务执行之前通知界面更新 + onPreExecute(); + new Thread(){ + public void run() { + doInBackground(); + handler.sendEmptyMessage(0); + }; + }.start(); + + } +} +``` + +- AsyncTask + +```java +new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + blackNumberInfos = dao.findByPage(startIndex, maxNumber); + return null; + } + @Override + protected void onPreExecute() { + loading.setVisibility(View.VISIBLE); + super.onPreExecute(); + } + @Override + protected void onPostExecute(Void result) { + loading.setVisibility(View.INVISIBLE); + if (adapter == null) {// 第一次加载数据 数据适配器还不存在 + adapter = new CallSmsAdapter(); + lv_callsms_safe.setAdapter(adapter); + } else {// 有新的数据被添加进来. + adapter.notifyDataSetChanged();// 通知数据适配器 数据变化了. + } + super.onPostExecute(result); + } + }.execute(); +类的构造方法中接收三个参数,这里我们不用参数就都给它传Void,new出来AsyncTask类之后然后重写这三个方法, +最后别忘了执行execute方法,其实它的内部和我们写的经典版的异步任务相同,也是里面写了一个在新的线程中去执行耗时的操作, +然后用handler发送Message对象,主线程收到这个Message之后去执行onPostExecute中的内容。 + + +//AsyncTask ,params 异步任务执行(doBackgroud方法)需要的参数这个参数的实参可以由execute()方法的参数传入, +// Progess 执行的进度,result是(doBackground方法)执行后的结果 +new AsyncTask() { + ProgressDialog pd; + @Override + protected Boolean doInBackground(String... params) { //这里返回的就是执行的接口,这个返回的结果会传递给onPostExecute的参数 + try { + String filename = params[0];//得到execute传入的参数 + File file = new File(Environment.getExternalStorageDirectory(),filename); + FileOutputStream fos = new FileOutputStream(file); + SmsUtils.backUp(getApplicationContext(), fos, new BackUpStatusListener() { + public void onBackUpProcess(int process) { + pd.setProgress(process); + } + + public void beforeBackup(int max) { + pd.setMax(max); + } + }); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + @Override + protected void onPreExecute() { + pd = new ProgressDialog(AtoolsActivity.this); + pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + pd.setMessage("正在备份短信"); + pd.show(); + super.onPreExecute(); + } + @Override + protected void onPostExecute(Boolean result) { + pd.dismiss(); + if(result){ + Toast.makeText(getApplicationContext(), "备份成功", 0).show(); + }else{ + Toast.makeText(getApplicationContext(), "备份失败", 0).show(); + } + super.onPostExecute(result); + } + + }.execute("backup.xml"); //这里传入的参数就是doInBackgound中的参数,会传入到doInBackground中 + +ProgressDialog有个方法 +incrementProgressBy(int num);方法,这个方法能够让进度条自动增加,如果参数为1就是进度条累加1。 + +可以给ProgressDialog添加一个监听dismiss的监听器。pd.setOnDismisListener(DismisListener listener);让其在取消显示后做什么事 +``` + +经过上面两部分,我们会发现`AsyncTask`太好了,他帮我们封装了`Handler`和`Thread`,当然他内部肯定会有线程池的管理,所以以后我们在开发中对于耗时的操作可以都用`AsyncTask`来搞定的。其实这种做法是错误的。今天发现公司项目中的网络请求都是用`AsyncTask`来做的(刚换的工作)。这样会有严重的问题。 + +`AsyncTask`存在的问题: + +- `AsyncTask`虽然有`cancel`方法,但是一旦执行了`doInBackground`方法,就算调用取消方法,也会执行完`doInBackground`方法中的内容才会停止。 +- 串行还是并行的问题。 + 在`1.6`之前,`AsyncTask`是串行执行任务的。`1.6`的时候开始采用线程池并行处理。但是从`3.0`开始为了解决`AsyncTask`的并发问题,`AsyncTask`又采用一个现成来串行执行任务。(串行啊,每个任务10秒,五个任务,最后一个就要到50秒的时候才执行完) +- 线程池的问题。 + + +先从源码的角度分析下: +打开源码后先看下他的注释,注释把我们所关心的内容说的很明白了。`AsyncTask`并不是设计来处理耗时操作的,耗时的上限最多为几秒钟。 + +``` +AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and + publish results on the UI thread without having to manipulate threads and/or handlers. +AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading + framework. ***AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep + threads running for long periods of time, it is highly recommended you use the various APIs provided by the + java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask. *** + +There are a few threading rules that must be followed for this class to work properly: + - The AsyncTask class must be loaded on the UI thread. This is done automatically as of + android.os.Build.VERSION_CODES.JELLY_BEAN. + - The task instance must be created on the UI thread. + - execute must be invoked on the UI thread. + - Do not call onPreExecute(), onPostExecute, doInBackground, onProgressUpdate manually. + - The task can be executed only once (an exception will be thrown if a second execution is attempted.) Memory + observability + + When first introduced, AsyncTasks were executed serially on a single background thread. Starting with +android.os.Build.VERSION_CODES.DONUT, this was changed to a pool of threads allowing multiple tasks to operate +in parallel. Starting with android.os.Build.VERSION_CODES.HONEYCOMB, tasks are executed on a single thread to +avoid common application errors caused by parallel execution. +If you truly want parallel execution, you can invoke executeOnExecutor(java.util.concurrent.Executor, Object[]) with + THREAD_POOL_EXECUTOR. +``` + +拿到源码我们应该从哪里入手: 使用的时候我们都是 `new AsyncTask<>.execute()`所以我们可以先从构造方法和`execute`方法入手: + +```java +/** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ +public AsyncTask() { + // 初始化mWorker + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + // 修改该变量值 + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + // 熟悉的doInBackground方法,并且返回该方法的返回值。 + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + // 初始化mFuture并且将mWorker作为参数。这个FutureTask是什么...我也不知道,放狗查了一下。FutureTask是一种可以取消的异步的计算任务实现了Runnable接口, + // 它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。其实就是FutureTask就是个子线程,会去执行mWorker回调中的耗时的操作 + // 然后在执行完后执行done回调方法。 + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + // 执行完成后的操作 + postResultIfNotInvoked(get()); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } + } + }; +} +``` + +`WorkerRunnable`是`Callable`接口的抽象实现类: + +```java +private static abstract class WorkerRunnable implements Callable { + Params[] mParams; +} +``` + +下面上`postResultIfNotInvoked()`源码: +```java +private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } +} +// 通过Handler和Message将结果发布出去 +private Result postResult(Result result) { + @SuppressWarnings("unchecked") + // 调用getHandler去发送Message + Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; +} +``` + +那我们再看一下`getHandler()`方法得到的是哪个`Handler`: +```java +private static Handler getHandler() { + synchronized (AsyncTask.class) { + if (sHandler == null) { + sHandler = new InternalHandler(); + } + return sHandler; + } +} +``` +那接下来再看一下`InternalHandler`的实现: +```java +private static class InternalHandler extends Handler { + public InternalHandler() { + super(Looper.getMainLooper()); + } + + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } +} +``` +我们看到如果判断消息类型为`MESSAGE_POST_RESULT`时,回去执行`finish()`方法,接着看一下`result.mTask.finish()`方法的源码: +```java +private void finish(Result result) { + if (isCancelled()) { + // 如果被取消了就执行onCancelled方法,这就是为什么虽然AsyncTask可以取消,但是doInBackground方法还是会执行完的原因。 + onCancelled(result); + } else { + // 没被取消就执行oPostExecute方法 + onPostExecute(result); + } + mStatus = Status.FINISHED; +} +``` + +到这里我们会发现已经分析完了`doInBackground`方法执行完后的一系列操作。那`onPreExecute`方法是在哪里? + +好了,接着看`execute()`方法 : +```java +public final AsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); +} +``` +里面调用了`executeOnExecutor()`,我们看一下`executeOnExecutor()`方法: +```java +public final AsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + // 看到我们熟悉的onPreExecute()方法。 + onPreExecute(); + // 将参数设置给mWorker变量 + mWorker.mParams = params; + // 执行了Executor的execute方法并用mFuture为参数,这个exec就是上面的sDefaultExecutor + exec.execute(mFuture); + + return this; +} +``` +我们看一下`sDefaultExecutor`是什么: +```java +/** + * An {@link Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ +public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); + +private static final int MESSAGE_POST_RESULT = 0x1; +private static final int MESSAGE_POST_PROGRESS = 0x2; + +private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; +``` +从上面的部分能够看出`sDefaultExecutor`是一个`SerialExecutor`对象,好了,接下来看一下`SerialExecutor`类: +```java +private static class SerialExecutor implements Executor { + // 用一个队列来管理所有的runnable。offer是把要执行的添加进来,在scheduleNext中取出来去执行。 + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + // 终于找到了sDefaultExecutor.execute()所真正执行的部分。 + mTasks.offer(new Runnable() { + public void run() { + try { + // 就是mFuture的run方法,他会去调用mWorker.call方法,这样就会执行doInBackground方法,执行完后会把返回值用Handler发送出去 + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + // 去取队列中的runnable去执行,这个mActive其实就是mFuture对象。 + THREAD_POOL_EXECUTOR.execute(mActive); + } + } +} +``` + +所以从`SerialExecutor`中我们能看到这就是为什么会串行的去执行了。因为他只会取队列的第一个去执行,其他的都在队列中等待。 + +但是这里`THREAD_POOL_EXECUTOR`是什么呢?     +```java +/** + * An {@link Executor} that can be used to execute tasks in parallel. + */ +public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); +``` +静态常量,也就是所不管你用多少个`AsyncTask`都会用这同一个线程池。 + +接着我们重点看一下`THREAD_POOL_EXECUTOR.execute(mActive)`: +因为`mActive`就是`mFuture = new FutureTask(mWorker)`。所以在执行`execute`方法时会执行`FutureTask`的`run`方法: +```java +public void run() { + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, + null, Thread.currentThread())) + return; + try { + Callable c = callable; + if (c != null && state == NEW) { + V result; + boolean ran; + try { + // 他会去调用 Callable的call()方法,而上面传入的Callable参数是mWorker。所以这里就会调用mWorker的call方法。 + // 通过这里就和之前我们讲的doInBackground方法联系上了. + result = c.call(); + ran = true; + } catch (Throwable ex) { + result = null; + ran = false; + setException(ex); + } + if (ran) + set(result); + } + } finally { + // runner must be non-null until state is settled to + // prevent concurrent calls to run() + runner = null; + // state must be re-read after nulling runner to prevent + // leaked interrupts + int s = state; + if (s >= INTERRUPTING) + handlePossibleCancellationInterrupt(s); + } +} +``` + +到这里就分析完了。 + +下面把完整的代码粘贴上(5.1.1): +```java +public abstract class AsyncTask { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT + 1; + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(128); + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); + + /** + * An {@link Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ + public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + private static InternalHandler sHandler; + + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + private static class SerialExecutor implements Executor { + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + THREAD_POOL_EXECUTOR.execute(mActive); + } + } + } + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + private static Handler getHandler() { + synchronized (AsyncTask.class) { + if (sHandler == null) { + sHandler = new InternalHandler(); + } + return sHandler; + } + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTask() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + postResultIfNotInvoked(get()); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + @SuppressWarnings("unchecked") + Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ * + *

This method won't be invoked if the task was cancelled.

+ * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

+ * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + *

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ * + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return true if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mCancelled.get(); + } + + /** + *

Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ * + *

Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + * + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being + * executed on a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings + * on its use. + * + *

This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + * @see #execute(Runnable) + */ + public final AsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + // 这里就多插一嘴了。 sDefaultExecutor在上面我们分析过了,就是一个队列来保证串行进行。从3.0开始都是这样。 + // 那在1.6到3.0之间是怎么并行执行的呢? 按照下面的方式改就可以了 + // return executeOnExecutor(THREAD_POOL_EXECUTOR, params); + // 就是将sDefaultExecutor改成THREAD_POOL_EXECUTOR, THREAD_POOL_EXECUTOR就是线程池。 + // new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); + // 原来的CORE_POOL_SIZE是5, KEEP_ALIVE是10, MAXIMUM_POOL_SIZE是128 + // 也就是说可以同时执行的线程是5个,如果超过5个后,超过的部分就会放到缓存队列中,如果超过了128那就挂了 + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link Executor} for custom + * behavior. + * + *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. Such changes are best + * executed in serial; to guarantee such work is serialized regardless of + * platform version you can use this function with {@link #SERIAL_EXECUTOR}. + * + *

This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #execute(Object[]) + */ + public final AsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. See {@link #execute(Object[])} for more + * information on the order of execution. + * + * @see #execute(Object[]) + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * {@link #onProgressUpdate} will not be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + getHandler().obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + public InternalHandler() { + super(Looper.getMainLooper()); + } + + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} + +``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" "b/AndroidAdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" similarity index 98% rename from "Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" rename to "AndroidAdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" index 29689919..1485641a 100644 --- "a/Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" +++ "b/AndroidAdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" @@ -1,49 +1,49 @@ -BroadcastReceiver安全问题 -=== - -`BroadcastReceiver`设计的初衷是从全局考虑可以方便应用程序和系统、应用程序之间、应用程序内的通信,所以对单个应用程序而言`BroadcastReceiver`是存在安全性问题的(恶意程序脚本不断的去发送你所接收的广播) -- 保证发送的广播要发送给指定的对象 - 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 - -- 保证我接收到的广播室指定对象发送过来的 - 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 - -`android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: -- 因广播数据在本应用范围内传播,你不用担心隐私数据泄露的问题。 -- 不用担心别的应用伪造广播,造成安全隐患。 -- 相比在系统内发送全局广播,它更高效。 - -```java - LocalBroadcastManager mLocalBroadcastManager; - BroadcastReceiver mReceiver; - -@Override -protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntentFilter filter = new IntentFilter(); - filter.addAction("test"); - - mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals("test")) { - //Do Something - } - } - }; - mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); - mLocalBroadcastManager.registerReceiver(mReceiver, filter); -} - - -@Override -protected void onDestroy() { - mLocalBroadcastManager.unregisterReceiver(mReceiver); - super.onDestroy(); -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com +BroadcastReceiver安全问题 +=== + +`BroadcastReceiver`设计的初衷是从全局考虑可以方便应用程序和系统、应用程序之间、应用程序内的通信,所以对单个应用程序而言`BroadcastReceiver`是存在安全性问题的(恶意程序脚本不断的去发送你所接收的广播) +- 保证发送的广播要发送给指定的对象 + 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 + +- 保证我接收到的广播室指定对象发送过来的 + 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 + +`android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: +- 因广播数据在本应用范围内传播,你不用担心隐私数据泄露的问题。 +- 不用担心别的应用伪造广播,造成安全隐患。 +- 相比在系统内发送全局广播,它更高效。 + +```java + LocalBroadcastManager mLocalBroadcastManager; + BroadcastReceiver mReceiver; + +@Override +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntentFilter filter = new IntentFilter(); + filter.addAction("test"); + + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals("test")) { + //Do Something + } + } + }; + mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); + mLocalBroadcastManager.registerReceiver(mReceiver, filter); +} + + +@Override +protected void onDestroy() { + mLocalBroadcastManager.unregisterReceiver(mReceiver); + super.onDestroy(); +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Fiddler\346\212\223\345\217\226Android\346\211\213\346\234\272\346\225\260\346\215\256\345\214\205.md" "b/AndroidAdavancedPart/Fiddler\346\212\223\345\217\226Android\346\211\213\346\234\272\346\225\260\346\215\256\345\214\205.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Fiddler\346\212\223\345\217\226Android\346\211\213\346\234\272\346\225\260\346\215\256\345\214\205.md" rename to "AndroidAdavancedPart/Fiddler\346\212\223\345\217\226Android\346\211\213\346\234\272\346\225\260\346\215\256\345\214\205.md" diff --git "a/Android\345\212\240\345\274\272/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" "b/AndroidAdavancedPart/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" similarity index 97% rename from "Android\345\212\240\345\274\272/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" rename to "AndroidAdavancedPart/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" index 0453398c..19f43825 100644 --- "a/Android\345\212\240\345\274\272/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" +++ "b/AndroidAdavancedPart/Github\344\270\252\344\272\272\344\270\273\351\241\265\347\273\221\345\256\232\345\237\237\345\220\215.md" @@ -1,33 +1,33 @@ -Github个人主页绑定域名 -=== - -`Github`虽然很好,可毕竟是免费的,还是有不少限制的。写到这里,特意去看了下`Github`对免费用户究竟有什么限制。发现除了300M的空间限制(还是所谓软限制),没有其他限制。所以用它来作为博客平台,真是再理想不过了。 - -创建步骤 ---- - -1. 建立一个博客`repository` - 建立一个命名为`username.github.io`的`repository`, `username`就是你在`Github`上的用户名或机构名 - -2. 增加主页 - `clone`该`repository`到本地,增加`index.html` - -3. 提交 - `commit`并且`push`该次修改。 - -4. OK - 打开浏览器输入 `username.github.io` 即可。注意提交之后可能需要一小段时间的延迟。 - - -绑定域名 ---- - -1. 在`repository`根目录新建`CNAME`文件, 内容为`xxx.com`(要绑定的域名),然后`commit`、`push`. -2. 在自己的域名管理页面中,进入域名解析. - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/bindhost.jpg?raw=true) - **注意记录值 `username.github.io.` (最后面有一个.)** - ---- - -- 邮箱 :charon.chui@gmail.com +Github个人主页绑定域名 +=== + +`Github`虽然很好,可毕竟是免费的,还是有不少限制的。写到这里,特意去看了下`Github`对免费用户究竟有什么限制。发现除了300M的空间限制(还是所谓软限制),没有其他限制。所以用它来作为博客平台,真是再理想不过了。 + +创建步骤 +--- + +1. 建立一个博客`repository` + 建立一个命名为`username.github.io`的`repository`, `username`就是你在`Github`上的用户名或机构名 + +2. 增加主页 + `clone`该`repository`到本地,增加`index.html` + +3. 提交 + `commit`并且`push`该次修改。 + +4. OK + 打开浏览器输入 `username.github.io` 即可。注意提交之后可能需要一小段时间的延迟。 + + +绑定域名 +--- + +1. 在`repository`根目录新建`CNAME`文件, 内容为`xxx.com`(要绑定的域名),然后`commit`、`push`. +2. 在自己的域名管理页面中,进入域名解析. + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/bindhost.jpg?raw=true) + **注意记录值 `username.github.io.` (最后面有一个.)** + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" "b/AndroidAdavancedPart/Gradle\344\270\223\351\242\230.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Gradle\344\270\223\351\242\230.md" rename to "AndroidAdavancedPart/Gradle\344\270\223\351\242\230.md" diff --git "a/Android\345\212\240\345\274\272/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" "b/AndroidAdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" similarity index 97% rename from "Android\345\212\240\345\274\272/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" rename to "AndroidAdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" index 235283e0..b49c4bdf 100644 --- "a/Android\345\212\240\345\274\272/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" +++ "b/AndroidAdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" @@ -1,104 +1,104 @@ -Handler导致内存泄露分析 -=== - -有关内存泄露请猛戳[内存泄露](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%9F%BA%E7%A1%80/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) - -```java -Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something. - } -} -``` -当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 - -一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: -- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. -- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, -当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, -会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收` -- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** - -##具体分析 -```java -public class SampleActivity extends Activity { - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // 发送一个10分钟后执行的一个消息 - mHandler.postDelayed(new Runnable() { - @Override - public void run() { } - }, 600000); - - // 结束当前的Activity - finish(); -} -``` -在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. - -##解决方法 -- 通过程序逻辑来进行保护。 - - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, - `Activity`自然会在合适的时候被回收。 - - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 - - 关于`Handler.remove*`方法 - - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 - - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 - - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 - - `removeMessages(int what)` ——按what来匹配 - - `removeMessages(int what, Object object)` ——按what来匹配 - 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; -- 将`Handler`声明为静态类。 - 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 - -```java -public class MyActivity extends Activity { - private MyHandler mHandler; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mHandler = new MyHandler(this); - } - - @Override - protected void onDestroy() { - // Remove all Runnable and Message. - mHandler.removeCallbacksAndMessages(null); - super.onDestroy(); - } - - static class MyHandler extends Handler { - // WeakReference to the outer class's instance. - private WeakReference mOuter; - - public MyHandler(MyActivity activity) { - mOuter = new WeakReference(activity); - } - - @Override - public void handleMessage(Message msg) { - MyActivity outer = mOuter.get(); - if (outer != null) { - // Do something with outer as your wish. - } - } - } -} -``` - - ---- - -- 邮箱 :charon.chui@gmail.com +Handler导致内存泄露分析 +=== + +有关内存泄露请猛戳[内存泄露](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%9F%BA%E7%A1%80/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) + +```java +Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something. + } +} +``` +当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 + +一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: +- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. +- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, +当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, +会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收` +- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** + +##具体分析 +```java +public class SampleActivity extends Activity { + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 发送一个10分钟后执行的一个消息 + mHandler.postDelayed(new Runnable() { + @Override + public void run() { } + }, 600000); + + // 结束当前的Activity + finish(); +} +``` +在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. + +##解决方法 +- 通过程序逻辑来进行保护。 + - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, + `Activity`自然会在合适的时候被回收。 + - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 + - 关于`Handler.remove*`方法 + - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 + - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 + - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 + - `removeMessages(int what)` ——按what来匹配 + - `removeMessages(int what, Object object)` ——按what来匹配 + 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; +- 将`Handler`声明为静态类。 + 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 + +```java +public class MyActivity extends Activity { + private MyHandler mHandler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHandler = new MyHandler(this); + } + + @Override + protected void onDestroy() { + // Remove all Runnable and Message. + mHandler.removeCallbacksAndMessages(null); + super.onDestroy(); + } + + static class MyHandler extends Handler { + // WeakReference to the outer class's instance. + private WeakReference mOuter; + + public MyHandler(MyActivity activity) { + mOuter = new WeakReference(activity); + } + + @Override + public void handleMessage(Message msg) { + MyActivity outer = mOuter.get(); + if (outer != null) { + // Do something with outer as your wish. + } + } + } +} +``` + + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/HttpURLConnection\344\270\216HttpClient.md" "b/AndroidAdavancedPart/HttpURLConnection\344\270\216HttpClient.md" similarity index 97% rename from "Android\345\212\240\345\274\272/HttpURLConnection\344\270\216HttpClient.md" rename to "AndroidAdavancedPart/HttpURLConnection\344\270\216HttpClient.md" index eca01cb6..aa8688af 100644 --- "a/Android\345\212\240\345\274\272/HttpURLConnection\344\270\216HttpClient.md" +++ "b/AndroidAdavancedPart/HttpURLConnection\344\270\216HttpClient.md" @@ -1,15 +1,15 @@ -HttpURLConnection与HttpClient -=== - -- `Java`的`HttpURLConnection` - 请求默认带`Gzip`压缩。 -- `Apache`的`HttpClient` - 请求默认不带`Gzip`压缩。 - -一般对于`API`请求返回的数据大多是`Json`类的字符串,`Gzip`压缩可以使数据大小大幅降低。 -`Retrofit`及`Volley`框架默认在`Android Gingerbread(API 9)`及以上都是用`HttpURLConnection`,9以下用`HttpClient`。 - ---- - -- 邮箱 :charon.chui@gmail.com +HttpURLConnection与HttpClient +=== + +- `Java`的`HttpURLConnection` + 请求默认带`Gzip`压缩。 +- `Apache`的`HttpClient` + 请求默认不带`Gzip`压缩。 + +一般对于`API`请求返回的数据大多是`Json`类的字符串,`Gzip`压缩可以使数据大小大幅降低。 +`Retrofit`及`Volley`框架默认在`Android Gingerbread(API 9)`及以上都是用`HttpURLConnection`,9以下用`HttpClient`。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/HttpURLConnection\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/HttpURLConnection\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/HttpURLConnection\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/HttpURLConnection\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/InstantRun\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/InstantRun\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/InstantRun\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" "b/AndroidAdavancedPart/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" rename to "AndroidAdavancedPart/Library\351\241\271\347\233\256\344\270\255\350\265\204\346\272\220id\344\275\277\347\224\250case\346\227\266\346\212\245\351\224\231.md" diff --git "a/Android\345\212\240\345\274\272/ListView\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/AndroidAdavancedPart/ListView\346\272\220\347\240\201\345\210\206\346\236\220.md" similarity index 100% rename from "Android\345\212\240\345\274\272/ListView\346\272\220\347\240\201\345\210\206\346\236\220.md" rename to "AndroidAdavancedPart/ListView\346\272\220\347\240\201\345\210\206\346\236\220.md" diff --git "a/Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" "b/AndroidAdavancedPart/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" similarity index 98% rename from "Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" rename to "AndroidAdavancedPart/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" index 1d9e42c3..9e6eae47 100644 --- "a/Android\345\212\240\345\274\272/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" +++ "b/AndroidAdavancedPart/MAT\345\206\205\345\255\230\345\210\206\346\236\220.md" @@ -1,30 +1,30 @@ -MAT内存分析 -=== - -`Eclipse Memory Analyzer(MAT)`是著名的跨平台集成开发环境`Eclipse Galileo`版本的33个组成项目中之一,它是一个功能丰富的`JAVA` 堆转储文件分析工具,可以帮助你发现内存漏洞和减少内存消耗。 - -[内存泄露介绍]{https://raw.githubusercontent.com/CharonChui/AndroidNote/blob/master/Android%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md} - -[MAT(Memory Analyzer)官网]{http://www.eclipse.org/mat/} -- 安装: - - 单机版,解压后直接使用 - - `Eclipse`插件,直接装一个插件,然后`open perspective`打开 `Memory Analysis` - -- 使用 - - DDMS - 进入`DDMS`页面,选择要分析的进程,然后点击`Update Heap`按钮。然后在右侧`Heap`页面点击一下`Cause GC`按钮,点击`Cause GC`按钮就是手动触发`Java`垃圾回收。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mat_1.png) - 如果想要看某个`Activity`是否发生内存泄露,可以反复多次进入和退出该页面, 然后点击几次`Cause GC`触发垃圾回收, - 看一下上图中`data object`这一栏中的`Total Size`的大小是保持稳定还是有明显的变大趋势,如果有明显的变大趋势就说明这个页面存在内存泄露的问题,需要进行具体分析。 - - -很长时间之前学习的,一直想记录出来,总是忙,到现在才开始整理,但是现在已经很少用`MAT`了,因为它太费劲了。 -自从良心企业发布了`leakCanary`后,都已经转投到大神门下。 - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +MAT内存分析 +=== + +`Eclipse Memory Analyzer(MAT)`是著名的跨平台集成开发环境`Eclipse Galileo`版本的33个组成项目中之一,它是一个功能丰富的`JAVA` 堆转储文件分析工具,可以帮助你发现内存漏洞和减少内存消耗。 + +[内存泄露介绍]{https://raw.githubusercontent.com/CharonChui/AndroidNote/blob/master/Android%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md} + +[MAT(Memory Analyzer)官网]{http://www.eclipse.org/mat/} +- 安装: + - 单机版,解压后直接使用 + - `Eclipse`插件,直接装一个插件,然后`open perspective`打开 `Memory Analysis` + +- 使用 + - DDMS + 进入`DDMS`页面,选择要分析的进程,然后点击`Update Heap`按钮。然后在右侧`Heap`页面点击一下`Cause GC`按钮,点击`Cause GC`按钮就是手动触发`Java`垃圾回收。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mat_1.png) + 如果想要看某个`Activity`是否发生内存泄露,可以反复多次进入和退出该页面, 然后点击几次`Cause GC`触发垃圾回收, + 看一下上图中`data object`这一栏中的`Total Size`的大小是保持稳定还是有明显的变大趋势,如果有明显的变大趋势就说明这个页面存在内存泄露的问题,需要进行具体分析。 + + +很长时间之前学习的,一直想记录出来,总是忙,到现在才开始整理,但是现在已经很少用`MAT`了,因为它太费劲了。 +自从良心企业发布了`leakCanary`后,都已经转投到大神门下。 + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" "b/AndroidAdavancedPart/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" similarity index 97% rename from "Android\345\212\240\345\274\272/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" rename to "AndroidAdavancedPart/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" index a060cbd9..0f0ff7b6 100644 --- "a/Android\345\212\240\345\274\272/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" +++ "b/AndroidAdavancedPart/Mac\344\270\213\351\205\215\347\275\256adb\345\217\212Android\345\221\275\344\273\244.md" @@ -1,34 +1,34 @@ -Mac下配置adb及Android命令 -=== - -带着欣喜若狂的心情,买了一个`Mac`本,刚拿到它的时候感觉真的是一件艺术品,哈哈。废话不多说,里面配置`Android`开发环境。 - -1. 找到`android sdk`的本地路径, - 我的是: - - `ADB` - `/Android/adt-bundle-mac-x86_64-20131030/sdk/platform-tools` - - `Android` - `/Android/adt-bundle-mac-x86_64-20131030/sdk/tools` - -2. 打开终端输入 - - `touch .bash_profile`创建 - - `open -e .bash_profile`打开 - -3. 添加路径 - `.bash_profile`打开了,我们在这里添加路径, - - 如果打开的文档里面已经有内容,我们只要之后添加`:XXXX`**(注意前面一定要用冒号隔开)** - - 如果是一个空白文档的话,我们就输入一下内容 - `export PATH=${PATH}:XXXX` - 我的是 - `export PATH=${PATH}:/Android/adt-bundle-mac-x86_64-20131030/sdk/tools:${PATH}:/Android/adt-bundle-mac-x86_64-20131030/sdk/platform-tools;` - 保存,关掉这个文档, -4. 终端输入命令 - `source .bash_profile` -5. 大功告成 - - - - --- - -- 邮箱 :charon.chui@gmail.com +Mac下配置adb及Android命令 +=== + +带着欣喜若狂的心情,买了一个`Mac`本,刚拿到它的时候感觉真的是一件艺术品,哈哈。废话不多说,里面配置`Android`开发环境。 + +1. 找到`android sdk`的本地路径, + 我的是: + - `ADB` + `/Android/adt-bundle-mac-x86_64-20131030/sdk/platform-tools` + - `Android` + `/Android/adt-bundle-mac-x86_64-20131030/sdk/tools` + +2. 打开终端输入 + - `touch .bash_profile`创建 + - `open -e .bash_profile`打开 + +3. 添加路径 + `.bash_profile`打开了,我们在这里添加路径, + - 如果打开的文档里面已经有内容,我们只要之后添加`:XXXX`**(注意前面一定要用冒号隔开)** + - 如果是一个空白文档的话,我们就输入一下内容 + `export PATH=${PATH}:XXXX` + 我的是 + `export PATH=${PATH}:/Android/adt-bundle-mac-x86_64-20131030/sdk/tools:${PATH}:/Android/adt-bundle-mac-x86_64-20131030/sdk/platform-tools;` + 保存,关掉这个文档, +4. 终端输入命令 + `source .bash_profile` +5. 大功告成 + + + + --- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" "b/AndroidAdavancedPart/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" rename to "AndroidAdavancedPart/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" diff --git "a/Android\345\212\240\345\274\272/MaterialDesign\344\275\277\347\224\250.md" "b/AndroidAdavancedPart/MaterialDesign\344\275\277\347\224\250.md" similarity index 100% rename from "Android\345\212\240\345\274\272/MaterialDesign\344\275\277\347\224\250.md" rename to "AndroidAdavancedPart/MaterialDesign\344\275\277\347\224\250.md" diff --git "a/Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" "b/AndroidAdavancedPart/RecyclerView\344\270\223\351\242\230.md" similarity index 100% rename from "Android\345\212\240\345\274\272/RecyclerView\344\270\223\351\242\230.md" rename to "AndroidAdavancedPart/RecyclerView\344\270\223\351\242\230.md" diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" "b/AndroidAdavancedPart/Retrofit\350\257\246\350\247\243(\344\270\212).md" similarity index 100% rename from "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\212).md" rename to "AndroidAdavancedPart/Retrofit\350\257\246\350\247\243(\344\270\212).md" diff --git "a/Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" "b/AndroidAdavancedPart/Retrofit\350\257\246\350\247\243(\344\270\213).md" similarity index 100% rename from "Android\345\212\240\345\274\272/Retrofit\350\257\246\350\247\243(\344\270\213).md" rename to "AndroidAdavancedPart/Retrofit\350\257\246\350\247\243(\344\270\213).md" diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" "b/AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\212).md" similarity index 100% rename from "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\212).md" rename to "AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\212).md" diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" "b/AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\213).md" similarity index 100% rename from "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\213).md" rename to "AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\213).md" diff --git "a/Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" "b/AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\255).md" similarity index 100% rename from "Android\345\212\240\345\274\272/RxJava\350\257\246\350\247\243(\344\270\255).md" rename to "AndroidAdavancedPart/RxJava\350\257\246\350\247\243(\344\270\255).md" diff --git "a/Android\345\212\240\345\274\272/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/AndroidAdavancedPart/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" similarity index 97% rename from "Android\345\212\240\345\274\272/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" rename to "AndroidAdavancedPart/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" index ee14261c..bece3204 100644 --- "a/Android\345\212\240\345\274\272/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ "b/AndroidAdavancedPart/VideoView\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -1,2594 +1,2594 @@ -VideoView源码分析 -=== - -VideoView ---- - -基于Android4.4源码进行分析 - -- 简介 - ```java - /** - * Displays a video file. The VideoView class - * can load images from various sources (such as resources or content - * providers), takes care of computing its measurement from the video so that - * it can be used in any layout manager, and provides various display options - * such as scaling and tinting.

- * - * Note: VideoView does not retain its full state when going into the - * background. In particular, it does not restore the current play state, - * play position, selected tracks, or any subtitle tracks added via - * {@link #addSubtitleSource addSubtitleSource()}. Applications should - * save and restore these on their own in - * {@link android.app.Activity#onSaveInstanceState} and - * {@link android.app.Activity#onRestoreInstanceState}.

- * Also note that the audio session id (from {@link #getAudioSessionId}) may - * change from its previously returned value when the VideoView is restored. - */ - ``` - -- 关系 - ```java - public class VideoView extends SurfaceView - implements MediaPlayerControl - ``` - -- 成员 - - 播放器所有的状态 - ```java - // all possible internal states - private static final int STATE_ERROR = -1; - private static final int STATE_IDLE = 0; - private static final int STATE_PREPARING = 1; - private static final int STATE_PREPARED = 2; - private static final int STATE_PLAYING = 3; - private static final int STATE_PAUSED = 4; - private static final int STATE_PLAYBACK_COMPLETED = 5; - ``` - - - 记录播放器状态 - ```java - // mCurrentState is a VideoView object's current state. - // mTargetState is the state that a method caller intends to reach. - // For instance, regardless the VideoView object's current state, - // calling pause() intends to bring the object to a target state - // of STATE_PAUSED. - private int mCurrentState = STATE_IDLE; - private int mTargetState = STATE_IDLE; - ``` - - - 主要功能部分 - ```java - private SurfaceHolder mSurfaceHolder = null;// 显示图像 - private MediaPlayer mMediaPlayer = null; // 声音、播放 - private MediaController mMediaController; // 播放控制 - ``` - - - 其他 - ```java - private int mVideoWidth; // 视频宽度 在onVideoSizeChanged() 和 onPrepared() 中可以得到具体大小 - private int mVideoHeight; //视频高度 - private int mSurfaceWidth; // Surface宽度 在SurfaceHolder.Callback.surfaceChanged() 中可以得到具体大小 - private int mSurfaceHeight; // Surface高度 - private int mSeekWhenPrepared; // recording the seek position while preparing - ``` - -- 具体实现 - - 构造方法 - ```java - public VideoView(Context context) { - super(context); - initVideoView(); - } - - public VideoView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - initVideoView(); - } - - public VideoView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initVideoView(); - } - ``` - - ```java - // 进行一些必要信息的初始化设置 - private void initVideoView() { - mVideoWidth = 0; - mVideoHeight = 0; - - // 通过SurfaceHolder去控制SurfaceView - getHolder().addCallback(mSHCallback); - // Deprecated. this is ignored, this value is set automatically when needed.Android3.0以上会自动设置,但是为了兼容还需设置 - getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); - - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - - // 字幕相关,用不到 - mPendingSubtitleTracks = new Vector>(); - - mCurrentState = STATE_IDLE; - mTargetState = STATE_IDLE; - } - ``` - - SurfaceHolder.Callback源码 - ```java - SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() - { - public void surfaceChanged(SurfaceHolder holder, int format, - int w, int h) - { - mSurfaceWidth = w; - mSurfaceHeight = h; - boolean isValidState = (mTargetState == STATE_PLAYING); - boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); - if (mMediaPlayer != null && isValidState && hasValidSize) { - if (mSeekWhenPrepared != 0) { - seekTo(mSeekWhenPrepared); - } - // 如果当前已经是播放状态的话就调用mediaplaer.start() 方法,并且把当前状态以及目标状态进行改变 - start(); - } - } - - public void surfaceCreated(SurfaceHolder holder) - { - mSurfaceHolder = holder; - // Surface创建后就开始调用播放 - openVideo(); - } - - public void surfaceDestroyed(SurfaceHolder holder) - { - // after we return from this we can't use the surface any more - mSurfaceHolder = null; - if (mMediaController != null) mMediaController.hide(); - release(true); - } - }; - ``` - - - 重写onMeasure()方法 - ```java - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = getDefaultSize(mVideoWidth, widthMeasureSpec); - int height = getDefaultSize(mVideoHeight, heightMeasureSpec); - - // ....根据视频的宽高比进行处理, 为了更好的宽展,提供一些用户能自己选择的模式,一般会另外提供方法, 这部分代码可以先不看 start - int width = getDefaultSize(mVideoWidth, widthMeasureSpec); - int height = getDefaultSize(mVideoHeight, heightMeasureSpec); - if (mVideoWidth > 0 && mVideoHeight > 0) { - - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { - // the size is fixed - width = widthSpecSize; - height = heightSpecSize; - - // for compatibility, we adjust size based on aspect ratio - if ( mVideoWidth * height < width * mVideoHeight ) { - //Log.i("@@@", "image too wide, correcting"); - width = height * mVideoWidth / mVideoHeight; - } else if ( mVideoWidth * height > width * mVideoHeight ) { - //Log.i("@@@", "image too tall, correcting"); - height = width * mVideoHeight / mVideoWidth; - } - } else if (widthSpecMode == MeasureSpec.EXACTLY) { - // only the width is fixed, adjust the height to match aspect ratio if possible - width = widthSpecSize; - height = width * mVideoHeight / mVideoWidth; - if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { - // couldn't match aspect ratio within the constraints - height = heightSpecSize; - } - } else if (heightSpecMode == MeasureSpec.EXACTLY) { - // only the height is fixed, adjust the width to match aspect ratio if possible - height = heightSpecSize; - width = height * mVideoWidth / mVideoHeight; - if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { - // couldn't match aspect ratio within the constraints - width = widthSpecSize; - } - } else { - // neither the width nor the height are fixed, try to use actual video size - width = mVideoWidth; - height = mVideoHeight; - if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { - // too tall, decrease both width and height - height = heightSpecSize; - width = height * mVideoWidth / mVideoHeight; - } - if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { - // too wide, decrease both width and height - width = widthSpecSize; - height = width * mVideoHeight / mVideoWidth; - } - } - } else { - // no size yet, just adopt the given spec sizes - } - // end - - setMeasuredDimension(width, height); - } - ``` - - - 附上getDefaultSize()源码 - ```java - /** - * Utility to return a default size. Uses the supplied size if the - * MeasureSpec imposed no constraints. Will get larger if allowed - * by the MeasureSpec. - * - * @param size Default size for this view - * @param measureSpec Constraints imposed by the parent - * @return The size this view should be. - */ - public static int getDefaultSize(int size, int measureSpec) { - int result = size; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.AT_MOST: - case MeasureSpec.EXACTLY: - result = specSize; - break; - } - return result; - } - ``` - - - 外部进行播放调用 - ```java - public void setVideoPath(String path) { - setVideoURI(Uri.parse(path)); - } - - public void setVideoURI(Uri uri) { - setVideoURI(uri, null); - } - - /** - * @hide - */ - public void setVideoURI(Uri uri, Map headers) { - mUri = uri; - mHeaders = headers; - mSeekWhenPrepared = 0; - - openVideo(); - - requestLayout(); - invalidate(); - } - ``` - - - openVide() 源码 - ```java - private void openVideo() { - if (mUri == null || mSurfaceHolder == null) { - // not ready for playback just yet, will try again later - return; - } - - // Tell the music playback service to pause - // TODO: these constants need to be published somewhere in the framework. - Intent i = new Intent("com.android.music.musicservicecommand"); - i.putExtra("command", "pause"); - mContext.sendBroadcast(i); - - // we shouldn't clear the target state, because somebody might have - // called start() previously // 先把已经存在的MediaPlayer释放掉,然后重新创建一个, 不一定只在SetVideoPath() 中调用,在其他地方也会调用 - release(false); - - try { - // 创建一个MediaPlayer - mMediaPlayer = new MediaPlayer(); - // TODO: create SubtitleController in MediaPlayer, but we need - // a context for the subtitle renderers - final Context context = getContext(); - final SubtitleController controller = new SubtitleController( - context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); - controller.registerRenderer(new WebVttRenderer(context)); - mMediaPlayer.setSubtitleAnchor(controller, this); - - if (mAudioSession != 0) { - mMediaPlayer.setAudioSessionId(mAudioSession); - } else { - mAudioSession = mMediaPlayer.getAudioSessionId(); - } - - // 设置一些必要的监听 - mMediaPlayer.setOnPreparedListener(mPreparedListener); - mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); - mMediaPlayer.setOnCompletionListener(mCompletionListener); - mMediaPlayer.setOnErrorListener(mErrorListener); - mMediaPlayer.setOnInfoListener(mInfoListener); - mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); - mCurrentBufferPercentage = 0; - // 让MediaPlayer进行播放 - mMediaPlayer.setDataSource(mContext, mUri, mHeaders); - // 让SurfaceView进行画面显示 - mMediaPlayer.setDisplay(mSurfaceHolder); - // 设置音频类型 - mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - // 播放时屏幕常亮 - mMediaPlayer.setScreenOnWhilePlaying(true); - // Prepares the player for playback, asynchronously. After setting the datasource and the display surface, you need to either call prepare() or prepareAsync(). For streams, you should call prepareAsync(), - // which returns immediately, rather than blocking until enough data has been buffered. - mMediaPlayer.prepareAsync(); - - for (Pair pending: mPendingSubtitleTracks) { - try { - mMediaPlayer.addSubtitleSource(pending.first, pending.second); - } catch (IllegalStateException e) { - mInfoListener.onInfo( - mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); - } - } - - // we don't set the target state here either, but preserve the - // target state that was there before. - mCurrentState = STATE_PREPARING; - // 如果已经调用过SetMediaController() 方法,这里会直接显示 - attachMediaController(); - } catch (IOException ex) { - Log.w(TAG, "Unable to open content: " + mUri, ex); - mCurrentState = STATE_ERROR; - mTargetState = STATE_ERROR; - mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - return; - } catch (IllegalArgumentException ex) { - Log.w(TAG, "Unable to open content: " + mUri, ex); - mCurrentState = STATE_ERROR; - mTargetState = STATE_ERROR; - mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - return; - } finally { - mPendingSubtitleTracks.clear(); - } - } - ``` - - release() 方法,在开始播放一个视频的时候会先调用该方法,然后重新创建一个,在SurfaceView销毁的时候也会调用该方法 - ```java - /* - * release the media player in any state - */ - private void release(boolean cleartargetstate) { - if (mMediaPlayer != null) { - mMediaPlayer.reset(); - mMediaPlayer.release(); - mMediaPlayer = null; - mPendingSubtitleTracks.clear(); - mCurrentState = STATE_IDLE; - if (cleartargetstate) { - mTargetState = STATE_IDLE; - } - } - } - ``` - - - 外部停止播放调用 - ```java - public void stopPlayback() { - if (mMediaPlayer != null) { - mMediaPlayer.stop(); - mMediaPlayer.release(); - mMediaPlayer = null; - mCurrentState = STATE_IDLE; - mTargetState = STATE_IDLE; - } - } - ``` - - - 外部设置控制栏部分 - ```java - public void setMediaController(MediaController controller) { - if (mMediaController != null) { - mMediaController.hide(); - } - mMediaController = controller; - attachMediaController(); - } - - private void attachMediaController() { - if (mMediaPlayer != null && mMediaController != null) { - // setMediaPlayer(MediaPlayerControl player), 让MediaPlayer相应的控制部分调用本类中的实现方法 - mMediaController.setMediaPlayer(this); - View anchorView = this.getParent() instanceof View ? - (View)this.getParent() : this; - - // 创建Controller并且依据AnchorView的位置进行显示 - mMediaController.setAnchorView(anchorView); - mMediaController.setEnabled(isInPlaybackState()); - } - } - ``` - - - MediaPlayer必要监听 - - OnVideoSizeChangedListener - ```java - MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = - new MediaPlayer.OnVideoSizeChangedListener() { - public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { - mVideoWidth = mp.getVideoWidth(); - mVideoHeight = mp.getVideoHeight(); - if (mVideoWidth != 0 && mVideoHeight != 0) { - // 这个方法是设置Surface分辨率,而不是设置视频播放窗口的大小,视频播放窗口大小是由SurfaceView的布局控制,要分清Surface与SurfaceView的区别,Surface是Window中整个的一个控件(句柄), - // 而SurfaceView是一个包含Surface的View,SurfaceView覆盖到Surface上(可以这样理解),我们只能通过SurfaceView来看Surface中的内容,至于在SurfaceView显示之外的Surface我们是不可见的. - getHolder().setFixedSize(mVideoWidth, mVideoHeight); - requestLayout(); - } - } - }; - ``` - - - OnPreparedListener - ```java - MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { - public void onPrepared(MediaPlayer mp) { - mCurrentState = STATE_PREPARED; - - // Get the capabilities of the player for this stream - Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, - MediaPlayer.BYPASS_METADATA_FILTER); - - if (data != null) { - mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) - || data.getBoolean(Metadata.PAUSE_AVAILABLE); - mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) - || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); - mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) - || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); - } else { - mCanPause = mCanSeekBack = mCanSeekForward = true; - } - - if (mOnPreparedListener != null) { - mOnPreparedListener.onPrepared(mMediaPlayer); - } - if (mMediaController != null) { - mMediaController.setEnabled(true); - } - mVideoWidth = mp.getVideoWidth(); - mVideoHeight = mp.getVideoHeight(); - - int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call - if (seekToPosition != 0) { - seekTo(seekToPosition); - } - if (mVideoWidth != 0 && mVideoHeight != 0) { - //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); - getHolder().setFixedSize(mVideoWidth, mVideoHeight); - if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { - // We didn't actually change the size (it was already at the size - // we need), so we won't get a "surface changed" callback, so - // start the video here instead of in the callback. - if (mTargetState == STATE_PLAYING) { - start(); - if (mMediaController != null) { - mMediaController.show(); - } - } else if (!isPlaying() && - (seekToPosition != 0 || getCurrentPosition() > 0)) { - if (mMediaController != null) { - // Show the media controls when we're paused into a video and make 'em stick. - mMediaController.show(0); - } - } - } - } else { - // We don't know the video size yet, but should start anyway. - // The video size might be reported to us later. - if (mTargetState == STATE_PLAYING) { - start(); - } - } - } - }; - ``` - - - OnCompletionListener - ```java - private MediaPlayer.OnCompletionListener mCompletionListener = - new MediaPlayer.OnCompletionListener() { - public void onCompletion(MediaPlayer mp) { - mCurrentState = STATE_PLAYBACK_COMPLETED; - mTargetState = STATE_PLAYBACK_COMPLETED; - if (mMediaController != null) { - mMediaController.hide(); - } - if (mOnCompletionListener != null) { - mOnCompletionListener.onCompletion(mMediaPlayer); - } - } - }; - ``` - - - Touch以及Key的监听 - ```java - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (isInPlaybackState() && mMediaController != null) { - // 控制MediaController的显示与隐藏 - toggleMediaControlsVisiblity(); - } - return false; - } - ``` - - - toggleMediaControlsVisiblity - ```java - private void toggleMediaControlsVisiblity() { - if (mMediaController.isShowing()) { - mMediaController.hide(); - } else { - mMediaController.show(); - } - } - ``` - - - Key - ```java - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) - { - boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && - keyCode != KeyEvent.KEYCODE_VOLUME_UP && - keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && - keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && - keyCode != KeyEvent.KEYCODE_MENU && - keyCode != KeyEvent.KEYCODE_CALL && - keyCode != KeyEvent.KEYCODE_ENDCALL; - if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { - if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { - if (mMediaPlayer.isPlaying()) { - pause(); - mMediaController.show(); - } else { - start(); - mMediaController.hide(); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { - if (!mMediaPlayer.isPlaying()) { - start(); - mMediaController.hide(); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { - if (mMediaPlayer.isPlaying()) { - pause(); - mMediaController.show(); - } - return true; - } else { - toggleMediaControlsVisiblity(); - } - } - - return super.onKeyDown(keyCode, event); - } - ``` - -华丽丽的分割线 上源码 -============== - ----------------------- -```java -/* - * Copyright (C) 2006 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. - */ - -/** - * Displays a video file. The VideoView class - * can load images from various sources (such as resources or content - * providers), takes care of computing its measurement from the video so that - * it can be used in any layout manager, and provides various display options - * such as scaling and tinting.

- * - * Note: VideoView does not retain its full state when going into the - * background. In particular, it does not restore the current play state, - * play position, selected tracks, or any subtitle tracks added via - * {@link #addSubtitleSource addSubtitleSource()}. Applications should - * save and restore these on their own in - * {@link android.app.Activity#onSaveInstanceState} and - * {@link android.app.Activity#onRestoreInstanceState}.

- * Also note that the audio session id (from {@link #getAudioSessionId}) may - * change from its previously returned value when the VideoView is restored. - */ -public class VideoView extends SurfaceView - implements MediaPlayerControl, SubtitleController.Anchor { - private String TAG = "VideoView"; - // settable by the client - private Uri mUri; - private Map mHeaders; - - // all possible internal states - private static final int STATE_ERROR = -1; - private static final int STATE_IDLE = 0; - private static final int STATE_PREPARING = 1; - private static final int STATE_PREPARED = 2; - private static final int STATE_PLAYING = 3; - private static final int STATE_PAUSED = 4; - private static final int STATE_PLAYBACK_COMPLETED = 5; - - // mCurrentState is a VideoView object's current state. - // mTargetState is the state that a method caller intends to reach. - // For instance, regardless the VideoView object's current state, - // calling pause() intends to bring the object to a target state - // of STATE_PAUSED. - private int mCurrentState = STATE_IDLE; - private int mTargetState = STATE_IDLE; - - // All the stuff we need for playing and showing a video - private SurfaceHolder mSurfaceHolder = null; - private MediaPlayer mMediaPlayer = null; - private int mAudioSession; - private int mVideoWidth; - private int mVideoHeight; - private int mSurfaceWidth; - private int mSurfaceHeight; - private MediaController mMediaController; - private OnCompletionListener mOnCompletionListener; - private MediaPlayer.OnPreparedListener mOnPreparedListener; - private int mCurrentBufferPercentage; - private OnErrorListener mOnErrorListener; - private OnInfoListener mOnInfoListener; - private int mSeekWhenPrepared; // recording the seek position while preparing - private boolean mCanPause; - private boolean mCanSeekBack; - private boolean mCanSeekForward; - - /** Subtitle rendering widget overlaid on top of the video. */ - private RenderingWidget mSubtitleWidget; - - /** Listener for changes to subtitle data, used to redraw when needed. */ - private RenderingWidget.OnChangedListener mSubtitlesChangedListener; - - public VideoView(Context context) { - super(context); - initVideoView(); - } - - public VideoView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - initVideoView(); - } - - public VideoView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initVideoView(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " - // + MeasureSpec.toString(heightMeasureSpec) + ")"); - - int width = getDefaultSize(mVideoWidth, widthMeasureSpec); - int height = getDefaultSize(mVideoHeight, heightMeasureSpec); - if (mVideoWidth > 0 && mVideoHeight > 0) { - - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { - // the size is fixed - width = widthSpecSize; - height = heightSpecSize; - - // for compatibility, we adjust size based on aspect ratio - if ( mVideoWidth * height < width * mVideoHeight ) { - //Log.i("@@@", "image too wide, correcting"); - width = height * mVideoWidth / mVideoHeight; - } else if ( mVideoWidth * height > width * mVideoHeight ) { - //Log.i("@@@", "image too tall, correcting"); - height = width * mVideoHeight / mVideoWidth; - } - } else if (widthSpecMode == MeasureSpec.EXACTLY) { - // only the width is fixed, adjust the height to match aspect ratio if possible - width = widthSpecSize; - height = width * mVideoHeight / mVideoWidth; - if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { - // couldn't match aspect ratio within the constraints - height = heightSpecSize; - } - } else if (heightSpecMode == MeasureSpec.EXACTLY) { - // only the height is fixed, adjust the width to match aspect ratio if possible - height = heightSpecSize; - width = height * mVideoWidth / mVideoHeight; - if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { - // couldn't match aspect ratio within the constraints - width = widthSpecSize; - } - } else { - // neither the width nor the height are fixed, try to use actual video size - width = mVideoWidth; - height = mVideoHeight; - if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { - // too tall, decrease both width and height - height = heightSpecSize; - width = height * mVideoWidth / mVideoHeight; - } - if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { - // too wide, decrease both width and height - width = widthSpecSize; - height = width * mVideoHeight / mVideoWidth; - } - } - } else { - // no size yet, just adopt the given spec sizes - } - setMeasuredDimension(width, height); - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(VideoView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(VideoView.class.getName()); - } - - public int resolveAdjustedSize(int desiredSize, int measureSpec) { - return getDefaultSize(desiredSize, measureSpec); - } - - private void initVideoView() { - mVideoWidth = 0; - mVideoHeight = 0; - getHolder().addCallback(mSHCallback); - getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - mPendingSubtitleTracks = new Vector>(); - mCurrentState = STATE_IDLE; - mTargetState = STATE_IDLE; - } - - public void setVideoPath(String path) { - setVideoURI(Uri.parse(path)); - } - - public void setVideoURI(Uri uri) { - setVideoURI(uri, null); - } - - /** - * @hide - */ - public void setVideoURI(Uri uri, Map headers) { - mUri = uri; - mHeaders = headers; - mSeekWhenPrepared = 0; - openVideo(); - requestLayout(); - invalidate(); - } - - /** - * Adds an external subtitle source file (from the provided input stream.) - * - * Note that a single external subtitle source may contain multiple or no - * supported tracks in it. If the source contained at least one track in - * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE} - * info message. Otherwise, if reading the source takes excessive time, - * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT} - * message. If the source contained no supported track (including an empty - * source file or null input stream), one will receive a {@link - * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the - * total number of available tracks using {@link MediaPlayer#getTrackInfo()} - * to see what additional tracks become available after this method call. - * - * @param is input stream containing the subtitle data. It will be - * closed by the media framework. - * @param format the format of the subtitle track(s). Must contain at least - * the mime type ({@link MediaFormat#KEY_MIME}) and the - * language ({@link MediaFormat#KEY_LANGUAGE}) of the file. - * If the file itself contains the language information, - * specify "und" for the language. - */ - public void addSubtitleSource(InputStream is, MediaFormat format) { - if (mMediaPlayer == null) { - mPendingSubtitleTracks.add(Pair.create(is, format)); - } else { - try { - mMediaPlayer.addSubtitleSource(is, format); - } catch (IllegalStateException e) { - mInfoListener.onInfo( - mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); - } - } - } - - private Vector> mPendingSubtitleTracks; - - public void stopPlayback() { - if (mMediaPlayer != null) { - mMediaPlayer.stop(); - mMediaPlayer.release(); - mMediaPlayer = null; - mCurrentState = STATE_IDLE; - mTargetState = STATE_IDLE; - } - } - - private void openVideo() { - if (mUri == null || mSurfaceHolder == null) { - // not ready for playback just yet, will try again later - return; - } - // Tell the music playback service to pause - // TODO: these constants need to be published somewhere in the framework. - Intent i = new Intent("com.android.music.musicservicecommand"); - i.putExtra("command", "pause"); - mContext.sendBroadcast(i); - - // we shouldn't clear the target state, because somebody might have - // called start() previously - release(false); - try { - mMediaPlayer = new MediaPlayer(); - // TODO: create SubtitleController in MediaPlayer, but we need - // a context for the subtitle renderers - final Context context = getContext(); - final SubtitleController controller = new SubtitleController( - context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); - controller.registerRenderer(new WebVttRenderer(context)); - mMediaPlayer.setSubtitleAnchor(controller, this); - - if (mAudioSession != 0) { - mMediaPlayer.setAudioSessionId(mAudioSession); - } else { - mAudioSession = mMediaPlayer.getAudioSessionId(); - } - mMediaPlayer.setOnPreparedListener(mPreparedListener); - mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); - mMediaPlayer.setOnCompletionListener(mCompletionListener); - mMediaPlayer.setOnErrorListener(mErrorListener); - mMediaPlayer.setOnInfoListener(mInfoListener); - mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); - mCurrentBufferPercentage = 0; - mMediaPlayer.setDataSource(mContext, mUri, mHeaders); - mMediaPlayer.setDisplay(mSurfaceHolder); - mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mMediaPlayer.setScreenOnWhilePlaying(true); - mMediaPlayer.prepareAsync(); - - for (Pair pending: mPendingSubtitleTracks) { - try { - mMediaPlayer.addSubtitleSource(pending.first, pending.second); - } catch (IllegalStateException e) { - mInfoListener.onInfo( - mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); - } - } - - // we don't set the target state here either, but preserve the - // target state that was there before. - mCurrentState = STATE_PREPARING; - attachMediaController(); - } catch (IOException ex) { - Log.w(TAG, "Unable to open content: " + mUri, ex); - mCurrentState = STATE_ERROR; - mTargetState = STATE_ERROR; - mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - return; - } catch (IllegalArgumentException ex) { - Log.w(TAG, "Unable to open content: " + mUri, ex); - mCurrentState = STATE_ERROR; - mTargetState = STATE_ERROR; - mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - return; - } finally { - mPendingSubtitleTracks.clear(); - } - } - - public void setMediaController(MediaController controller) { - if (mMediaController != null) { - mMediaController.hide(); - } - mMediaController = controller; - attachMediaController(); - } - - private void attachMediaController() { - if (mMediaPlayer != null && mMediaController != null) { - mMediaController.setMediaPlayer(this); - View anchorView = this.getParent() instanceof View ? - (View)this.getParent() : this; - mMediaController.setAnchorView(anchorView); - mMediaController.setEnabled(isInPlaybackState()); - } - } - - MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = - new MediaPlayer.OnVideoSizeChangedListener() { - public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { - mVideoWidth = mp.getVideoWidth(); - mVideoHeight = mp.getVideoHeight(); - if (mVideoWidth != 0 && mVideoHeight != 0) { - getHolder().setFixedSize(mVideoWidth, mVideoHeight); - requestLayout(); - } - } - }; - - MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { - public void onPrepared(MediaPlayer mp) { - mCurrentState = STATE_PREPARED; - - // Get the capabilities of the player for this stream - Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, - MediaPlayer.BYPASS_METADATA_FILTER); - - if (data != null) { - mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) - || data.getBoolean(Metadata.PAUSE_AVAILABLE); - mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) - || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); - mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) - || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); - } else { - mCanPause = mCanSeekBack = mCanSeekForward = true; - } - - if (mOnPreparedListener != null) { - mOnPreparedListener.onPrepared(mMediaPlayer); - } - if (mMediaController != null) { - mMediaController.setEnabled(true); - } - mVideoWidth = mp.getVideoWidth(); - mVideoHeight = mp.getVideoHeight(); - - int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call - if (seekToPosition != 0) { - seekTo(seekToPosition); - } - if (mVideoWidth != 0 && mVideoHeight != 0) { - //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); - getHolder().setFixedSize(mVideoWidth, mVideoHeight); - if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { - // We didn't actually change the size (it was already at the size - // we need), so we won't get a "surface changed" callback, so - // start the video here instead of in the callback. - if (mTargetState == STATE_PLAYING) { - start(); - if (mMediaController != null) { - mMediaController.show(); - } - } else if (!isPlaying() && - (seekToPosition != 0 || getCurrentPosition() > 0)) { - if (mMediaController != null) { - // Show the media controls when we're paused into a video and make 'em stick. - mMediaController.show(0); - } - } - } - } else { - // We don't know the video size yet, but should start anyway. - // The video size might be reported to us later. - if (mTargetState == STATE_PLAYING) { - start(); - } - } - } - }; - - private MediaPlayer.OnCompletionListener mCompletionListener = - new MediaPlayer.OnCompletionListener() { - public void onCompletion(MediaPlayer mp) { - mCurrentState = STATE_PLAYBACK_COMPLETED; - mTargetState = STATE_PLAYBACK_COMPLETED; - if (mMediaController != null) { - mMediaController.hide(); - } - if (mOnCompletionListener != null) { - mOnCompletionListener.onCompletion(mMediaPlayer); - } - } - }; - - private MediaPlayer.OnInfoListener mInfoListener = - new MediaPlayer.OnInfoListener() { - public boolean onInfo(MediaPlayer mp, int arg1, int arg2) { - if (mOnInfoListener != null) { - mOnInfoListener.onInfo(mp, arg1, arg2); - } - return true; - } - }; - - private MediaPlayer.OnErrorListener mErrorListener = - new MediaPlayer.OnErrorListener() { - public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { - Log.d(TAG, "Error: " + framework_err + "," + impl_err); - mCurrentState = STATE_ERROR; - mTargetState = STATE_ERROR; - if (mMediaController != null) { - mMediaController.hide(); - } - - /* If an error handler has been supplied, use it and finish. */ - if (mOnErrorListener != null) { - if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) { - return true; - } - } - - /* Otherwise, pop up an error dialog so the user knows that - * something bad has happened. Only try and pop up the dialog - * if we're attached to a window. When we're going away and no - * longer have a window, don't bother showing the user an error. - */ - if (getWindowToken() != null) { - Resources r = mContext.getResources(); - int messageId; - - if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { - messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback; - } else { - messageId = com.android.internal.R.string.VideoView_error_text_unknown; - } - - new AlertDialog.Builder(mContext) - .setMessage(messageId) - .setPositiveButton(com.android.internal.R.string.VideoView_error_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - /* If we get here, there is no onError listener, so - * at least inform them that the video is over. - */ - if (mOnCompletionListener != null) { - mOnCompletionListener.onCompletion(mMediaPlayer); - } - } - }) - .setCancelable(false) - .show(); - } - return true; - } - }; - - private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = - new MediaPlayer.OnBufferingUpdateListener() { - public void onBufferingUpdate(MediaPlayer mp, int percent) { - mCurrentBufferPercentage = percent; - } - }; - - /** - * Register a callback to be invoked when the media file - * is loaded and ready to go. - * - * @param l The callback that will be run - */ - public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) - { - mOnPreparedListener = l; - } - - /** - * Register a callback to be invoked when the end of a media file - * has been reached during playback. - * - * @param l The callback that will be run - */ - public void setOnCompletionListener(OnCompletionListener l) - { - mOnCompletionListener = l; - } - - /** - * Register a callback to be invoked when an error occurs - * during playback or setup. If no listener is specified, - * or if the listener returned false, VideoView will inform - * the user of any errors. - * - * @param l The callback that will be run - */ - public void setOnErrorListener(OnErrorListener l) - { - mOnErrorListener = l; - } - - /** - * Register a callback to be invoked when an informational event - * occurs during playback or setup. - * - * @param l The callback that will be run - */ - public void setOnInfoListener(OnInfoListener l) { - mOnInfoListener = l; - } - - SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() - { - public void surfaceChanged(SurfaceHolder holder, int format, - int w, int h) - { - mSurfaceWidth = w; - mSurfaceHeight = h; - boolean isValidState = (mTargetState == STATE_PLAYING); - boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); - if (mMediaPlayer != null && isValidState && hasValidSize) { - if (mSeekWhenPrepared != 0) { - seekTo(mSeekWhenPrepared); - } - start(); - } - } - - public void surfaceCreated(SurfaceHolder holder) - { - mSurfaceHolder = holder; - openVideo(); - } - - public void surfaceDestroyed(SurfaceHolder holder) - { - // after we return from this we can't use the surface any more - mSurfaceHolder = null; - if (mMediaController != null) mMediaController.hide(); - release(true); - } - }; - - /* - * release the media player in any state - */ - private void release(boolean cleartargetstate) { - if (mMediaPlayer != null) { - mMediaPlayer.reset(); - mMediaPlayer.release(); - mMediaPlayer = null; - mPendingSubtitleTracks.clear(); - mCurrentState = STATE_IDLE; - if (cleartargetstate) { - mTargetState = STATE_IDLE; - } - } - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (isInPlaybackState() && mMediaController != null) { - toggleMediaControlsVisiblity(); - } - return false; - } - - @Override - public boolean onTrackballEvent(MotionEvent ev) { - if (isInPlaybackState() && mMediaController != null) { - toggleMediaControlsVisiblity(); - } - return false; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) - { - boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && - keyCode != KeyEvent.KEYCODE_VOLUME_UP && - keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && - keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && - keyCode != KeyEvent.KEYCODE_MENU && - keyCode != KeyEvent.KEYCODE_CALL && - keyCode != KeyEvent.KEYCODE_ENDCALL; - if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { - if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { - if (mMediaPlayer.isPlaying()) { - pause(); - mMediaController.show(); - } else { - start(); - mMediaController.hide(); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { - if (!mMediaPlayer.isPlaying()) { - start(); - mMediaController.hide(); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { - if (mMediaPlayer.isPlaying()) { - pause(); - mMediaController.show(); - } - return true; - } else { - toggleMediaControlsVisiblity(); - } - } - - return super.onKeyDown(keyCode, event); - } - - private void toggleMediaControlsVisiblity() { - if (mMediaController.isShowing()) { - mMediaController.hide(); - } else { - mMediaController.show(); - } - } - - @Override - public void start() { - if (isInPlaybackState()) { - mMediaPlayer.start(); - mCurrentState = STATE_PLAYING; - } - mTargetState = STATE_PLAYING; - } - - @Override - public void pause() { - if (isInPlaybackState()) { - if (mMediaPlayer.isPlaying()) { - mMediaPlayer.pause(); - mCurrentState = STATE_PAUSED; - } - } - mTargetState = STATE_PAUSED; - } - - public void suspend() { - release(false); - } - - public void resume() { - openVideo(); - } - - @Override - public int getDuration() { - if (isInPlaybackState()) { - return mMediaPlayer.getDuration(); - } - - return -1; - } - - @Override - public int getCurrentPosition() { - if (isInPlaybackState()) { - return mMediaPlayer.getCurrentPosition(); - } - return 0; - } - - @Override - public void seekTo(int msec) { - if (isInPlaybackState()) { - mMediaPlayer.seekTo(msec); - mSeekWhenPrepared = 0; - } else { - mSeekWhenPrepared = msec; - } - } - - @Override - public boolean isPlaying() { - return isInPlaybackState() && mMediaPlayer.isPlaying(); - } - - @Override - public int getBufferPercentage() { - if (mMediaPlayer != null) { - return mCurrentBufferPercentage; - } - return 0; - } - - private boolean isInPlaybackState() { - return (mMediaPlayer != null && - mCurrentState != STATE_ERROR && - mCurrentState != STATE_IDLE && - mCurrentState != STATE_PREPARING); - } - - @Override - public boolean canPause() { - return mCanPause; - } - - @Override - public boolean canSeekBackward() { - return mCanSeekBack; - } - - @Override - public boolean canSeekForward() { - return mCanSeekForward; - } - - @Override - public int getAudioSessionId() { - if (mAudioSession == 0) { - MediaPlayer foo = new MediaPlayer(); - mAudioSession = foo.getAudioSessionId(); - foo.release(); - } - return mAudioSession; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (mSubtitleWidget != null) { - mSubtitleWidget.onAttachedToWindow(); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (mSubtitleWidget != null) { - mSubtitleWidget.onDetachedFromWindow(); - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mSubtitleWidget != null) { - measureAndLayoutSubtitleWidget(); - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - if (mSubtitleWidget != null) { - final int saveCount = canvas.save(); - canvas.translate(getPaddingLeft(), getPaddingTop()); - mSubtitleWidget.draw(canvas); - canvas.restoreToCount(saveCount); - } - } - - /** - * Forces a measurement and layout pass for all overlaid views. - * - * @see #setSubtitleWidget(RenderingWidget) - */ - private void measureAndLayoutSubtitleWidget() { - final int width = getWidth() - getPaddingLeft() - getPaddingRight(); - final int height = getHeight() - getPaddingTop() - getPaddingBottom(); - - mSubtitleWidget.setSize(width, height); - } - - /** @hide */ - @Override - public void setSubtitleWidget(RenderingWidget subtitleWidget) { - if (mSubtitleWidget == subtitleWidget) { - return; - } - - final boolean attachedToWindow = isAttachedToWindow(); - if (mSubtitleWidget != null) { - if (attachedToWindow) { - mSubtitleWidget.onDetachedFromWindow(); - } - - mSubtitleWidget.setOnChangedListener(null); - } - - mSubtitleWidget = subtitleWidget; - - if (subtitleWidget != null) { - if (mSubtitlesChangedListener == null) { - mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() { - @Override - public void onChanged(RenderingWidget renderingWidget) { - invalidate(); - } - }; - } - - setWillNotDraw(false); - subtitleWidget.setOnChangedListener(mSubtitlesChangedListener); - - if (attachedToWindow) { - subtitleWidget.onAttachedToWindow(); - requestLayout(); - } - } else { - setWillNotDraw(true); - } - - invalidate(); - } - - /** @hide */ - @Override - public Looper getSubtitleLooper() { - return Looper.getMainLooper(); - } -} -``` - -MediaPlayerControl ---- - - 通过该接口来打通MediaController以及VideoView - - ```java - public interface MediaPlayerControl { - void start(); - void pause(); - int getDuration(); - int getCurrentPosition(); - void seekTo(int pos); - boolean isPlaying(); - int getBufferPercentage(); - boolean canPause(); - boolean canSeekBackward(); - boolean canSeekForward(); - - /** - * Get the audio session id for the player used by this VideoView. This can be used to - * apply audio effects to the audio track of a video. - * @return The audio session, or 0 if there was an error. - */ - int getAudioSessionId(); -} - ``` - -MediaController ---- - -- 简介 - ```java - /** - * A view containing controls for a MediaPlayer. Typically contains the - * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress - * slider. It takes care of synchronizing the controls with the state - * of the MediaPlayer. - *

- * The way to use this class is to instantiate it programatically. - * The MediaController will create a default set of controls - * and put them in a window floating above your application. Specifically, - * the controls will float above the view specified with setAnchorView(). - * The window will disappear if left idle for three seconds and reappear - * when the user touches the anchor view. - *

- * Functions like show() and hide() have no effect when MediaController - * is created in an xml layout. - * - * MediaController will hide and - * show the buttons according to these rules: - *

    - *
  • The "previous" and "next" buttons are hidden until setPrevNextListeners() - * has been called - *
  • The "previous" and "next" buttons are visible but disabled if - * setPrevNextListeners() was called with null listeners - *
  • The "rewind" and "fastforward" buttons are shown unless requested - * otherwise by using the MediaController(Context, boolean) constructor - * with the boolean set to false - *
- */ - ``` - -- 关系 - ```java - public class MediaController extends FrameLayout - ``` - -- 成员 - ```java - // 一些控制功能的接口 - private MediaPlayerControl mPlayer; - private Context mContext; - // VideoView中调用setAnchorView()设置进来的View,MediaController显示的时候会感觉该AnchorView的位置进行显示 - private View mAnchor; - // MediaController最外层的根布局 - private View mRoot; - - // 通过Window的方式来显示MediaController,MediaController是一个填充屏幕的布局,但是背景是透明的 - private WindowManager mWindowManager; - private Window mWindow; - private View mDecor; - // 理解为当前整个MediaController的布局 - private WindowManager.LayoutParams mDecorLayoutParams; - private ProgressBar mProgress; - private TextView mEndTime, mCurrentTime; - private boolean mShowing; - private boolean mDragging; - // 默认自动消失的时间 - private static final int sDefaultTimeout = 3000; - private static final int FADE_OUT = 1; - private static final int SHOW_PROGRESS = 2; - private boolean mUseFastForward; - private boolean mFromXml; - private boolean mListenersSet; - private View.OnClickListener mNextListener, mPrevListener; - StringBuilder mFormatBuilder; - Formatter mFormatter; - private ImageButton mPauseButton; - private ImageButton mFfwdButton; - private ImageButton mRewButton; - private ImageButton mNextButton; - private ImageButton mPrevButton; - ``` - -- 构造方法 - ```java - public MediaController(Context context, AttributeSet attrs) { - super(context, attrs); - mRoot = this; - mContext = context; - mUseFastForward = true; - mFromXml = true; - } - - @Override - public void onFinishInflate() { - if (mRoot != null) - initControllerView(mRoot); - } - - public MediaController(Context context, boolean useFastForward) { - super(context); - mContext = context; - mUseFastForward = useFastForward; - // 创建该MediaController的布局 - initFloatingWindowLayout(); - initFloatingWindow(); - } - - public MediaController(Context context) { - this(context, true); - } - ``` - - - initFloatingWindowLayout - ```java - // Allocate and initialize the static parts of mDecorLayoutParams. Must - // also call updateFloatingWindowLayout() to fill in the dynamic parts - // (y and width) before mDecorLayoutParams can be used. - private void initFloatingWindowLayout() { - mDecorLayoutParams = new WindowManager.LayoutParams(); - WindowManager.LayoutParams p = mDecorLayoutParams; - p.gravity = Gravity.TOP | Gravity.LEFT; - p.height = LayoutParams.WRAP_CONTENT; - p.x = 0; - p.format = PixelFormat.TRANSLUCENT; - p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; - p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; - p.token = null; - p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; - } - ``` - - - initFloatingWindow - ```java - private void initFloatingWindow() { - // Android内核剖析 中有介绍 - mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); - mWindow = PolicyManager.makeNewWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - mWindow.requestFeature(Window.FEATURE_NO_TITLE); - // 通过WindowManager去add该Decor以及remove来实现MediaController的显示与隐藏 - mDecor = mWindow.getDecorView(); - mDecor.setOnTouchListener(mTouchListener); - mWindow.setContentView(this); - mWindow.setBackgroundDrawableResource(android.R.color.transparent); - - // While the media controller is up, the volume control keys should - // affect the media stream type - mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); - - setFocusable(true); - setFocusableInTouchMode(true); - setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); - requestFocus(); - } - ``` - - - mTouchListener - ```java - private OnTouchListener mTouchListener = new OnTouchListener() { - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (mShowing) { - hide(); - } - } - return false; - } - }; - ``` - - - setMediaPlayer - VideoView调用setMediaController的时候会调用到该方法 - ```java - public void setMediaPlayer(MediaPlayerControl player) { - mPlayer = player; - updatePausePlay(); - } - ``` - - - setAnchorView - VideoView调用setMediaController的时候会调用到该方法 - ```java - /** - * Set the view that acts as the anchor for the control view. - * This can for example be a VideoView, or your Activity's main view. - * When VideoView calls this method, it will use the VideoView's parent - * as the anchor. - * @param view The view to which to anchor the controller when it is visible. - */ - public void setAnchorView(View view) { - if (mAnchor != null) { - mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); - } - mAnchor = view; - if (mAnchor != null) { - mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); - } - - FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ); - - removeAllViews(); - View v = makeControllerView(); - addView(v, frameParams); - } - ``` - - - mLayoutChangeListener - ```java - // This is called whenever mAnchor's layout bound changes - private OnLayoutChangeListener mLayoutChangeListener = - new OnLayoutChangeListener() { - public void onLayoutChange(View v, int left, int top, int right, - int bottom, int oldLeft, int oldTop, int oldRight, - int oldBottom) { - // 更新布局 - updateFloatingWindowLayout(); - if (mShowing) { - mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); - } - } - }; - ``` - - - updateFloatingWindowLayout - ```java - // Update the dynamic parts of mDecorLayoutParams - // Must be called with mAnchor != NULL. - private void updateFloatingWindowLayout() { - int [] anchorPos = new int[2]; - mAnchor.getLocationOnScreen(anchorPos); - - // we need to know the size of the controller so we can properly position it - // within its space - mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); - - WindowManager.LayoutParams p = mDecorLayoutParams; - p.width = mAnchor.getWidth(); - p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; - p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); - } - ``` - - - makeControllerView - ```java - /** - * Create the view that holds the widgets that control playback. - * Derived classes can override this to create their own. - * @return The controller view. - * @hide This doesn't work as advertised - */ - protected View makeControllerView() { - LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); - // 对Controller中的一些按钮、功能进行事件设置 - initControllerView(mRoot); - - return mRoot; - } - ``` - - - touch事件处理 - ```java - @Override - public boolean onTouchEvent(MotionEvent event) { - show(sDefaultTimeout); - return true; - } - ``` - - - 进度的处理 - - seekBar的处理 - ```java - // There are two scenarios that can trigger the seekbar listener to trigger: - // - // The first is the user using the touchpad to adjust the posititon of the - // seekbar's thumb. In this case onStartTrackingTouch is called followed by - // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. - // We're setting the field "mDragging" to true for the duration of the dragging - // session to avoid jumps in the position in case of ongoing playback. - // - // The second scenario involves the user operating the scroll ball, in this - // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, - // we will simply apply the updated position without suspending regular updates. - private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { - public void onStartTrackingTouch(SeekBar bar) { - show(3600000); - - mDragging = true; - - // By removing these pending progress messages we make sure - // that a) we won't update the progress while the user adjusts - // the seekbar and b) once the user is done dragging the thumb - // we will post one of these messages to the queue again and - // this ensures that there will be exactly one message queued up. - mHandler.removeMessages(SHOW_PROGRESS); - } - - public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { - if (!fromuser) { - // We're not interested in programmatically generated changes to - // the progress bar's position. - return; - } - - long duration = mPlayer.getDuration(); - long newposition = (duration * progress) / 1000L; - mPlayer.seekTo( (int) newposition); - if (mCurrentTime != null) - mCurrentTime.setText(stringForTime( (int) newposition)); - } - - public void onStopTrackingTouch(SeekBar bar) { - mDragging = false; - setProgress(); - updatePausePlay(); - show(sDefaultTimeout); - - // Ensure that progress is properly updated in the future, - // the call to show() does not guarantee this because it is a - // no-op if we are already showing. - mHandler.sendEmptyMessage(SHOW_PROGRESS); - } - }; - ``` - - - SetProgress - ```java - private int setProgress() { - if (mPlayer == null || mDragging) { - return 0; - } - int position = mPlayer.getCurrentPosition(); - int duration = mPlayer.getDuration(); - if (mProgress != null) { - if (duration > 0) { - // use long to avoid overflow - long pos = 1000L * position / duration; - mProgress.setProgress( (int) pos); - } - int percent = mPlayer.getBufferPercentage(); - mProgress.setSecondaryProgress(percent * 10); - } - - if (mEndTime != null) - mEndTime.setText(stringForTime(duration)); - if (mCurrentTime != null) - mCurrentTime.setText(stringForTime(position)); - - return position; - } - ``` - - - show - ```java - /** - * Show the controller on screen. It will go away - * automatically after 'timeout' milliseconds of inactivity. - * @param timeout The timeout in milliseconds. Use 0 to show - * the controller until hide() is called. - */ - public void show(int timeout) { - if (!mShowing && mAnchor != null) { - // 先去设置一下进度 - setProgress(); - if (mPauseButton != null) { - mPauseButton.requestFocus(); - } - disableUnsupportedButtons(); - updateFloatingWindowLayout(); - mWindowManager.addView(mDecor, mDecorLayoutParams); - mShowing = true; - } - updatePausePlay(); - - // cause the progress bar to be updated even if mShowing - // was already true. This happens, for example, if we're - // paused with the progress bar showing the user hits play. - // 发送定期更新进度的消息 - mHandler.sendEmptyMessage(SHOW_PROGRESS); - - Message msg = mHandler.obtainMessage(FADE_OUT); - if (timeout != 0) { - mHandler.removeMessages(FADE_OUT); - mHandler.sendMessageDelayed(msg, timeout); - } - } - ``` - - - hide - ```java - /** - * Remove the controller from the screen. - */ - public void hide() { - if (mAnchor == null) - return; - - if (mShowing) { - try { - // 移除定期更新消息 - mHandler.removeMessages(SHOW_PROGRESS); - mWindowManager.removeView(mDecor); - } catch (IllegalArgumentException ex) { - Log.w("MediaController", "already removed"); - } - mShowing = false; - } - } - ``` - -上源码 -=== - -```java -/* - * Copyright (C) 2006 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.widget; - -import android.content.Context; -import android.graphics.PixelFormat; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Message; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.SeekBar.OnSeekBarChangeListener; - -import com.android.internal.policy.PolicyManager; - -import java.util.Formatter; -import java.util.Locale; - -/** - * A view containing controls for a MediaPlayer. Typically contains the - * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress - * slider. It takes care of synchronizing the controls with the state - * of the MediaPlayer. - *

- * The way to use this class is to instantiate it programatically. - * The MediaController will create a default set of controls - * and put them in a window floating above your application. Specifically, - * the controls will float above the view specified with setAnchorView(). - * The window will disappear if left idle for three seconds and reappear - * when the user touches the anchor view. - *

- * Functions like show() and hide() have no effect when MediaController - * is created in an xml layout. - * - * MediaController will hide and - * show the buttons according to these rules: - *

    - *
  • The "previous" and "next" buttons are hidden until setPrevNextListeners() - * has been called - *
  • The "previous" and "next" buttons are visible but disabled if - * setPrevNextListeners() was called with null listeners - *
  • The "rewind" and "fastforward" buttons are shown unless requested - * otherwise by using the MediaController(Context, boolean) constructor - * with the boolean set to false - *
- */ -public class MediaController extends FrameLayout { - - private MediaPlayerControl mPlayer; - private Context mContext; - private View mAnchor; - private View mRoot; - private WindowManager mWindowManager; - private Window mWindow; - private View mDecor; - private WindowManager.LayoutParams mDecorLayoutParams; - private ProgressBar mProgress; - private TextView mEndTime, mCurrentTime; - private boolean mShowing; - private boolean mDragging; - private static final int sDefaultTimeout = 3000; - private static final int FADE_OUT = 1; - private static final int SHOW_PROGRESS = 2; - private boolean mUseFastForward; - private boolean mFromXml; - private boolean mListenersSet; - private View.OnClickListener mNextListener, mPrevListener; - StringBuilder mFormatBuilder; - Formatter mFormatter; - private ImageButton mPauseButton; - private ImageButton mFfwdButton; - private ImageButton mRewButton; - private ImageButton mNextButton; - private ImageButton mPrevButton; - - public MediaController(Context context, AttributeSet attrs) { - super(context, attrs); - mRoot = this; - mContext = context; - mUseFastForward = true; - mFromXml = true; - } - - @Override - public void onFinishInflate() { - if (mRoot != null) - initControllerView(mRoot); - } - - public MediaController(Context context, boolean useFastForward) { - super(context); - mContext = context; - mUseFastForward = useFastForward; - initFloatingWindowLayout(); - initFloatingWindow(); - } - - public MediaController(Context context) { - this(context, true); - } - - private void initFloatingWindow() { - mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); - mWindow = PolicyManager.makeNewWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - mWindow.requestFeature(Window.FEATURE_NO_TITLE); - mDecor = mWindow.getDecorView(); - mDecor.setOnTouchListener(mTouchListener); - mWindow.setContentView(this); - mWindow.setBackgroundDrawableResource(android.R.color.transparent); - - // While the media controller is up, the volume control keys should - // affect the media stream type - mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); - - setFocusable(true); - setFocusableInTouchMode(true); - setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); - requestFocus(); - } - - // Allocate and initialize the static parts of mDecorLayoutParams. Must - // also call updateFloatingWindowLayout() to fill in the dynamic parts - // (y and width) before mDecorLayoutParams can be used. - private void initFloatingWindowLayout() { - mDecorLayoutParams = new WindowManager.LayoutParams(); - WindowManager.LayoutParams p = mDecorLayoutParams; - p.gravity = Gravity.TOP | Gravity.LEFT; - p.height = LayoutParams.WRAP_CONTENT; - p.x = 0; - p.format = PixelFormat.TRANSLUCENT; - p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; - p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; - p.token = null; - p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; - } - - // Update the dynamic parts of mDecorLayoutParams - // Must be called with mAnchor != NULL. - private void updateFloatingWindowLayout() { - int [] anchorPos = new int[2]; - mAnchor.getLocationOnScreen(anchorPos); - - // we need to know the size of the controller so we can properly position it - // within its space - mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); - - WindowManager.LayoutParams p = mDecorLayoutParams; - p.width = mAnchor.getWidth(); - p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; - p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); - } - - // This is called whenever mAnchor's layout bound changes - private OnLayoutChangeListener mLayoutChangeListener = - new OnLayoutChangeListener() { - public void onLayoutChange(View v, int left, int top, int right, - int bottom, int oldLeft, int oldTop, int oldRight, - int oldBottom) { - updateFloatingWindowLayout(); - if (mShowing) { - mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); - } - } - }; - - private OnTouchListener mTouchListener = new OnTouchListener() { - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (mShowing) { - hide(); - } - } - return false; - } - }; - - public void setMediaPlayer(MediaPlayerControl player) { - mPlayer = player; - updatePausePlay(); - } - - /** - * Set the view that acts as the anchor for the control view. - * This can for example be a VideoView, or your Activity's main view. - * When VideoView calls this method, it will use the VideoView's parent - * as the anchor. - * @param view The view to which to anchor the controller when it is visible. - */ - public void setAnchorView(View view) { - if (mAnchor != null) { - mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); - } - mAnchor = view; - if (mAnchor != null) { - mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); - } - - FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ); - - removeAllViews(); - View v = makeControllerView(); - addView(v, frameParams); - } - - /** - * Create the view that holds the widgets that control playback. - * Derived classes can override this to create their own. - * @return The controller view. - * @hide This doesn't work as advertised - */ - protected View makeControllerView() { - LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); - - initControllerView(mRoot); - - return mRoot; - } - - private void initControllerView(View v) { - mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause); - if (mPauseButton != null) { - mPauseButton.requestFocus(); - mPauseButton.setOnClickListener(mPauseListener); - } - - mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd); - if (mFfwdButton != null) { - mFfwdButton.setOnClickListener(mFfwdListener); - if (!mFromXml) { - mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); - } - } - - mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew); - if (mRewButton != null) { - mRewButton.setOnClickListener(mRewListener); - if (!mFromXml) { - mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); - } - } - - // By default these are hidden. They will be enabled when setPrevNextListeners() is called - mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next); - if (mNextButton != null && !mFromXml && !mListenersSet) { - mNextButton.setVisibility(View.GONE); - } - mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev); - if (mPrevButton != null && !mFromXml && !mListenersSet) { - mPrevButton.setVisibility(View.GONE); - } - - mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress); - if (mProgress != null) { - if (mProgress instanceof SeekBar) { - SeekBar seeker = (SeekBar) mProgress; - seeker.setOnSeekBarChangeListener(mSeekListener); - } - mProgress.setMax(1000); - } - - mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time); - mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current); - mFormatBuilder = new StringBuilder(); - mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); - - installPrevNextListeners(); - } - - /** - * Show the controller on screen. It will go away - * automatically after 3 seconds of inactivity. - */ - public void show() { - show(sDefaultTimeout); - } - - /** - * Disable pause or seek buttons if the stream cannot be paused or seeked. - * This requires the control interface to be a MediaPlayerControlExt - */ - private void disableUnsupportedButtons() { - try { - if (mPauseButton != null && !mPlayer.canPause()) { - mPauseButton.setEnabled(false); - } - if (mRewButton != null && !mPlayer.canSeekBackward()) { - mRewButton.setEnabled(false); - } - if (mFfwdButton != null && !mPlayer.canSeekForward()) { - mFfwdButton.setEnabled(false); - } - } catch (IncompatibleClassChangeError ex) { - // We were given an old version of the interface, that doesn't have - // the canPause/canSeekXYZ methods. This is OK, it just means we - // assume the media can be paused and seeked, and so we don't disable - // the buttons. - } - } - - /** - * Show the controller on screen. It will go away - * automatically after 'timeout' milliseconds of inactivity. - * @param timeout The timeout in milliseconds. Use 0 to show - * the controller until hide() is called. - */ - public void show(int timeout) { - if (!mShowing && mAnchor != null) { - setProgress(); - if (mPauseButton != null) { - mPauseButton.requestFocus(); - } - disableUnsupportedButtons(); - updateFloatingWindowLayout(); - mWindowManager.addView(mDecor, mDecorLayoutParams); - mShowing = true; - } - updatePausePlay(); - - // cause the progress bar to be updated even if mShowing - // was already true. This happens, for example, if we're - // paused with the progress bar showing the user hits play. - mHandler.sendEmptyMessage(SHOW_PROGRESS); - - Message msg = mHandler.obtainMessage(FADE_OUT); - if (timeout != 0) { - mHandler.removeMessages(FADE_OUT); - mHandler.sendMessageDelayed(msg, timeout); - } - } - - public boolean isShowing() { - return mShowing; - } - - /** - * Remove the controller from the screen. - */ - public void hide() { - if (mAnchor == null) - return; - - if (mShowing) { - try { - mHandler.removeMessages(SHOW_PROGRESS); - mWindowManager.removeView(mDecor); - } catch (IllegalArgumentException ex) { - Log.w("MediaController", "already removed"); - } - mShowing = false; - } - } - - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - int pos; - switch (msg.what) { - case FADE_OUT: - hide(); - break; - case SHOW_PROGRESS: - pos = setProgress(); - if (!mDragging && mShowing && mPlayer.isPlaying()) { - msg = obtainMessage(SHOW_PROGRESS); - sendMessageDelayed(msg, 1000 - (pos % 1000)); - } - break; - } - } - }; - - private String stringForTime(int timeMs) { - int totalSeconds = timeMs / 1000; - - int seconds = totalSeconds % 60; - int minutes = (totalSeconds / 60) % 60; - int hours = totalSeconds / 3600; - - mFormatBuilder.setLength(0); - if (hours > 0) { - return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); - } else { - return mFormatter.format("%02d:%02d", minutes, seconds).toString(); - } - } - - private int setProgress() { - if (mPlayer == null || mDragging) { - return 0; - } - int position = mPlayer.getCurrentPosition(); - int duration = mPlayer.getDuration(); - if (mProgress != null) { - if (duration > 0) { - // use long to avoid overflow - long pos = 1000L * position / duration; - mProgress.setProgress( (int) pos); - } - int percent = mPlayer.getBufferPercentage(); - mProgress.setSecondaryProgress(percent * 10); - } - - if (mEndTime != null) - mEndTime.setText(stringForTime(duration)); - if (mCurrentTime != null) - mCurrentTime.setText(stringForTime(position)); - - return position; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - show(sDefaultTimeout); - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent ev) { - show(sDefaultTimeout); - return false; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - final boolean uniqueDown = event.getRepeatCount() == 0 - && event.getAction() == KeyEvent.ACTION_DOWN; - if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - || keyCode == KeyEvent.KEYCODE_SPACE) { - if (uniqueDown) { - doPauseResume(); - show(sDefaultTimeout); - if (mPauseButton != null) { - mPauseButton.requestFocus(); - } - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { - if (uniqueDown && !mPlayer.isPlaying()) { - mPlayer.start(); - updatePausePlay(); - show(sDefaultTimeout); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { - if (uniqueDown && mPlayer.isPlaying()) { - mPlayer.pause(); - updatePausePlay(); - show(sDefaultTimeout); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN - || keyCode == KeyEvent.KEYCODE_VOLUME_UP - || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE - || keyCode == KeyEvent.KEYCODE_CAMERA) { - // don't show the controls for volume adjustment - return super.dispatchKeyEvent(event); - } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { - if (uniqueDown) { - hide(); - } - return true; - } - - show(sDefaultTimeout); - return super.dispatchKeyEvent(event); - } - - private View.OnClickListener mPauseListener = new View.OnClickListener() { - public void onClick(View v) { - doPauseResume(); - show(sDefaultTimeout); - } - }; - - private void updatePausePlay() { - if (mRoot == null || mPauseButton == null) - return; - - if (mPlayer.isPlaying()) { - mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause); - } else { - mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play); - } - } - - private void doPauseResume() { - if (mPlayer.isPlaying()) { - mPlayer.pause(); - } else { - mPlayer.start(); - } - updatePausePlay(); - } - - // There are two scenarios that can trigger the seekbar listener to trigger: - // - // The first is the user using the touchpad to adjust the posititon of the - // seekbar's thumb. In this case onStartTrackingTouch is called followed by - // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. - // We're setting the field "mDragging" to true for the duration of the dragging - // session to avoid jumps in the position in case of ongoing playback. - // - // The second scenario involves the user operating the scroll ball, in this - // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, - // we will simply apply the updated position without suspending regular updates. - private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { - public void onStartTrackingTouch(SeekBar bar) { - show(3600000); - - mDragging = true; - - // By removing these pending progress messages we make sure - // that a) we won't update the progress while the user adjusts - // the seekbar and b) once the user is done dragging the thumb - // we will post one of these messages to the queue again and - // this ensures that there will be exactly one message queued up. - mHandler.removeMessages(SHOW_PROGRESS); - } - - public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { - if (!fromuser) { - // We're not interested in programmatically generated changes to - // the progress bar's position. - return; - } - - long duration = mPlayer.getDuration(); - long newposition = (duration * progress) / 1000L; - mPlayer.seekTo( (int) newposition); - if (mCurrentTime != null) - mCurrentTime.setText(stringForTime( (int) newposition)); - } - - public void onStopTrackingTouch(SeekBar bar) { - mDragging = false; - setProgress(); - updatePausePlay(); - show(sDefaultTimeout); - - // Ensure that progress is properly updated in the future, - // the call to show() does not guarantee this because it is a - // no-op if we are already showing. - mHandler.sendEmptyMessage(SHOW_PROGRESS); - } - }; - - @Override - public void setEnabled(boolean enabled) { - if (mPauseButton != null) { - mPauseButton.setEnabled(enabled); - } - if (mFfwdButton != null) { - mFfwdButton.setEnabled(enabled); - } - if (mRewButton != null) { - mRewButton.setEnabled(enabled); - } - if (mNextButton != null) { - mNextButton.setEnabled(enabled && mNextListener != null); - } - if (mPrevButton != null) { - mPrevButton.setEnabled(enabled && mPrevListener != null); - } - if (mProgress != null) { - mProgress.setEnabled(enabled); - } - disableUnsupportedButtons(); - super.setEnabled(enabled); - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(MediaController.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(MediaController.class.getName()); - } - - private View.OnClickListener mRewListener = new View.OnClickListener() { - public void onClick(View v) { - int pos = mPlayer.getCurrentPosition(); - pos -= 5000; // milliseconds - mPlayer.seekTo(pos); - setProgress(); - - show(sDefaultTimeout); - } - }; - - private View.OnClickListener mFfwdListener = new View.OnClickListener() { - public void onClick(View v) { - int pos = mPlayer.getCurrentPosition(); - pos += 15000; // milliseconds - mPlayer.seekTo(pos); - setProgress(); - - show(sDefaultTimeout); - } - }; - - private void installPrevNextListeners() { - if (mNextButton != null) { - mNextButton.setOnClickListener(mNextListener); - mNextButton.setEnabled(mNextListener != null); - } - - if (mPrevButton != null) { - mPrevButton.setOnClickListener(mPrevListener); - mPrevButton.setEnabled(mPrevListener != null); - } - } - - public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { - mNextListener = next; - mPrevListener = prev; - mListenersSet = true; - - if (mRoot != null) { - installPrevNextListeners(); - - if (mNextButton != null && !mFromXml) { - mNextButton.setVisibility(View.VISIBLE); - } - if (mPrevButton != null && !mFromXml) { - mPrevButton.setVisibility(View.VISIBLE); - } - } - } - - public interface MediaPlayerControl { - void start(); - void pause(); - int getDuration(); - int getCurrentPosition(); - void seekTo(int pos); - boolean isPlaying(); - int getBufferPercentage(); - boolean canPause(); - boolean canSeekBackward(); - boolean canSeekForward(); - - /** - * Get the audio session id for the player used by this VideoView. This can be used to - * apply audio effects to the audio track of a video. - * @return The audio session, or 0 if there was an error. - */ - int getAudioSessionId(); - } -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +VideoView源码分析 +=== + +VideoView +--- + +基于Android4.4源码进行分析 + +- 简介 + ```java + /** + * Displays a video file. The VideoView class + * can load images from various sources (such as resources or content + * providers), takes care of computing its measurement from the video so that + * it can be used in any layout manager, and provides various display options + * such as scaling and tinting.

+ * + * Note: VideoView does not retain its full state when going into the + * background. In particular, it does not restore the current play state, + * play position, selected tracks, or any subtitle tracks added via + * {@link #addSubtitleSource addSubtitleSource()}. Applications should + * save and restore these on their own in + * {@link android.app.Activity#onSaveInstanceState} and + * {@link android.app.Activity#onRestoreInstanceState}.

+ * Also note that the audio session id (from {@link #getAudioSessionId}) may + * change from its previously returned value when the VideoView is restored. + */ + ``` + +- 关系 + ```java + public class VideoView extends SurfaceView + implements MediaPlayerControl + ``` + +- 成员 + - 播放器所有的状态 + ```java + // all possible internal states + private static final int STATE_ERROR = -1; + private static final int STATE_IDLE = 0; + private static final int STATE_PREPARING = 1; + private static final int STATE_PREPARED = 2; + private static final int STATE_PLAYING = 3; + private static final int STATE_PAUSED = 4; + private static final int STATE_PLAYBACK_COMPLETED = 5; + ``` + + - 记录播放器状态 + ```java + // mCurrentState is a VideoView object's current state. + // mTargetState is the state that a method caller intends to reach. + // For instance, regardless the VideoView object's current state, + // calling pause() intends to bring the object to a target state + // of STATE_PAUSED. + private int mCurrentState = STATE_IDLE; + private int mTargetState = STATE_IDLE; + ``` + + - 主要功能部分 + ```java + private SurfaceHolder mSurfaceHolder = null;// 显示图像 + private MediaPlayer mMediaPlayer = null; // 声音、播放 + private MediaController mMediaController; // 播放控制 + ``` + + - 其他 + ```java + private int mVideoWidth; // 视频宽度 在onVideoSizeChanged() 和 onPrepared() 中可以得到具体大小 + private int mVideoHeight; //视频高度 + private int mSurfaceWidth; // Surface宽度 在SurfaceHolder.Callback.surfaceChanged() 中可以得到具体大小 + private int mSurfaceHeight; // Surface高度 + private int mSeekWhenPrepared; // recording the seek position while preparing + ``` + +- 具体实现 + - 构造方法 + ```java + public VideoView(Context context) { + super(context); + initVideoView(); + } + + public VideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + initVideoView(); + } + + public VideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initVideoView(); + } + ``` + + ```java + // 进行一些必要信息的初始化设置 + private void initVideoView() { + mVideoWidth = 0; + mVideoHeight = 0; + + // 通过SurfaceHolder去控制SurfaceView + getHolder().addCallback(mSHCallback); + // Deprecated. this is ignored, this value is set automatically when needed.Android3.0以上会自动设置,但是为了兼容还需设置 + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + + // 字幕相关,用不到 + mPendingSubtitleTracks = new Vector>(); + + mCurrentState = STATE_IDLE; + mTargetState = STATE_IDLE; + } + ``` + + SurfaceHolder.Callback源码 + ```java + SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() + { + public void surfaceChanged(SurfaceHolder holder, int format, + int w, int h) + { + mSurfaceWidth = w; + mSurfaceHeight = h; + boolean isValidState = (mTargetState == STATE_PLAYING); + boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); + if (mMediaPlayer != null && isValidState && hasValidSize) { + if (mSeekWhenPrepared != 0) { + seekTo(mSeekWhenPrepared); + } + // 如果当前已经是播放状态的话就调用mediaplaer.start() 方法,并且把当前状态以及目标状态进行改变 + start(); + } + } + + public void surfaceCreated(SurfaceHolder holder) + { + mSurfaceHolder = holder; + // Surface创建后就开始调用播放 + openVideo(); + } + + public void surfaceDestroyed(SurfaceHolder holder) + { + // after we return from this we can't use the surface any more + mSurfaceHolder = null; + if (mMediaController != null) mMediaController.hide(); + release(true); + } + }; + ``` + + - 重写onMeasure()方法 + ```java + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = getDefaultSize(mVideoWidth, widthMeasureSpec); + int height = getDefaultSize(mVideoHeight, heightMeasureSpec); + + // ....根据视频的宽高比进行处理, 为了更好的宽展,提供一些用户能自己选择的模式,一般会另外提供方法, 这部分代码可以先不看 start + int width = getDefaultSize(mVideoWidth, widthMeasureSpec); + int height = getDefaultSize(mVideoHeight, heightMeasureSpec); + if (mVideoWidth > 0 && mVideoHeight > 0) { + + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { + // the size is fixed + width = widthSpecSize; + height = heightSpecSize; + + // for compatibility, we adjust size based on aspect ratio + if ( mVideoWidth * height < width * mVideoHeight ) { + //Log.i("@@@", "image too wide, correcting"); + width = height * mVideoWidth / mVideoHeight; + } else if ( mVideoWidth * height > width * mVideoHeight ) { + //Log.i("@@@", "image too tall, correcting"); + height = width * mVideoHeight / mVideoWidth; + } + } else if (widthSpecMode == MeasureSpec.EXACTLY) { + // only the width is fixed, adjust the height to match aspect ratio if possible + width = widthSpecSize; + height = width * mVideoHeight / mVideoWidth; + if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { + // couldn't match aspect ratio within the constraints + height = heightSpecSize; + } + } else if (heightSpecMode == MeasureSpec.EXACTLY) { + // only the height is fixed, adjust the width to match aspect ratio if possible + height = heightSpecSize; + width = height * mVideoWidth / mVideoHeight; + if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { + // couldn't match aspect ratio within the constraints + width = widthSpecSize; + } + } else { + // neither the width nor the height are fixed, try to use actual video size + width = mVideoWidth; + height = mVideoHeight; + if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { + // too tall, decrease both width and height + height = heightSpecSize; + width = height * mVideoWidth / mVideoHeight; + } + if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { + // too wide, decrease both width and height + width = widthSpecSize; + height = width * mVideoHeight / mVideoWidth; + } + } + } else { + // no size yet, just adopt the given spec sizes + } + // end + + setMeasuredDimension(width, height); + } + ``` + + - 附上getDefaultSize()源码 + ```java + /** + * Utility to return a default size. Uses the supplied size if the + * MeasureSpec imposed no constraints. Will get larger if allowed + * by the MeasureSpec. + * + * @param size Default size for this view + * @param measureSpec Constraints imposed by the parent + * @return The size this view should be. + */ + public static int getDefaultSize(int size, int measureSpec) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; + } + ``` + + - 外部进行播放调用 + ```java + public void setVideoPath(String path) { + setVideoURI(Uri.parse(path)); + } + + public void setVideoURI(Uri uri) { + setVideoURI(uri, null); + } + + /** + * @hide + */ + public void setVideoURI(Uri uri, Map headers) { + mUri = uri; + mHeaders = headers; + mSeekWhenPrepared = 0; + + openVideo(); + + requestLayout(); + invalidate(); + } + ``` + + - openVide() 源码 + ```java + private void openVideo() { + if (mUri == null || mSurfaceHolder == null) { + // not ready for playback just yet, will try again later + return; + } + + // Tell the music playback service to pause + // TODO: these constants need to be published somewhere in the framework. + Intent i = new Intent("com.android.music.musicservicecommand"); + i.putExtra("command", "pause"); + mContext.sendBroadcast(i); + + // we shouldn't clear the target state, because somebody might have + // called start() previously // 先把已经存在的MediaPlayer释放掉,然后重新创建一个, 不一定只在SetVideoPath() 中调用,在其他地方也会调用 + release(false); + + try { + // 创建一个MediaPlayer + mMediaPlayer = new MediaPlayer(); + // TODO: create SubtitleController in MediaPlayer, but we need + // a context for the subtitle renderers + final Context context = getContext(); + final SubtitleController controller = new SubtitleController( + context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); + controller.registerRenderer(new WebVttRenderer(context)); + mMediaPlayer.setSubtitleAnchor(controller, this); + + if (mAudioSession != 0) { + mMediaPlayer.setAudioSessionId(mAudioSession); + } else { + mAudioSession = mMediaPlayer.getAudioSessionId(); + } + + // 设置一些必要的监听 + mMediaPlayer.setOnPreparedListener(mPreparedListener); + mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); + mMediaPlayer.setOnCompletionListener(mCompletionListener); + mMediaPlayer.setOnErrorListener(mErrorListener); + mMediaPlayer.setOnInfoListener(mInfoListener); + mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); + mCurrentBufferPercentage = 0; + // 让MediaPlayer进行播放 + mMediaPlayer.setDataSource(mContext, mUri, mHeaders); + // 让SurfaceView进行画面显示 + mMediaPlayer.setDisplay(mSurfaceHolder); + // 设置音频类型 + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + // 播放时屏幕常亮 + mMediaPlayer.setScreenOnWhilePlaying(true); + // Prepares the player for playback, asynchronously. After setting the datasource and the display surface, you need to either call prepare() or prepareAsync(). For streams, you should call prepareAsync(), + // which returns immediately, rather than blocking until enough data has been buffered. + mMediaPlayer.prepareAsync(); + + for (Pair pending: mPendingSubtitleTracks) { + try { + mMediaPlayer.addSubtitleSource(pending.first, pending.second); + } catch (IllegalStateException e) { + mInfoListener.onInfo( + mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); + } + } + + // we don't set the target state here either, but preserve the + // target state that was there before. + mCurrentState = STATE_PREPARING; + // 如果已经调用过SetMediaController() 方法,这里会直接显示 + attachMediaController(); + } catch (IOException ex) { + Log.w(TAG, "Unable to open content: " + mUri, ex); + mCurrentState = STATE_ERROR; + mTargetState = STATE_ERROR; + mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + return; + } catch (IllegalArgumentException ex) { + Log.w(TAG, "Unable to open content: " + mUri, ex); + mCurrentState = STATE_ERROR; + mTargetState = STATE_ERROR; + mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + return; + } finally { + mPendingSubtitleTracks.clear(); + } + } + ``` + + release() 方法,在开始播放一个视频的时候会先调用该方法,然后重新创建一个,在SurfaceView销毁的时候也会调用该方法 + ```java + /* + * release the media player in any state + */ + private void release(boolean cleartargetstate) { + if (mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + mPendingSubtitleTracks.clear(); + mCurrentState = STATE_IDLE; + if (cleartargetstate) { + mTargetState = STATE_IDLE; + } + } + } + ``` + + - 外部停止播放调用 + ```java + public void stopPlayback() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + mCurrentState = STATE_IDLE; + mTargetState = STATE_IDLE; + } + } + ``` + + - 外部设置控制栏部分 + ```java + public void setMediaController(MediaController controller) { + if (mMediaController != null) { + mMediaController.hide(); + } + mMediaController = controller; + attachMediaController(); + } + + private void attachMediaController() { + if (mMediaPlayer != null && mMediaController != null) { + // setMediaPlayer(MediaPlayerControl player), 让MediaPlayer相应的控制部分调用本类中的实现方法 + mMediaController.setMediaPlayer(this); + View anchorView = this.getParent() instanceof View ? + (View)this.getParent() : this; + + // 创建Controller并且依据AnchorView的位置进行显示 + mMediaController.setAnchorView(anchorView); + mMediaController.setEnabled(isInPlaybackState()); + } + } + ``` + + - MediaPlayer必要监听 + - OnVideoSizeChangedListener + ```java + MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = + new MediaPlayer.OnVideoSizeChangedListener() { + public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { + mVideoWidth = mp.getVideoWidth(); + mVideoHeight = mp.getVideoHeight(); + if (mVideoWidth != 0 && mVideoHeight != 0) { + // 这个方法是设置Surface分辨率,而不是设置视频播放窗口的大小,视频播放窗口大小是由SurfaceView的布局控制,要分清Surface与SurfaceView的区别,Surface是Window中整个的一个控件(句柄), + // 而SurfaceView是一个包含Surface的View,SurfaceView覆盖到Surface上(可以这样理解),我们只能通过SurfaceView来看Surface中的内容,至于在SurfaceView显示之外的Surface我们是不可见的. + getHolder().setFixedSize(mVideoWidth, mVideoHeight); + requestLayout(); + } + } + }; + ``` + + - OnPreparedListener + ```java + MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mp) { + mCurrentState = STATE_PREPARED; + + // Get the capabilities of the player for this stream + Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, + MediaPlayer.BYPASS_METADATA_FILTER); + + if (data != null) { + mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) + || data.getBoolean(Metadata.PAUSE_AVAILABLE); + mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) + || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); + mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) + || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); + } else { + mCanPause = mCanSeekBack = mCanSeekForward = true; + } + + if (mOnPreparedListener != null) { + mOnPreparedListener.onPrepared(mMediaPlayer); + } + if (mMediaController != null) { + mMediaController.setEnabled(true); + } + mVideoWidth = mp.getVideoWidth(); + mVideoHeight = mp.getVideoHeight(); + + int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call + if (seekToPosition != 0) { + seekTo(seekToPosition); + } + if (mVideoWidth != 0 && mVideoHeight != 0) { + //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); + getHolder().setFixedSize(mVideoWidth, mVideoHeight); + if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { + // We didn't actually change the size (it was already at the size + // we need), so we won't get a "surface changed" callback, so + // start the video here instead of in the callback. + if (mTargetState == STATE_PLAYING) { + start(); + if (mMediaController != null) { + mMediaController.show(); + } + } else if (!isPlaying() && + (seekToPosition != 0 || getCurrentPosition() > 0)) { + if (mMediaController != null) { + // Show the media controls when we're paused into a video and make 'em stick. + mMediaController.show(0); + } + } + } + } else { + // We don't know the video size yet, but should start anyway. + // The video size might be reported to us later. + if (mTargetState == STATE_PLAYING) { + start(); + } + } + } + }; + ``` + + - OnCompletionListener + ```java + private MediaPlayer.OnCompletionListener mCompletionListener = + new MediaPlayer.OnCompletionListener() { + public void onCompletion(MediaPlayer mp) { + mCurrentState = STATE_PLAYBACK_COMPLETED; + mTargetState = STATE_PLAYBACK_COMPLETED; + if (mMediaController != null) { + mMediaController.hide(); + } + if (mOnCompletionListener != null) { + mOnCompletionListener.onCompletion(mMediaPlayer); + } + } + }; + ``` + + - Touch以及Key的监听 + ```java + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (isInPlaybackState() && mMediaController != null) { + // 控制MediaController的显示与隐藏 + toggleMediaControlsVisiblity(); + } + return false; + } + ``` + + - toggleMediaControlsVisiblity + ```java + private void toggleMediaControlsVisiblity() { + if (mMediaController.isShowing()) { + mMediaController.hide(); + } else { + mMediaController.show(); + } + } + ``` + + - Key + ```java + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) + { + boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && + keyCode != KeyEvent.KEYCODE_VOLUME_UP && + keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && + keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && + keyCode != KeyEvent.KEYCODE_MENU && + keyCode != KeyEvent.KEYCODE_CALL && + keyCode != KeyEvent.KEYCODE_ENDCALL; + if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { + if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + if (mMediaPlayer.isPlaying()) { + pause(); + mMediaController.show(); + } else { + start(); + mMediaController.hide(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { + if (!mMediaPlayer.isPlaying()) { + start(); + mMediaController.hide(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { + if (mMediaPlayer.isPlaying()) { + pause(); + mMediaController.show(); + } + return true; + } else { + toggleMediaControlsVisiblity(); + } + } + + return super.onKeyDown(keyCode, event); + } + ``` + +华丽丽的分割线 上源码 +============== + +---------------------- +```java +/* + * Copyright (C) 2006 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. + */ + +/** + * Displays a video file. The VideoView class + * can load images from various sources (such as resources or content + * providers), takes care of computing its measurement from the video so that + * it can be used in any layout manager, and provides various display options + * such as scaling and tinting.

+ * + * Note: VideoView does not retain its full state when going into the + * background. In particular, it does not restore the current play state, + * play position, selected tracks, or any subtitle tracks added via + * {@link #addSubtitleSource addSubtitleSource()}. Applications should + * save and restore these on their own in + * {@link android.app.Activity#onSaveInstanceState} and + * {@link android.app.Activity#onRestoreInstanceState}.

+ * Also note that the audio session id (from {@link #getAudioSessionId}) may + * change from its previously returned value when the VideoView is restored. + */ +public class VideoView extends SurfaceView + implements MediaPlayerControl, SubtitleController.Anchor { + private String TAG = "VideoView"; + // settable by the client + private Uri mUri; + private Map mHeaders; + + // all possible internal states + private static final int STATE_ERROR = -1; + private static final int STATE_IDLE = 0; + private static final int STATE_PREPARING = 1; + private static final int STATE_PREPARED = 2; + private static final int STATE_PLAYING = 3; + private static final int STATE_PAUSED = 4; + private static final int STATE_PLAYBACK_COMPLETED = 5; + + // mCurrentState is a VideoView object's current state. + // mTargetState is the state that a method caller intends to reach. + // For instance, regardless the VideoView object's current state, + // calling pause() intends to bring the object to a target state + // of STATE_PAUSED. + private int mCurrentState = STATE_IDLE; + private int mTargetState = STATE_IDLE; + + // All the stuff we need for playing and showing a video + private SurfaceHolder mSurfaceHolder = null; + private MediaPlayer mMediaPlayer = null; + private int mAudioSession; + private int mVideoWidth; + private int mVideoHeight; + private int mSurfaceWidth; + private int mSurfaceHeight; + private MediaController mMediaController; + private OnCompletionListener mOnCompletionListener; + private MediaPlayer.OnPreparedListener mOnPreparedListener; + private int mCurrentBufferPercentage; + private OnErrorListener mOnErrorListener; + private OnInfoListener mOnInfoListener; + private int mSeekWhenPrepared; // recording the seek position while preparing + private boolean mCanPause; + private boolean mCanSeekBack; + private boolean mCanSeekForward; + + /** Subtitle rendering widget overlaid on top of the video. */ + private RenderingWidget mSubtitleWidget; + + /** Listener for changes to subtitle data, used to redraw when needed. */ + private RenderingWidget.OnChangedListener mSubtitlesChangedListener; + + public VideoView(Context context) { + super(context); + initVideoView(); + } + + public VideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + initVideoView(); + } + + public VideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initVideoView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " + // + MeasureSpec.toString(heightMeasureSpec) + ")"); + + int width = getDefaultSize(mVideoWidth, widthMeasureSpec); + int height = getDefaultSize(mVideoHeight, heightMeasureSpec); + if (mVideoWidth > 0 && mVideoHeight > 0) { + + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { + // the size is fixed + width = widthSpecSize; + height = heightSpecSize; + + // for compatibility, we adjust size based on aspect ratio + if ( mVideoWidth * height < width * mVideoHeight ) { + //Log.i("@@@", "image too wide, correcting"); + width = height * mVideoWidth / mVideoHeight; + } else if ( mVideoWidth * height > width * mVideoHeight ) { + //Log.i("@@@", "image too tall, correcting"); + height = width * mVideoHeight / mVideoWidth; + } + } else if (widthSpecMode == MeasureSpec.EXACTLY) { + // only the width is fixed, adjust the height to match aspect ratio if possible + width = widthSpecSize; + height = width * mVideoHeight / mVideoWidth; + if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { + // couldn't match aspect ratio within the constraints + height = heightSpecSize; + } + } else if (heightSpecMode == MeasureSpec.EXACTLY) { + // only the height is fixed, adjust the width to match aspect ratio if possible + height = heightSpecSize; + width = height * mVideoWidth / mVideoHeight; + if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { + // couldn't match aspect ratio within the constraints + width = widthSpecSize; + } + } else { + // neither the width nor the height are fixed, try to use actual video size + width = mVideoWidth; + height = mVideoHeight; + if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { + // too tall, decrease both width and height + height = heightSpecSize; + width = height * mVideoWidth / mVideoHeight; + } + if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { + // too wide, decrease both width and height + width = widthSpecSize; + height = width * mVideoHeight / mVideoWidth; + } + } + } else { + // no size yet, just adopt the given spec sizes + } + setMeasuredDimension(width, height); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(VideoView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(VideoView.class.getName()); + } + + public int resolveAdjustedSize(int desiredSize, int measureSpec) { + return getDefaultSize(desiredSize, measureSpec); + } + + private void initVideoView() { + mVideoWidth = 0; + mVideoHeight = 0; + getHolder().addCallback(mSHCallback); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + mPendingSubtitleTracks = new Vector>(); + mCurrentState = STATE_IDLE; + mTargetState = STATE_IDLE; + } + + public void setVideoPath(String path) { + setVideoURI(Uri.parse(path)); + } + + public void setVideoURI(Uri uri) { + setVideoURI(uri, null); + } + + /** + * @hide + */ + public void setVideoURI(Uri uri, Map headers) { + mUri = uri; + mHeaders = headers; + mSeekWhenPrepared = 0; + openVideo(); + requestLayout(); + invalidate(); + } + + /** + * Adds an external subtitle source file (from the provided input stream.) + * + * Note that a single external subtitle source may contain multiple or no + * supported tracks in it. If the source contained at least one track in + * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE} + * info message. Otherwise, if reading the source takes excessive time, + * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT} + * message. If the source contained no supported track (including an empty + * source file or null input stream), one will receive a {@link + * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the + * total number of available tracks using {@link MediaPlayer#getTrackInfo()} + * to see what additional tracks become available after this method call. + * + * @param is input stream containing the subtitle data. It will be + * closed by the media framework. + * @param format the format of the subtitle track(s). Must contain at least + * the mime type ({@link MediaFormat#KEY_MIME}) and the + * language ({@link MediaFormat#KEY_LANGUAGE}) of the file. + * If the file itself contains the language information, + * specify "und" for the language. + */ + public void addSubtitleSource(InputStream is, MediaFormat format) { + if (mMediaPlayer == null) { + mPendingSubtitleTracks.add(Pair.create(is, format)); + } else { + try { + mMediaPlayer.addSubtitleSource(is, format); + } catch (IllegalStateException e) { + mInfoListener.onInfo( + mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); + } + } + } + + private Vector> mPendingSubtitleTracks; + + public void stopPlayback() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + mCurrentState = STATE_IDLE; + mTargetState = STATE_IDLE; + } + } + + private void openVideo() { + if (mUri == null || mSurfaceHolder == null) { + // not ready for playback just yet, will try again later + return; + } + // Tell the music playback service to pause + // TODO: these constants need to be published somewhere in the framework. + Intent i = new Intent("com.android.music.musicservicecommand"); + i.putExtra("command", "pause"); + mContext.sendBroadcast(i); + + // we shouldn't clear the target state, because somebody might have + // called start() previously + release(false); + try { + mMediaPlayer = new MediaPlayer(); + // TODO: create SubtitleController in MediaPlayer, but we need + // a context for the subtitle renderers + final Context context = getContext(); + final SubtitleController controller = new SubtitleController( + context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); + controller.registerRenderer(new WebVttRenderer(context)); + mMediaPlayer.setSubtitleAnchor(controller, this); + + if (mAudioSession != 0) { + mMediaPlayer.setAudioSessionId(mAudioSession); + } else { + mAudioSession = mMediaPlayer.getAudioSessionId(); + } + mMediaPlayer.setOnPreparedListener(mPreparedListener); + mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); + mMediaPlayer.setOnCompletionListener(mCompletionListener); + mMediaPlayer.setOnErrorListener(mErrorListener); + mMediaPlayer.setOnInfoListener(mInfoListener); + mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); + mCurrentBufferPercentage = 0; + mMediaPlayer.setDataSource(mContext, mUri, mHeaders); + mMediaPlayer.setDisplay(mSurfaceHolder); + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mMediaPlayer.setScreenOnWhilePlaying(true); + mMediaPlayer.prepareAsync(); + + for (Pair pending: mPendingSubtitleTracks) { + try { + mMediaPlayer.addSubtitleSource(pending.first, pending.second); + } catch (IllegalStateException e) { + mInfoListener.onInfo( + mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); + } + } + + // we don't set the target state here either, but preserve the + // target state that was there before. + mCurrentState = STATE_PREPARING; + attachMediaController(); + } catch (IOException ex) { + Log.w(TAG, "Unable to open content: " + mUri, ex); + mCurrentState = STATE_ERROR; + mTargetState = STATE_ERROR; + mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + return; + } catch (IllegalArgumentException ex) { + Log.w(TAG, "Unable to open content: " + mUri, ex); + mCurrentState = STATE_ERROR; + mTargetState = STATE_ERROR; + mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + return; + } finally { + mPendingSubtitleTracks.clear(); + } + } + + public void setMediaController(MediaController controller) { + if (mMediaController != null) { + mMediaController.hide(); + } + mMediaController = controller; + attachMediaController(); + } + + private void attachMediaController() { + if (mMediaPlayer != null && mMediaController != null) { + mMediaController.setMediaPlayer(this); + View anchorView = this.getParent() instanceof View ? + (View)this.getParent() : this; + mMediaController.setAnchorView(anchorView); + mMediaController.setEnabled(isInPlaybackState()); + } + } + + MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = + new MediaPlayer.OnVideoSizeChangedListener() { + public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { + mVideoWidth = mp.getVideoWidth(); + mVideoHeight = mp.getVideoHeight(); + if (mVideoWidth != 0 && mVideoHeight != 0) { + getHolder().setFixedSize(mVideoWidth, mVideoHeight); + requestLayout(); + } + } + }; + + MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mp) { + mCurrentState = STATE_PREPARED; + + // Get the capabilities of the player for this stream + Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, + MediaPlayer.BYPASS_METADATA_FILTER); + + if (data != null) { + mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) + || data.getBoolean(Metadata.PAUSE_AVAILABLE); + mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) + || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); + mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) + || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); + } else { + mCanPause = mCanSeekBack = mCanSeekForward = true; + } + + if (mOnPreparedListener != null) { + mOnPreparedListener.onPrepared(mMediaPlayer); + } + if (mMediaController != null) { + mMediaController.setEnabled(true); + } + mVideoWidth = mp.getVideoWidth(); + mVideoHeight = mp.getVideoHeight(); + + int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call + if (seekToPosition != 0) { + seekTo(seekToPosition); + } + if (mVideoWidth != 0 && mVideoHeight != 0) { + //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); + getHolder().setFixedSize(mVideoWidth, mVideoHeight); + if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { + // We didn't actually change the size (it was already at the size + // we need), so we won't get a "surface changed" callback, so + // start the video here instead of in the callback. + if (mTargetState == STATE_PLAYING) { + start(); + if (mMediaController != null) { + mMediaController.show(); + } + } else if (!isPlaying() && + (seekToPosition != 0 || getCurrentPosition() > 0)) { + if (mMediaController != null) { + // Show the media controls when we're paused into a video and make 'em stick. + mMediaController.show(0); + } + } + } + } else { + // We don't know the video size yet, but should start anyway. + // The video size might be reported to us later. + if (mTargetState == STATE_PLAYING) { + start(); + } + } + } + }; + + private MediaPlayer.OnCompletionListener mCompletionListener = + new MediaPlayer.OnCompletionListener() { + public void onCompletion(MediaPlayer mp) { + mCurrentState = STATE_PLAYBACK_COMPLETED; + mTargetState = STATE_PLAYBACK_COMPLETED; + if (mMediaController != null) { + mMediaController.hide(); + } + if (mOnCompletionListener != null) { + mOnCompletionListener.onCompletion(mMediaPlayer); + } + } + }; + + private MediaPlayer.OnInfoListener mInfoListener = + new MediaPlayer.OnInfoListener() { + public boolean onInfo(MediaPlayer mp, int arg1, int arg2) { + if (mOnInfoListener != null) { + mOnInfoListener.onInfo(mp, arg1, arg2); + } + return true; + } + }; + + private MediaPlayer.OnErrorListener mErrorListener = + new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { + Log.d(TAG, "Error: " + framework_err + "," + impl_err); + mCurrentState = STATE_ERROR; + mTargetState = STATE_ERROR; + if (mMediaController != null) { + mMediaController.hide(); + } + + /* If an error handler has been supplied, use it and finish. */ + if (mOnErrorListener != null) { + if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) { + return true; + } + } + + /* Otherwise, pop up an error dialog so the user knows that + * something bad has happened. Only try and pop up the dialog + * if we're attached to a window. When we're going away and no + * longer have a window, don't bother showing the user an error. + */ + if (getWindowToken() != null) { + Resources r = mContext.getResources(); + int messageId; + + if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { + messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback; + } else { + messageId = com.android.internal.R.string.VideoView_error_text_unknown; + } + + new AlertDialog.Builder(mContext) + .setMessage(messageId) + .setPositiveButton(com.android.internal.R.string.VideoView_error_button, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + /* If we get here, there is no onError listener, so + * at least inform them that the video is over. + */ + if (mOnCompletionListener != null) { + mOnCompletionListener.onCompletion(mMediaPlayer); + } + } + }) + .setCancelable(false) + .show(); + } + return true; + } + }; + + private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = + new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + mCurrentBufferPercentage = percent; + } + }; + + /** + * Register a callback to be invoked when the media file + * is loaded and ready to go. + * + * @param l The callback that will be run + */ + public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) + { + mOnPreparedListener = l; + } + + /** + * Register a callback to be invoked when the end of a media file + * has been reached during playback. + * + * @param l The callback that will be run + */ + public void setOnCompletionListener(OnCompletionListener l) + { + mOnCompletionListener = l; + } + + /** + * Register a callback to be invoked when an error occurs + * during playback or setup. If no listener is specified, + * or if the listener returned false, VideoView will inform + * the user of any errors. + * + * @param l The callback that will be run + */ + public void setOnErrorListener(OnErrorListener l) + { + mOnErrorListener = l; + } + + /** + * Register a callback to be invoked when an informational event + * occurs during playback or setup. + * + * @param l The callback that will be run + */ + public void setOnInfoListener(OnInfoListener l) { + mOnInfoListener = l; + } + + SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() + { + public void surfaceChanged(SurfaceHolder holder, int format, + int w, int h) + { + mSurfaceWidth = w; + mSurfaceHeight = h; + boolean isValidState = (mTargetState == STATE_PLAYING); + boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); + if (mMediaPlayer != null && isValidState && hasValidSize) { + if (mSeekWhenPrepared != 0) { + seekTo(mSeekWhenPrepared); + } + start(); + } + } + + public void surfaceCreated(SurfaceHolder holder) + { + mSurfaceHolder = holder; + openVideo(); + } + + public void surfaceDestroyed(SurfaceHolder holder) + { + // after we return from this we can't use the surface any more + mSurfaceHolder = null; + if (mMediaController != null) mMediaController.hide(); + release(true); + } + }; + + /* + * release the media player in any state + */ + private void release(boolean cleartargetstate) { + if (mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + mPendingSubtitleTracks.clear(); + mCurrentState = STATE_IDLE; + if (cleartargetstate) { + mTargetState = STATE_IDLE; + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (isInPlaybackState() && mMediaController != null) { + toggleMediaControlsVisiblity(); + } + return false; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + if (isInPlaybackState() && mMediaController != null) { + toggleMediaControlsVisiblity(); + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) + { + boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && + keyCode != KeyEvent.KEYCODE_VOLUME_UP && + keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && + keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && + keyCode != KeyEvent.KEYCODE_MENU && + keyCode != KeyEvent.KEYCODE_CALL && + keyCode != KeyEvent.KEYCODE_ENDCALL; + if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { + if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + if (mMediaPlayer.isPlaying()) { + pause(); + mMediaController.show(); + } else { + start(); + mMediaController.hide(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { + if (!mMediaPlayer.isPlaying()) { + start(); + mMediaController.hide(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { + if (mMediaPlayer.isPlaying()) { + pause(); + mMediaController.show(); + } + return true; + } else { + toggleMediaControlsVisiblity(); + } + } + + return super.onKeyDown(keyCode, event); + } + + private void toggleMediaControlsVisiblity() { + if (mMediaController.isShowing()) { + mMediaController.hide(); + } else { + mMediaController.show(); + } + } + + @Override + public void start() { + if (isInPlaybackState()) { + mMediaPlayer.start(); + mCurrentState = STATE_PLAYING; + } + mTargetState = STATE_PLAYING; + } + + @Override + public void pause() { + if (isInPlaybackState()) { + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + mCurrentState = STATE_PAUSED; + } + } + mTargetState = STATE_PAUSED; + } + + public void suspend() { + release(false); + } + + public void resume() { + openVideo(); + } + + @Override + public int getDuration() { + if (isInPlaybackState()) { + return mMediaPlayer.getDuration(); + } + + return -1; + } + + @Override + public int getCurrentPosition() { + if (isInPlaybackState()) { + return mMediaPlayer.getCurrentPosition(); + } + return 0; + } + + @Override + public void seekTo(int msec) { + if (isInPlaybackState()) { + mMediaPlayer.seekTo(msec); + mSeekWhenPrepared = 0; + } else { + mSeekWhenPrepared = msec; + } + } + + @Override + public boolean isPlaying() { + return isInPlaybackState() && mMediaPlayer.isPlaying(); + } + + @Override + public int getBufferPercentage() { + if (mMediaPlayer != null) { + return mCurrentBufferPercentage; + } + return 0; + } + + private boolean isInPlaybackState() { + return (mMediaPlayer != null && + mCurrentState != STATE_ERROR && + mCurrentState != STATE_IDLE && + mCurrentState != STATE_PREPARING); + } + + @Override + public boolean canPause() { + return mCanPause; + } + + @Override + public boolean canSeekBackward() { + return mCanSeekBack; + } + + @Override + public boolean canSeekForward() { + return mCanSeekForward; + } + + @Override + public int getAudioSessionId() { + if (mAudioSession == 0) { + MediaPlayer foo = new MediaPlayer(); + mAudioSession = foo.getAudioSessionId(); + foo.release(); + } + return mAudioSession; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mSubtitleWidget != null) { + mSubtitleWidget.onAttachedToWindow(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mSubtitleWidget != null) { + mSubtitleWidget.onDetachedFromWindow(); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mSubtitleWidget != null) { + measureAndLayoutSubtitleWidget(); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mSubtitleWidget != null) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + mSubtitleWidget.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** + * Forces a measurement and layout pass for all overlaid views. + * + * @see #setSubtitleWidget(RenderingWidget) + */ + private void measureAndLayoutSubtitleWidget() { + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + mSubtitleWidget.setSize(width, height); + } + + /** @hide */ + @Override + public void setSubtitleWidget(RenderingWidget subtitleWidget) { + if (mSubtitleWidget == subtitleWidget) { + return; + } + + final boolean attachedToWindow = isAttachedToWindow(); + if (mSubtitleWidget != null) { + if (attachedToWindow) { + mSubtitleWidget.onDetachedFromWindow(); + } + + mSubtitleWidget.setOnChangedListener(null); + } + + mSubtitleWidget = subtitleWidget; + + if (subtitleWidget != null) { + if (mSubtitlesChangedListener == null) { + mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() { + @Override + public void onChanged(RenderingWidget renderingWidget) { + invalidate(); + } + }; + } + + setWillNotDraw(false); + subtitleWidget.setOnChangedListener(mSubtitlesChangedListener); + + if (attachedToWindow) { + subtitleWidget.onAttachedToWindow(); + requestLayout(); + } + } else { + setWillNotDraw(true); + } + + invalidate(); + } + + /** @hide */ + @Override + public Looper getSubtitleLooper() { + return Looper.getMainLooper(); + } +} +``` + +MediaPlayerControl +--- + + 通过该接口来打通MediaController以及VideoView + + ```java + public interface MediaPlayerControl { + void start(); + void pause(); + int getDuration(); + int getCurrentPosition(); + void seekTo(int pos); + boolean isPlaying(); + int getBufferPercentage(); + boolean canPause(); + boolean canSeekBackward(); + boolean canSeekForward(); + + /** + * Get the audio session id for the player used by this VideoView. This can be used to + * apply audio effects to the audio track of a video. + * @return The audio session, or 0 if there was an error. + */ + int getAudioSessionId(); +} + ``` + +MediaController +--- + +- 简介 + ```java + /** + * A view containing controls for a MediaPlayer. Typically contains the + * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress + * slider. It takes care of synchronizing the controls with the state + * of the MediaPlayer. + *

+ * The way to use this class is to instantiate it programatically. + * The MediaController will create a default set of controls + * and put them in a window floating above your application. Specifically, + * the controls will float above the view specified with setAnchorView(). + * The window will disappear if left idle for three seconds and reappear + * when the user touches the anchor view. + *

+ * Functions like show() and hide() have no effect when MediaController + * is created in an xml layout. + * + * MediaController will hide and + * show the buttons according to these rules: + *

    + *
  • The "previous" and "next" buttons are hidden until setPrevNextListeners() + * has been called + *
  • The "previous" and "next" buttons are visible but disabled if + * setPrevNextListeners() was called with null listeners + *
  • The "rewind" and "fastforward" buttons are shown unless requested + * otherwise by using the MediaController(Context, boolean) constructor + * with the boolean set to false + *
+ */ + ``` + +- 关系 + ```java + public class MediaController extends FrameLayout + ``` + +- 成员 + ```java + // 一些控制功能的接口 + private MediaPlayerControl mPlayer; + private Context mContext; + // VideoView中调用setAnchorView()设置进来的View,MediaController显示的时候会感觉该AnchorView的位置进行显示 + private View mAnchor; + // MediaController最外层的根布局 + private View mRoot; + + // 通过Window的方式来显示MediaController,MediaController是一个填充屏幕的布局,但是背景是透明的 + private WindowManager mWindowManager; + private Window mWindow; + private View mDecor; + // 理解为当前整个MediaController的布局 + private WindowManager.LayoutParams mDecorLayoutParams; + private ProgressBar mProgress; + private TextView mEndTime, mCurrentTime; + private boolean mShowing; + private boolean mDragging; + // 默认自动消失的时间 + private static final int sDefaultTimeout = 3000; + private static final int FADE_OUT = 1; + private static final int SHOW_PROGRESS = 2; + private boolean mUseFastForward; + private boolean mFromXml; + private boolean mListenersSet; + private View.OnClickListener mNextListener, mPrevListener; + StringBuilder mFormatBuilder; + Formatter mFormatter; + private ImageButton mPauseButton; + private ImageButton mFfwdButton; + private ImageButton mRewButton; + private ImageButton mNextButton; + private ImageButton mPrevButton; + ``` + +- 构造方法 + ```java + public MediaController(Context context, AttributeSet attrs) { + super(context, attrs); + mRoot = this; + mContext = context; + mUseFastForward = true; + mFromXml = true; + } + + @Override + public void onFinishInflate() { + if (mRoot != null) + initControllerView(mRoot); + } + + public MediaController(Context context, boolean useFastForward) { + super(context); + mContext = context; + mUseFastForward = useFastForward; + // 创建该MediaController的布局 + initFloatingWindowLayout(); + initFloatingWindow(); + } + + public MediaController(Context context) { + this(context, true); + } + ``` + + - initFloatingWindowLayout + ```java + // Allocate and initialize the static parts of mDecorLayoutParams. Must + // also call updateFloatingWindowLayout() to fill in the dynamic parts + // (y and width) before mDecorLayoutParams can be used. + private void initFloatingWindowLayout() { + mDecorLayoutParams = new WindowManager.LayoutParams(); + WindowManager.LayoutParams p = mDecorLayoutParams; + p.gravity = Gravity.TOP | Gravity.LEFT; + p.height = LayoutParams.WRAP_CONTENT; + p.x = 0; + p.format = PixelFormat.TRANSLUCENT; + p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; + p.token = null; + p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; + } + ``` + + - initFloatingWindow + ```java + private void initFloatingWindow() { + // Android内核剖析 中有介绍 + mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); + mWindow = PolicyManager.makeNewWindow(mContext); + mWindow.setWindowManager(mWindowManager, null, null); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + // 通过WindowManager去add该Decor以及remove来实现MediaController的显示与隐藏 + mDecor = mWindow.getDecorView(); + mDecor.setOnTouchListener(mTouchListener); + mWindow.setContentView(this); + mWindow.setBackgroundDrawableResource(android.R.color.transparent); + + // While the media controller is up, the volume control keys should + // affect the media stream type + mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); + + setFocusable(true); + setFocusableInTouchMode(true); + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + requestFocus(); + } + ``` + + - mTouchListener + ```java + private OnTouchListener mTouchListener = new OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (mShowing) { + hide(); + } + } + return false; + } + }; + ``` + + - setMediaPlayer + VideoView调用setMediaController的时候会调用到该方法 + ```java + public void setMediaPlayer(MediaPlayerControl player) { + mPlayer = player; + updatePausePlay(); + } + ``` + + - setAnchorView + VideoView调用setMediaController的时候会调用到该方法 + ```java + /** + * Set the view that acts as the anchor for the control view. + * This can for example be a VideoView, or your Activity's main view. + * When VideoView calls this method, it will use the VideoView's parent + * as the anchor. + * @param view The view to which to anchor the controller when it is visible. + */ + public void setAnchorView(View view) { + if (mAnchor != null) { + mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); + } + mAnchor = view; + if (mAnchor != null) { + mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); + } + + FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ); + + removeAllViews(); + View v = makeControllerView(); + addView(v, frameParams); + } + ``` + + - mLayoutChangeListener + ```java + // This is called whenever mAnchor's layout bound changes + private OnLayoutChangeListener mLayoutChangeListener = + new OnLayoutChangeListener() { + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, + int oldBottom) { + // 更新布局 + updateFloatingWindowLayout(); + if (mShowing) { + mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); + } + } + }; + ``` + + - updateFloatingWindowLayout + ```java + // Update the dynamic parts of mDecorLayoutParams + // Must be called with mAnchor != NULL. + private void updateFloatingWindowLayout() { + int [] anchorPos = new int[2]; + mAnchor.getLocationOnScreen(anchorPos); + + // we need to know the size of the controller so we can properly position it + // within its space + mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); + + WindowManager.LayoutParams p = mDecorLayoutParams; + p.width = mAnchor.getWidth(); + p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; + p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); + } + ``` + + - makeControllerView + ```java + /** + * Create the view that holds the widgets that control playback. + * Derived classes can override this to create their own. + * @return The controller view. + * @hide This doesn't work as advertised + */ + protected View makeControllerView() { + LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); + // 对Controller中的一些按钮、功能进行事件设置 + initControllerView(mRoot); + + return mRoot; + } + ``` + + - touch事件处理 + ```java + @Override + public boolean onTouchEvent(MotionEvent event) { + show(sDefaultTimeout); + return true; + } + ``` + + - 进度的处理 + - seekBar的处理 + ```java + // There are two scenarios that can trigger the seekbar listener to trigger: + // + // The first is the user using the touchpad to adjust the posititon of the + // seekbar's thumb. In this case onStartTrackingTouch is called followed by + // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. + // We're setting the field "mDragging" to true for the duration of the dragging + // session to avoid jumps in the position in case of ongoing playback. + // + // The second scenario involves the user operating the scroll ball, in this + // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, + // we will simply apply the updated position without suspending regular updates. + private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { + public void onStartTrackingTouch(SeekBar bar) { + show(3600000); + + mDragging = true; + + // By removing these pending progress messages we make sure + // that a) we won't update the progress while the user adjusts + // the seekbar and b) once the user is done dragging the thumb + // we will post one of these messages to the queue again and + // this ensures that there will be exactly one message queued up. + mHandler.removeMessages(SHOW_PROGRESS); + } + + public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { + if (!fromuser) { + // We're not interested in programmatically generated changes to + // the progress bar's position. + return; + } + + long duration = mPlayer.getDuration(); + long newposition = (duration * progress) / 1000L; + mPlayer.seekTo( (int) newposition); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime( (int) newposition)); + } + + public void onStopTrackingTouch(SeekBar bar) { + mDragging = false; + setProgress(); + updatePausePlay(); + show(sDefaultTimeout); + + // Ensure that progress is properly updated in the future, + // the call to show() does not guarantee this because it is a + // no-op if we are already showing. + mHandler.sendEmptyMessage(SHOW_PROGRESS); + } + }; + ``` + + - SetProgress + ```java + private int setProgress() { + if (mPlayer == null || mDragging) { + return 0; + } + int position = mPlayer.getCurrentPosition(); + int duration = mPlayer.getDuration(); + if (mProgress != null) { + if (duration > 0) { + // use long to avoid overflow + long pos = 1000L * position / duration; + mProgress.setProgress( (int) pos); + } + int percent = mPlayer.getBufferPercentage(); + mProgress.setSecondaryProgress(percent * 10); + } + + if (mEndTime != null) + mEndTime.setText(stringForTime(duration)); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime(position)); + + return position; + } + ``` + + - show + ```java + /** + * Show the controller on screen. It will go away + * automatically after 'timeout' milliseconds of inactivity. + * @param timeout The timeout in milliseconds. Use 0 to show + * the controller until hide() is called. + */ + public void show(int timeout) { + if (!mShowing && mAnchor != null) { + // 先去设置一下进度 + setProgress(); + if (mPauseButton != null) { + mPauseButton.requestFocus(); + } + disableUnsupportedButtons(); + updateFloatingWindowLayout(); + mWindowManager.addView(mDecor, mDecorLayoutParams); + mShowing = true; + } + updatePausePlay(); + + // cause the progress bar to be updated even if mShowing + // was already true. This happens, for example, if we're + // paused with the progress bar showing the user hits play. + // 发送定期更新进度的消息 + mHandler.sendEmptyMessage(SHOW_PROGRESS); + + Message msg = mHandler.obtainMessage(FADE_OUT); + if (timeout != 0) { + mHandler.removeMessages(FADE_OUT); + mHandler.sendMessageDelayed(msg, timeout); + } + } + ``` + + - hide + ```java + /** + * Remove the controller from the screen. + */ + public void hide() { + if (mAnchor == null) + return; + + if (mShowing) { + try { + // 移除定期更新消息 + mHandler.removeMessages(SHOW_PROGRESS); + mWindowManager.removeView(mDecor); + } catch (IllegalArgumentException ex) { + Log.w("MediaController", "already removed"); + } + mShowing = false; + } + } + ``` + +上源码 +=== + +```java +/* + * Copyright (C) 2006 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.widget; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.android.internal.policy.PolicyManager; + +import java.util.Formatter; +import java.util.Locale; + +/** + * A view containing controls for a MediaPlayer. Typically contains the + * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress + * slider. It takes care of synchronizing the controls with the state + * of the MediaPlayer. + *

+ * The way to use this class is to instantiate it programatically. + * The MediaController will create a default set of controls + * and put them in a window floating above your application. Specifically, + * the controls will float above the view specified with setAnchorView(). + * The window will disappear if left idle for three seconds and reappear + * when the user touches the anchor view. + *

+ * Functions like show() and hide() have no effect when MediaController + * is created in an xml layout. + * + * MediaController will hide and + * show the buttons according to these rules: + *

    + *
  • The "previous" and "next" buttons are hidden until setPrevNextListeners() + * has been called + *
  • The "previous" and "next" buttons are visible but disabled if + * setPrevNextListeners() was called with null listeners + *
  • The "rewind" and "fastforward" buttons are shown unless requested + * otherwise by using the MediaController(Context, boolean) constructor + * with the boolean set to false + *
+ */ +public class MediaController extends FrameLayout { + + private MediaPlayerControl mPlayer; + private Context mContext; + private View mAnchor; + private View mRoot; + private WindowManager mWindowManager; + private Window mWindow; + private View mDecor; + private WindowManager.LayoutParams mDecorLayoutParams; + private ProgressBar mProgress; + private TextView mEndTime, mCurrentTime; + private boolean mShowing; + private boolean mDragging; + private static final int sDefaultTimeout = 3000; + private static final int FADE_OUT = 1; + private static final int SHOW_PROGRESS = 2; + private boolean mUseFastForward; + private boolean mFromXml; + private boolean mListenersSet; + private View.OnClickListener mNextListener, mPrevListener; + StringBuilder mFormatBuilder; + Formatter mFormatter; + private ImageButton mPauseButton; + private ImageButton mFfwdButton; + private ImageButton mRewButton; + private ImageButton mNextButton; + private ImageButton mPrevButton; + + public MediaController(Context context, AttributeSet attrs) { + super(context, attrs); + mRoot = this; + mContext = context; + mUseFastForward = true; + mFromXml = true; + } + + @Override + public void onFinishInflate() { + if (mRoot != null) + initControllerView(mRoot); + } + + public MediaController(Context context, boolean useFastForward) { + super(context); + mContext = context; + mUseFastForward = useFastForward; + initFloatingWindowLayout(); + initFloatingWindow(); + } + + public MediaController(Context context) { + this(context, true); + } + + private void initFloatingWindow() { + mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); + mWindow = PolicyManager.makeNewWindow(mContext); + mWindow.setWindowManager(mWindowManager, null, null); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + mDecor = mWindow.getDecorView(); + mDecor.setOnTouchListener(mTouchListener); + mWindow.setContentView(this); + mWindow.setBackgroundDrawableResource(android.R.color.transparent); + + // While the media controller is up, the volume control keys should + // affect the media stream type + mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); + + setFocusable(true); + setFocusableInTouchMode(true); + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + requestFocus(); + } + + // Allocate and initialize the static parts of mDecorLayoutParams. Must + // also call updateFloatingWindowLayout() to fill in the dynamic parts + // (y and width) before mDecorLayoutParams can be used. + private void initFloatingWindowLayout() { + mDecorLayoutParams = new WindowManager.LayoutParams(); + WindowManager.LayoutParams p = mDecorLayoutParams; + p.gravity = Gravity.TOP | Gravity.LEFT; + p.height = LayoutParams.WRAP_CONTENT; + p.x = 0; + p.format = PixelFormat.TRANSLUCENT; + p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; + p.token = null; + p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; + } + + // Update the dynamic parts of mDecorLayoutParams + // Must be called with mAnchor != NULL. + private void updateFloatingWindowLayout() { + int [] anchorPos = new int[2]; + mAnchor.getLocationOnScreen(anchorPos); + + // we need to know the size of the controller so we can properly position it + // within its space + mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); + + WindowManager.LayoutParams p = mDecorLayoutParams; + p.width = mAnchor.getWidth(); + p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; + p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); + } + + // This is called whenever mAnchor's layout bound changes + private OnLayoutChangeListener mLayoutChangeListener = + new OnLayoutChangeListener() { + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, + int oldBottom) { + updateFloatingWindowLayout(); + if (mShowing) { + mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); + } + } + }; + + private OnTouchListener mTouchListener = new OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (mShowing) { + hide(); + } + } + return false; + } + }; + + public void setMediaPlayer(MediaPlayerControl player) { + mPlayer = player; + updatePausePlay(); + } + + /** + * Set the view that acts as the anchor for the control view. + * This can for example be a VideoView, or your Activity's main view. + * When VideoView calls this method, it will use the VideoView's parent + * as the anchor. + * @param view The view to which to anchor the controller when it is visible. + */ + public void setAnchorView(View view) { + if (mAnchor != null) { + mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); + } + mAnchor = view; + if (mAnchor != null) { + mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); + } + + FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ); + + removeAllViews(); + View v = makeControllerView(); + addView(v, frameParams); + } + + /** + * Create the view that holds the widgets that control playback. + * Derived classes can override this to create their own. + * @return The controller view. + * @hide This doesn't work as advertised + */ + protected View makeControllerView() { + LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); + + initControllerView(mRoot); + + return mRoot; + } + + private void initControllerView(View v) { + mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause); + if (mPauseButton != null) { + mPauseButton.requestFocus(); + mPauseButton.setOnClickListener(mPauseListener); + } + + mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd); + if (mFfwdButton != null) { + mFfwdButton.setOnClickListener(mFfwdListener); + if (!mFromXml) { + mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); + } + } + + mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew); + if (mRewButton != null) { + mRewButton.setOnClickListener(mRewListener); + if (!mFromXml) { + mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); + } + } + + // By default these are hidden. They will be enabled when setPrevNextListeners() is called + mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next); + if (mNextButton != null && !mFromXml && !mListenersSet) { + mNextButton.setVisibility(View.GONE); + } + mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev); + if (mPrevButton != null && !mFromXml && !mListenersSet) { + mPrevButton.setVisibility(View.GONE); + } + + mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress); + if (mProgress != null) { + if (mProgress instanceof SeekBar) { + SeekBar seeker = (SeekBar) mProgress; + seeker.setOnSeekBarChangeListener(mSeekListener); + } + mProgress.setMax(1000); + } + + mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time); + mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current); + mFormatBuilder = new StringBuilder(); + mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); + + installPrevNextListeners(); + } + + /** + * Show the controller on screen. It will go away + * automatically after 3 seconds of inactivity. + */ + public void show() { + show(sDefaultTimeout); + } + + /** + * Disable pause or seek buttons if the stream cannot be paused or seeked. + * This requires the control interface to be a MediaPlayerControlExt + */ + private void disableUnsupportedButtons() { + try { + if (mPauseButton != null && !mPlayer.canPause()) { + mPauseButton.setEnabled(false); + } + if (mRewButton != null && !mPlayer.canSeekBackward()) { + mRewButton.setEnabled(false); + } + if (mFfwdButton != null && !mPlayer.canSeekForward()) { + mFfwdButton.setEnabled(false); + } + } catch (IncompatibleClassChangeError ex) { + // We were given an old version of the interface, that doesn't have + // the canPause/canSeekXYZ methods. This is OK, it just means we + // assume the media can be paused and seeked, and so we don't disable + // the buttons. + } + } + + /** + * Show the controller on screen. It will go away + * automatically after 'timeout' milliseconds of inactivity. + * @param timeout The timeout in milliseconds. Use 0 to show + * the controller until hide() is called. + */ + public void show(int timeout) { + if (!mShowing && mAnchor != null) { + setProgress(); + if (mPauseButton != null) { + mPauseButton.requestFocus(); + } + disableUnsupportedButtons(); + updateFloatingWindowLayout(); + mWindowManager.addView(mDecor, mDecorLayoutParams); + mShowing = true; + } + updatePausePlay(); + + // cause the progress bar to be updated even if mShowing + // was already true. This happens, for example, if we're + // paused with the progress bar showing the user hits play. + mHandler.sendEmptyMessage(SHOW_PROGRESS); + + Message msg = mHandler.obtainMessage(FADE_OUT); + if (timeout != 0) { + mHandler.removeMessages(FADE_OUT); + mHandler.sendMessageDelayed(msg, timeout); + } + } + + public boolean isShowing() { + return mShowing; + } + + /** + * Remove the controller from the screen. + */ + public void hide() { + if (mAnchor == null) + return; + + if (mShowing) { + try { + mHandler.removeMessages(SHOW_PROGRESS); + mWindowManager.removeView(mDecor); + } catch (IllegalArgumentException ex) { + Log.w("MediaController", "already removed"); + } + mShowing = false; + } + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + int pos; + switch (msg.what) { + case FADE_OUT: + hide(); + break; + case SHOW_PROGRESS: + pos = setProgress(); + if (!mDragging && mShowing && mPlayer.isPlaying()) { + msg = obtainMessage(SHOW_PROGRESS); + sendMessageDelayed(msg, 1000 - (pos % 1000)); + } + break; + } + } + }; + + private String stringForTime(int timeMs) { + int totalSeconds = timeMs / 1000; + + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + int hours = totalSeconds / 3600; + + mFormatBuilder.setLength(0); + if (hours > 0) { + return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); + } else { + return mFormatter.format("%02d:%02d", minutes, seconds).toString(); + } + } + + private int setProgress() { + if (mPlayer == null || mDragging) { + return 0; + } + int position = mPlayer.getCurrentPosition(); + int duration = mPlayer.getDuration(); + if (mProgress != null) { + if (duration > 0) { + // use long to avoid overflow + long pos = 1000L * position / duration; + mProgress.setProgress( (int) pos); + } + int percent = mPlayer.getBufferPercentage(); + mProgress.setSecondaryProgress(percent * 10); + } + + if (mEndTime != null) + mEndTime.setText(stringForTime(duration)); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime(position)); + + return position; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + show(sDefaultTimeout); + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + show(sDefaultTimeout); + return false; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + final boolean uniqueDown = event.getRepeatCount() == 0 + && event.getAction() == KeyEvent.ACTION_DOWN; + if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_SPACE) { + if (uniqueDown) { + doPauseResume(); + show(sDefaultTimeout); + if (mPauseButton != null) { + mPauseButton.requestFocus(); + } + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { + if (uniqueDown && !mPlayer.isPlaying()) { + mPlayer.start(); + updatePausePlay(); + show(sDefaultTimeout); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { + if (uniqueDown && mPlayer.isPlaying()) { + mPlayer.pause(); + updatePausePlay(); + show(sDefaultTimeout); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE + || keyCode == KeyEvent.KEYCODE_CAMERA) { + // don't show the controls for volume adjustment + return super.dispatchKeyEvent(event); + } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { + if (uniqueDown) { + hide(); + } + return true; + } + + show(sDefaultTimeout); + return super.dispatchKeyEvent(event); + } + + private View.OnClickListener mPauseListener = new View.OnClickListener() { + public void onClick(View v) { + doPauseResume(); + show(sDefaultTimeout); + } + }; + + private void updatePausePlay() { + if (mRoot == null || mPauseButton == null) + return; + + if (mPlayer.isPlaying()) { + mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause); + } else { + mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play); + } + } + + private void doPauseResume() { + if (mPlayer.isPlaying()) { + mPlayer.pause(); + } else { + mPlayer.start(); + } + updatePausePlay(); + } + + // There are two scenarios that can trigger the seekbar listener to trigger: + // + // The first is the user using the touchpad to adjust the posititon of the + // seekbar's thumb. In this case onStartTrackingTouch is called followed by + // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. + // We're setting the field "mDragging" to true for the duration of the dragging + // session to avoid jumps in the position in case of ongoing playback. + // + // The second scenario involves the user operating the scroll ball, in this + // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, + // we will simply apply the updated position without suspending regular updates. + private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { + public void onStartTrackingTouch(SeekBar bar) { + show(3600000); + + mDragging = true; + + // By removing these pending progress messages we make sure + // that a) we won't update the progress while the user adjusts + // the seekbar and b) once the user is done dragging the thumb + // we will post one of these messages to the queue again and + // this ensures that there will be exactly one message queued up. + mHandler.removeMessages(SHOW_PROGRESS); + } + + public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { + if (!fromuser) { + // We're not interested in programmatically generated changes to + // the progress bar's position. + return; + } + + long duration = mPlayer.getDuration(); + long newposition = (duration * progress) / 1000L; + mPlayer.seekTo( (int) newposition); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime( (int) newposition)); + } + + public void onStopTrackingTouch(SeekBar bar) { + mDragging = false; + setProgress(); + updatePausePlay(); + show(sDefaultTimeout); + + // Ensure that progress is properly updated in the future, + // the call to show() does not guarantee this because it is a + // no-op if we are already showing. + mHandler.sendEmptyMessage(SHOW_PROGRESS); + } + }; + + @Override + public void setEnabled(boolean enabled) { + if (mPauseButton != null) { + mPauseButton.setEnabled(enabled); + } + if (mFfwdButton != null) { + mFfwdButton.setEnabled(enabled); + } + if (mRewButton != null) { + mRewButton.setEnabled(enabled); + } + if (mNextButton != null) { + mNextButton.setEnabled(enabled && mNextListener != null); + } + if (mPrevButton != null) { + mPrevButton.setEnabled(enabled && mPrevListener != null); + } + if (mProgress != null) { + mProgress.setEnabled(enabled); + } + disableUnsupportedButtons(); + super.setEnabled(enabled); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(MediaController.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(MediaController.class.getName()); + } + + private View.OnClickListener mRewListener = new View.OnClickListener() { + public void onClick(View v) { + int pos = mPlayer.getCurrentPosition(); + pos -= 5000; // milliseconds + mPlayer.seekTo(pos); + setProgress(); + + show(sDefaultTimeout); + } + }; + + private View.OnClickListener mFfwdListener = new View.OnClickListener() { + public void onClick(View v) { + int pos = mPlayer.getCurrentPosition(); + pos += 15000; // milliseconds + mPlayer.seekTo(pos); + setProgress(); + + show(sDefaultTimeout); + } + }; + + private void installPrevNextListeners() { + if (mNextButton != null) { + mNextButton.setOnClickListener(mNextListener); + mNextButton.setEnabled(mNextListener != null); + } + + if (mPrevButton != null) { + mPrevButton.setOnClickListener(mPrevListener); + mPrevButton.setEnabled(mPrevListener != null); + } + } + + public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { + mNextListener = next; + mPrevListener = prev; + mListenersSet = true; + + if (mRoot != null) { + installPrevNextListeners(); + + if (mNextButton != null && !mFromXml) { + mNextButton.setVisibility(View.VISIBLE); + } + if (mPrevButton != null && !mFromXml) { + mPrevButton.setVisibility(View.VISIBLE); + } + } + } + + public interface MediaPlayerControl { + void start(); + void pause(); + int getDuration(); + int getCurrentPosition(); + void seekTo(int pos); + boolean isPlaying(); + int getBufferPercentage(); + boolean canPause(); + boolean canSeekBackward(); + boolean canSeekForward(); + + /** + * Get the audio session id for the player used by this VideoView. This can be used to + * apply audio effects to the audio track of a video. + * @return The audio session, or 0 if there was an error. + */ + int getAudioSessionId(); + } +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" similarity index 97% rename from "Android\345\212\240\345\274\272/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" index 52db7530..57e2712b 100644 --- "a/Android\345\212\240\345\274\272/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" +++ "b/AndroidAdavancedPart/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" @@ -1,1576 +1,1576 @@ -View绘制过程详解 -=== - -界面窗口的根布局是`DecorView`,该类继承自`FrameLayout`.说到`View`绘制,想到的就是从这里入手,而`FrameLayout`继承自`ViewGroup`。感觉绘制肯定会在`ViewGroup`或者`View`中, -但是木有找到。发现`ViewGroup`实现`ViewParent`接口,而`ViewParent`有一个实现类是`ViewRootImpl`, `ViewGruop`中会使用`ViewRootImpl`... -```java -/** - * The top of a view hierarchy, implementing the needed protocol between View - * and the WindowManager. This is for the most part an internal implementation - * detail of {@link WindowManagerGlobal}. - * - * {@hide} - */ -@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) -public final class ViewRootImpl implements ViewParent, - View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { - - } -``` - -`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始。 -首先先说明一下,这部分代码比较多,逻辑也比较麻烦,很容易弄晕,如果感觉看起来费劲,就跳过这一块,直接到下面的Measure、Layout、Draw部分开始看。 -我也没有全部弄清楚,我只是把里面的步骤标注了下。 -```java -private void performTraversals() { - // ... 此处省略源代码N行 - - // 是否需要Measure - if (!mStopped) { - boolean focusChangedDueToTouchMode = ensureTouchModeLocally( - (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); - if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() - || mHeight != host.getMeasuredHeight() || contentInsetsChanged) { - // 这里是获取widthMeasureSpec,这俩参数不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值. - // getRootMeasureSpec方法内部会使用MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec, - // 当lp.width参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当lp.width等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。 - // 并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。 - int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); - int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); - - if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" - + mWidth + " measuredWidth=" + host.getMeasuredWidth() - + " mHeight=" + mHeight - + " measuredHeight=" + host.getMeasuredHeight() - + " coveredInsetsChanged=" + contentInsetsChanged); - - // 调用PerformMeasure方法。 - // Ask host how big it wants to be - performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); - - // Implementation of weights from WindowManager.LayoutParams - // We just grow the dimensions as needed and re-measure if - // needs be - int width = host.getMeasuredWidth(); - int height = host.getMeasuredHeight(); - boolean measureAgain = false; - - if (lp.horizontalWeight > 0.0f) { - width += (int) ((mWidth - width) * lp.horizontalWeight); - childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, - MeasureSpec.EXACTLY); - measureAgain = true; - } - if (lp.verticalWeight > 0.0f) { - height += (int) ((mHeight - height) * lp.verticalWeight); - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, - MeasureSpec.EXACTLY); - measureAgain = true; - } - - if (measureAgain) { - if (DEBUG_LAYOUT) Log.v(TAG, - "And hey let's measure once more: width=" + width - + " height=" + height); - performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); - } - - layoutRequested = true; - } - } - - final boolean didLayout = layoutRequested && !mStopped; - boolean triggerGlobalLayoutListener = didLayout - || mAttachInfo.mRecomputeGlobalAttributes; - // 是否需要Layout - if (didLayout) { - // 调用performLayout方法。 - performLayout(lp, desiredWindowWidth, desiredWindowHeight); - - // By this point all views have been sized and positioned - // We can compute the transparent area - - if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { - // start out transparent - // TODO: AVOID THAT CALL BY CACHING THE RESULT? - host.getLocationInWindow(mTmpLocation); - mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], - mTmpLocation[0] + host.mRight - host.mLeft, - mTmpLocation[1] + host.mBottom - host.mTop); - - host.gatherTransparentRegion(mTransparentRegion); - if (mTranslator != null) { - mTranslator.translateRegionInWindowToScreen(mTransparentRegion); - } - - if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { - mPreviousTransparentRegion.set(mTransparentRegion); - mFullRedrawNeeded = true; - // reconfigure window manager - try { - mWindowSession.setTransparentRegion(mWindow, mTransparentRegion); - } catch (RemoteException e) { - } - } - } - - if (DBG) { - System.out.println("======================================"); - System.out.println("performTraversals -- after setFrame"); - host.debug(); - } - } - - // 是否需要Draw - if (!cancelDraw && !newSurface) { - if (!skipDraw || mReportNextDraw) { - if (mPendingTransitions != null && mPendingTransitions.size() > 0) { - for (int i = 0; i < mPendingTransitions.size(); ++i) { - mPendingTransitions.get(i).startChangingAnimations(); - } - mPendingTransitions.clear(); - } - // 调用performDraw方法 - performDraw(); - } - } else { - if (viewVisibility == View.VISIBLE) { - // Try again - scheduleTraversals(); - } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { - for (int i = 0; i < mPendingTransitions.size(); ++i) { - mPendingTransitions.get(i).endChangingAnimations(); - } - mPendingTransitions.clear(); - } - } - - mIsInTraversal = false; -} -``` - -从上面源码可以看出,`performTraversals()`方法中会依次做三件事: -- `performMeasure()`, 内部是` mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);`测量`View`大小。这里顺便提一下,这个`mView`是什么?它就是`Window`最顶成的`View(DecorView)`,它是`FrameLayout`的子类。 -- `performLayout()`, 内部是`mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());`视图布局,确定`View`位置。 -- `performDraw()`, 内部是`draw(fullRedrawNeeded);` 绘制界面。 - -至此`View`绘制的三个过程已经展现: - -`Measure` -=== - -`performMeasure`方法如下: -```java -private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); - try { - mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } -} -``` - -在`performMeasure()`方法中会调用`View.measure()`方法, 源码如下: -```java -/** - *

- * This is called to find out how big a view should be. The parent - * supplies constraint information in the width and height parameters. - *

- * - *

- * The actual measurement work of a view is performed in - * {@link #onMeasure(int, int)}, called by this method. Therefore, only - * {@link #onMeasure(int, int)} can and must be overridden by subclasses. - *

- * - * - * @param widthMeasureSpec Horizontal space requirements as imposed by the - * parent - * @param heightMeasureSpec Vertical space requirements as imposed by the - * parent - * - * @see #onMeasure(int, int) - */ -public final void measure(int widthMeasureSpec, int heightMeasureSpec) { - boolean optical = isLayoutModeOptical(this); - if (optical != isLayoutModeOptical(mParent)) { - Insets insets = getOpticalInsets(); - int oWidth = insets.left + insets.right; - int oHeight = insets.top + insets.bottom; - widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); - heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); - } - - // Suppress sign extension for the low bytes - long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; - if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); - - if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || - widthMeasureSpec != mOldWidthMeasureSpec || - heightMeasureSpec != mOldHeightMeasureSpec) { - - // first clears the measured dimension flag - mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; - - resolveRtlPropertiesIfNeeded(); - - int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : - mMeasureCache.indexOfKey(key); - if (cacheIndex < 0 || sIgnoreMeasureCache) { - // 调用onMeasure方法 - // measure ourselves, this should set the measured dimension flag back - onMeasure(widthMeasureSpec, heightMeasureSpec); - mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; - } else { - long value = mMeasureCache.valueAt(cacheIndex); - // Casting a long to int drops the high 32 bits, no mask needed - setMeasuredDimensionRaw((int) (value >> 32), (int) value); - mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; - } - - // flag not set, setMeasuredDimension() was not invoked, we raise - // an exception to warn the developer - if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { - // 重写onMeausre方法的时,必须调用setMeasuredDimension或者super.onMeasure方法,不然就会走到这里报错。 - // setMeasuredDimension中回去改变mPrivateFlags的值 - throw new IllegalStateException("onMeasure() did not set the" - + " measured dimension by calling" - + " setMeasuredDimension()"); - } - - mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; - } - - mOldWidthMeasureSpec = widthMeasureSpec; - mOldHeightMeasureSpec = heightMeasureSpec; - - mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | - (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension -} -``` - -在`measure`方法中会调用`onMeasure`方法。`ViewGroup`的子类会重写该方法来进行测量大小,因为`mView`是`DecorView`, -而`DecorView`是`FrameLayout`的子类。所以我们看一下`FrameLayout.onMeasure`方法: -`FrameLayout.onMeasure`源码如下: -```java -/** - * {@inheritDoc} - */ -@Override -protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int count = getChildCount(); - - final boolean measureMatchParentChildren = - MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || - MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; - mMatchParentChildren.clear(); - - int maxHeight = 0; - int maxWidth = 0; - int childState = 0; - - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (mMeasureAllChildren || child.getVisibility() != GONE) { - // 调用该方法去测量每个子View - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - maxWidth = Math.max(maxWidth, - child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); - maxHeight = Math.max(maxHeight, - child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); - childState = combineMeasuredStates(childState, child.getMeasuredState()); - if (measureMatchParentChildren) { - if (lp.width == LayoutParams.MATCH_PARENT || - lp.height == LayoutParams.MATCH_PARENT) { - mMatchParentChildren.add(child); - } - } - } - } - - // Account for padding too - maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); - maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); - - // Check against our minimum height and width - maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); - maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); - - // Check against our foreground's minimum height and width - final Drawable drawable = getForeground(); - if (drawable != null) { - maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); - maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); - } - - setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), - resolveSizeAndState(maxHeight, heightMeasureSpec, - childState << MEASURED_HEIGHT_STATE_SHIFT)); - - count = mMatchParentChildren.size(); - if (count > 1) { - for (int i = 0; i < count; i++) { - final View child = mMatchParentChildren.get(i); - - final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - int childWidthMeasureSpec; - int childHeightMeasureSpec; - - if (lp.width == LayoutParams.MATCH_PARENT) { - childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - - lp.leftMargin - lp.rightMargin, - MeasureSpec.EXACTLY); - } else { - childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, - getPaddingLeftWithForeground() + getPaddingRightWithForeground() + - lp.leftMargin + lp.rightMargin, - lp.width); - } - - if (lp.height == LayoutParams.MATCH_PARENT) { - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - - lp.topMargin - lp.bottomMargin, - MeasureSpec.EXACTLY); - } else { - childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, - getPaddingTopWithForeground() + getPaddingBottomWithForeground() + - lp.topMargin + lp.bottomMargin, - lp.height); - } - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - } - } -} -``` - -我们看到内部会调用`measureChildWithMargins()`方法,该方法源码如下: -```java -/** - * Ask one of the children of this view to measure itself, taking into - * account both the MeasureSpec requirements for this view and its padding - * and margins. The child must have MarginLayoutParams The heavy lifting is - * done in getChildMeasureSpec. - * - * @param child The child to measure - * @param parentWidthMeasureSpec The width requirements for this view - * @param widthUsed Extra space that has been used up by the parent - * horizontally (possibly by other children of the parent) - * @param parentHeightMeasureSpec The height requirements for this view - * @param heightUsed Extra space that has been used up by the parent - * vertically (possibly by other children of the parent) - */ -protected void measureChildWithMargins(View child, - int parentWidthMeasureSpec, int widthUsed, - int parentHeightMeasureSpec, int heightUsed) { - final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - - final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, - mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin - + widthUsed, lp.width); - final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, - mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin - + heightUsed, lp.height); - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); -} -``` -里面就是对该子`View`调用了`measure`方法,我们假设这个`View`已经不是`ViewGroup`了,就会又和上面一样,又调用`onMeasure`方法, -下面我们直接看一下`View.onMeasure()`方法: -`View.onMeasure()`方法的源码如下: -```java -/** - *

- * Measure the view and its content to determine the measured width and the - * measured height. This method is invoked by {@link #measure(int, int)} and - * should be overriden by subclasses to provide accurate and efficient - * measurement of their contents. - *

- * - *

- * CONTRACT: When overriding this method, you - * must call {@link #setMeasuredDimension(int, int)} to store the - * measured width and height of this view. Failure to do so will trigger an - * IllegalStateException, thrown by - * {@link #measure(int, int)}. Calling the superclass' - * {@link #onMeasure(int, int)} is a valid use. - *

- * - *

- * The base class implementation of measure defaults to the background size, - * unless a larger size is allowed by the MeasureSpec. Subclasses should - * override {@link #onMeasure(int, int)} to provide better measurements of - * their content. - *

- * - *

- * If this method is overridden, it is the subclass's responsibility to make - * sure the measured height and width are at least the view's minimum height - * and width ({@link #getSuggestedMinimumHeight()} and - * {@link #getSuggestedMinimumWidth()}). - *

- * - * @param widthMeasureSpec horizontal space requirements as imposed by the parent. - * The requirements are encoded with - * {@link android.view.View.MeasureSpec}. - * @param heightMeasureSpec vertical space requirements as imposed by the parent. - * The requirements are encoded with - * {@link android.view.View.MeasureSpec}. - * - * @see #getMeasuredWidth() - * @see #getMeasuredHeight() - * @see #setMeasuredDimension(int, int) - * @see #getSuggestedMinimumHeight() - * @see #getSuggestedMinimumWidth() - * @see android.view.View.MeasureSpec#getMode(int) - * @see android.view.View.MeasureSpec#getSize(int) - */ -protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // 如果不重写onMeasure方法,默认会调用getDefaultSize获取大小,下面会说getDefaultSize这个方法。 - setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), - getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); -} -``` - -`setMeasuredDimension()`方法如下: -```java -/** - *

This method must be called by {@link #onMeasure(int, int)} to store the - * measured width and measured height. Failing to do so will trigger an - * exception at measurement time.

- * - * @param measuredWidth The measured width of this view. May be a complex - * bit mask as defined by {@link #MEASURED_SIZE_MASK} and - * {@link #MEASURED_STATE_TOO_SMALL}. - * @param measuredHeight The measured height of this view. May be a complex - * bit mask as defined by {@link #MEASURED_SIZE_MASK} and - * {@link #MEASURED_STATE_TOO_SMALL}. - */ -protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { - boolean optical = isLayoutModeOptical(this); - if (optical != isLayoutModeOptical(mParent)) { - Insets insets = getOpticalInsets(); - int opticalWidth = insets.left + insets.right; - int opticalHeight = insets.top + insets.bottom; - - measuredWidth += optical ? opticalWidth : -opticalWidth; - measuredHeight += optical ? opticalHeight : -opticalHeight; - } - setMeasuredDimensionRaw(measuredWidth, measuredHeight); -} -``` -`setMeasuredDimensionRaw()`方法如下: -```java -/** - * Sets the measured dimension without extra processing for things like optical bounds. - * Useful for reapplying consistent values that have already been cooked with adjustments - * for optical bounds, etc. such as those from the measurement cache. - * - * @param measuredWidth The measured width of this view. May be a complex - * bit mask as defined by {@link #MEASURED_SIZE_MASK} and - * {@link #MEASURED_STATE_TOO_SMALL}. - * @param measuredHeight The measured height of this view. May be a complex - * bit mask as defined by {@link #MEASURED_SIZE_MASK} and - * {@link #MEASURED_STATE_TOO_SMALL}. - */ -private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { - // 赋值给mMeasuredWidth,getMeasuredWidth就会调用该值。 - mMeasuredWidth = measuredWidth; - mMeasuredHeight = measuredHeight; - - // 这就是重写onMeasure方法时如果不调用setMeasuredDimension方法时为什么会报错的原因。 - mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; -} -``` - -我们接着看一下上面用到的`getDefaultSize()`方法,源码如下: -```java -/** - * Utility to return a default size. Uses the supplied size if the - * MeasureSpec imposed no constraints. Will get larger if allowed - * by the MeasureSpec. - * - * @param size Default size for this view - * @param measureSpec Constraints imposed by the parent - * @return The size this view should be. - */ -public static int getDefaultSize(int size, int measureSpec) { - int result = size; - // measureSpec值用于获取宽度(高度)的规格和大小,解析出对应的size和mode - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.AT_MOST: - case MeasureSpec.EXACTLY: - result = specSize; - break; - } - return result; -} -``` -`getDefaultSize`方法又会使用到`MeasureSpec`类,文档中对`MeasureSpec`是这样介绍的`A MeasureSpec is comprised of a size and a mode. There are three possible modes:` -- MeasureSpec.EXACTLY The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be. - 理解成MATCH_PARENT或者在布局中指定了宽高值,如layout:width='50dp' -- MeasureSpec.AT_MOST The child can be as large as it wants up to the specified size.理解成WRAP_CONTENT,这是的值是父View可以允许的最大的值,只要不超过这个值都可以。 -- MeasureSpec.UNSPECIFIED The parent has not imposed any constraint on the child. It can be whatever size it wants. 这种情况比较少,一般用不到。 - -这里简单总结一下上面的过程: -```java -performMeasure() { - - 1.调用View.measure方法 - mView.measure(): - - 2.measure内部会调用onMeasure方法,但是因为这里mView是DecorView,所以会调用FrameLayout的onMeasure方法。 - onMeasure(FrameLayout) - - 3. 内部设置ViewGroup的宽高 - setMeasuredDimension - 并且对每个子View进行遍历测量 - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - - 4. 对每个子View调用measureChildWithMargins方法 - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); - -5. measureChildWithMargins内部调用子View的measure方法 - meausre - - 6. measure方法内部又调用onMeasure方法 - onMeasure(View) - - 7. onMeasure方法内部调用setMeasuredDimension - setMeasuredDimension - - 8. setMeasuredDimension内部调用setMeasuredDimensionRaw - setMeasuredDimensionRaw - } -} -``` - -从上面代码中能看到`measure`是`final`的,我们可以重写`onMeasure`来实现`measure`过程。 -到这里基本都讲完了,我们在开发中会按照需要重写`onMeasure`方法,然后调用`setMeasuredDimension`方法设置大小, -ps:譬如我们设置了`setMeasuredDimension(10, 10)`,那么不管布局中怎么设置这个`View`的大小 -都是没用的,最后显示出来大小都是10*10。 - -`Layout` -=== -`performLayout`方法源码如下: -```java -private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, - int desiredWindowHeight) { - mLayoutRequested = false; - mScrollMayChange = true; - mInLayout = true; - - final View host = mView; - if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { - Log.v(TAG, "Laying out " + host + " to (" + - host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); - } - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); - try { - // 把刚才测量的宽高设置进来 - host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); - - mInLayout = false; - int numViewsRequestingLayout = mLayoutRequesters.size(); - if (numViewsRequestingLayout > 0) { - // requestLayout() was called during layout. - // If no layout-request flags are set on the requesting views, there is no problem. - // If some requests are still pending, then we need to clear those flags and do - // a full request/measure/layout pass to handle this situation. - ArrayList validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, - false); - if (validLayoutRequesters != null) { - // Set this flag to indicate that any further requests are happening during - // the second pass, which may result in posting those requests to the next - // frame instead - mHandlingLayoutInLayoutRequest = true; - - // Process fresh layout requests, then measure and layout - int numValidRequests = validLayoutRequesters.size(); - for (int i = 0; i < numValidRequests; ++i) { - final View view = validLayoutRequesters.get(i); - Log.w("View", "requestLayout() improperly called by " + view + - " during layout: running second layout pass"); - view.requestLayout(); - } - measureHierarchy(host, lp, mView.getContext().getResources(), - desiredWindowWidth, desiredWindowHeight); - mInLayout = true; - host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); - - mHandlingLayoutInLayoutRequest = false; - - // Check the valid requests again, this time without checking/clearing the - // layout flags, since requests happening during the second pass get noop'd - validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true); - if (validLayoutRequesters != null) { - final ArrayList finalRequesters = validLayoutRequesters; - // Post second-pass requests to the next frame - getRunQueue().post(new Runnable() { - @Override - public void run() { - int numValidRequests = finalRequesters.size(); - for (int i = 0; i < numValidRequests; ++i) { - final View view = finalRequesters.get(i); - Log.w("View", "requestLayout() improperly called by " + view + - " during second layout pass: posting in next frame"); - view.requestLayout(); - } - } - }); - } - } - - } - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - mInLayout = false; -} -``` - -内部会调用`layout()`方法,因为`host`是`mView`,`ViewGroup`中重写了`layout`方法,并调用了`super.layout`. -所以我们直接看`View.layout()`方法,该方法源码如下: -```java -/** - * Assign a size and position to a view and all of its - * descendants - * - *

This is the second phase of the layout mechanism. - * (The first is measuring). In this phase, each parent calls - * layout on all of its children to position them. - * This is typically done using the child measurements - * that were stored in the measure pass().

- * - *

Derived classes should not override this method. - * Derived classes with children should override - * onLayout. In that method, they should - * call layout on each of their children.

- * - * @param l Left position, relative to parent - * @param t Top position, relative to parent - * @param r Right position, relative to parent - * @param b Bottom position, relative to parent - */ -@SuppressWarnings({"unchecked"}) -public void layout(int l, int t, int r, int b) { - if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { - onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); - mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; - } - - int oldL = mLeft; - int oldT = mTop; - int oldB = mBottom; - int oldR = mRight; - - // 这部分是判断这个View的大小是否已经发生了变化,来判断是否需要重绘。 - boolean changed = isLayoutModeOptical(mParent) ? - setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); - - if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { - // 内部调用onLayout方法 - onLayout(changed, l, t, r, b); - mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; - - ListenerInfo li = mListenerInfo; - if (li != null && li.mOnLayoutChangeListeners != null) { - ArrayList listenersCopy = - (ArrayList)li.mOnLayoutChangeListeners.clone(); - int numListeners = listenersCopy.size(); - for (int i = 0; i < numListeners; ++i) { - listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); - } - } - } - - mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; - mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; -} -``` -这里会调用`onLayout`方法,同样因为`mView`是`FrameLayout`的子类,所以我们要看`FrameLayout`的`onLayout`方法, -这里我们先看一下`ViewGroup.onLayout`方法: -```java -/** - * {@inheritDoc} - */ -@Override -protected abstract void onLayout(boolean changed, - int l, int t, int r, int b); -``` -是个抽象方法,所以`ViewGroup`的子类都需要实现该方法。 -我们看一下`FrameLayout.onLayout`方法,源码如下: -```java - /** - * {@inheritDoc} - */ -@Override -protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - layoutChildren(left, top, right, bottom, false /* no force left gravity */); -} - -void layoutChildren(int left, int top, int right, int bottom, - boolean forceLeftGravity) { - final int count = getChildCount(); - - final int parentLeft = getPaddingLeftWithForeground(); - final int parentRight = right - left - getPaddingRightWithForeground(); - - final int parentTop = getPaddingTopWithForeground(); - final int parentBottom = bottom - top - getPaddingBottomWithForeground(); - - mForegroundBoundsChanged = true; - - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child.getVisibility() != GONE) { - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - final int width = child.getMeasuredWidth(); - final int height = child.getMeasuredHeight(); - - int childLeft; - int childTop; - - int gravity = lp.gravity; - if (gravity == -1) { - gravity = DEFAULT_CHILD_GRAVITY; - } - - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); - final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; - - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.CENTER_HORIZONTAL: - childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + - lp.leftMargin - lp.rightMargin; - break; - case Gravity.RIGHT: - if (!forceLeftGravity) { - childLeft = parentRight - width - lp.rightMargin; - break; - } - case Gravity.LEFT: - default: - childLeft = parentLeft + lp.leftMargin; - } - - switch (verticalGravity) { - case Gravity.TOP: - childTop = parentTop + lp.topMargin; - break; - case Gravity.CENTER_VERTICAL: - childTop = parentTop + (parentBottom - parentTop - height) / 2 + - lp.topMargin - lp.bottomMargin; - break; - case Gravity.BOTTOM: - childTop = parentBottom - height - lp.bottomMargin; - break; - default: - childTop = parentTop + lp.topMargin; - } - //调用子View的layout方法 - child.layout(childLeft, childTop, childLeft + width, childTop + height); - } - } -} -``` -而`View.layout`方法,又会调用到`View.onLayout`方法,我们假设这个子`View`不是`ViewGroup`. -看一下`View.onLayout`方法源码如下: -```java -/** - * Called from layout when this view should - * assign a size and position to each of its children. - * - * Derived classes with children should override - * this method and call layout on each of - * their children. - * @param changed This is a new size or position for this view - * @param left Left position, relative to parent - * @param top Top position, relative to parent - * @param right Right position, relative to parent - * @param bottom Bottom position, relative to parent - */ -protected void onLayout(boolean changed, int left, int top, int right, int bottom) { -} -``` -是一个空方法,这是因为`Layout`需要`ViewGroup`来控制进行。 - -这里也总结一下`layout`的过程。 -```java -private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, - int desiredWindowHeight) { - - 1. host.layout - host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); - -2. layout方法会分别调用setFrame()和onLayout()方法 - setFrame() - onLayout() - -3. 因为host是mView也就是DecorView也就是FrameLayout的子类。FrameLayout的onLayout方法如下 - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child.getVisibility() != GONE) { - -4. 遍历每个子View,并分别调用layout方法。 - child.layout(childLeft, childTop, childLeft + width, childTop + height); - } - } -} -``` - -`Draw` -=== - -绘制阶段是从`ViewRootImpl`中的`performDraw`方法开始的: -```java -private void performDraw() { - if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { - return; - } - - final boolean fullRedrawNeeded = mFullRedrawNeeded; - mFullRedrawNeeded = false; - - mIsDrawing = true; - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); - try { - // 开始draw了 - draw(fullRedrawNeeded); - } finally { - mIsDrawing = false; - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - - // For whatever reason we didn't create a HardwareRenderer, end any - // hardware animations that are now dangling - if (mAttachInfo.mPendingAnimatingRenderNodes != null) { - final int count = mAttachInfo.mPendingAnimatingRenderNodes.size(); - for (int i = 0; i < count; i++) { - mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators(); - } - mAttachInfo.mPendingAnimatingRenderNodes.clear(); - } - - if (mReportNextDraw) { - mReportNextDraw = false; - if (mAttachInfo.mHardwareRenderer != null) { - mAttachInfo.mHardwareRenderer.fence(); - } - - if (LOCAL_LOGV) { - Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); - } - if (mSurfaceHolder != null && mSurface.isValid()) { - mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); - SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); - if (callbacks != null) { - for (SurfaceHolder.Callback c : callbacks) { - if (c instanceof SurfaceHolder.Callback2) { - ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( - mSurfaceHolder); - } - } - } - } - try { - mWindowSession.finishDrawing(mWindow); - } catch (RemoteException e) { - } - } -} -``` -内部会调用`draw`方法,`draw`方法源码如下: -```java -private void draw(boolean fullRedrawNeeded) { - Surface surface = mSurface; - if (!surface.isValid()) { - return; - } - - if (DEBUG_FPS) { - trackFPS(); - } - - if (!sFirstDrawComplete) { - synchronized (sFirstDrawHandlers) { - sFirstDrawComplete = true; - final int count = sFirstDrawHandlers.size(); - for (int i = 0; i< count; i++) { - mHandler.post(sFirstDrawHandlers.get(i)); - } - } - } - - scrollToRectOrFocus(null, false); - - if (mAttachInfo.mViewScrollChanged) { - mAttachInfo.mViewScrollChanged = false; - mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); - } - - boolean animating = mScroller != null && mScroller.computeScrollOffset(); - final int curScrollY; - if (animating) { - curScrollY = mScroller.getCurrY(); - } else { - curScrollY = mScrollY; - } - if (mCurScrollY != curScrollY) { - mCurScrollY = curScrollY; - fullRedrawNeeded = true; - } - - final float appScale = mAttachInfo.mApplicationScale; - final boolean scalingRequired = mAttachInfo.mScalingRequired; - - int resizeAlpha = 0; - if (mResizeBuffer != null) { - long deltaTime = SystemClock.uptimeMillis() - mResizeBufferStartTime; - if (deltaTime < mResizeBufferDuration) { - float amt = deltaTime/(float) mResizeBufferDuration; - amt = mResizeInterpolator.getInterpolation(amt); - animating = true; - resizeAlpha = 255 - (int)(amt*255); - } else { - disposeResizeBuffer(); - } - } - - final Rect dirty = mDirty; - if (mSurfaceHolder != null) { - // The app owns the surface, we won't draw. - dirty.setEmpty(); - if (animating) { - if (mScroller != null) { - mScroller.abortAnimation(); - } - disposeResizeBuffer(); - } - return; - } - - if (fullRedrawNeeded) { - mAttachInfo.mIgnoreDirtyState = true; - dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); - } - - if (DEBUG_ORIENTATION || DEBUG_DRAW) { - Log.v(TAG, "Draw " + mView + "/" - + mWindowAttributes.getTitle() - + ": dirty={" + dirty.left + "," + dirty.top - + "," + dirty.right + "," + dirty.bottom + "} surface=" - + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" + - appScale + ", width=" + mWidth + ", height=" + mHeight); - } - - mAttachInfo.mTreeObserver.dispatchOnDraw(); - - int xOffset = 0; - int yOffset = curScrollY; - final WindowManager.LayoutParams params = mWindowAttributes; - final Rect surfaceInsets = params != null ? params.surfaceInsets : null; - if (surfaceInsets != null) { - xOffset -= surfaceInsets.left; - yOffset -= surfaceInsets.top; - - // Offset dirty rect for surface insets. - dirty.offset(surfaceInsets.left, surfaceInsets.right); - } - - if (!dirty.isEmpty() || mIsAnimating) { - if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { - // Draw with hardware renderer. - mIsAnimating = false; - boolean invalidateRoot = false; - if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) { - mHardwareYOffset = yOffset; - mHardwareXOffset = xOffset; - mAttachInfo.mHardwareRenderer.invalidateRoot(); - } - mResizeAlpha = resizeAlpha; - - dirty.setEmpty(); - - mBlockResizeBuffer = false; - mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this); - } else { - // If we get here with a disabled & requested hardware renderer, something went - // wrong (an invalidate posted right before we destroyed the hardware surface - // for instance) so we should just bail out. Locking the surface with software - // rendering at this point would lock it forever and prevent hardware renderer - // from doing its job when it comes back. - // Before we request a new frame we must however attempt to reinitiliaze the - // hardware renderer if it's in requested state. This would happen after an - // eglTerminate() for instance. - if (mAttachInfo.mHardwareRenderer != null && - !mAttachInfo.mHardwareRenderer.isEnabled() && - mAttachInfo.mHardwareRenderer.isRequested()) { - - try { - mAttachInfo.mHardwareRenderer.initializeIfNeeded( - mWidth, mHeight, mSurface, surfaceInsets); - } catch (OutOfResourcesException e) { - handleOutOfResourcesException(e); - return; - } - - mFullRedrawNeeded = true; - scheduleTraversals(); - return; - } - - // draw的部分在这里。。。内部会用canvas去画 - if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { - return; - } - } - } - - if (animating) { - mFullRedrawNeeded = true; - scheduleTraversals(); - } -} -``` -我们看一下`drawSoftware`方法: -```java -/** - * @return true if drawing was successful, false if an error occurred - */ -private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, - boolean scalingRequired, Rect dirty) { - - // Draw with software renderer. - final Canvas canvas; - try { - final int left = dirty.left; - final int top = dirty.top; - final int right = dirty.right; - final int bottom = dirty.bottom; - - canvas = mSurface.lockCanvas(dirty); - - // The dirty rectangle can be modified by Surface.lockCanvas() - //noinspection ConstantConditions - if (left != dirty.left || top != dirty.top || right != dirty.right - || bottom != dirty.bottom) { - attachInfo.mIgnoreDirtyState = true; - } - - // TODO: Do this in native - canvas.setDensity(mDensity); - } catch (Surface.OutOfResourcesException e) { - handleOutOfResourcesException(e); - return false; - } catch (IllegalArgumentException e) { - Log.e(TAG, "Could not lock surface", e); - // Don't assume this is due to out of memory, it could be - // something else, and if it is something else then we could - // kill stuff (or ourself) for no reason. - mLayoutRequested = true; // ask wm for a new surface next time. - return false; - } - - try { - if (DEBUG_ORIENTATION || DEBUG_DRAW) { - Log.v(TAG, "Surface " + surface + " drawing to bitmap w=" - + canvas.getWidth() + ", h=" + canvas.getHeight()); - //canvas.drawARGB(255, 255, 0, 0); - } - - // If this bitmap's format includes an alpha channel, we - // need to clear it before drawing so that the child will - // properly re-composite its drawing on a transparent - // background. This automatically respects the clip/dirty region - // or - // If we are applying an offset, we need to clear the area - // where the offset doesn't appear to avoid having garbage - // left in the blank areas. - if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - - dirty.setEmpty(); - mIsAnimating = false; - attachInfo.mDrawingTime = SystemClock.uptimeMillis(); - mView.mPrivateFlags |= View.PFLAG_DRAWN; - - if (DEBUG_DRAW) { - Context cxt = mView.getContext(); - Log.i(TAG, "Drawing: package:" + cxt.getPackageName() + - ", metrics=" + cxt.getResources().getDisplayMetrics() + - ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo()); - } - try { - canvas.translate(-xoff, -yoff); - if (mTranslator != null) { - mTranslator.translateCanvas(canvas); - } - canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); - attachInfo.mSetIgnoreDirtyState = false; - - // 内部会去调用View.draw(); - mView.draw(canvas); - } finally { - if (!attachInfo.mSetIgnoreDirtyState) { - // Only clear the flag if it was not set during the mView.draw() call - attachInfo.mIgnoreDirtyState = false; - } - } - } finally { - try { - surface.unlockCanvasAndPost(canvas); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Could not unlock surface", e); - mLayoutRequested = true; // ask wm for a new surface next time. - //noinspection ReturnInsideFinallyBlock - return false; - } - - if (LOCAL_LOGV) { - Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); - } - } - return true; -} -``` -代码中调用了`mView.draw()`方法,所以我们看一下`FrameLayout.draw()`方法: -```java -/** - * {@inheritDoc} - */ -@Override -public void draw(Canvas canvas) { - super.draw(canvas); - - if (mForeground != null) { - final Drawable foreground = mForeground; - - if (mForegroundBoundsChanged) { - mForegroundBoundsChanged = false; - final Rect selfBounds = mSelfBounds; - final Rect overlayBounds = mOverlayBounds; - - final int w = mRight-mLeft; - final int h = mBottom-mTop; - - if (mForegroundInPadding) { - selfBounds.set(0, 0, w, h); - } else { - selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); - } - - final int layoutDirection = getLayoutDirection(); - Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), - foreground.getIntrinsicHeight(), selfBounds, overlayBounds, - layoutDirection); - foreground.setBounds(overlayBounds); - } - - foreground.draw(canvas); - } -} -``` -内部调用了`super.draw()`,而`ViewGroup`没有重写该方法,所以直接看`View`的`draw()`方法. -`View.draw()`方法如下: -```java -/** - * Manually render this view (and all of its children) to the given Canvas. - * The view must have already done a full layout before this function is - * called. When implementing a view, implement - * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. - * If you do need to override this method, call the superclass version. - * - * @param canvas The Canvas to which the View is rendered. - */ -public void draw(Canvas canvas) { - final int privateFlags = mPrivateFlags; - final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && - (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); - mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; - - // 这里注释说的很明白了,draw的6个步骤。 - /* - * Draw traversal performs several drawing steps which must be executed - * in the appropriate order: - * - * 1. Draw the background - * 2. If necessary, save the canvas' layers to prepare for fading - * 3. Draw view's content, 调用onDraw方法绘制自身 - * 4. Draw children, 调用dispatchDraw方法绘制子View - * 5. If necessary, draw the fading edges and restore layers - * 6. Draw decorations (scrollbars for instance) - */ - - // Step 1, draw the background, if needed - int saveCount; - - if (!dirtyOpaque) { - drawBackground(canvas); - } - - // skip step 2 & 5 if possible (common case) - final int viewFlags = mViewFlags; - boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; - boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; - if (!verticalEdges && !horizontalEdges) { - // Step 3, draw the content - if (!dirtyOpaque) onDraw(canvas); - - // Step 4, draw the children - dispatchDraw(canvas); - - // Step 6, draw decorations (scrollbars) - onDrawScrollBars(canvas); - - if (mOverlay != null && !mOverlay.isEmpty()) { - mOverlay.getOverlayView().dispatchDraw(canvas); - } - - // we're done... - return; - } - - /* - * Here we do the full fledged routine... - * (this is an uncommon case where speed matters less, - * this is why we repeat some of the tests that have been - * done above) - */ - - boolean drawTop = false; - boolean drawBottom = false; - boolean drawLeft = false; - boolean drawRight = false; - - float topFadeStrength = 0.0f; - float bottomFadeStrength = 0.0f; - float leftFadeStrength = 0.0f; - float rightFadeStrength = 0.0f; - - // Step 2, save the canvas' layers - int paddingLeft = mPaddingLeft; - - final boolean offsetRequired = isPaddingOffsetRequired(); - if (offsetRequired) { - paddingLeft += getLeftPaddingOffset(); - } - - int left = mScrollX + paddingLeft; - int right = left + mRight - mLeft - mPaddingRight - paddingLeft; - int top = mScrollY + getFadeTop(offsetRequired); - int bottom = top + getFadeHeight(offsetRequired); - - if (offsetRequired) { - right += getRightPaddingOffset(); - bottom += getBottomPaddingOffset(); - } - - final ScrollabilityCache scrollabilityCache = mScrollCache; - final float fadeHeight = scrollabilityCache.fadingEdgeLength; - int length = (int) fadeHeight; - - // clip the fade length if top and bottom fades overlap - // overlapping fades produce odd-looking artifacts - if (verticalEdges && (top + length > bottom - length)) { - length = (bottom - top) / 2; - } - - // also clip horizontal fades if necessary - if (horizontalEdges && (left + length > right - length)) { - length = (right - left) / 2; - } - - if (verticalEdges) { - topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); - drawTop = topFadeStrength * fadeHeight > 1.0f; - bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); - drawBottom = bottomFadeStrength * fadeHeight > 1.0f; - } - - if (horizontalEdges) { - leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); - drawLeft = leftFadeStrength * fadeHeight > 1.0f; - rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); - drawRight = rightFadeStrength * fadeHeight > 1.0f; - } - - saveCount = canvas.getSaveCount(); - - int solidColor = getSolidColor(); - if (solidColor == 0) { - final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; - - if (drawTop) { - canvas.saveLayer(left, top, right, top + length, null, flags); - } - - if (drawBottom) { - canvas.saveLayer(left, bottom - length, right, bottom, null, flags); - } - - if (drawLeft) { - canvas.saveLayer(left, top, left + length, bottom, null, flags); - } - - if (drawRight) { - canvas.saveLayer(right - length, top, right, bottom, null, flags); - } - } else { - scrollabilityCache.setFadeColor(solidColor); - } - - // Step 3, draw the content - if (!dirtyOpaque) onDraw(canvas); - - // Step 4, draw the children - dispatchDraw(canvas); - - // Step 5, draw the fade effect and restore layers - final Paint p = scrollabilityCache.paint; - final Matrix matrix = scrollabilityCache.matrix; - final Shader fade = scrollabilityCache.shader; - - if (drawTop) { - matrix.setScale(1, fadeHeight * topFadeStrength); - matrix.postTranslate(left, top); - fade.setLocalMatrix(matrix); - p.setShader(fade); - canvas.drawRect(left, top, right, top + length, p); - } - - if (drawBottom) { - matrix.setScale(1, fadeHeight * bottomFadeStrength); - matrix.postRotate(180); - matrix.postTranslate(left, bottom); - fade.setLocalMatrix(matrix); - p.setShader(fade); - canvas.drawRect(left, bottom - length, right, bottom, p); - } - - if (drawLeft) { - matrix.setScale(1, fadeHeight * leftFadeStrength); - matrix.postRotate(-90); - matrix.postTranslate(left, top); - fade.setLocalMatrix(matrix); - p.setShader(fade); - canvas.drawRect(left, top, left + length, bottom, p); - } - - if (drawRight) { - matrix.setScale(1, fadeHeight * rightFadeStrength); - matrix.postRotate(90); - matrix.postTranslate(right, top); - fade.setLocalMatrix(matrix); - p.setShader(fade); - canvas.drawRect(right - length, top, right, bottom, p); - } - - canvas.restoreToCount(saveCount); - - // Step 6, draw decorations (scrollbars) - onDrawScrollBars(canvas); - - if (mOverlay != null && !mOverlay.isEmpty()) { - mOverlay.getOverlayView().dispatchDraw(canvas); - } -} -``` - -上面会调用`onDraw`和`dispatchDraw`方法。 -我们先看一下`View.onDraw`方法: -```java -/** - * Implement this to do your drawing. - * - * @param canvas the canvas on which the background will be drawn - */ -protected void onDraw(Canvas canvas) { -} -``` -是空方法,这是也很好理解,因为每个`View`的展现都不一样,例如`TextView`、`ProgressBar`等, -所以`View`不会去实现`onDraw`方法,具体是要子类去根据自己的显示要求实现该方法。 - -再看一下`dispatchDraw`方法,这个方法是用来绘制子`View`的,所以要看`ViewGroup.dispatchDraw`方法,`View.dispatchDraw`是空的。 -```java -/** - * {@inheritDoc} - */ -@Override -protected void dispatchDraw(Canvas canvas) { - boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); - final int childrenCount = mChildrenCount; - final View[] children = mChildren; - int flags = mGroupFlags; - - if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { - final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; - - final boolean buildCache = !isHardwareAccelerated(); - for (int i = 0; i < childrenCount; i++) { - final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { - final LayoutParams params = child.getLayoutParams(); - attachLayoutAnimationParameters(child, params, i, childrenCount); - bindLayoutAnimation(child); - if (cache) { - child.setDrawingCacheEnabled(true); - if (buildCache) { - child.buildDrawingCache(true); - } - } - } - } - - final LayoutAnimationController controller = mLayoutAnimationController; - if (controller.willOverlap()) { - mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; - } - - controller.start(); - - mGroupFlags &= ~FLAG_RUN_ANIMATION; - mGroupFlags &= ~FLAG_ANIMATION_DONE; - - if (cache) { - mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE; - } - - if (mAnimationListener != null) { - mAnimationListener.onAnimationStart(controller.getAnimation()); - } - } - - int clipSaveCount = 0; - final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; - if (clipToPadding) { - clipSaveCount = canvas.save(); - canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, - mScrollX + mRight - mLeft - mPaddingRight, - mScrollY + mBottom - mTop - mPaddingBottom); - } - - // We will draw our child's animation, let's reset the flag - mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; - mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; - - boolean more = false; - final long drawingTime = getDrawingTime(); - - if (usingRenderNodeProperties) canvas.insertReorderBarrier(); - // Only use the preordered list if not HW accelerated, since the HW pipeline will do the - // draw reordering internally - final ArrayList preorderedList = usingRenderNodeProperties - ? null : buildOrderedChildList(); - final boolean customOrder = preorderedList == null - && isChildrenDrawingOrderEnabled(); - for (int i = 0; i < childrenCount; i++) { - int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; - final View child = (preorderedList == null) - ? children[childIndex] : preorderedList.get(childIndex); - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { - // 调用drawChild方法 - more |= drawChild(canvas, child, drawingTime); - } - } - if (preorderedList != null) preorderedList.clear(); - - // Draw any disappearing views that have animations - if (mDisappearingChildren != null) { - final ArrayList disappearingChildren = mDisappearingChildren; - final int disappearingCount = disappearingChildren.size() - 1; - // Go backwards -- we may delete as animations finish - for (int i = disappearingCount; i >= 0; i--) { - final View child = disappearingChildren.get(i); - more |= drawChild(canvas, child, drawingTime); - } - } - if (usingRenderNodeProperties) canvas.insertInorderBarrier(); - - if (debugDraw()) { - onDebugDraw(canvas); - } - - if (clipToPadding) { - canvas.restoreToCount(clipSaveCount); - } - - // mGroupFlags might have been updated by drawChild() - flags = mGroupFlags; - - if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { - invalidate(true); - } - - if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && - mLayoutAnimationController.isDone() && !more) { - // We want to erase the drawing cache and notify the listener after the - // next frame is drawn because one extra invalidate() is caused by - // drawChild() after the animation is over - mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; - final Runnable end = new Runnable() { - public void run() { - notifyAnimationListener(); - } - }; - post(end); - } -} -``` -可以看到上面的方法中会调用`drawChild`方法,该方法如下: -```java -/** - * Draw one child of this View Group. This method is responsible for getting - * the canvas in the right state. This includes clipping, translating so - * that the child's scrolled origin is at 0, 0, and applying any animation - * transformations. - * - * @param canvas The canvas on which to draw the child - * @param child Who to draw - * @param drawingTime The time at which draw is occurring - * @return True if an invalidate() was issued - */ -protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - return child.draw(canvas, this, drawingTime); -} -``` - -这里也简单总结一下`draw`的过程: -```java -// 1. ViewRootImpl.performDraw() -private void performDraw() { - // 2. ViewRootImpl.draw() - draw(fullRedrawNeeded); - // 3. ViewRootImpl.drawSoftware - drawSoftware - // 4. 内部调用mView.draw,也就是FrameLayout.draw(). - mView.draw()(FrameLayout) - // 5. FrameLayout.draw方法内部会调用super.draw方法,也就是View.draw方法. - super.draw(canvas); - // 6. View.draw方法内部会分别调用onDraw绘制自己以及dispatchDraw绘制子View. - onDraw - // 绘制子View - dispatchDraw - // 7. dispatchDraw方法内部会遍历所有子View. - for (int i = 0; i < childrenCount; i++) { - // 8. 对每个子View分别调用drawChild方法 - drawChild() - // 9. drawChild方法内部会对该子View调用draw方法,进行绘制。然后draw又会调用onDraw等,循环就开始了。 - child.draw() - } -} -``` - -最后补充一个小问题: `getWidth()`与`getMeasuredWidth()`有什么区别呢? -一般情况下这两个的值是相同的,`getMeasureWidth()`方法在`measure()`过程结束后就可以获取到了,而`getWidth()`方法要在`layout()`过程结束后才能获取到。 -而且`getMeasureWidth()`的值是通过`setMeasuredDimension()`设置的,但是`getWidth()`的值是通过视图右边的坐标减去左边的坐标计算出来的。如果我们在`layout`的时候将宽高 -不传`getMeasureWidth`的值,那么这时候`getWidth()`与`getMeasuredWidth`的值就不会再相同了,当然一般也不会这么干... - ---- - -- 邮箱 :charon.chui@gmail.com +View绘制过程详解 +=== + +界面窗口的根布局是`DecorView`,该类继承自`FrameLayout`.说到`View`绘制,想到的就是从这里入手,而`FrameLayout`继承自`ViewGroup`。感觉绘制肯定会在`ViewGroup`或者`View`中, +但是木有找到。发现`ViewGroup`实现`ViewParent`接口,而`ViewParent`有一个实现类是`ViewRootImpl`, `ViewGruop`中会使用`ViewRootImpl`... +```java +/** + * The top of a view hierarchy, implementing the needed protocol between View + * and the WindowManager. This is for the most part an internal implementation + * detail of {@link WindowManagerGlobal}. + * + * {@hide} + */ +@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) +public final class ViewRootImpl implements ViewParent, + View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { + + } +``` + +`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始。 +首先先说明一下,这部分代码比较多,逻辑也比较麻烦,很容易弄晕,如果感觉看起来费劲,就跳过这一块,直接到下面的Measure、Layout、Draw部分开始看。 +我也没有全部弄清楚,我只是把里面的步骤标注了下。 +```java +private void performTraversals() { + // ... 此处省略源代码N行 + + // 是否需要Measure + if (!mStopped) { + boolean focusChangedDueToTouchMode = ensureTouchModeLocally( + (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); + if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() + || mHeight != host.getMeasuredHeight() || contentInsetsChanged) { + // 这里是获取widthMeasureSpec,这俩参数不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值. + // getRootMeasureSpec方法内部会使用MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec, + // 当lp.width参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当lp.width等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。 + // 并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。 + int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); + int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); + + if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" + + mWidth + " measuredWidth=" + host.getMeasuredWidth() + + " mHeight=" + mHeight + + " measuredHeight=" + host.getMeasuredHeight() + + " coveredInsetsChanged=" + contentInsetsChanged); + + // 调用PerformMeasure方法。 + // Ask host how big it wants to be + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + + // Implementation of weights from WindowManager.LayoutParams + // We just grow the dimensions as needed and re-measure if + // needs be + int width = host.getMeasuredWidth(); + int height = host.getMeasuredHeight(); + boolean measureAgain = false; + + if (lp.horizontalWeight > 0.0f) { + width += (int) ((mWidth - width) * lp.horizontalWeight); + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, + MeasureSpec.EXACTLY); + measureAgain = true; + } + if (lp.verticalWeight > 0.0f) { + height += (int) ((mHeight - height) * lp.verticalWeight); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, + MeasureSpec.EXACTLY); + measureAgain = true; + } + + if (measureAgain) { + if (DEBUG_LAYOUT) Log.v(TAG, + "And hey let's measure once more: width=" + width + + " height=" + height); + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + layoutRequested = true; + } + } + + final boolean didLayout = layoutRequested && !mStopped; + boolean triggerGlobalLayoutListener = didLayout + || mAttachInfo.mRecomputeGlobalAttributes; + // 是否需要Layout + if (didLayout) { + // 调用performLayout方法。 + performLayout(lp, desiredWindowWidth, desiredWindowHeight); + + // By this point all views have been sized and positioned + // We can compute the transparent area + + if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { + // start out transparent + // TODO: AVOID THAT CALL BY CACHING THE RESULT? + host.getLocationInWindow(mTmpLocation); + mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], + mTmpLocation[0] + host.mRight - host.mLeft, + mTmpLocation[1] + host.mBottom - host.mTop); + + host.gatherTransparentRegion(mTransparentRegion); + if (mTranslator != null) { + mTranslator.translateRegionInWindowToScreen(mTransparentRegion); + } + + if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { + mPreviousTransparentRegion.set(mTransparentRegion); + mFullRedrawNeeded = true; + // reconfigure window manager + try { + mWindowSession.setTransparentRegion(mWindow, mTransparentRegion); + } catch (RemoteException e) { + } + } + } + + if (DBG) { + System.out.println("======================================"); + System.out.println("performTraversals -- after setFrame"); + host.debug(); + } + } + + // 是否需要Draw + if (!cancelDraw && !newSurface) { + if (!skipDraw || mReportNextDraw) { + if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).startChangingAnimations(); + } + mPendingTransitions.clear(); + } + // 调用performDraw方法 + performDraw(); + } + } else { + if (viewVisibility == View.VISIBLE) { + // Try again + scheduleTraversals(); + } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).endChangingAnimations(); + } + mPendingTransitions.clear(); + } + } + + mIsInTraversal = false; +} +``` + +从上面源码可以看出,`performTraversals()`方法中会依次做三件事: +- `performMeasure()`, 内部是` mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);`测量`View`大小。这里顺便提一下,这个`mView`是什么?它就是`Window`最顶成的`View(DecorView)`,它是`FrameLayout`的子类。 +- `performLayout()`, 内部是`mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());`视图布局,确定`View`位置。 +- `performDraw()`, 内部是`draw(fullRedrawNeeded);` 绘制界面。 + +至此`View`绘制的三个过程已经展现: + +`Measure` +=== + +`performMeasure`方法如下: +```java +private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); + try { + mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } +} +``` + +在`performMeasure()`方法中会调用`View.measure()`方法, 源码如下: +```java +/** + *

+ * This is called to find out how big a view should be. The parent + * supplies constraint information in the width and height parameters. + *

+ * + *

+ * The actual measurement work of a view is performed in + * {@link #onMeasure(int, int)}, called by this method. Therefore, only + * {@link #onMeasure(int, int)} can and must be overridden by subclasses. + *

+ * + * + * @param widthMeasureSpec Horizontal space requirements as imposed by the + * parent + * @param heightMeasureSpec Vertical space requirements as imposed by the + * parent + * + * @see #onMeasure(int, int) + */ +public final void measure(int widthMeasureSpec, int heightMeasureSpec) { + boolean optical = isLayoutModeOptical(this); + if (optical != isLayoutModeOptical(mParent)) { + Insets insets = getOpticalInsets(); + int oWidth = insets.left + insets.right; + int oHeight = insets.top + insets.bottom; + widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); + heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); + } + + // Suppress sign extension for the low bytes + long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; + if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); + + if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || + widthMeasureSpec != mOldWidthMeasureSpec || + heightMeasureSpec != mOldHeightMeasureSpec) { + + // first clears the measured dimension flag + mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; + + resolveRtlPropertiesIfNeeded(); + + int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : + mMeasureCache.indexOfKey(key); + if (cacheIndex < 0 || sIgnoreMeasureCache) { + // 调用onMeasure方法 + // measure ourselves, this should set the measured dimension flag back + onMeasure(widthMeasureSpec, heightMeasureSpec); + mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } else { + long value = mMeasureCache.valueAt(cacheIndex); + // Casting a long to int drops the high 32 bits, no mask needed + setMeasuredDimensionRaw((int) (value >> 32), (int) value); + mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } + + // flag not set, setMeasuredDimension() was not invoked, we raise + // an exception to warn the developer + if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { + // 重写onMeausre方法的时,必须调用setMeasuredDimension或者super.onMeasure方法,不然就会走到这里报错。 + // setMeasuredDimension中回去改变mPrivateFlags的值 + throw new IllegalStateException("onMeasure() did not set the" + + " measured dimension by calling" + + " setMeasuredDimension()"); + } + + mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; + } + + mOldWidthMeasureSpec = widthMeasureSpec; + mOldHeightMeasureSpec = heightMeasureSpec; + + mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | + (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension +} +``` + +在`measure`方法中会调用`onMeasure`方法。`ViewGroup`的子类会重写该方法来进行测量大小,因为`mView`是`DecorView`, +而`DecorView`是`FrameLayout`的子类。所以我们看一下`FrameLayout.onMeasure`方法: +`FrameLayout.onMeasure`源码如下: +```java +/** + * {@inheritDoc} + */ +@Override +protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int count = getChildCount(); + + final boolean measureMatchParentChildren = + MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || + MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; + mMatchParentChildren.clear(); + + int maxHeight = 0; + int maxWidth = 0; + int childState = 0; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (mMeasureAllChildren || child.getVisibility() != GONE) { + // 调用该方法去测量每个子View + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + maxWidth = Math.max(maxWidth, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + maxHeight = Math.max(maxHeight, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + if (measureMatchParentChildren) { + if (lp.width == LayoutParams.MATCH_PARENT || + lp.height == LayoutParams.MATCH_PARENT) { + mMatchParentChildren.add(child); + } + } + } + } + + // Account for padding too + maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); + maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); + + // Check against our minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + // Check against our foreground's minimum height and width + final Drawable drawable = getForeground(); + if (drawable != null) { + maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); + maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); + } + + setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), + resolveSizeAndState(maxHeight, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + + count = mMatchParentChildren.size(); + if (count > 1) { + for (int i = 0; i < count; i++) { + final View child = mMatchParentChildren.get(i); + + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + if (lp.width == LayoutParams.MATCH_PARENT) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - + getPaddingLeftWithForeground() - getPaddingRightWithForeground() - + lp.leftMargin - lp.rightMargin, + MeasureSpec.EXACTLY); + } else { + childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeftWithForeground() + getPaddingRightWithForeground() + + lp.leftMargin + lp.rightMargin, + lp.width); + } + + if (lp.height == LayoutParams.MATCH_PARENT) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - + getPaddingTopWithForeground() - getPaddingBottomWithForeground() - + lp.topMargin - lp.bottomMargin, + MeasureSpec.EXACTLY); + } else { + childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTopWithForeground() + getPaddingBottomWithForeground() + + lp.topMargin + lp.bottomMargin, + lp.height); + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } +} +``` + +我们看到内部会调用`measureChildWithMargins()`方法,该方法源码如下: +```java +/** + * Ask one of the children of this view to measure itself, taking into + * account both the MeasureSpec requirements for this view and its padding + * and margins. The child must have MarginLayoutParams The heavy lifting is + * done in getChildMeasureSpec. + * + * @param child The child to measure + * @param parentWidthMeasureSpec The width requirements for this view + * @param widthUsed Extra space that has been used up by the parent + * horizontally (possibly by other children of the parent) + * @param parentHeightMeasureSpec The height requirements for this view + * @param heightUsed Extra space that has been used up by the parent + * vertically (possibly by other children of the parent) + */ +protected void measureChildWithMargins(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + + heightUsed, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); +} +``` +里面就是对该子`View`调用了`measure`方法,我们假设这个`View`已经不是`ViewGroup`了,就会又和上面一样,又调用`onMeasure`方法, +下面我们直接看一下`View.onMeasure()`方法: +`View.onMeasure()`方法的源码如下: +```java +/** + *

+ * Measure the view and its content to determine the measured width and the + * measured height. This method is invoked by {@link #measure(int, int)} and + * should be overriden by subclasses to provide accurate and efficient + * measurement of their contents. + *

+ * + *

+ * CONTRACT: When overriding this method, you + * must call {@link #setMeasuredDimension(int, int)} to store the + * measured width and height of this view. Failure to do so will trigger an + * IllegalStateException, thrown by + * {@link #measure(int, int)}. Calling the superclass' + * {@link #onMeasure(int, int)} is a valid use. + *

+ * + *

+ * The base class implementation of measure defaults to the background size, + * unless a larger size is allowed by the MeasureSpec. Subclasses should + * override {@link #onMeasure(int, int)} to provide better measurements of + * their content. + *

+ * + *

+ * If this method is overridden, it is the subclass's responsibility to make + * sure the measured height and width are at least the view's minimum height + * and width ({@link #getSuggestedMinimumHeight()} and + * {@link #getSuggestedMinimumWidth()}). + *

+ * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent. + * The requirements are encoded with + * {@link android.view.View.MeasureSpec}. + * @param heightMeasureSpec vertical space requirements as imposed by the parent. + * The requirements are encoded with + * {@link android.view.View.MeasureSpec}. + * + * @see #getMeasuredWidth() + * @see #getMeasuredHeight() + * @see #setMeasuredDimension(int, int) + * @see #getSuggestedMinimumHeight() + * @see #getSuggestedMinimumWidth() + * @see android.view.View.MeasureSpec#getMode(int) + * @see android.view.View.MeasureSpec#getSize(int) + */ +protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // 如果不重写onMeasure方法,默认会调用getDefaultSize获取大小,下面会说getDefaultSize这个方法。 + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); +} +``` + +`setMeasuredDimension()`方法如下: +```java +/** + *

This method must be called by {@link #onMeasure(int, int)} to store the + * measured width and measured height. Failing to do so will trigger an + * exception at measurement time.

+ * + * @param measuredWidth The measured width of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * @param measuredHeight The measured height of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + */ +protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { + boolean optical = isLayoutModeOptical(this); + if (optical != isLayoutModeOptical(mParent)) { + Insets insets = getOpticalInsets(); + int opticalWidth = insets.left + insets.right; + int opticalHeight = insets.top + insets.bottom; + + measuredWidth += optical ? opticalWidth : -opticalWidth; + measuredHeight += optical ? opticalHeight : -opticalHeight; + } + setMeasuredDimensionRaw(measuredWidth, measuredHeight); +} +``` +`setMeasuredDimensionRaw()`方法如下: +```java +/** + * Sets the measured dimension without extra processing for things like optical bounds. + * Useful for reapplying consistent values that have already been cooked with adjustments + * for optical bounds, etc. such as those from the measurement cache. + * + * @param measuredWidth The measured width of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * @param measuredHeight The measured height of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + */ +private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { + // 赋值给mMeasuredWidth,getMeasuredWidth就会调用该值。 + mMeasuredWidth = measuredWidth; + mMeasuredHeight = measuredHeight; + + // 这就是重写onMeasure方法时如果不调用setMeasuredDimension方法时为什么会报错的原因。 + mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; +} +``` + +我们接着看一下上面用到的`getDefaultSize()`方法,源码如下: +```java +/** + * Utility to return a default size. Uses the supplied size if the + * MeasureSpec imposed no constraints. Will get larger if allowed + * by the MeasureSpec. + * + * @param size Default size for this view + * @param measureSpec Constraints imposed by the parent + * @return The size this view should be. + */ +public static int getDefaultSize(int size, int measureSpec) { + int result = size; + // measureSpec值用于获取宽度(高度)的规格和大小,解析出对应的size和mode + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; +} +``` +`getDefaultSize`方法又会使用到`MeasureSpec`类,文档中对`MeasureSpec`是这样介绍的`A MeasureSpec is comprised of a size and a mode. There are three possible modes:` +- MeasureSpec.EXACTLY The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be. + 理解成MATCH_PARENT或者在布局中指定了宽高值,如layout:width='50dp' +- MeasureSpec.AT_MOST The child can be as large as it wants up to the specified size.理解成WRAP_CONTENT,这是的值是父View可以允许的最大的值,只要不超过这个值都可以。 +- MeasureSpec.UNSPECIFIED The parent has not imposed any constraint on the child. It can be whatever size it wants. 这种情况比较少,一般用不到。 + +这里简单总结一下上面的过程: +```java +performMeasure() { + - 1.调用View.measure方法 + mView.measure(): + - 2.measure内部会调用onMeasure方法,但是因为这里mView是DecorView,所以会调用FrameLayout的onMeasure方法。 + onMeasure(FrameLayout) + - 3. 内部设置ViewGroup的宽高 + setMeasuredDimension + 并且对每个子View进行遍历测量 + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + - 4. 对每个子View调用measureChildWithMargins方法 + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + -5. measureChildWithMargins内部调用子View的measure方法 + meausre + - 6. measure方法内部又调用onMeasure方法 + onMeasure(View) + - 7. onMeasure方法内部调用setMeasuredDimension + setMeasuredDimension + - 8. setMeasuredDimension内部调用setMeasuredDimensionRaw + setMeasuredDimensionRaw + } +} +``` + +从上面代码中能看到`measure`是`final`的,我们可以重写`onMeasure`来实现`measure`过程。 +到这里基本都讲完了,我们在开发中会按照需要重写`onMeasure`方法,然后调用`setMeasuredDimension`方法设置大小, +ps:譬如我们设置了`setMeasuredDimension(10, 10)`,那么不管布局中怎么设置这个`View`的大小 +都是没用的,最后显示出来大小都是10*10。 + +`Layout` +=== +`performLayout`方法源码如下: +```java +private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, + int desiredWindowHeight) { + mLayoutRequested = false; + mScrollMayChange = true; + mInLayout = true; + + final View host = mView; + if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { + Log.v(TAG, "Laying out " + host + " to (" + + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); + } + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); + try { + // 把刚才测量的宽高设置进来 + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + + mInLayout = false; + int numViewsRequestingLayout = mLayoutRequesters.size(); + if (numViewsRequestingLayout > 0) { + // requestLayout() was called during layout. + // If no layout-request flags are set on the requesting views, there is no problem. + // If some requests are still pending, then we need to clear those flags and do + // a full request/measure/layout pass to handle this situation. + ArrayList validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, + false); + if (validLayoutRequesters != null) { + // Set this flag to indicate that any further requests are happening during + // the second pass, which may result in posting those requests to the next + // frame instead + mHandlingLayoutInLayoutRequest = true; + + // Process fresh layout requests, then measure and layout + int numValidRequests = validLayoutRequesters.size(); + for (int i = 0; i < numValidRequests; ++i) { + final View view = validLayoutRequesters.get(i); + Log.w("View", "requestLayout() improperly called by " + view + + " during layout: running second layout pass"); + view.requestLayout(); + } + measureHierarchy(host, lp, mView.getContext().getResources(), + desiredWindowWidth, desiredWindowHeight); + mInLayout = true; + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + + mHandlingLayoutInLayoutRequest = false; + + // Check the valid requests again, this time without checking/clearing the + // layout flags, since requests happening during the second pass get noop'd + validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true); + if (validLayoutRequesters != null) { + final ArrayList finalRequesters = validLayoutRequesters; + // Post second-pass requests to the next frame + getRunQueue().post(new Runnable() { + @Override + public void run() { + int numValidRequests = finalRequesters.size(); + for (int i = 0; i < numValidRequests; ++i) { + final View view = finalRequesters.get(i); + Log.w("View", "requestLayout() improperly called by " + view + + " during second layout pass: posting in next frame"); + view.requestLayout(); + } + } + }); + } + } + + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + mInLayout = false; +} +``` + +内部会调用`layout()`方法,因为`host`是`mView`,`ViewGroup`中重写了`layout`方法,并调用了`super.layout`. +所以我们直接看`View.layout()`方法,该方法源码如下: +```java +/** + * Assign a size and position to a view and all of its + * descendants + * + *

This is the second phase of the layout mechanism. + * (The first is measuring). In this phase, each parent calls + * layout on all of its children to position them. + * This is typically done using the child measurements + * that were stored in the measure pass().

+ * + *

Derived classes should not override this method. + * Derived classes with children should override + * onLayout. In that method, they should + * call layout on each of their children.

+ * + * @param l Left position, relative to parent + * @param t Top position, relative to parent + * @param r Right position, relative to parent + * @param b Bottom position, relative to parent + */ +@SuppressWarnings({"unchecked"}) +public void layout(int l, int t, int r, int b) { + if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { + onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); + mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } + + int oldL = mLeft; + int oldT = mTop; + int oldB = mBottom; + int oldR = mRight; + + // 这部分是判断这个View的大小是否已经发生了变化,来判断是否需要重绘。 + boolean changed = isLayoutModeOptical(mParent) ? + setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); + + if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { + // 内部调用onLayout方法 + onLayout(changed, l, t, r, b); + mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; + + ListenerInfo li = mListenerInfo; + if (li != null && li.mOnLayoutChangeListeners != null) { + ArrayList listenersCopy = + (ArrayList)li.mOnLayoutChangeListeners.clone(); + int numListeners = listenersCopy.size(); + for (int i = 0; i < numListeners; ++i) { + listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); + } + } + } + + mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; + mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; +} +``` +这里会调用`onLayout`方法,同样因为`mView`是`FrameLayout`的子类,所以我们要看`FrameLayout`的`onLayout`方法, +这里我们先看一下`ViewGroup.onLayout`方法: +```java +/** + * {@inheritDoc} + */ +@Override +protected abstract void onLayout(boolean changed, + int l, int t, int r, int b); +``` +是个抽象方法,所以`ViewGroup`的子类都需要实现该方法。 +我们看一下`FrameLayout.onLayout`方法,源码如下: +```java + /** + * {@inheritDoc} + */ +@Override +protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + layoutChildren(left, top, right, bottom, false /* no force left gravity */); +} + +void layoutChildren(int left, int top, int right, int bottom, + boolean forceLeftGravity) { + final int count = getChildCount(); + + final int parentLeft = getPaddingLeftWithForeground(); + final int parentRight = right - left - getPaddingRightWithForeground(); + + final int parentTop = getPaddingTopWithForeground(); + final int parentBottom = bottom - top - getPaddingBottomWithForeground(); + + mForegroundBoundsChanged = true; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + + int childLeft; + int childTop; + + int gravity = lp.gravity; + if (gravity == -1) { + gravity = DEFAULT_CHILD_GRAVITY; + } + + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + + lp.leftMargin - lp.rightMargin; + break; + case Gravity.RIGHT: + if (!forceLeftGravity) { + childLeft = parentRight - width - lp.rightMargin; + break; + } + case Gravity.LEFT: + default: + childLeft = parentLeft + lp.leftMargin; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = parentTop + lp.topMargin; + break; + case Gravity.CENTER_VERTICAL: + childTop = parentTop + (parentBottom - parentTop - height) / 2 + + lp.topMargin - lp.bottomMargin; + break; + case Gravity.BOTTOM: + childTop = parentBottom - height - lp.bottomMargin; + break; + default: + childTop = parentTop + lp.topMargin; + } + //调用子View的layout方法 + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } +} +``` +而`View.layout`方法,又会调用到`View.onLayout`方法,我们假设这个子`View`不是`ViewGroup`. +看一下`View.onLayout`方法源码如下: +```java +/** + * Called from layout when this view should + * assign a size and position to each of its children. + * + * Derived classes with children should override + * this method and call layout on each of + * their children. + * @param changed This is a new size or position for this view + * @param left Left position, relative to parent + * @param top Top position, relative to parent + * @param right Right position, relative to parent + * @param bottom Bottom position, relative to parent + */ +protected void onLayout(boolean changed, int left, int top, int right, int bottom) { +} +``` +是一个空方法,这是因为`Layout`需要`ViewGroup`来控制进行。 + +这里也总结一下`layout`的过程。 +```java +private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, + int desiredWindowHeight) { + - 1. host.layout + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + -2. layout方法会分别调用setFrame()和onLayout()方法 + setFrame() + onLayout() + -3. 因为host是mView也就是DecorView也就是FrameLayout的子类。FrameLayout的onLayout方法如下 + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + -4. 遍历每个子View,并分别调用layout方法。 + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } +} +``` + +`Draw` +=== + +绘制阶段是从`ViewRootImpl`中的`performDraw`方法开始的: +```java +private void performDraw() { + if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { + return; + } + + final boolean fullRedrawNeeded = mFullRedrawNeeded; + mFullRedrawNeeded = false; + + mIsDrawing = true; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); + try { + // 开始draw了 + draw(fullRedrawNeeded); + } finally { + mIsDrawing = false; + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + // For whatever reason we didn't create a HardwareRenderer, end any + // hardware animations that are now dangling + if (mAttachInfo.mPendingAnimatingRenderNodes != null) { + final int count = mAttachInfo.mPendingAnimatingRenderNodes.size(); + for (int i = 0; i < count; i++) { + mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators(); + } + mAttachInfo.mPendingAnimatingRenderNodes.clear(); + } + + if (mReportNextDraw) { + mReportNextDraw = false; + if (mAttachInfo.mHardwareRenderer != null) { + mAttachInfo.mHardwareRenderer.fence(); + } + + if (LOCAL_LOGV) { + Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); + } + if (mSurfaceHolder != null && mSurface.isValid()) { + mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); + SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + if (c instanceof SurfaceHolder.Callback2) { + ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( + mSurfaceHolder); + } + } + } + } + try { + mWindowSession.finishDrawing(mWindow); + } catch (RemoteException e) { + } + } +} +``` +内部会调用`draw`方法,`draw`方法源码如下: +```java +private void draw(boolean fullRedrawNeeded) { + Surface surface = mSurface; + if (!surface.isValid()) { + return; + } + + if (DEBUG_FPS) { + trackFPS(); + } + + if (!sFirstDrawComplete) { + synchronized (sFirstDrawHandlers) { + sFirstDrawComplete = true; + final int count = sFirstDrawHandlers.size(); + for (int i = 0; i< count; i++) { + mHandler.post(sFirstDrawHandlers.get(i)); + } + } + } + + scrollToRectOrFocus(null, false); + + if (mAttachInfo.mViewScrollChanged) { + mAttachInfo.mViewScrollChanged = false; + mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); + } + + boolean animating = mScroller != null && mScroller.computeScrollOffset(); + final int curScrollY; + if (animating) { + curScrollY = mScroller.getCurrY(); + } else { + curScrollY = mScrollY; + } + if (mCurScrollY != curScrollY) { + mCurScrollY = curScrollY; + fullRedrawNeeded = true; + } + + final float appScale = mAttachInfo.mApplicationScale; + final boolean scalingRequired = mAttachInfo.mScalingRequired; + + int resizeAlpha = 0; + if (mResizeBuffer != null) { + long deltaTime = SystemClock.uptimeMillis() - mResizeBufferStartTime; + if (deltaTime < mResizeBufferDuration) { + float amt = deltaTime/(float) mResizeBufferDuration; + amt = mResizeInterpolator.getInterpolation(amt); + animating = true; + resizeAlpha = 255 - (int)(amt*255); + } else { + disposeResizeBuffer(); + } + } + + final Rect dirty = mDirty; + if (mSurfaceHolder != null) { + // The app owns the surface, we won't draw. + dirty.setEmpty(); + if (animating) { + if (mScroller != null) { + mScroller.abortAnimation(); + } + disposeResizeBuffer(); + } + return; + } + + if (fullRedrawNeeded) { + mAttachInfo.mIgnoreDirtyState = true; + dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); + } + + if (DEBUG_ORIENTATION || DEBUG_DRAW) { + Log.v(TAG, "Draw " + mView + "/" + + mWindowAttributes.getTitle() + + ": dirty={" + dirty.left + "," + dirty.top + + "," + dirty.right + "," + dirty.bottom + "} surface=" + + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" + + appScale + ", width=" + mWidth + ", height=" + mHeight); + } + + mAttachInfo.mTreeObserver.dispatchOnDraw(); + + int xOffset = 0; + int yOffset = curScrollY; + final WindowManager.LayoutParams params = mWindowAttributes; + final Rect surfaceInsets = params != null ? params.surfaceInsets : null; + if (surfaceInsets != null) { + xOffset -= surfaceInsets.left; + yOffset -= surfaceInsets.top; + + // Offset dirty rect for surface insets. + dirty.offset(surfaceInsets.left, surfaceInsets.right); + } + + if (!dirty.isEmpty() || mIsAnimating) { + if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { + // Draw with hardware renderer. + mIsAnimating = false; + boolean invalidateRoot = false; + if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) { + mHardwareYOffset = yOffset; + mHardwareXOffset = xOffset; + mAttachInfo.mHardwareRenderer.invalidateRoot(); + } + mResizeAlpha = resizeAlpha; + + dirty.setEmpty(); + + mBlockResizeBuffer = false; + mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this); + } else { + // If we get here with a disabled & requested hardware renderer, something went + // wrong (an invalidate posted right before we destroyed the hardware surface + // for instance) so we should just bail out. Locking the surface with software + // rendering at this point would lock it forever and prevent hardware renderer + // from doing its job when it comes back. + // Before we request a new frame we must however attempt to reinitiliaze the + // hardware renderer if it's in requested state. This would happen after an + // eglTerminate() for instance. + if (mAttachInfo.mHardwareRenderer != null && + !mAttachInfo.mHardwareRenderer.isEnabled() && + mAttachInfo.mHardwareRenderer.isRequested()) { + + try { + mAttachInfo.mHardwareRenderer.initializeIfNeeded( + mWidth, mHeight, mSurface, surfaceInsets); + } catch (OutOfResourcesException e) { + handleOutOfResourcesException(e); + return; + } + + mFullRedrawNeeded = true; + scheduleTraversals(); + return; + } + + // draw的部分在这里。。。内部会用canvas去画 + if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { + return; + } + } + } + + if (animating) { + mFullRedrawNeeded = true; + scheduleTraversals(); + } +} +``` +我们看一下`drawSoftware`方法: +```java +/** + * @return true if drawing was successful, false if an error occurred + */ +private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, + boolean scalingRequired, Rect dirty) { + + // Draw with software renderer. + final Canvas canvas; + try { + final int left = dirty.left; + final int top = dirty.top; + final int right = dirty.right; + final int bottom = dirty.bottom; + + canvas = mSurface.lockCanvas(dirty); + + // The dirty rectangle can be modified by Surface.lockCanvas() + //noinspection ConstantConditions + if (left != dirty.left || top != dirty.top || right != dirty.right + || bottom != dirty.bottom) { + attachInfo.mIgnoreDirtyState = true; + } + + // TODO: Do this in native + canvas.setDensity(mDensity); + } catch (Surface.OutOfResourcesException e) { + handleOutOfResourcesException(e); + return false; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not lock surface", e); + // Don't assume this is due to out of memory, it could be + // something else, and if it is something else then we could + // kill stuff (or ourself) for no reason. + mLayoutRequested = true; // ask wm for a new surface next time. + return false; + } + + try { + if (DEBUG_ORIENTATION || DEBUG_DRAW) { + Log.v(TAG, "Surface " + surface + " drawing to bitmap w=" + + canvas.getWidth() + ", h=" + canvas.getHeight()); + //canvas.drawARGB(255, 255, 0, 0); + } + + // If this bitmap's format includes an alpha channel, we + // need to clear it before drawing so that the child will + // properly re-composite its drawing on a transparent + // background. This automatically respects the clip/dirty region + // or + // If we are applying an offset, we need to clear the area + // where the offset doesn't appear to avoid having garbage + // left in the blank areas. + if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + + dirty.setEmpty(); + mIsAnimating = false; + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + mView.mPrivateFlags |= View.PFLAG_DRAWN; + + if (DEBUG_DRAW) { + Context cxt = mView.getContext(); + Log.i(TAG, "Drawing: package:" + cxt.getPackageName() + + ", metrics=" + cxt.getResources().getDisplayMetrics() + + ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo()); + } + try { + canvas.translate(-xoff, -yoff); + if (mTranslator != null) { + mTranslator.translateCanvas(canvas); + } + canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); + attachInfo.mSetIgnoreDirtyState = false; + + // 内部会去调用View.draw(); + mView.draw(canvas); + } finally { + if (!attachInfo.mSetIgnoreDirtyState) { + // Only clear the flag if it was not set during the mView.draw() call + attachInfo.mIgnoreDirtyState = false; + } + } + } finally { + try { + surface.unlockCanvasAndPost(canvas); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not unlock surface", e); + mLayoutRequested = true; // ask wm for a new surface next time. + //noinspection ReturnInsideFinallyBlock + return false; + } + + if (LOCAL_LOGV) { + Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + } + } + return true; +} +``` +代码中调用了`mView.draw()`方法,所以我们看一下`FrameLayout.draw()`方法: +```java +/** + * {@inheritDoc} + */ +@Override +public void draw(Canvas canvas) { + super.draw(canvas); + + if (mForeground != null) { + final Drawable foreground = mForeground; + + if (mForegroundBoundsChanged) { + mForegroundBoundsChanged = false; + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; + + final int w = mRight-mLeft; + final int h = mBottom-mTop; + + if (mForegroundInPadding) { + selfBounds.set(0, 0, w, h); + } else { + selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); + } + + final int layoutDirection = getLayoutDirection(); + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds, + layoutDirection); + foreground.setBounds(overlayBounds); + } + + foreground.draw(canvas); + } +} +``` +内部调用了`super.draw()`,而`ViewGroup`没有重写该方法,所以直接看`View`的`draw()`方法. +`View.draw()`方法如下: +```java +/** + * Manually render this view (and all of its children) to the given Canvas. + * The view must have already done a full layout before this function is + * called. When implementing a view, implement + * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. + * If you do need to override this method, call the superclass version. + * + * @param canvas The Canvas to which the View is rendered. + */ +public void draw(Canvas canvas) { + final int privateFlags = mPrivateFlags; + final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && + (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); + mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; + + // 这里注释说的很明白了,draw的6个步骤。 + /* + * Draw traversal performs several drawing steps which must be executed + * in the appropriate order: + * + * 1. Draw the background + * 2. If necessary, save the canvas' layers to prepare for fading + * 3. Draw view's content, 调用onDraw方法绘制自身 + * 4. Draw children, 调用dispatchDraw方法绘制子View + * 5. If necessary, draw the fading edges and restore layers + * 6. Draw decorations (scrollbars for instance) + */ + + // Step 1, draw the background, if needed + int saveCount; + + if (!dirtyOpaque) { + drawBackground(canvas); + } + + // skip step 2 & 5 if possible (common case) + final int viewFlags = mViewFlags; + boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; + boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; + if (!verticalEdges && !horizontalEdges) { + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } + + // we're done... + return; + } + + /* + * Here we do the full fledged routine... + * (this is an uncommon case where speed matters less, + * this is why we repeat some of the tests that have been + * done above) + */ + + boolean drawTop = false; + boolean drawBottom = false; + boolean drawLeft = false; + boolean drawRight = false; + + float topFadeStrength = 0.0f; + float bottomFadeStrength = 0.0f; + float leftFadeStrength = 0.0f; + float rightFadeStrength = 0.0f; + + // Step 2, save the canvas' layers + int paddingLeft = mPaddingLeft; + + final boolean offsetRequired = isPaddingOffsetRequired(); + if (offsetRequired) { + paddingLeft += getLeftPaddingOffset(); + } + + int left = mScrollX + paddingLeft; + int right = left + mRight - mLeft - mPaddingRight - paddingLeft; + int top = mScrollY + getFadeTop(offsetRequired); + int bottom = top + getFadeHeight(offsetRequired); + + if (offsetRequired) { + right += getRightPaddingOffset(); + bottom += getBottomPaddingOffset(); + } + + final ScrollabilityCache scrollabilityCache = mScrollCache; + final float fadeHeight = scrollabilityCache.fadingEdgeLength; + int length = (int) fadeHeight; + + // clip the fade length if top and bottom fades overlap + // overlapping fades produce odd-looking artifacts + if (verticalEdges && (top + length > bottom - length)) { + length = (bottom - top) / 2; + } + + // also clip horizontal fades if necessary + if (horizontalEdges && (left + length > right - length)) { + length = (right - left) / 2; + } + + if (verticalEdges) { + topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); + drawTop = topFadeStrength * fadeHeight > 1.0f; + bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); + drawBottom = bottomFadeStrength * fadeHeight > 1.0f; + } + + if (horizontalEdges) { + leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); + drawLeft = leftFadeStrength * fadeHeight > 1.0f; + rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); + drawRight = rightFadeStrength * fadeHeight > 1.0f; + } + + saveCount = canvas.getSaveCount(); + + int solidColor = getSolidColor(); + if (solidColor == 0) { + final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; + + if (drawTop) { + canvas.saveLayer(left, top, right, top + length, null, flags); + } + + if (drawBottom) { + canvas.saveLayer(left, bottom - length, right, bottom, null, flags); + } + + if (drawLeft) { + canvas.saveLayer(left, top, left + length, bottom, null, flags); + } + + if (drawRight) { + canvas.saveLayer(right - length, top, right, bottom, null, flags); + } + } else { + scrollabilityCache.setFadeColor(solidColor); + } + + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Step 5, draw the fade effect and restore layers + final Paint p = scrollabilityCache.paint; + final Matrix matrix = scrollabilityCache.matrix; + final Shader fade = scrollabilityCache.shader; + + if (drawTop) { + matrix.setScale(1, fadeHeight * topFadeStrength); + matrix.postTranslate(left, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, top, right, top + length, p); + } + + if (drawBottom) { + matrix.setScale(1, fadeHeight * bottomFadeStrength); + matrix.postRotate(180); + matrix.postTranslate(left, bottom); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, bottom - length, right, bottom, p); + } + + if (drawLeft) { + matrix.setScale(1, fadeHeight * leftFadeStrength); + matrix.postRotate(-90); + matrix.postTranslate(left, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, top, left + length, bottom, p); + } + + if (drawRight) { + matrix.setScale(1, fadeHeight * rightFadeStrength); + matrix.postRotate(90); + matrix.postTranslate(right, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(right - length, top, right, bottom, p); + } + + canvas.restoreToCount(saveCount); + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } +} +``` + +上面会调用`onDraw`和`dispatchDraw`方法。 +我们先看一下`View.onDraw`方法: +```java +/** + * Implement this to do your drawing. + * + * @param canvas the canvas on which the background will be drawn + */ +protected void onDraw(Canvas canvas) { +} +``` +是空方法,这是也很好理解,因为每个`View`的展现都不一样,例如`TextView`、`ProgressBar`等, +所以`View`不会去实现`onDraw`方法,具体是要子类去根据自己的显示要求实现该方法。 + +再看一下`dispatchDraw`方法,这个方法是用来绘制子`View`的,所以要看`ViewGroup.dispatchDraw`方法,`View.dispatchDraw`是空的。 +```java +/** + * {@inheritDoc} + */ +@Override +protected void dispatchDraw(Canvas canvas) { + boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); + final int childrenCount = mChildrenCount; + final View[] children = mChildren; + int flags = mGroupFlags; + + if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { + final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; + + final boolean buildCache = !isHardwareAccelerated(); + for (int i = 0; i < childrenCount; i++) { + final View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { + final LayoutParams params = child.getLayoutParams(); + attachLayoutAnimationParameters(child, params, i, childrenCount); + bindLayoutAnimation(child); + if (cache) { + child.setDrawingCacheEnabled(true); + if (buildCache) { + child.buildDrawingCache(true); + } + } + } + } + + final LayoutAnimationController controller = mLayoutAnimationController; + if (controller.willOverlap()) { + mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; + } + + controller.start(); + + mGroupFlags &= ~FLAG_RUN_ANIMATION; + mGroupFlags &= ~FLAG_ANIMATION_DONE; + + if (cache) { + mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE; + } + + if (mAnimationListener != null) { + mAnimationListener.onAnimationStart(controller.getAnimation()); + } + } + + int clipSaveCount = 0; + final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + if (clipToPadding) { + clipSaveCount = canvas.save(); + canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, + mScrollX + mRight - mLeft - mPaddingRight, + mScrollY + mBottom - mTop - mPaddingBottom); + } + + // We will draw our child's animation, let's reset the flag + mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; + mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; + + boolean more = false; + final long drawingTime = getDrawingTime(); + + if (usingRenderNodeProperties) canvas.insertReorderBarrier(); + // Only use the preordered list if not HW accelerated, since the HW pipeline will do the + // draw reordering internally + final ArrayList preorderedList = usingRenderNodeProperties + ? null : buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + for (int i = 0; i < childrenCount; i++) { + int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { + // 调用drawChild方法 + more |= drawChild(canvas, child, drawingTime); + } + } + if (preorderedList != null) preorderedList.clear(); + + // Draw any disappearing views that have animations + if (mDisappearingChildren != null) { + final ArrayList disappearingChildren = mDisappearingChildren; + final int disappearingCount = disappearingChildren.size() - 1; + // Go backwards -- we may delete as animations finish + for (int i = disappearingCount; i >= 0; i--) { + final View child = disappearingChildren.get(i); + more |= drawChild(canvas, child, drawingTime); + } + } + if (usingRenderNodeProperties) canvas.insertInorderBarrier(); + + if (debugDraw()) { + onDebugDraw(canvas); + } + + if (clipToPadding) { + canvas.restoreToCount(clipSaveCount); + } + + // mGroupFlags might have been updated by drawChild() + flags = mGroupFlags; + + if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { + invalidate(true); + } + + if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && + mLayoutAnimationController.isDone() && !more) { + // We want to erase the drawing cache and notify the listener after the + // next frame is drawn because one extra invalidate() is caused by + // drawChild() after the animation is over + mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; + final Runnable end = new Runnable() { + public void run() { + notifyAnimationListener(); + } + }; + post(end); + } +} +``` +可以看到上面的方法中会调用`drawChild`方法,该方法如下: +```java +/** + * Draw one child of this View Group. This method is responsible for getting + * the canvas in the right state. This includes clipping, translating so + * that the child's scrolled origin is at 0, 0, and applying any animation + * transformations. + * + * @param canvas The canvas on which to draw the child + * @param child Who to draw + * @param drawingTime The time at which draw is occurring + * @return True if an invalidate() was issued + */ +protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return child.draw(canvas, this, drawingTime); +} +``` + +这里也简单总结一下`draw`的过程: +```java +// 1. ViewRootImpl.performDraw() +private void performDraw() { + // 2. ViewRootImpl.draw() + draw(fullRedrawNeeded); + // 3. ViewRootImpl.drawSoftware + drawSoftware + // 4. 内部调用mView.draw,也就是FrameLayout.draw(). + mView.draw()(FrameLayout) + // 5. FrameLayout.draw方法内部会调用super.draw方法,也就是View.draw方法. + super.draw(canvas); + // 6. View.draw方法内部会分别调用onDraw绘制自己以及dispatchDraw绘制子View. + onDraw + // 绘制子View + dispatchDraw + // 7. dispatchDraw方法内部会遍历所有子View. + for (int i = 0; i < childrenCount; i++) { + // 8. 对每个子View分别调用drawChild方法 + drawChild() + // 9. drawChild方法内部会对该子View调用draw方法,进行绘制。然后draw又会调用onDraw等,循环就开始了。 + child.draw() + } +} +``` + +最后补充一个小问题: `getWidth()`与`getMeasuredWidth()`有什么区别呢? +一般情况下这两个的值是相同的,`getMeasureWidth()`方法在`measure()`过程结束后就可以获取到了,而`getWidth()`方法要在`layout()`过程结束后才能获取到。 +而且`getMeasureWidth()`的值是通过`setMeasuredDimension()`设置的,但是`getWidth()`的值是通过视图右边的坐标减去左边的坐标计算出来的。如果我们在`layout`的时候将宽高 +不传`getMeasureWidth`的值,那么这时候`getWidth()`与`getMeasuredWidth`的值就不会再相同了,当然一般也不会这么干... + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/AndroidAdavancedPart/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" rename to "AndroidAdavancedPart/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" diff --git "a/Android\345\212\240\345\274\272/Zipalign\344\274\230\345\214\226.md" "b/AndroidAdavancedPart/Zipalign\344\274\230\345\214\226.md" similarity index 100% rename from "Android\345\212\240\345\274\272/Zipalign\344\274\230\345\214\226.md" rename to "AndroidAdavancedPart/Zipalign\344\274\230\345\214\226.md" diff --git "a/Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" "b/AndroidAdavancedPart/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" similarity index 100% rename from "Android\345\212\240\345\274\272/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" rename to "AndroidAdavancedPart/volley-retrofit-okhttp\344\271\213\346\210\221\344\273\254\350\257\245\345\246\202\344\275\225\351\200\211\346\213\251\347\275\221\350\267\257\346\241\206\346\236\266.md" diff --git "a/Android\345\212\240\345\274\272/\345\210\233\345\273\272\345\277\253\346\215\267\346\226\271\345\274\217.md" "b/AndroidAdavancedPart/\345\210\233\345\273\272\345\277\253\346\215\267\346\226\271\345\274\217.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\345\210\233\345\273\272\345\277\253\346\215\267\346\226\271\345\274\217.md" rename to "AndroidAdavancedPart/\345\210\233\345\273\272\345\277\253\346\215\267\346\226\271\345\274\217.md" diff --git "a/Android\345\212\240\345\274\272/\345\217\221\345\270\203library\345\210\260Maven\344\273\223\345\272\223.md" "b/AndroidAdavancedPart/\345\217\221\345\270\203library\345\210\260Maven\344\273\223\345\272\223.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\345\217\221\345\270\203library\345\210\260Maven\344\273\223\345\272\223.md" rename to "AndroidAdavancedPart/\345\217\221\345\270\203library\345\210\260Maven\344\273\223\345\272\223.md" diff --git "a/Android\345\212\240\345\274\272/\345\246\202\344\275\225\350\256\251Service\345\270\270\351\251\273\345\206\205\345\255\230.md" "b/AndroidAdavancedPart/\345\246\202\344\275\225\350\256\251Service\345\270\270\351\251\273\345\206\205\345\255\230.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\345\246\202\344\275\225\350\256\251Service\345\270\270\351\251\273\345\206\205\345\255\230.md" rename to "AndroidAdavancedPart/\345\246\202\344\275\225\350\256\251Service\345\270\270\351\251\273\345\206\205\345\255\230.md" diff --git "a/Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/\345\270\203\345\261\200\344\274\230\345\214\226.md" "b/AndroidAdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" similarity index 97% rename from "Android\345\212\240\345\274\272/\345\270\203\345\261\200\344\274\230\345\214\226.md" rename to "AndroidAdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" index c12a4cca..b5c34641 100644 --- "a/Android\345\212\240\345\274\272/\345\270\203\345\261\200\344\274\230\345\214\226.md" +++ "b/AndroidAdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" @@ -1,153 +1,153 @@ -布局优化 -=== - -- 去除不必要的嵌套和节点 - 这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。 - - 首次不需要的节点设置为`GONE`或使用`ViewStud`. - - 使用`Relativelayout`代替`LinearLayout`. - 平时写布局的时候要多注意,写完后可以通过`Hierarchy Viewer`或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。 - -- 使用`include` - `include`可以用于将布局中一些公共的部分提取出来。在需要的时候使用即可,比喻一些页面统一的`loading`页。 - `include`标签的`layout`属性指定所要包含的布局文件,我们也可以通过`android:id`或者一些其他的属性来覆盖被引入布局的根节点所对应 - 的属性值。 - ```xml - - ``` - `loading.xml`内容为: - ```xml - - - - - - - ``` - -- 使用``标签 - `merge`可以有效的解决布局的层级关系。我们通过一个例子来说明一下: - ```xml - - - - - - - - ``` - - 我们在一个页面中显示该部分内容,运行后观察`Hierarchy Viewer`。 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/merge_1.png) - 我们会发现除了我们布局最外层还会有一层`FrameLayout`,这是因为`Activity`内容视图的`parent view`就是一个`FrameLayout`,所以对于我们来说无意中已经多了一层毫无意义的布局。 - 接下来`merge`的功能就能发挥了,修改代码如下。 - ```xml - - - - - - - - ``` - 接下来我们在用`Hierarchy Viewer`观察就会发现完美的去掉了一层`FrameLayout` - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/merge_2.png) - 当然上面我们使用`merge`是因为跟布局正好是`FrameLayout`并且没有`backgroud`和`padding`等这些属性。如果根本局是`LinearLayout`等,就没法直接使用`merge`了。 - 在`include`的时候很容易造成布局层级嵌套过多,结合`merge`使用能有效解决这个问题。 - -- 使用`ViewStub` - `ViewStub`标签与`include`一样可以用来引入一个外部布局,但是`Viewstub`引入的布局默认不会解析与显示,宽高为0,`View`也为`null`,这样就会在解析`layout`时节省`cpu`和内存。简单的理解就是`ViewStub`是 -`include`加上`GONE`.`ViewStub`常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等. - ```xml - - - - …… - - - - ``` - 在代码中通过`(ViewStub)findViewById(id)`找到`ViewStub`,使用`inflate()`展开`ViewStub`,然后得到子`View`,如下: - ```java - private View mNetErrorView; - - private void showNetError() { - if (mNetErrorView != null) { - mNetErrorView.setVisibility(View.VISIBLE); - return; - } - - ViewStub stub = (ViewStub)findViewById(R.id.network_unreachble); - // 解析并且显示该部分,返回值就是解析后的该`View` - mNetErrorView = stub.inflate(); - Button networkSetting = (Button)mNetErrorView.findViewById(R.id.bt_network); - } - - private void showNormal() { - if (mNetErrorView != null) { - mNetErrorView.setVisibility(View.GONE); - } - } - ``` - 或者也可以通过第二种方式: - ```java - View viewStub = findViewById(R.id.network_unreachble); - // ViewStub被展开后的布局所替换 - viewStub.setVisibility(View.VISIBLE); - // 获取展开后的布局 - mNetErrorView = findViewById(R.id.network_unreachble); - ``` - -- 减少不必要的`Inflate` - 如上一步中`stub.infalte()`后将该`View`进行记录或者是`ListView`中`item inflate`的时候。 - ---- - -- 邮箱 :charon.chui@gmail.com +布局优化 +=== + +- 去除不必要的嵌套和节点 + 这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。 + - 首次不需要的节点设置为`GONE`或使用`ViewStud`. + - 使用`Relativelayout`代替`LinearLayout`. + 平时写布局的时候要多注意,写完后可以通过`Hierarchy Viewer`或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。 + +- 使用`include` + `include`可以用于将布局中一些公共的部分提取出来。在需要的时候使用即可,比喻一些页面统一的`loading`页。 + `include`标签的`layout`属性指定所要包含的布局文件,我们也可以通过`android:id`或者一些其他的属性来覆盖被引入布局的根节点所对应 + 的属性值。 + ```xml + + ``` + `loading.xml`内容为: + ```xml + + + + + + + ``` + +- 使用``标签 + `merge`可以有效的解决布局的层级关系。我们通过一个例子来说明一下: + ```xml + + + + + + + + ``` + + 我们在一个页面中显示该部分内容,运行后观察`Hierarchy Viewer`。 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/merge_1.png) + 我们会发现除了我们布局最外层还会有一层`FrameLayout`,这是因为`Activity`内容视图的`parent view`就是一个`FrameLayout`,所以对于我们来说无意中已经多了一层毫无意义的布局。 + 接下来`merge`的功能就能发挥了,修改代码如下。 + ```xml + + + + + + + + ``` + 接下来我们在用`Hierarchy Viewer`观察就会发现完美的去掉了一层`FrameLayout` + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/merge_2.png) + 当然上面我们使用`merge`是因为跟布局正好是`FrameLayout`并且没有`backgroud`和`padding`等这些属性。如果根本局是`LinearLayout`等,就没法直接使用`merge`了。 + 在`include`的时候很容易造成布局层级嵌套过多,结合`merge`使用能有效解决这个问题。 + +- 使用`ViewStub` + `ViewStub`标签与`include`一样可以用来引入一个外部布局,但是`Viewstub`引入的布局默认不会解析与显示,宽高为0,`View`也为`null`,这样就会在解析`layout`时节省`cpu`和内存。简单的理解就是`ViewStub`是 +`include`加上`GONE`.`ViewStub`常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等. + ```xml + + + + …… + + + + ``` + 在代码中通过`(ViewStub)findViewById(id)`找到`ViewStub`,使用`inflate()`展开`ViewStub`,然后得到子`View`,如下: + ```java + private View mNetErrorView; + + private void showNetError() { + if (mNetErrorView != null) { + mNetErrorView.setVisibility(View.VISIBLE); + return; + } + + ViewStub stub = (ViewStub)findViewById(R.id.network_unreachble); + // 解析并且显示该部分,返回值就是解析后的该`View` + mNetErrorView = stub.inflate(); + Button networkSetting = (Button)mNetErrorView.findViewById(R.id.bt_network); + } + + private void showNormal() { + if (mNetErrorView != null) { + mNetErrorView.setVisibility(View.GONE); + } + } + ``` + 或者也可以通过第二种方式: + ```java + View viewStub = findViewById(R.id.network_unreachble); + // ViewStub被展开后的布局所替换 + viewStub.setVisibility(View.VISIBLE); + // 获取展开后的布局 + mNetErrorView = findViewById(R.id.network_unreachble); + ``` + +- 减少不必要的`Inflate` + 如上一步中`stub.infalte()`后将该`View`进行记录或者是`ListView`中`item inflate`的时候。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226.md" similarity index 98% rename from "Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226.md" rename to "AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226.md" index 0eb91f22..90d58479 100644 --- "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -1,43 +1,43 @@ -性能优化 -=== - -代码优化原则: ---- - -- 时间换时间: - 如禁用电脑的一些开机启动项,通过减少这些没必要的启动项的时间从而节省开机时间 - 如网站界面上数据的分批获取,AJAX技术 -- 时间换空间: - 如拷贝文件时new一个字节数组当缓冲器,即byte[] buffer = new byte[1024]。 - 为什么只new一个1024个字节的数组呢,new一个更大的字节数组不是一下就把文件拷贝完了么?这么做就是为了牺牲时间节省有限的内存空间 -- 空间换时间: - 如用Windows系统自带的搜索文件的功能搜索文件时会很慢,但是我们牺牲电脑硬盘空间安装一个everything软件来搜索文件就特别快 -- 空间换空间: - 如虚拟内存 - -Android开发中的体现 ---- - -- 采用硬件加速,在清单文件中`application`节点添加`android:hardwareAccelerated=”true”`。不过这个需要在`android 3.0`才可以使用。`android4.0`这个选项是默认开启的。 -- `View`中设置缓存属性`setDrawingCache`为`true`. -- 优化你的布局. -- 动态加载`View`. 采用`ViewStub`避免一些不经常的视图长期握住引用. -- 将`Acitivity`中的`Window`的背景图设置为空。`getWindow().setBackgroundDrawable(null);``android`的默认背景是不是为空。 -- 采用``优化布局层数。 采用``来共享布局。 -- 利用`TraceView`查看跟踪函数调用。有的放矢的优化。 -- `cursor`的使用。不过要注意管理好`cursor`,不要每次打开关闭`cursor`.因为打开关闭`Cursor`非常耗时。 `Cursor.require`用于刷`cursor`. -- 采用`SurfaceView`在子线程刷新`UI`, 避免手势的处理和绘制在同一`UI`线程(普通`View`都这样做)。 -- 采用`JNI`,将耗时间的处理放到`c/c++`层来处理。 -- 有些能用文件操作的,尽量采用文件操作,文件操作的速度比数据库的操作要快10倍左右。 -- 避免创建不必要的对象 -- 如果方法用不到成员变量,可以把方法申明为`static`,性能会提高到15%到20% -- 避免使用`getter/setter`存取`field`,可以把`field`申明为`public`,直接访问 -- `static`的变量如果不需要修改,应该使用`static final`修饰符定义为常量 -- 使用增强`for`循环,比普通`for`循环效率高,但是也有缺点就是在遍历 集合过程中,不能对集合本身进行操作 -- 合理利用浮点数,浮点数比整型慢两倍; -- 针对ListView的性能优化 - ---- - -- 邮箱 :charon.chui@gmail.com +性能优化 +=== + +代码优化原则: +--- + +- 时间换时间: + 如禁用电脑的一些开机启动项,通过减少这些没必要的启动项的时间从而节省开机时间 + 如网站界面上数据的分批获取,AJAX技术 +- 时间换空间: + 如拷贝文件时new一个字节数组当缓冲器,即byte[] buffer = new byte[1024]。 + 为什么只new一个1024个字节的数组呢,new一个更大的字节数组不是一下就把文件拷贝完了么?这么做就是为了牺牲时间节省有限的内存空间 +- 空间换时间: + 如用Windows系统自带的搜索文件的功能搜索文件时会很慢,但是我们牺牲电脑硬盘空间安装一个everything软件来搜索文件就特别快 +- 空间换空间: + 如虚拟内存 + +Android开发中的体现 +--- + +- 采用硬件加速,在清单文件中`application`节点添加`android:hardwareAccelerated=”true”`。不过这个需要在`android 3.0`才可以使用。`android4.0`这个选项是默认开启的。 +- `View`中设置缓存属性`setDrawingCache`为`true`. +- 优化你的布局. +- 动态加载`View`. 采用`ViewStub`避免一些不经常的视图长期握住引用. +- 将`Acitivity`中的`Window`的背景图设置为空。`getWindow().setBackgroundDrawable(null);``android`的默认背景是不是为空。 +- 采用``优化布局层数。 采用``来共享布局。 +- 利用`TraceView`查看跟踪函数调用。有的放矢的优化。 +- `cursor`的使用。不过要注意管理好`cursor`,不要每次打开关闭`cursor`.因为打开关闭`Cursor`非常耗时。 `Cursor.require`用于刷`cursor`. +- 采用`SurfaceView`在子线程刷新`UI`, 避免手势的处理和绘制在同一`UI`线程(普通`View`都这样做)。 +- 采用`JNI`,将耗时间的处理放到`c/c++`层来处理。 +- 有些能用文件操作的,尽量采用文件操作,文件操作的速度比数据库的操作要快10倍左右。 +- 避免创建不必要的对象 +- 如果方法用不到成员变量,可以把方法申明为`static`,性能会提高到15%到20% +- 避免使用`getter/setter`存取`field`,可以把`field`申明为`public`,直接访问 +- `static`的变量如果不需要修改,应该使用`static final`修饰符定义为常量 +- 使用增强`for`循环,比普通`for`循环效率高,但是也有缺点就是在遍历 集合过程中,不能对集合本身进行操作 +- 合理利用浮点数,浮点数比整型慢两倍; +- 针对ListView的性能优化 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" rename to "AndroidAdavancedPart/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" diff --git "a/Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" "b/AndroidAdavancedPart/\346\263\250\350\247\243\344\275\277\347\224\250.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\346\263\250\350\247\243\344\275\277\347\224\250.md" rename to "AndroidAdavancedPart/\346\263\250\350\247\243\344\275\277\347\224\250.md" diff --git "a/Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" "b/AndroidAdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" rename to "AndroidAdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" diff --git "a/Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" "b/AndroidAdavancedPart/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" rename to "AndroidAdavancedPart/\347\233\256\345\211\215\346\265\201\350\241\214\347\232\204\345\274\200\345\217\221\347\273\204\345\220\210.md" diff --git "a/Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" "b/AndroidAdavancedPart/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" rename to "AndroidAdavancedPart/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" "b/AndroidAdavancedPart/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" similarity index 98% rename from "Android\345\212\240\345\274\272/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" rename to "AndroidAdavancedPart/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" index ce1aad2b..619f31e1 100644 --- "a/Android\345\212\240\345\274\272/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" +++ "b/AndroidAdavancedPart/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" @@ -1,123 +1,123 @@ -视频播放相关内容总结 -=== - -多媒体常识: ---- - -- 什么是多媒体 - - 多媒体是计算机和视频技术的结合,实际上它是两个媒体;声音和图像,或者用现在的术语:音响和电视 - -- 常用的视频格式 - - Android系统默认:mp4、3gp - 常用格式:ts、3gpp、3g2、3gpp2、avi、mkv、flv、divx、f4v、rm、rmvb、rv、wmv、asf、mov、mpg、v8、ram、mpeg、 - swf、m2v、asx、ra、ram、ndivx、xvid等 - -- 常用音频格式: - - Android系统:mp3、ogg; - 常用格式:wma、mid、m4a、xmf、aac、mpa、midi、ar等 - -- 常用图片格式:PNG、GIF、BMP、jpg - -- 国内各个视频网站采用的解码框架: - - - 优酷、搜狐、奇艺、pps、暴风影音土豆、56网都是用的ffmpeg. - - pptv已经使用p2p技术 - 点对点技术(peer-to-peer, 简称P2P)又称对等互联网络技术,是一种网络新技术,依赖网络中参与者的计算能力和带宽, - 而不是把依赖都聚集在较少的几台服务器上。P2P网络通常用于通过Ad Hoc连接来连接节点。这类网络可以用于多种用途, - 各种档案分享软件已经得到了广泛的使用。P2P技术也被使用在类似VoIP等实时媒体业务的数据通信中。 - -目前常用的开发框架: - -- VLC框架: - VLC是一个开源项目,基于ffmpeg框架的自定义播放器。其中LibVLC是VLC的核心部分,就相当于MediaPlayer类 - VLC一个最主要的部分,它可以播放各种类型的媒体文件和流媒体文件,并且可以创造媒体流并保存成各种格式的媒体文件 - VLC是一种跨平台的媒体播放器和流媒体服务器,最初为videolan的客户端,它是一种非常简便的多媒体播放器, - 它可以用来播放各种各样的音视频的格式文件(MPEG-1、MPEG-2、MPEG- 4、DivX、WMV、mp3、OGG、Vorbis、AC3、AAC等等)流媒体协议 - 最具特色的功能是可以边下载边观看Divx媒体文件,并可以播放不完全的AVI文件。并且支持界面的更改。 - 缺点:有C/C++代码,还有Java代码,代码太庞大 - -- ffmpeg框架: - 优点:轻量级框架,易于维护 - FFmpeg是一个集录制、转换、音/视频编码解码功能为一体的完整的开源解决方案 - FFMPEG几乎为你把所有的繁重工作都做了,比如解码、编码、复用和解复用。 - 这使得多媒体应用程序变得容易编写。它是一个简单的,用C编写的,快速的并且能够解码几乎所有你能用到的格式,当然也包括编码多种格式。 - FFmpeg支持MPEG、DivX、MPEG4、AC3、DV、FLV等40多种编码,支持AVI、MPEG、OGG、Matroska、ASF等90多种解码 - FFmpeg主目录下主要有libavcodec、libavformat和libavutil等子目录。其中libavcodec用于存放各个encode/decode模块 - libavformat用于存放muxer/demuxer模块,libavutil用于存放内存操作等辅助性模块 - -- vitamio框架: - vitamio也是基于ffmpeg开源框架 - VPlayer是vitamio的一个产品,vitamio和VPlayer是同一个团队开发的,VPlayer能播放的vitamio也能播放 - - -##Surface简介 -- `Surface`就是“表面”的意思。在`SDK`的文档中,对`Surface`的描述是这样的:“`Handle onto a raw buffer that is being managed by the screen compositor`”, - 翻译成中文就是“由屏幕显示内容合成器`(screen compositor)`所管理的原生缓冲器的句柄”, 这句话包括下面两个意思: - - 通过Surface(因为Surface是句柄)就可以获得原生缓冲器以及其中的内容。就像在C语言中,可以通过一个文件的句柄,就可以获得文件的内容一样; - - 原生缓冲器(rawbuffer)是用于保存当前窗口的像素数据的。 -- 简单的说`Surface`对应了一块屏幕缓冲区,每个`Window`对应一个`Surface`,任何`View`都是画在`Surface`上的,传统的`view`共享一块屏幕缓冲区,所有的绘制必须在`UI`线程中进行 -- 我们不能直接操作Surface实例,要通过SurfaceHolder,在SurfaceView中可以通过getHolder()方法获取到SurfaceHolder实例。 -- `Surface`是一个用来画图形的地方,但是我们知道画图都是在一个`Canvas`对象上面进行的,*`Surface`中的`Canvas`成员,是专门用于提供画图的地方,就像黑板一样,其中的原始缓冲区是用来保存数据的地方, - `Surface`本身的作用类似一个句柄,得到了这个句柄就可以得到其中的`Canvas`、原始缓冲区以及其他方面的内容,所以简单的说`Surface`是用来管理数据的(句柄)*。 - -##SurfaceView简介 -- 简单的说`SurfaceView`就是一个有`Surface`的`View`里面内嵌了一个专门用于绘制的`Surface`,`SurfaceView`控制这个`Surface`的格式和尺寸以及绘制位置.`SurfaceView`是一个`View`也许不够严谨, - 然而从定义中 `public class SurfaceView extends View`显示`SurfaceView`确实是派生自`View`,但是`SurfaceView`却有着自己的`Surface`,源码: - ```java - if (mWindow == null) { - mWindow = new MyWindow(this); - mLayout.type = mWindowType; - mLayout.gravity = Gravity.LEFT|Gravity.TOP; - mSession.addWithoutInputChannel(mWindow, mWindow.mSeq, mLayout, - mVisible ? VISIBLE : GONE, mContentInsets); - } - ``` - 很明显,每个`SurfaceView`创建的时候都会创建一个`MyWindow`,`new MyWindow(this)`中的`this`正是`SurfaceView`自身,因此将`SurfaceView`和`window`绑定在一起,而前面提到过每个`window`对应一个`Surface`, - 所以`SurfaceView`也就内嵌了一个自己的`Surface`,可以认为`SurfaceView`是来控制`Surface`的位置和尺寸。传统`View`及其派生类的更新只能在`UI`线程,然而`UI`线程还同时处理其他交互逻辑, - 这就无法保证`view`更新的速度和帧率了,而`SurfaceView`可以用独立的线程来进行绘制,因此可以提供更高的帧率,例如游戏,摄像头取景等场景就比较适合用`SurfaceView`来实现。 - -- `Surface`是纵深排序`(Z-ordered)`的,这表明它总在自己所在窗口的后面。 -- `Surfaceview`提供了一个可见区域,只有在这个可见区域内的`Surface`部分内容才可见,可见区域外的部分不可见,所以可以认为**`SurfaceView`就是展示`Surface`中数据的地方**,`Surface`就是管理数据的地方, - `SurfaceView`就是展示数据的地方,只有通过`SurfaceView`才能展现`Surface`中的数据。 - ![image](https://github.com/CharonChui/Pictures/master/SurfaceView.png?raw=true) -- `Surface`的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者`Surface`的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物`(overlays)`(例如,文本和按钮等控件)。 - 注意,如果`Surface`上面有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。surfaceview变得可见时,surface被创建;surfaceview隐藏前,surface被销毁。 - 这样能节省资源。如果你要查看 surface被创建和销毁的时机,可以重载surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder)。 - **`SurfaceView`的核心在于提供了两个线程:`UI`线程和渲染线程**,两个线程通过“双缓冲”机制来达到高效的界面适时更新。 - -##SurfaceHolder简介 - -显示一个`Surface`的抽象接口,使你可以控制`Surface`的大小和格式以及在`Surface`上编辑像素,和监视`Surace`的改变。这个接口通常通过`SurfaceView`类实现。 -简单的说就是我们无法直接操作`Surface`只能通过`SurfaceHolder`这个接口来获取和操作`Surface`。 -SurfaceHolder`中提供了一些`lockCanvas()`:获取一个Canvas对象,并锁定之。所得到的Canvas对象,其实就是Surface中一个成员。加锁的目的其实就是为了在绘制的过程中, -Surface中的数据不会被改变。lockCanvas是为了防止同一时刻多个线程对同一canvas写入。 - - -*从设计模式的角度来看,`Surface、SurfaceView、SurfaceHolder`实质上就是`MVC(Model-View-Controller)`,`Model`就是模型或者说是数据模型,更简单的可以理解成数据,在这里也就是`Surface`, -`View`就是视图,代表用户交互界面,这里就是`SurfaceView`,`SurfaceHolder`就是`Controller.`* - - -##MediaController - -- `MediaController`继承`FrameLayout`,通过`MediaPlayerControl`接口与`VideoView`进行结合控制,内部是通过`PopupWindow`将整个控制栏界面显示到界面上, - 而该`PopupWindow`所显示在的位置就是通过`setAnchorView()`设置进来的`Anchor`一般可以使当前的`VideoView`或者是整个`Activity`的根布局。这里要分为小屏和全屏两种情况来进行设置。 - 如果当前的`MediaController`只是播放前下面的控制栏部分(进度条、快进、快退、暂停等)这样我们可以通过对VideoView设置点击事件,控制它的显示和隐藏。 - 如果`MediaController`为整个屏幕包括了控制栏部分、上端的信息显示部分、以及左右栏的功能部分、这时候就可以通过对`MediaController`本身设置点击事件来控制显示和隐藏。 - -`Controller`可以用`PopupWindow`来实现,具体有两种方式: - - - 整个控制栏(上面的信息部分、下面的控制部分以及左右边)都在`Controller`中,`setAnchorView()`的时候就会让`Controller`中的`PopupWindow`显示出来(一直显示,但是这个`PopupWindow`是透明的), - 真正的显示与隐藏是控制在`PopupWindow`中的`View`部分的显示与隐藏来实现。开始的时候我是想用这种方式,当时我想的是播放就播放、控制就控制,分离开来多好,但是没想到, - 一旦有`PopupWindow`显示出来后,Activity是接收不到任何`Touch`事件的,所有的重试界面等都要放到`Controller`中实现(手势处理等)。但是也有好处,就是不管显示还是隐藏都可以去处理手势. - - - `PopupWindow`不是全屏的,只包含下面真正的控制部分(快进、快退、暂停等,不包含上面的信息部分和左右边),而且也不是开始就显示,显示隐藏是通过控制`PopupWindow`的显示与隐藏来进行的。 - 而对于信息部分、以及左右边都是在`Activity`的布局当中,我们通过接口回调得到`PopupWindow`的显示与隐藏来控制这些布局的显示与隐藏即可。 - 这样的话我们就需要将手势等全部放到`Activity`中去处理,但是也有一个问题,就是如果`Controller`正在显示的话`Activity`是接收不到`Touch`事件的,就无法处理手势,只能是让`Controller`消失后才能处理手势。 - ---- - -- 邮箱 :charon.chui@gmail.com +视频播放相关内容总结 +=== + +多媒体常识: +--- + +- 什么是多媒体 + + 多媒体是计算机和视频技术的结合,实际上它是两个媒体;声音和图像,或者用现在的术语:音响和电视 + +- 常用的视频格式 + + Android系统默认:mp4、3gp + 常用格式:ts、3gpp、3g2、3gpp2、avi、mkv、flv、divx、f4v、rm、rmvb、rv、wmv、asf、mov、mpg、v8、ram、mpeg、 + swf、m2v、asx、ra、ram、ndivx、xvid等 + +- 常用音频格式: + + Android系统:mp3、ogg; + 常用格式:wma、mid、m4a、xmf、aac、mpa、midi、ar等 + +- 常用图片格式:PNG、GIF、BMP、jpg + +- 国内各个视频网站采用的解码框架: + + - 优酷、搜狐、奇艺、pps、暴风影音土豆、56网都是用的ffmpeg. + - pptv已经使用p2p技术 + 点对点技术(peer-to-peer, 简称P2P)又称对等互联网络技术,是一种网络新技术,依赖网络中参与者的计算能力和带宽, + 而不是把依赖都聚集在较少的几台服务器上。P2P网络通常用于通过Ad Hoc连接来连接节点。这类网络可以用于多种用途, + 各种档案分享软件已经得到了广泛的使用。P2P技术也被使用在类似VoIP等实时媒体业务的数据通信中。 + +目前常用的开发框架: + +- VLC框架: + VLC是一个开源项目,基于ffmpeg框架的自定义播放器。其中LibVLC是VLC的核心部分,就相当于MediaPlayer类 + VLC一个最主要的部分,它可以播放各种类型的媒体文件和流媒体文件,并且可以创造媒体流并保存成各种格式的媒体文件 + VLC是一种跨平台的媒体播放器和流媒体服务器,最初为videolan的客户端,它是一种非常简便的多媒体播放器, + 它可以用来播放各种各样的音视频的格式文件(MPEG-1、MPEG-2、MPEG- 4、DivX、WMV、mp3、OGG、Vorbis、AC3、AAC等等)流媒体协议 + 最具特色的功能是可以边下载边观看Divx媒体文件,并可以播放不完全的AVI文件。并且支持界面的更改。 + 缺点:有C/C++代码,还有Java代码,代码太庞大 + +- ffmpeg框架: + 优点:轻量级框架,易于维护 + FFmpeg是一个集录制、转换、音/视频编码解码功能为一体的完整的开源解决方案 + FFMPEG几乎为你把所有的繁重工作都做了,比如解码、编码、复用和解复用。 + 这使得多媒体应用程序变得容易编写。它是一个简单的,用C编写的,快速的并且能够解码几乎所有你能用到的格式,当然也包括编码多种格式。 + FFmpeg支持MPEG、DivX、MPEG4、AC3、DV、FLV等40多种编码,支持AVI、MPEG、OGG、Matroska、ASF等90多种解码 + FFmpeg主目录下主要有libavcodec、libavformat和libavutil等子目录。其中libavcodec用于存放各个encode/decode模块 + libavformat用于存放muxer/demuxer模块,libavutil用于存放内存操作等辅助性模块 + +- vitamio框架: + vitamio也是基于ffmpeg开源框架 + VPlayer是vitamio的一个产品,vitamio和VPlayer是同一个团队开发的,VPlayer能播放的vitamio也能播放 + + +##Surface简介 +- `Surface`就是“表面”的意思。在`SDK`的文档中,对`Surface`的描述是这样的:“`Handle onto a raw buffer that is being managed by the screen compositor`”, + 翻译成中文就是“由屏幕显示内容合成器`(screen compositor)`所管理的原生缓冲器的句柄”, 这句话包括下面两个意思: + - 通过Surface(因为Surface是句柄)就可以获得原生缓冲器以及其中的内容。就像在C语言中,可以通过一个文件的句柄,就可以获得文件的内容一样; + - 原生缓冲器(rawbuffer)是用于保存当前窗口的像素数据的。 +- 简单的说`Surface`对应了一块屏幕缓冲区,每个`Window`对应一个`Surface`,任何`View`都是画在`Surface`上的,传统的`view`共享一块屏幕缓冲区,所有的绘制必须在`UI`线程中进行 +- 我们不能直接操作Surface实例,要通过SurfaceHolder,在SurfaceView中可以通过getHolder()方法获取到SurfaceHolder实例。 +- `Surface`是一个用来画图形的地方,但是我们知道画图都是在一个`Canvas`对象上面进行的,*`Surface`中的`Canvas`成员,是专门用于提供画图的地方,就像黑板一样,其中的原始缓冲区是用来保存数据的地方, + `Surface`本身的作用类似一个句柄,得到了这个句柄就可以得到其中的`Canvas`、原始缓冲区以及其他方面的内容,所以简单的说`Surface`是用来管理数据的(句柄)*。 + +##SurfaceView简介 +- 简单的说`SurfaceView`就是一个有`Surface`的`View`里面内嵌了一个专门用于绘制的`Surface`,`SurfaceView`控制这个`Surface`的格式和尺寸以及绘制位置.`SurfaceView`是一个`View`也许不够严谨, + 然而从定义中 `public class SurfaceView extends View`显示`SurfaceView`确实是派生自`View`,但是`SurfaceView`却有着自己的`Surface`,源码: + ```java + if (mWindow == null) { + mWindow = new MyWindow(this); + mLayout.type = mWindowType; + mLayout.gravity = Gravity.LEFT|Gravity.TOP; + mSession.addWithoutInputChannel(mWindow, mWindow.mSeq, mLayout, + mVisible ? VISIBLE : GONE, mContentInsets); + } + ``` + 很明显,每个`SurfaceView`创建的时候都会创建一个`MyWindow`,`new MyWindow(this)`中的`this`正是`SurfaceView`自身,因此将`SurfaceView`和`window`绑定在一起,而前面提到过每个`window`对应一个`Surface`, + 所以`SurfaceView`也就内嵌了一个自己的`Surface`,可以认为`SurfaceView`是来控制`Surface`的位置和尺寸。传统`View`及其派生类的更新只能在`UI`线程,然而`UI`线程还同时处理其他交互逻辑, + 这就无法保证`view`更新的速度和帧率了,而`SurfaceView`可以用独立的线程来进行绘制,因此可以提供更高的帧率,例如游戏,摄像头取景等场景就比较适合用`SurfaceView`来实现。 + +- `Surface`是纵深排序`(Z-ordered)`的,这表明它总在自己所在窗口的后面。 +- `Surfaceview`提供了一个可见区域,只有在这个可见区域内的`Surface`部分内容才可见,可见区域外的部分不可见,所以可以认为**`SurfaceView`就是展示`Surface`中数据的地方**,`Surface`就是管理数据的地方, + `SurfaceView`就是展示数据的地方,只有通过`SurfaceView`才能展现`Surface`中的数据。 + ![image](https://github.com/CharonChui/Pictures/master/SurfaceView.png?raw=true) +- `Surface`的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者`Surface`的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物`(overlays)`(例如,文本和按钮等控件)。 + 注意,如果`Surface`上面有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。surfaceview变得可见时,surface被创建;surfaceview隐藏前,surface被销毁。 + 这样能节省资源。如果你要查看 surface被创建和销毁的时机,可以重载surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder)。 + **`SurfaceView`的核心在于提供了两个线程:`UI`线程和渲染线程**,两个线程通过“双缓冲”机制来达到高效的界面适时更新。 + +##SurfaceHolder简介 + +显示一个`Surface`的抽象接口,使你可以控制`Surface`的大小和格式以及在`Surface`上编辑像素,和监视`Surace`的改变。这个接口通常通过`SurfaceView`类实现。 +简单的说就是我们无法直接操作`Surface`只能通过`SurfaceHolder`这个接口来获取和操作`Surface`。 +SurfaceHolder`中提供了一些`lockCanvas()`:获取一个Canvas对象,并锁定之。所得到的Canvas对象,其实就是Surface中一个成员。加锁的目的其实就是为了在绘制的过程中, +Surface中的数据不会被改变。lockCanvas是为了防止同一时刻多个线程对同一canvas写入。 + + +*从设计模式的角度来看,`Surface、SurfaceView、SurfaceHolder`实质上就是`MVC(Model-View-Controller)`,`Model`就是模型或者说是数据模型,更简单的可以理解成数据,在这里也就是`Surface`, +`View`就是视图,代表用户交互界面,这里就是`SurfaceView`,`SurfaceHolder`就是`Controller.`* + + +##MediaController + +- `MediaController`继承`FrameLayout`,通过`MediaPlayerControl`接口与`VideoView`进行结合控制,内部是通过`PopupWindow`将整个控制栏界面显示到界面上, + 而该`PopupWindow`所显示在的位置就是通过`setAnchorView()`设置进来的`Anchor`一般可以使当前的`VideoView`或者是整个`Activity`的根布局。这里要分为小屏和全屏两种情况来进行设置。 + 如果当前的`MediaController`只是播放前下面的控制栏部分(进度条、快进、快退、暂停等)这样我们可以通过对VideoView设置点击事件,控制它的显示和隐藏。 + 如果`MediaController`为整个屏幕包括了控制栏部分、上端的信息显示部分、以及左右栏的功能部分、这时候就可以通过对`MediaController`本身设置点击事件来控制显示和隐藏。 + +`Controller`可以用`PopupWindow`来实现,具体有两种方式: + + - 整个控制栏(上面的信息部分、下面的控制部分以及左右边)都在`Controller`中,`setAnchorView()`的时候就会让`Controller`中的`PopupWindow`显示出来(一直显示,但是这个`PopupWindow`是透明的), + 真正的显示与隐藏是控制在`PopupWindow`中的`View`部分的显示与隐藏来实现。开始的时候我是想用这种方式,当时我想的是播放就播放、控制就控制,分离开来多好,但是没想到, + 一旦有`PopupWindow`显示出来后,Activity是接收不到任何`Touch`事件的,所有的重试界面等都要放到`Controller`中实现(手势处理等)。但是也有好处,就是不管显示还是隐藏都可以去处理手势. + + - `PopupWindow`不是全屏的,只包含下面真正的控制部分(快进、快退、暂停等,不包含上面的信息部分和左右边),而且也不是开始就显示,显示隐藏是通过控制`PopupWindow`的显示与隐藏来进行的。 + 而对于信息部分、以及左右边都是在`Activity`的布局当中,我们通过接口回调得到`PopupWindow`的显示与隐藏来控制这些布局的显示与隐藏即可。 + 这样的话我们就需要将手势等全部放到`Activity`中去处理,但是也有一个问题,就是如果`Controller`正在显示的话`Activity`是接收不到`Touch`事件的,就无法处理手势,只能是让`Controller`消失后才能处理手势。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\212\240\345\274\272/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" "b/AndroidAdavancedPart/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" rename to "AndroidAdavancedPart/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" diff --git "a/Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" "b/AndroidAdavancedPart/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" similarity index 100% rename from "Android\345\212\240\345\274\272/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" rename to "AndroidAdavancedPart/\351\200\232\350\277\207Hardware Layer\346\217\220\351\253\230\345\212\250\347\224\273\346\200\247\350\203\275.md" diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" similarity index 99% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" index 37affaa8..e997843c 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\200\345\274\271).md" @@ -1,115 +1,115 @@ -AndroidStudio使用教程(第一弹) -=== - -`Android Studio`是一套面世不久的`IDE`(即集成开发环境),免费向谷歌及`Android`的开发人员发放。`Android Studio`以`IntelliJ IDEA`为基础, -旨在取代`Eclipse`和`ADT`(`Android`开发者工具)为开发者提供更好的开发工具。 -运行相应速度、智能提示、布局文件适时多屏预览等都比`Eclipse`要强,但也不能说全部都是有点现在`Studio`中无法在一个窗口管理多个`Project`, -每个`Project`都要打开一个窗口,或者是`close`当前的后再打开别的。 - -当但是毕竟是预览版,所以只是暂时试用了下,并没有过多接触,开发中还是使用`Eclipse`。 -经过一年多的沉淀,如果已到0.8.4版本,最近准备在工作用正式开始使用,所以看了下官网的教程。准备开始了。 - -- 安装 - 这个我就不多说了,大家都知道,官网下载安装即可。安装完成后界面和`Eclipse`有些类似,然后就新建一个`Project`,完成之后会发现一直在提示下载, - 这是在下载`Gradle`,大约二三十M的大小,由于伟大的防火墙,所以可能需要很长时间,这里就不教大家了,对程序猿来说不是难题,大家都会科学上网。 - -- 区别 - - 此`Project`非彼`Project`, `Android Studio`的目录结构(`Project`)代表一个`Workspace`,一个`Workspace`里面可以有多个`Module`, - 这里`Module`可以理解成`Eclipse`中的一个`Project`. - `Project`代表一个完整的`Android app`,而`modules`则是`app`的一个组件,并且这个组件可以单独`build,test,debug`。`modules`可以分为下面几种: - - Java library modules - - Android library modules: 包含android相关代码和资源,最后生成AAR(Android ARchive)包 - - Android application modules - - - 结构发生了变化,在`src`目录下有一个`main`的分组同时包含了`java`和`res`. - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_1.png?raw=true) - 如图:`MyApplication`就是`Project`,而`app`就是`Module`. - -- 设置 - 进入后你会发现字体或样式等不符合你的习惯。 - `Windows`下点击左上角`File` -> `Settings`进入设置页面(`Mac`下为 `Android Studio` -> `Preferences`),在搜索框搜`Font`找到`Colors&Font`下的`Font`选项, - 我们会发现无法修改右侧字体大小。这里修改必须 - 要通过新建`Theme`进行修改的,点击`Save as`输入一个名字后,就可以修改字体了。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2.png?raw=true) - - 这里可能有些人会发现我的主题是黑色的,和`IO`大会演示的一样,但是安装后默认是白色的,有些刺眼。这里可以通过设置页面中修改`Theme`来改变, - 默认是`Intellij`, 改为`Darcula`就是黑色的了. - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3.png?raw=true) - 很酷有木有. - -- 运行 - 设置好字体后,当然要走你了。 - 运行和`Eclipse`中比较像,点击绿色的箭头。 可以通过箭头左边的下拉菜单选择不同的`Module`,快捷键是`Shift+F10` - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_4.png?raw=true) - - `AndroidStudio`默认安装会启动模拟器,如果想让安装到真机上可以配置一下。在下拉菜单中选择`Edit Configurations`选择提示或者是`USB`设备。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_5.png?raw=true) - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_6.png?raw=true) - -- 常用快捷键介绍 - `AndroidStudio`中可以将快捷键设置成`Eclipse`中的快捷键。具体方法为在设置页面搜索`keymap`然后选择为`Eclipse`就可以了. - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_7.png?raw=true) - - 强迫症的人伤不起,非想用默认的快捷键。 - 这里我整理下下个人常用的几个快捷键。 每个人的习惯不同,大家各取所需 - `Ctrl+S` 开个玩笑,这个键算是彻底废掉了, 因为`AndroidStudio`与`Eclipse`不同,他是自动保存的,所以我们再也不需要`Ctrl+S`了. - - | 功能 | Windows | Mac | - | ------------------------------------------------------------ |:-------------------------------------------:| --------------------------------------------:| - | 代码提示 (同`Eclipse`中`Alt+/`) | `Ctrl+空格` | `Ctrl+空格` | - | 查找文件 (同`Eclipse`中`Ctrl+Shift+R`) | `Ctrl+Shjft+N` | `双击Shift` | - | 显示当前文件的结构 (同`Eclipse`中`Ctrl+0`) | `Ctrl+F12` | `Command+F12` | - | 格式化 (同`Eclipse`中`Ctrl+Shift+F`) | `Ctrl+Alt+L` | `Command+Option+L` | - | 优化导入的包 (同`Eclipse`中`Ctrl+Shift+O`) | `Ctrl+Alt+O` | `Ctrl+Option+O` | - | 查看文档 (同`Eclipse`中`F2`) | `Ctrl+Q` | `F1` | - | 查找使用位 (同`Eclipse`中`File Search`) | `Alt+F7` | `Option+F7` | - | 上下移动代码 | `Alt+Shift+Up/Down` | `Option+Shift+Up/Down` | - | 剪切当前行 | `Ctrl+X` | `Command+x` | - | 删除当前行 | `Ctrl+Y` | `Command+Delete` | - | 快速重写方法 override | `Ctrl+O` | `Ctrl+O` | - | 折叠展开代码块 | `Ctrl+Plus/Minus` | `Command+Plus/Minus` | - | 折叠展开全部代码块 | `Ctrl+Shift+Plus/Minus` | `Command+Shift+Plus/Minus` | - | 大小写转换 | `Ctrl+Shift+U` | `Command+Shift+U` | - | 新建文件或生成代码(`GetSet`) | `Alt+Insert` | `Command+N` | - | 快速修复(同`Eclipse`中`F1`) | `Alt+Enter` | `Option+Enter` | - | 显示方法参数 | `Ctrl+P` | `Command+P` | - | 运行项目 | `Shift+F10` | `Ctrl+R` | - | 跳转到上次修改的地方 | `Ctrl+Shift+Backspace` | `Command+Shift+Backspace` | - | 快捷生成结构体(加try catch等) | `Ctrl+Alt+T` | `Command+Option+T` | - | 显示最近编辑列表 | `Ctrl+E` | `Command+E` | - | 跳转到大括号开头或结尾 | `Ctrl+{或}` | ..... | - | 在方法间移动 | `Alt+↑或↓` | `Ctrl+↑或↓` | - | 切换已打开的文件视图 | `Alt+←或→` | `Ctrl+←或→` | - | 重命名 | `Shift+F6` | `Shift+F6` | - | 直接进入源码 | `F4` | | - | 快速打开该类或方法(与F4虽然大部分情况下相同,但他俩不一样) | `Ctrl+B`如在布局文件上点会直接进入布局文件 | `Command+B` | - | 快速定位到文件错误或警告位置 | `F2` | `F2` | - | 在当前位置复制当前行 | `Ctrl+D` | `Command+D` | - | 选中两个文件或目录后进行比较(活生生一个简单的BeyondCompare) | `Ctrl+D` | `Command+D` | - | 开关`Project`视图 | `Alt+1` | `Command+1` | - | 关闭当前窗口 | `Ctrl+_F4` | `Command+W` | - | 实现接口方法 implement | `Ctrl+I` | `Ctrl+I` | - | 抽象方法查看具体有哪些实现类 | `Ctrl+Alt+B` | `Command+Option+B` | - | 单行注释 | `Ctrl+/` | `Command+/` | - | 多行注释 | `Ctrl+Shit+/` | `Command+Option+/` | - | 复制,如果当前行没有选中内容就复制当前行 | `Ctrl+C` | `Command+C` | - | 打开该类的关系图,查看该类的继承或实现 | `Ctrl+H` | `Ctrl+H` | - | 提示代码缩写,如so后点击提示System.out.print等 | `Ctrl+J` | `Command+J` | - | 进入父类方法的实现 UP | `Ctrl+U` | `Command+U` | - | 抽取某一个块代码为单独的变量或者方法 | `Ctrl+Alt+V` | `Command+Option+V`  方法是`Command+Option+M` | - | 选择最近所有复制过内容的列表 | `Ctrl+Shift+V` | `Command+Shift+V` | - | 通过卡片的方式,直接查看该方法的具体内容或者图片 | `Ctrl+Shift+I` | `Option+Space` | - | 全局搜索 | `Ctrl+Shift+F` | `Command+Shift+F` | - | 上一步、下一步 | `` | `Command+[或]` | - | 高亮所有相同变量 | `Ctrl+Shift+F7` | `Command+Shift+F7` | +AndroidStudio使用教程(第一弹) +=== + +`Android Studio`是一套面世不久的`IDE`(即集成开发环境),免费向谷歌及`Android`的开发人员发放。`Android Studio`以`IntelliJ IDEA`为基础, +旨在取代`Eclipse`和`ADT`(`Android`开发者工具)为开发者提供更好的开发工具。 +运行相应速度、智能提示、布局文件适时多屏预览等都比`Eclipse`要强,但也不能说全部都是有点现在`Studio`中无法在一个窗口管理多个`Project`, +每个`Project`都要打开一个窗口,或者是`close`当前的后再打开别的。 + +当但是毕竟是预览版,所以只是暂时试用了下,并没有过多接触,开发中还是使用`Eclipse`。 +经过一年多的沉淀,如果已到0.8.4版本,最近准备在工作用正式开始使用,所以看了下官网的教程。准备开始了。 + +- 安装 + 这个我就不多说了,大家都知道,官网下载安装即可。安装完成后界面和`Eclipse`有些类似,然后就新建一个`Project`,完成之后会发现一直在提示下载, + 这是在下载`Gradle`,大约二三十M的大小,由于伟大的防火墙,所以可能需要很长时间,这里就不教大家了,对程序猿来说不是难题,大家都会科学上网。 + +- 区别 + - 此`Project`非彼`Project`, `Android Studio`的目录结构(`Project`)代表一个`Workspace`,一个`Workspace`里面可以有多个`Module`, + 这里`Module`可以理解成`Eclipse`中的一个`Project`. + `Project`代表一个完整的`Android app`,而`modules`则是`app`的一个组件,并且这个组件可以单独`build,test,debug`。`modules`可以分为下面几种: + - Java library modules + - Android library modules: 包含android相关代码和资源,最后生成AAR(Android ARchive)包 + - Android application modules + + - 结构发生了变化,在`src`目录下有一个`main`的分组同时包含了`java`和`res`. + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_1.png?raw=true) + 如图:`MyApplication`就是`Project`,而`app`就是`Module`. + +- 设置 + 进入后你会发现字体或样式等不符合你的习惯。 + `Windows`下点击左上角`File` -> `Settings`进入设置页面(`Mac`下为 `Android Studio` -> `Preferences`),在搜索框搜`Font`找到`Colors&Font`下的`Font`选项, + 我们会发现无法修改右侧字体大小。这里修改必须 + 要通过新建`Theme`进行修改的,点击`Save as`输入一个名字后,就可以修改字体了。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2.png?raw=true) + + 这里可能有些人会发现我的主题是黑色的,和`IO`大会演示的一样,但是安装后默认是白色的,有些刺眼。这里可以通过设置页面中修改`Theme`来改变, + 默认是`Intellij`, 改为`Darcula`就是黑色的了. + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3.png?raw=true) + 很酷有木有. + +- 运行 + 设置好字体后,当然要走你了。 + 运行和`Eclipse`中比较像,点击绿色的箭头。 可以通过箭头左边的下拉菜单选择不同的`Module`,快捷键是`Shift+F10` + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_4.png?raw=true) + + `AndroidStudio`默认安装会启动模拟器,如果想让安装到真机上可以配置一下。在下拉菜单中选择`Edit Configurations`选择提示或者是`USB`设备。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_5.png?raw=true) + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_6.png?raw=true) + +- 常用快捷键介绍 + `AndroidStudio`中可以将快捷键设置成`Eclipse`中的快捷键。具体方法为在设置页面搜索`keymap`然后选择为`Eclipse`就可以了. + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_7.png?raw=true) + + 强迫症的人伤不起,非想用默认的快捷键。 + 这里我整理下下个人常用的几个快捷键。 每个人的习惯不同,大家各取所需 + `Ctrl+S` 开个玩笑,这个键算是彻底废掉了, 因为`AndroidStudio`与`Eclipse`不同,他是自动保存的,所以我们再也不需要`Ctrl+S`了. + + | 功能 | Windows | Mac | + | ------------------------------------------------------------ |:-------------------------------------------:| --------------------------------------------:| + | 代码提示 (同`Eclipse`中`Alt+/`) | `Ctrl+空格` | `Ctrl+空格` | + | 查找文件 (同`Eclipse`中`Ctrl+Shift+R`) | `Ctrl+Shjft+N` | `双击Shift` | + | 显示当前文件的结构 (同`Eclipse`中`Ctrl+0`) | `Ctrl+F12` | `Command+F12` | + | 格式化 (同`Eclipse`中`Ctrl+Shift+F`) | `Ctrl+Alt+L` | `Command+Option+L` | + | 优化导入的包 (同`Eclipse`中`Ctrl+Shift+O`) | `Ctrl+Alt+O` | `Ctrl+Option+O` | + | 查看文档 (同`Eclipse`中`F2`) | `Ctrl+Q` | `F1` | + | 查找使用位 (同`Eclipse`中`File Search`) | `Alt+F7` | `Option+F7` | + | 上下移动代码 | `Alt+Shift+Up/Down` | `Option+Shift+Up/Down` | + | 剪切当前行 | `Ctrl+X` | `Command+x` | + | 删除当前行 | `Ctrl+Y` | `Command+Delete` | + | 快速重写方法 override | `Ctrl+O` | `Ctrl+O` | + | 折叠展开代码块 | `Ctrl+Plus/Minus` | `Command+Plus/Minus` | + | 折叠展开全部代码块 | `Ctrl+Shift+Plus/Minus` | `Command+Shift+Plus/Minus` | + | 大小写转换 | `Ctrl+Shift+U` | `Command+Shift+U` | + | 新建文件或生成代码(`GetSet`) | `Alt+Insert` | `Command+N` | + | 快速修复(同`Eclipse`中`F1`) | `Alt+Enter` | `Option+Enter` | + | 显示方法参数 | `Ctrl+P` | `Command+P` | + | 运行项目 | `Shift+F10` | `Ctrl+R` | + | 跳转到上次修改的地方 | `Ctrl+Shift+Backspace` | `Command+Shift+Backspace` | + | 快捷生成结构体(加try catch等) | `Ctrl+Alt+T` | `Command+Option+T` | + | 显示最近编辑列表 | `Ctrl+E` | `Command+E` | + | 跳转到大括号开头或结尾 | `Ctrl+{或}` | ..... | + | 在方法间移动 | `Alt+↑或↓` | `Ctrl+↑或↓` | + | 切换已打开的文件视图 | `Alt+←或→` | `Ctrl+←或→` | + | 重命名 | `Shift+F6` | `Shift+F6` | + | 直接进入源码 | `F4` | | + | 快速打开该类或方法(与F4虽然大部分情况下相同,但他俩不一样) | `Ctrl+B`如在布局文件上点会直接进入布局文件 | `Command+B` | + | 快速定位到文件错误或警告位置 | `F2` | `F2` | + | 在当前位置复制当前行 | `Ctrl+D` | `Command+D` | + | 选中两个文件或目录后进行比较(活生生一个简单的BeyondCompare) | `Ctrl+D` | `Command+D` | + | 开关`Project`视图 | `Alt+1` | `Command+1` | + | 关闭当前窗口 | `Ctrl+_F4` | `Command+W` | + | 实现接口方法 implement | `Ctrl+I` | `Ctrl+I` | + | 抽象方法查看具体有哪些实现类 | `Ctrl+Alt+B` | `Command+Option+B` | + | 单行注释 | `Ctrl+/` | `Command+/` | + | 多行注释 | `Ctrl+Shit+/` | `Command+Option+/` | + | 复制,如果当前行没有选中内容就复制当前行 | `Ctrl+C` | `Command+C` | + | 打开该类的关系图,查看该类的继承或实现 | `Ctrl+H` | `Ctrl+H` | + | 提示代码缩写,如so后点击提示System.out.print等 | `Ctrl+J` | `Command+J` | + | 进入父类方法的实现 UP | `Ctrl+U` | `Command+U` | + | 抽取某一个块代码为单独的变量或者方法 | `Ctrl+Alt+V` | `Command+Option+V`  方法是`Command+Option+M` | + | 选择最近所有复制过内容的列表 | `Ctrl+Shift+V` | `Command+Shift+V` | + | 通过卡片的方式,直接查看该方法的具体内容或者图片 | `Ctrl+Shift+I` | `Option+Space` | + | 全局搜索 | `Ctrl+Shift+F` | `Command+Shift+F` | + | 上一步、下一步 | `` | `Command+[或]` | + | 高亮所有相同变量 | `Ctrl+Shift+F7` | `Command+Shift+F7` | | 方法调用层级弹窗 | `Ctrl+Alt+H` | `Control+Option+H` | |书签(在当前行打上书签) | `F11` | `F3` | |展示书签 | `Shift+F11` | `Command+F3` | |整行代码上下移动 | `Alt+Shift++↑或↓` | `Option+Shift+↑或↓` | |搜索设置操作命令 | `Ctrl+Shift+A` | `Command+Shift+A` | - ---- - -- 邮箱 :charon.chui@gmail.com + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" similarity index 97% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" index 0a6bd87b..579704d2 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\203\345\274\271).md" @@ -1,414 +1,414 @@ -AndroidStudio使用教程(第七弹) -=== - -本文讲解一下`Gradle`的应用,大家都知道`Gradle`使用起来非常方便,那他究竟方便在哪里?                           - -- 很多时候我们在打印`Log`日志的时候都是需要在`Debug`版本中进行打印,而在正式版本中关闭。 - 通常我们都是用一个`Config`文件来配置,不知道大家有没有遇到过正式版中忘记关闭`Log`日志的情况。 -- 多渠道包非常让人头疼。现在国内市场这么多。一个个的打多麻烦,虽然我们会用友盟打包工具等。 - 怎么破? - - -先把项目中的`build.gradle`展现一下,然后慢慢分析。 -```xml -apply plugin: 'com.android.application' - -android { - compileSdkVersion 22 - buildToolsVersion "22.0.1" - - defaultConfig { - applicationId "com.charon.*" - minSdkVersion 11 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - - multiDexEnabled true - // default umeng channel name - manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"] - } - - signingConfigs { - debug { - storeFile file("debug.keystore") - } - - release { - storeFile file("keystore.keystore") - storePassword "android" - keyAlias "androiddebugkey" - keyPassword "android" - } - } - - buildTypes { - debug { - versionNameSuffix "-debug" - minifyEnabled false - zipAlignEnabled false - shrinkResources false - signingConfig signingConfigs.debug - } - - release { - zipAlignEnabled true - // remove unused resources - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release - } - } - - productFlavors { - xiaomi {} - _360 {} - baidu {} - qq {} - } - - productFlavors.all { - // change UMENG_CHANNEL_VALUE to the product channel name - flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] - } - - lintOptions { - // if true, stop the gradle build if errors are found - abortOnError false - - // set to true to turn off analysis progress reporting by lint - // quiet true - // if true, only report errors -// ignoreWarnings true - // if true, emit full/absolute paths to files with errors (true by default) - //absolutePaths true - // if true, check all issues, including those that are off by default -// checkAllWarnings true - // if true, treat all warnings as errors -// warningsAsErrors true - // turn off checking the given issue id's -// disable 'TypographyFractions','TypographyQuotes' - // turn on the given issue id's -// enable 'RtlHardcoded','RtlCompat', 'RtlEnabled' - // check *only* the given issue id's -// check 'NewApi', 'InlinedApi' - // if true, don't include source code lines in the error output -// noLines true - // if true, show all locations for an error, do not truncate lists, etc. -// showAll true - // Fallback lint configuration (default severities, etc.) - lintConfig file("default-lint.xml") - // if true, generate a text report of issues (false by default) -// textReport true - // location to write the output; can be a file or 'stdout' -// textOutput 'stdout' - // if true, generate an XML report for use by for example Jenkins -// xmlReport false - // file to write report to (if not specified, defaults to lint-results.xml) -// xmlOutput file("lint-report.xml") - // if true, generate an HTML report (with issue explanations, sourcecode, etc) -// htmlReport true - // optional path to report (default will be lint-results.html in the builddir) -// htmlOutput file("lint-report.html") - - // set to true to have all release builds run lint on issues with severity=fatal - // and abort the build (controlled by abortOnError above) if fatal issues are found -// checkReleaseBuilds true - // Set the severity of the given issues to fatal (which means they will be - // checked during release builds (even if the lint target is not included) -// fatal 'NewApi', 'InlineApi' - // Set the severity of the given issues to error -// error 'Wakelock', 'TextViewEdits' - // Set the severity of the given issues to warning -// warning 'ResourceAsColor' - // Set the severity of the given issues to ignore (same as disabling the check) -// ignore 'TypographyQuotes' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - applicationVariants.all { variant -> - variant.outputs.each { output -> - def outputFile = output.outputFile - if (outputFile != null && outputFile.name.endsWith('.apk')) { - def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk") - output.outputFile = new File(outputFile.parent, fileName) - } - } - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile project(':libraries:framework') - // square leakcanary - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' -} - -repositories { - mavenCentral() - maven{ - url "[maven reposity path]" - } -} - - -``` - - -下面来详细讲几个地方: - -```xml -apply plugin: 'com.android.application' - -android { - ... - defaultConfig { - // 支持方法数超过65536后的处理 - multiDexEnabled true - // 这里就是上面提到的替换友盟统计中channel的值,下面这句话的意思就是默认值为umeng - manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"] - } - - // 签名操作 - signingConfigs { - debug { - // debug签名文件配置 - storeFile file("debug.keystore") - } - - release { - // 正式版签名文件配置 - storeFile file("keystore.keystore") - storePassword "android" - keyAlias "androiddebugkey" - keyPassword "android" - } - } - - buildTypes { - debug { - // debug的签名处理 - versionNameSuffix "-debug" - minifyEnabled false - zipAlignEnabled false - shrinkResources false - signingConfig signingConfigs.debug - } - - release { - // 正式版签名处理 - zipAlignEnabled true - // remove unused resources - shrinkResources true - // proguard 混淆 - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release - } - } - - // 多渠道打包 - productFlavors { - xiaomi {} - _360 {} - baidu {} - qq {} - free { - // 当然这里还可以指定 applicationId 版本等这些内容,比如我们程序有一个收费版一个付费版,他俩的包名不同,这时候就可以通过这种方式来指定。 - applicationId = 'com.test.test' - versionName = '1.0' - versionCode = 1 - } - } - - productFlavors.all { - // 统一将manifest中的UMENG_CHANNEL_VALUE值替换为上面productFlavors中对应的渠道名 - flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] - } - - lintOptions { - // if true, stop the gradle build if errors are found - abortOnError false - - // 下面是一些其他的选项,一般都用不到 - // set to true to turn off analysis progress reporting by lint - // quiet true - // if true, only report errors -// ignoreWarnings true - // if true, emit full/absolute paths to files with errors (true by default) - //absolutePaths true - // if true, check all issues, including those that are off by default -// checkAllWarnings true - // if true, treat all warnings as errors -// warningsAsErrors true - // turn off checking the given issue id's -// disable 'TypographyFractions','TypographyQuotes' - // turn on the given issue id's -// enable 'RtlHardcoded','RtlCompat', 'RtlEnabled' - // check *only* the given issue id's -// check 'NewApi', 'InlinedApi' - // if true, don't include source code lines in the error output -// noLines true - // if true, show all locations for an error, do not truncate lists, etc. -// showAll true - // Fallback lint configuration (default severities, etc.) - lintConfig file("default-lint.xml") - // if true, generate a text report of issues (false by default) -// textReport true - // location to write the output; can be a file or 'stdout' -// textOutput 'stdout' - // if true, generate an XML report for use by for example Jenkins -// xmlReport false - // file to write report to (if not specified, defaults to lint-results.xml) -// xmlOutput file("lint-report.xml") - // if true, generate an HTML report (with issue explanations, sourcecode, etc) -// htmlReport true - // optional path to report (default will be lint-results.html in the builddir) -// htmlOutput file("lint-report.html") - - // set to true to have all release builds run lint on issues with severity=fatal - // and abort the build (controlled by abortOnError above) if fatal issues are found -// checkReleaseBuilds true - // Set the severity of the given issues to fatal (which means they will be - // checked during release builds (even if the lint target is not included) -// fatal 'NewApi', 'InlineApi' - // Set the severity of the given issues to error -// error 'Wakelock', 'TextViewEdits' - // Set the severity of the given issues to warning -// warning 'ResourceAsColor' - // Set the severity of the given issues to ignore (same as disabling the check) -// ignore 'TypographyQuotes' - } - - // 可以指定用具体哪个JDK版本来进行编译 - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - // 更改生成的apk文件名字,方便区分多渠道 - applicationVariants.all { variant -> - variant.outputs.each { output -> - def outputFile = output.outputFile - if (outputFile != null && outputFile.name.endsWith('.apk')) { - def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk") - output.outputFile = new File(outputFile.parent, fileName) - } - } - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile project(':libraries:framework') - // square leakcanary - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' -} - -repositories { - //从中央库里面获取依赖 - mavenCentral() - //或者使用指定的本地maven 库 - maven{ - url "file://F:/githubrepo/releases" - } - //或者使用指定的远程maven库 - maven{ - url "远程库地址" - } -} - -上面`build.gradle`中的配置基本就是这些,那么`manifest`中的清单文件该如何对`umeng`渠道进行修改呢? -```xml - - - - - - - - - - - - -``` - -上面讲解了如何进行多渠道打包。还剩下一个问题,就是`Log`开关的问题。这就要用到`BuildConfig.DEBUG`。 `Gradle`脚本默认有`debug`和`release`两种模式,对应的`BuildCondig.DEBUG`字段分别为`true`和`false`,而且不可更改。该字段编译后自动生成,在`app/build/source/BuildConfig/Build Varients/package name/BuildConfig`文件中。所以我们可以在`LogUtil`中这样配置。 -```java -public class LogUtil { - /** - * If print log here. - */ - private static int LOG_LEVEL = BuildConfig.DEBUG ? 6 : 1; - - private static final int VERBOSE = 5; - private static final int DEBUG = 4; - private static final int INFO = 3; - private static final int WARN = 2; - private static final int ERROR = 1; - - ... -} -``` - -这里再多提一句,就是如果我们不想使用`BuildConfig.DEBUG`,想额外的使用一些其他的配置该如何操作呢? -可以在`gradle`文件中的`buildTypes`中进行添加。 -```xml -buildTypes { - debug { - // 显示Log - buildConfigField "boolean", "LOG_DEBUG", "true" - versionNameSuffix "-debug" - minifyEnabled false - zipAlignEnabled false - shrinkResources false - signingConfig signingConfigs.debug - } - - release { - // 不显示Log - buildConfigField "boolean", "LOG_DEBUG", "false" - zipAlignEnabled true - // remove unused resources - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release - } -} -``` - -在代码中使用`BuildConfig.LOG_DEBUG`就可以了。 - - -更多内容请参考[Gradle Plugin User Guide](http://tools.android.com/tech-docs/new-build-system/user-guide) - - -最后附上一张`Build`流程图: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/build.png?raw=true) - - ---- - -- 邮箱 :charon.chui@gmail.com +AndroidStudio使用教程(第七弹) +=== + +本文讲解一下`Gradle`的应用,大家都知道`Gradle`使用起来非常方便,那他究竟方便在哪里?                           + +- 很多时候我们在打印`Log`日志的时候都是需要在`Debug`版本中进行打印,而在正式版本中关闭。 + 通常我们都是用一个`Config`文件来配置,不知道大家有没有遇到过正式版中忘记关闭`Log`日志的情况。 +- 多渠道包非常让人头疼。现在国内市场这么多。一个个的打多麻烦,虽然我们会用友盟打包工具等。 + 怎么破? + + +先把项目中的`build.gradle`展现一下,然后慢慢分析。 +```xml +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + applicationId "com.charon.*" + minSdkVersion 11 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + + multiDexEnabled true + // default umeng channel name + manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"] + } + + signingConfigs { + debug { + storeFile file("debug.keystore") + } + + release { + storeFile file("keystore.keystore") + storePassword "android" + keyAlias "androiddebugkey" + keyPassword "android" + } + } + + buildTypes { + debug { + versionNameSuffix "-debug" + minifyEnabled false + zipAlignEnabled false + shrinkResources false + signingConfig signingConfigs.debug + } + + release { + zipAlignEnabled true + // remove unused resources + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + + productFlavors { + xiaomi {} + _360 {} + baidu {} + qq {} + } + + productFlavors.all { + // change UMENG_CHANNEL_VALUE to the product channel name + flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] + } + + lintOptions { + // if true, stop the gradle build if errors are found + abortOnError false + + // set to true to turn off analysis progress reporting by lint + // quiet true + // if true, only report errors +// ignoreWarnings true + // if true, emit full/absolute paths to files with errors (true by default) + //absolutePaths true + // if true, check all issues, including those that are off by default +// checkAllWarnings true + // if true, treat all warnings as errors +// warningsAsErrors true + // turn off checking the given issue id's +// disable 'TypographyFractions','TypographyQuotes' + // turn on the given issue id's +// enable 'RtlHardcoded','RtlCompat', 'RtlEnabled' + // check *only* the given issue id's +// check 'NewApi', 'InlinedApi' + // if true, don't include source code lines in the error output +// noLines true + // if true, show all locations for an error, do not truncate lists, etc. +// showAll true + // Fallback lint configuration (default severities, etc.) + lintConfig file("default-lint.xml") + // if true, generate a text report of issues (false by default) +// textReport true + // location to write the output; can be a file or 'stdout' +// textOutput 'stdout' + // if true, generate an XML report for use by for example Jenkins +// xmlReport false + // file to write report to (if not specified, defaults to lint-results.xml) +// xmlOutput file("lint-report.xml") + // if true, generate an HTML report (with issue explanations, sourcecode, etc) +// htmlReport true + // optional path to report (default will be lint-results.html in the builddir) +// htmlOutput file("lint-report.html") + + // set to true to have all release builds run lint on issues with severity=fatal + // and abort the build (controlled by abortOnError above) if fatal issues are found +// checkReleaseBuilds true + // Set the severity of the given issues to fatal (which means they will be + // checked during release builds (even if the lint target is not included) +// fatal 'NewApi', 'InlineApi' + // Set the severity of the given issues to error +// error 'Wakelock', 'TextViewEdits' + // Set the severity of the given issues to warning +// warning 'ResourceAsColor' + // Set the severity of the given issues to ignore (same as disabling the check) +// ignore 'TypographyQuotes' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + applicationVariants.all { variant -> + variant.outputs.each { output -> + def outputFile = output.outputFile + if (outputFile != null && outputFile.name.endsWith('.apk')) { + def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk") + output.outputFile = new File(outputFile.parent, fileName) + } + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':libraries:framework') + // square leakcanary + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' +} + +repositories { + mavenCentral() + maven{ + url "[maven reposity path]" + } +} + + +``` + + +下面来详细讲几个地方: + +```xml +apply plugin: 'com.android.application' + +android { + ... + defaultConfig { + // 支持方法数超过65536后的处理 + multiDexEnabled true + // 这里就是上面提到的替换友盟统计中channel的值,下面这句话的意思就是默认值为umeng + manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"] + } + + // 签名操作 + signingConfigs { + debug { + // debug签名文件配置 + storeFile file("debug.keystore") + } + + release { + // 正式版签名文件配置 + storeFile file("keystore.keystore") + storePassword "android" + keyAlias "androiddebugkey" + keyPassword "android" + } + } + + buildTypes { + debug { + // debug的签名处理 + versionNameSuffix "-debug" + minifyEnabled false + zipAlignEnabled false + shrinkResources false + signingConfig signingConfigs.debug + } + + release { + // 正式版签名处理 + zipAlignEnabled true + // remove unused resources + shrinkResources true + // proguard 混淆 + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + + // 多渠道打包 + productFlavors { + xiaomi {} + _360 {} + baidu {} + qq {} + free { + // 当然这里还可以指定 applicationId 版本等这些内容,比如我们程序有一个收费版一个付费版,他俩的包名不同,这时候就可以通过这种方式来指定。 + applicationId = 'com.test.test' + versionName = '1.0' + versionCode = 1 + } + } + + productFlavors.all { + // 统一将manifest中的UMENG_CHANNEL_VALUE值替换为上面productFlavors中对应的渠道名 + flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] + } + + lintOptions { + // if true, stop the gradle build if errors are found + abortOnError false + + // 下面是一些其他的选项,一般都用不到 + // set to true to turn off analysis progress reporting by lint + // quiet true + // if true, only report errors +// ignoreWarnings true + // if true, emit full/absolute paths to files with errors (true by default) + //absolutePaths true + // if true, check all issues, including those that are off by default +// checkAllWarnings true + // if true, treat all warnings as errors +// warningsAsErrors true + // turn off checking the given issue id's +// disable 'TypographyFractions','TypographyQuotes' + // turn on the given issue id's +// enable 'RtlHardcoded','RtlCompat', 'RtlEnabled' + // check *only* the given issue id's +// check 'NewApi', 'InlinedApi' + // if true, don't include source code lines in the error output +// noLines true + // if true, show all locations for an error, do not truncate lists, etc. +// showAll true + // Fallback lint configuration (default severities, etc.) + lintConfig file("default-lint.xml") + // if true, generate a text report of issues (false by default) +// textReport true + // location to write the output; can be a file or 'stdout' +// textOutput 'stdout' + // if true, generate an XML report for use by for example Jenkins +// xmlReport false + // file to write report to (if not specified, defaults to lint-results.xml) +// xmlOutput file("lint-report.xml") + // if true, generate an HTML report (with issue explanations, sourcecode, etc) +// htmlReport true + // optional path to report (default will be lint-results.html in the builddir) +// htmlOutput file("lint-report.html") + + // set to true to have all release builds run lint on issues with severity=fatal + // and abort the build (controlled by abortOnError above) if fatal issues are found +// checkReleaseBuilds true + // Set the severity of the given issues to fatal (which means they will be + // checked during release builds (even if the lint target is not included) +// fatal 'NewApi', 'InlineApi' + // Set the severity of the given issues to error +// error 'Wakelock', 'TextViewEdits' + // Set the severity of the given issues to warning +// warning 'ResourceAsColor' + // Set the severity of the given issues to ignore (same as disabling the check) +// ignore 'TypographyQuotes' + } + + // 可以指定用具体哪个JDK版本来进行编译 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + // 更改生成的apk文件名字,方便区分多渠道 + applicationVariants.all { variant -> + variant.outputs.each { output -> + def outputFile = output.outputFile + if (outputFile != null && outputFile.name.endsWith('.apk')) { + def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk") + output.outputFile = new File(outputFile.parent, fileName) + } + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':libraries:framework') + // square leakcanary + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' +} + +repositories { + //从中央库里面获取依赖 + mavenCentral() + //或者使用指定的本地maven 库 + maven{ + url "file://F:/githubrepo/releases" + } + //或者使用指定的远程maven库 + maven{ + url "远程库地址" + } +} + +上面`build.gradle`中的配置基本就是这些,那么`manifest`中的清单文件该如何对`umeng`渠道进行修改呢? +```xml + + + + + + + + + + + + +``` + +上面讲解了如何进行多渠道打包。还剩下一个问题,就是`Log`开关的问题。这就要用到`BuildConfig.DEBUG`。 `Gradle`脚本默认有`debug`和`release`两种模式,对应的`BuildCondig.DEBUG`字段分别为`true`和`false`,而且不可更改。该字段编译后自动生成,在`app/build/source/BuildConfig/Build Varients/package name/BuildConfig`文件中。所以我们可以在`LogUtil`中这样配置。 +```java +public class LogUtil { + /** + * If print log here. + */ + private static int LOG_LEVEL = BuildConfig.DEBUG ? 6 : 1; + + private static final int VERBOSE = 5; + private static final int DEBUG = 4; + private static final int INFO = 3; + private static final int WARN = 2; + private static final int ERROR = 1; + + ... +} +``` + +这里再多提一句,就是如果我们不想使用`BuildConfig.DEBUG`,想额外的使用一些其他的配置该如何操作呢? +可以在`gradle`文件中的`buildTypes`中进行添加。 +```xml +buildTypes { + debug { + // 显示Log + buildConfigField "boolean", "LOG_DEBUG", "true" + versionNameSuffix "-debug" + minifyEnabled false + zipAlignEnabled false + shrinkResources false + signingConfig signingConfigs.debug + } + + release { + // 不显示Log + buildConfigField "boolean", "LOG_DEBUG", "false" + zipAlignEnabled true + // remove unused resources + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } +} +``` + +在代码中使用`BuildConfig.LOG_DEBUG`就可以了。 + + +更多内容请参考[Gradle Plugin User Guide](http://tools.android.com/tech-docs/new-build-system/user-guide) + + +最后附上一张`Build`流程图: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/build.png?raw=true) + + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" similarity index 98% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" index 00270f75..d1ee66ef 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\270\211\345\274\271).md" @@ -1,54 +1,54 @@ -AndroidStudio使用教程(第三弹) -=== - -熟悉了基本的使用之后,可能关心的就是版本控制了。 - -- `SVN` - - 下载`Subversion command line` - - 方法一 - 下载地址是[Subversion](http://subversion.apache.org/packages.html)里面有不同系统的版本。 - 以`Windows`为例,我们采用熟悉的`VisualSVN`. - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_1.png?raw=true) - 进入下载页后下载`Apache Subversion command line tools`, 解压即可。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_2.png?raw=true) - - - 方法二 - `Windows`下的`Tortoise SVN`也是带有`command line`的,但是安装的时候默认是不安装这个选项的,所以安装时要注意选择一下。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_5.png?raw=true) - 选择安装即可 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_6.png?raw=true) - - - 配置`SVN` - 进入设置中心,搜索`Version Control`后选择`Subversion`, 将右侧的`Use command line client`设置你本地的`command line`路径即可。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_3.png?raw=true) - 如果是用第二种方式安装`Tortoise SVN`的话地址就是: - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_4.png?raw=true) - PS: 设置成自己的路径啊,不要写我的... - -- `Git` - 安装`Git`, [Git](http://git-scm.com/) - 选择相应系统版本安装即可。 - 安装完成后进入`Android Studio`设置中心-> `Version Control` -> `Git`设置`Path to Git executable`即可。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_7.png?raw=true) - -- `Github` - 怎么能少了它呢?哈哈 - 这个就不用安装了,直接配置下用户名和密码就好了。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_8.png?raw=true) - -- `Checkout` - 在工具栏中点击 `VCS` 能看到相应检出和导入功能。这里以`Github`检出为例介绍一下。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_9.png?raw=true) - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_10.png?raw=true) - 确定之后就能在底部导航栏看到检出进度 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_11.png?raw=true) - - 完成之后会提示是否想要把刚才检出的项目导入`AndroidStudio`中。 - Why not? - 以`Gradle`方式导入, 然后`next`, 然后`next`然后就没有然后了。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_12.png?raw=true) - ---- - -- 邮箱 :charon.chui@gmail.com +AndroidStudio使用教程(第三弹) +=== + +熟悉了基本的使用之后,可能关心的就是版本控制了。 + +- `SVN` + - 下载`Subversion command line` + - 方法一 + 下载地址是[Subversion](http://subversion.apache.org/packages.html)里面有不同系统的版本。 + 以`Windows`为例,我们采用熟悉的`VisualSVN`. + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_1.png?raw=true) + 进入下载页后下载`Apache Subversion command line tools`, 解压即可。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_2.png?raw=true) + + - 方法二 + `Windows`下的`Tortoise SVN`也是带有`command line`的,但是安装的时候默认是不安装这个选项的,所以安装时要注意选择一下。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_5.png?raw=true) + 选择安装即可 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_6.png?raw=true) + + - 配置`SVN` + 进入设置中心,搜索`Version Control`后选择`Subversion`, 将右侧的`Use command line client`设置你本地的`command line`路径即可。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_3.png?raw=true) + 如果是用第二种方式安装`Tortoise SVN`的话地址就是: + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_4.png?raw=true) + PS: 设置成自己的路径啊,不要写我的... + +- `Git` + 安装`Git`, [Git](http://git-scm.com/) + 选择相应系统版本安装即可。 + 安装完成后进入`Android Studio`设置中心-> `Version Control` -> `Git`设置`Path to Git executable`即可。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_7.png?raw=true) + +- `Github` + 怎么能少了它呢?哈哈 + 这个就不用安装了,直接配置下用户名和密码就好了。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_8.png?raw=true) + +- `Checkout` + 在工具栏中点击 `VCS` 能看到相应检出和导入功能。这里以`Github`检出为例介绍一下。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_9.png?raw=true) + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_10.png?raw=true) + 确定之后就能在底部导航栏看到检出进度 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_11.png?raw=true) + + 完成之后会提示是否想要把刚才检出的项目导入`AndroidStudio`中。 + Why not? + 以`Gradle`方式导入, 然后`next`, 然后`next`然后就没有然后了。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_3_12.png?raw=true) + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" similarity index 98% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" index 90479053..beb44600 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\214\345\274\271).md" @@ -1,65 +1,65 @@ -AndroidStudio使用教程(第二弹) -=== - -- 迁移`Eclipse`工程到`Android Studio` - - 官方文档中说`Android Studio`可以兼容`Eclipse`的现有工程,但需要做一些操作: - - - `Eclipse`进行项目构建 - 首先升级`ADT`到最新版本, 好像是22之后,选择需要从`Eclipse`导出的工程,右键选择`Export`并选择`Android`下的`Generate Gradle Build Files`, - 运行完成之后你会发现在项目目录中多了一个`build.gradle`, 这就是`Android Studio`所识别的文件。 - PS:官方文档中说明如果没有`Grade build`文件,也是可以将项目导入到`Android Studio`中,它会用现有的`Ant build`文件进行配置. - 但为了更好地使用之后的功能和充分使用构建变量, - 还是强烈地建议先从`ADT`插件中生成`Gradle`文件再导入`Android Studio`. - - - 导入 - 在`Android Studio`中选择`Import Project`,并选择刚才工程目录下的`build.gradle`即可。 - - 有些时候会发现导入之后在运行按钮左边显示不出`Module`来,可能是你导入之前的`SDK`版本不同导致的,只要在`build.gradle`中配置相应的`SDK`版本就可以了。 - ```java - android { - compileSdkVersion 19 - buildToolsVersion "21.1.1" - ... - } - ``` - -- 创建工程 - 创建工程和`Eclipse`流程基本差不多,大家一看就明白了,这里就不说了。 - -- 使用`Android`项目视图 - 这里纯粹看个人爱好,不过对于标准的`AndroidStudio`工程,一般我们常用的部分,都在`Android`项目视图中显示出来了。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_1.png?raw=true) - 效果如下图: - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_2.png?raw=true) - -- 使用布局编辑器 - - 布局编辑器的适时和多屏幕预览的确是一个亮点。 - - - 点击布局页面右侧的`Preview`按钮,可以进行预览。 - - 想预览多屏幕效果时可以在预览界面设备的下拉菜单上选择`Preview All Screen Sizes`. - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_3.png?raw=true) - - - 选择主题 - - 想给应用设置一个主题,可以点击`Theme`图标![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_4.png?raw=true), - 就会显示出选择对话框。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_5.png?raw=true) - - - 国际化 - 对于国际化的适配选择国际化图标![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_6.png?raw=true), - 然后在弹出的列表中选择需要进行国际化的国家进行适配即可。 - -- 常用功能 - 有些人进来之后可能找不到`DDMS`了. - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_7.png?raw=true) - 上图中的三个图标分别为, `AVD Manager`、`SDK Manager`、`DDMS` - - - ---- - -- 邮箱 :charon.chui@gmail.com +AndroidStudio使用教程(第二弹) +=== + +- 迁移`Eclipse`工程到`Android Studio` + + 官方文档中说`Android Studio`可以兼容`Eclipse`的现有工程,但需要做一些操作: + + - `Eclipse`进行项目构建 + 首先升级`ADT`到最新版本, 好像是22之后,选择需要从`Eclipse`导出的工程,右键选择`Export`并选择`Android`下的`Generate Gradle Build Files`, + 运行完成之后你会发现在项目目录中多了一个`build.gradle`, 这就是`Android Studio`所识别的文件。 + PS:官方文档中说明如果没有`Grade build`文件,也是可以将项目导入到`Android Studio`中,它会用现有的`Ant build`文件进行配置. + 但为了更好地使用之后的功能和充分使用构建变量, + 还是强烈地建议先从`ADT`插件中生成`Gradle`文件再导入`Android Studio`. + + - 导入 + 在`Android Studio`中选择`Import Project`,并选择刚才工程目录下的`build.gradle`即可。 + + 有些时候会发现导入之后在运行按钮左边显示不出`Module`来,可能是你导入之前的`SDK`版本不同导致的,只要在`build.gradle`中配置相应的`SDK`版本就可以了。 + ```java + android { + compileSdkVersion 19 + buildToolsVersion "21.1.1" + ... + } + ``` + +- 创建工程 + 创建工程和`Eclipse`流程基本差不多,大家一看就明白了,这里就不说了。 + +- 使用`Android`项目视图 + 这里纯粹看个人爱好,不过对于标准的`AndroidStudio`工程,一般我们常用的部分,都在`Android`项目视图中显示出来了。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_1.png?raw=true) + 效果如下图: + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_2.png?raw=true) + +- 使用布局编辑器 + + 布局编辑器的适时和多屏幕预览的确是一个亮点。 + + - 点击布局页面右侧的`Preview`按钮,可以进行预览。 + + 想预览多屏幕效果时可以在预览界面设备的下拉菜单上选择`Preview All Screen Sizes`. + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_3.png?raw=true) + + - 选择主题 + + 想给应用设置一个主题,可以点击`Theme`图标![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_4.png?raw=true), + 就会显示出选择对话框。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_5.png?raw=true) + + - 国际化 + 对于国际化的适配选择国际化图标![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_6.png?raw=true), + 然后在弹出的列表中选择需要进行国际化的国家进行适配即可。 + +- 常用功能 + 有些人进来之后可能找不到`DDMS`了. + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_2_7.png?raw=true) + 上图中的三个图标分别为, `AVD Manager`、`SDK Manager`、`DDMS` + + + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" similarity index 97% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" index c844f828..4901e48c 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\344\272\224\345\274\271).md" @@ -1,185 +1,185 @@ -AndroidStudio使用教程(第五弹) -=== - -Create and Build an Android Studio Project ---- - -接下来是以下这四个部分: -- Create projects and modules. -- Work with the project structure. -- Eidt build files to configure the build process. -- Build and run your app. - -关于如何创建`Project`这里就不说了, 默认创建的`Project`中有一个`app`的`Module`。 - -Add a library module ---- - -接下来的部分说一下如何在`Project`中创建一个`library module`并且把该`library`变成程序的一个依赖`module`。 - -### Create a new library module - -- 点击`File`菜单后选择`New Module`或在`Project`上右键选`New Module`. -- 展开页面下方的`More Modules`选择`Android Library`后`Next`. -- 输入名字这里为了演示方便名字叫做`mylibrary`后一直`Next`即可. -完成之后打开该`Module`中的`build.gradle`你会看到`apply plugin: 'com.android.library'`说明这是一个`library`. - -### Add a dependency on a library module -上一步我们创建了`mylibrary module`, 现在我们想让`app module`依赖与`mylibrary module`, 但是构建系统还不知道, -我们需要修改`app module`下的`build.gradle`文件添加`mylibrary module`就可以了。 - -```xml -... -dependencies { - ... - compile project(":mylibrary") -} -``` - -### Build the project in Android Studio -`Android Studio`中`build project`点击上面导航栏中的`Build`菜单然后选择`Make Project`, 这时窗口底部的状态栏就会显示`build`的进度。 -点击窗口右边底部的![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_2.png)图标来显示`Gradle Console`. -![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_3.png?raw=true) - -在窗口右边栏点击`Gradle`窗口可以看到当前所有可用的`build tasks`, 双击里面的`task`即可执行。 -![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_4.png?raw=true) - -### Build a release version -点击`Gradle tasks`页面, 展开`app`中的`task`然后双击`assembleRelease`即可。 - -Configure the Build ---- - -接下来以`MyApplication Project`说明以下几个部分: -- Use the syntax from the Android plugin for Gradle in build files. -- Declare dependencies. -- Configure ProGurad settings. -- Configure signing settings. -- Work with build variants. - -###Build file basics -`Android Studio projects`中包含一个`build file`,每个`module`中也有一个`build file`名字为`build.gradle`. -下面是`Project`中`app module`的`build.gradle`文件 -``` -apply plugin: 'com.android.application' - -android { - compileSdkVersion 19 - buildToolsVersion "21.1.1" - - defaultConfig { - applicationId "com.charon.myapplication" - minSdkVersion 15 - targetSdkVersion 19 - versionCode 1 - versionName "1.0" - } - buildTypes { - release { - runProguard false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile project(":mylibrary") -} - -``` -```apply plugin: 'com.android.application’``` 声明了`Gradle`的类型为`Android`应用程序,这样会在最高级的`build tasks`中添加 -一个`Android`程序特有的`build`任务并且创建`android {…}`来声明`Android`程序特殊的`build`配置。 -`android {…}`部分配置了所有`Android`程序的`build`配置: -- `compileSdkVersion `表明了编译的目标`SDK`版本。 -- `buildToolsVersion` 声明了当前 `build`的版本, 可以使用`SDK Manager`来下载多个`build`版本。 - **注意:**最好使用高版本的`build`工具或者是和编译是目标`SDK`版本对应的`build`版本。 -- `defaultConfig`配置了`AndroidManifest.xml`中的重要设置。 -- `buildTyppes`部分控制着如何去`build`和打包你的程序,默认时会定义两种`build`类型:`debug 和 release`. `debug`类型带有默认的 -`debugging`标示,用`debug key`进行签名, `release`版本默认时没有签名,上面的配置中`release`时没有使用`ProGuard`. -`dependencies`部分是在`android`之外,该部分声明了依赖的`module`。 -**注意:**当修改项目中得`build files`时,`Android Studio`需要进行项目同步来导入相应的`build`配置变化, 点击`Android Studio`中黄色通知部分的`Sync Now` -来进行变化的导入。 -![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_5.png) - -###Declare dependencies -```java -dependencies { - // Module dependency - compile project(":lib") - - // Remote binary dependency - compile 'com.android.support:appcompat-v7:19.0.1' - - // Local binary dependency - compile fileTree(dir: 'libs', include: ['*.jar']) -} -``` -- Module dependency - 为本地依赖的`Module`. -- Remote binary dependency - 为远程依赖的二进制文件, 例子中为`Android SDK`仓库中所有的`support v7`包。 -- Local binary dependency - 为本地项目中依赖的`jar`包,这些`jar`包是在项目中的`libs`目录中。 - -###Run ProGuard -构建过程中可以使用`ProGuard`进行代码混淆, 修改`build`文件中的`runProGuard`选项为`true`即可。 -```java - -... -android { - ... - buildTypes { - release { - runProguard true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} -... -``` -```getDefaultProguardFile(`proguard-android.txt`)```包含了`Android SDK`安装时默认的`ProGuard`设置。`Android Studio`在`module`的根目录中 -添加了`proguard-rules.pro`文件,可以在这里配置相应的`ProGuard`规则。 - - -###Configure signing settings -`debug`版本和`release`版本的应用区别在于应用程序能不能在一些稳定的设备上进行`debug`和`APK`是怎样进行签名的。 构建系统对`debug`版本使用默认的签名并且使用已知的 -证书以便在构建过程中不会进行代码提示。如果你不指定签名配置时构建系统不会对`release`版本进行签名。 -下面是如何让程序在`release`版本进行签名 -- 拷贝签名文件到`app module`的根目录。 - 这样就可以保证当你在其他机器上构建项目的时候可以找到你的签名文件, 如果你没有签名文件,可以先创建一个。 -- 在`app module`中的`build file`中配置签名选项。 - ```java - ... - android { - ... - defaultConfig { ... } - signingConfigs { - release { - storeFile file("myreleasekey.keystore") - storePassword "password" - keyAlias "MyReleaseKey" - keyPassword "password" - } - } - buildTypes { - release { - ... - signingConfig signingConfigs.release - } - } - } - ... - ``` - -- 在`Android Studio`中的`build task`页面运行`assembleRelease`。 -在`app/build/apk/app-release.apk`下的包现在就是使用签名文件签过名的了。 -**注意:**把签名密码等写到`build`文件中不是很安全, 可以把密码配置到环境变量中或者是让其在构建的过程中提示输入密码。 这里我们就先不介绍如何配置了,可以自己搜索下。 - -至于开始所说利用`Gradle`可以很简单的进行多渠道打包会在以后专门讲解, 这里先到此为止了。 - - ---- - -- 邮箱 :charon.chui@gmail.com +AndroidStudio使用教程(第五弹) +=== + +Create and Build an Android Studio Project +--- + +接下来是以下这四个部分: +- Create projects and modules. +- Work with the project structure. +- Eidt build files to configure the build process. +- Build and run your app. + +关于如何创建`Project`这里就不说了, 默认创建的`Project`中有一个`app`的`Module`。 + +Add a library module +--- + +接下来的部分说一下如何在`Project`中创建一个`library module`并且把该`library`变成程序的一个依赖`module`。 + +### Create a new library module + +- 点击`File`菜单后选择`New Module`或在`Project`上右键选`New Module`. +- 展开页面下方的`More Modules`选择`Android Library`后`Next`. +- 输入名字这里为了演示方便名字叫做`mylibrary`后一直`Next`即可. +完成之后打开该`Module`中的`build.gradle`你会看到`apply plugin: 'com.android.library'`说明这是一个`library`. + +### Add a dependency on a library module +上一步我们创建了`mylibrary module`, 现在我们想让`app module`依赖与`mylibrary module`, 但是构建系统还不知道, +我们需要修改`app module`下的`build.gradle`文件添加`mylibrary module`就可以了。 + +```xml +... +dependencies { + ... + compile project(":mylibrary") +} +``` + +### Build the project in Android Studio +`Android Studio`中`build project`点击上面导航栏中的`Build`菜单然后选择`Make Project`, 这时窗口底部的状态栏就会显示`build`的进度。 +点击窗口右边底部的![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_2.png)图标来显示`Gradle Console`. +![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_3.png?raw=true) + +在窗口右边栏点击`Gradle`窗口可以看到当前所有可用的`build tasks`, 双击里面的`task`即可执行。 +![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_4.png?raw=true) + +### Build a release version +点击`Gradle tasks`页面, 展开`app`中的`task`然后双击`assembleRelease`即可。 + +Configure the Build +--- + +接下来以`MyApplication Project`说明以下几个部分: +- Use the syntax from the Android plugin for Gradle in build files. +- Declare dependencies. +- Configure ProGurad settings. +- Configure signing settings. +- Work with build variants. + +###Build file basics +`Android Studio projects`中包含一个`build file`,每个`module`中也有一个`build file`名字为`build.gradle`. +下面是`Project`中`app module`的`build.gradle`文件 +``` +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "21.1.1" + + defaultConfig { + applicationId "com.charon.myapplication" + minSdkVersion 15 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(":mylibrary") +} + +``` +```apply plugin: 'com.android.application’``` 声明了`Gradle`的类型为`Android`应用程序,这样会在最高级的`build tasks`中添加 +一个`Android`程序特有的`build`任务并且创建`android {…}`来声明`Android`程序特殊的`build`配置。 +`android {…}`部分配置了所有`Android`程序的`build`配置: +- `compileSdkVersion `表明了编译的目标`SDK`版本。 +- `buildToolsVersion` 声明了当前 `build`的版本, 可以使用`SDK Manager`来下载多个`build`版本。 + **注意:**最好使用高版本的`build`工具或者是和编译是目标`SDK`版本对应的`build`版本。 +- `defaultConfig`配置了`AndroidManifest.xml`中的重要设置。 +- `buildTyppes`部分控制着如何去`build`和打包你的程序,默认时会定义两种`build`类型:`debug 和 release`. `debug`类型带有默认的 +`debugging`标示,用`debug key`进行签名, `release`版本默认时没有签名,上面的配置中`release`时没有使用`ProGuard`. +`dependencies`部分是在`android`之外,该部分声明了依赖的`module`。 +**注意:**当修改项目中得`build files`时,`Android Studio`需要进行项目同步来导入相应的`build`配置变化, 点击`Android Studio`中黄色通知部分的`Sync Now` +来进行变化的导入。 +![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_5_5.png) + +###Declare dependencies +```java +dependencies { + // Module dependency + compile project(":lib") + + // Remote binary dependency + compile 'com.android.support:appcompat-v7:19.0.1' + + // Local binary dependency + compile fileTree(dir: 'libs', include: ['*.jar']) +} +``` +- Module dependency + 为本地依赖的`Module`. +- Remote binary dependency + 为远程依赖的二进制文件, 例子中为`Android SDK`仓库中所有的`support v7`包。 +- Local binary dependency + 为本地项目中依赖的`jar`包,这些`jar`包是在项目中的`libs`目录中。 + +###Run ProGuard +构建过程中可以使用`ProGuard`进行代码混淆, 修改`build`文件中的`runProGuard`选项为`true`即可。 +```java + +... +android { + ... + buildTypes { + release { + runProguard true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} +... +``` +```getDefaultProguardFile(`proguard-android.txt`)```包含了`Android SDK`安装时默认的`ProGuard`设置。`Android Studio`在`module`的根目录中 +添加了`proguard-rules.pro`文件,可以在这里配置相应的`ProGuard`规则。 + + +###Configure signing settings +`debug`版本和`release`版本的应用区别在于应用程序能不能在一些稳定的设备上进行`debug`和`APK`是怎样进行签名的。 构建系统对`debug`版本使用默认的签名并且使用已知的 +证书以便在构建过程中不会进行代码提示。如果你不指定签名配置时构建系统不会对`release`版本进行签名。 +下面是如何让程序在`release`版本进行签名 +- 拷贝签名文件到`app module`的根目录。 + 这样就可以保证当你在其他机器上构建项目的时候可以找到你的签名文件, 如果你没有签名文件,可以先创建一个。 +- 在`app module`中的`build file`中配置签名选项。 + ```java + ... + android { + ... + defaultConfig { ... } + signingConfigs { + release { + storeFile file("myreleasekey.keystore") + storePassword "password" + keyAlias "MyReleaseKey" + keyPassword "password" + } + } + buildTypes { + release { + ... + signingConfig signingConfigs.release + } + } + } + ... + ``` + +- 在`Android Studio`中的`build task`页面运行`assembleRelease`。 +在`app/build/apk/app-release.apk`下的包现在就是使用签名文件签过名的了。 +**注意:**把签名密码等写到`build`文件中不是很安全, 可以把密码配置到环境变量中或者是让其在构建的过程中提示输入密码。 这里我们就先不介绍如何配置了,可以自己搜索下。 + +至于开始所说利用`Gradle`可以很简单的进行多渠道打包会在以后专门讲解, 这里先到此为止了。 + + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" similarity index 98% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" index 9d92c1cc..fc09cb7e 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\205\255\345\274\271).md" @@ -1,45 +1,45 @@ -AndroidStudio使用教程(第六弹) -=== - -Debug ---- - -`Andorid Studio`中进行`debug`: -- 在`Android Studio`中打开应用程序。 -- 点击状态栏中的`Debug`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_1.png?raw=true)图标。 -- 在接下来的选择设备窗口选择相应的设备或创建虚拟机, 点击`OK`即可。 -`Android Studio`在`debug`时会打开`Debug`工具栏, 可以点击`Debug`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_2.png?raw=true)图标打开`Debug`窗口。 - -###设置断点 -与`Eclipse`十分相似, 在代码左侧位置点击一下即可, 圆点的颜色变了。 - -###Attach the debugger to a running process -在`debug`时不用每次都去重启应用程序。 我们可以对正在运行的程序进行`debug`: -- 点击`Attach debugger to Android proccess`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_3.png?raw=true)图标。 -- 设备选择窗口选择想要`debug`的设备。 -- 点击`Debug`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_2.png?raw=true)图标打开`Debug`工具栏。 -- 在`Debug`工具栏中有`Stop Over`, `Stop Into`等图标和快捷键,这些就不仔细说明了, 和`Eclipse`都差不多。 - -### View the system log -在`Android DDMS`中和`Debug`工具栏中都可以查看系统`log`日志, -- 在窗口底部栏点击`Android`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_4.png?raw=true) 图标打开`Android DDMS`工具栏。 -- 如果此时`Logcat`窗口中的日志是空得,点击`Restart`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_5.png?raw=true)图标。 -- 如果想要只显示当前某个进程的信息点击`Only Show Logcat from Selected Process`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_6.png?raw=true). 如果当时设备窗口不可见,点击右上角的`Restore Devices View`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_7.png?raw=true)图标,该图标只有设备窗口不可见时才会显示。 - -删除`Project`及`Module` ---- - -很多人都在问`AndroidStudio`中如何删除`Project`,如何删除`Module`?怎么和`Eclipse`不同啊,找不到`delete`或`remove`选项。 - - 删除`Project` - 点击左侧`File`-->`Close project`,关闭当前工程, 然后直接找到工程所在本地文件进行删除(慎重啊), 删除完之后点击最近列表中的该项目就会提示不存在,我们把他从最近项目中移除即可.![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_11.png?raw=true)你会发现,点击`remove`之后没效果,以后估计会解决。 - - 删除`Module` - 在该`Module`邮件选择`Open Module Settings`。 - ![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_8.png?raw=true) - 进入设置页后选中要删除的`Module`点击左上角的删除图标`-`后点击确定。 - ![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_9.png?raw=true) - - ---- - -- 邮箱 :charon.chui@gmail.com +AndroidStudio使用教程(第六弹) +=== + +Debug +--- + +`Andorid Studio`中进行`debug`: +- 在`Android Studio`中打开应用程序。 +- 点击状态栏中的`Debug`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_1.png?raw=true)图标。 +- 在接下来的选择设备窗口选择相应的设备或创建虚拟机, 点击`OK`即可。 +`Android Studio`在`debug`时会打开`Debug`工具栏, 可以点击`Debug`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_2.png?raw=true)图标打开`Debug`窗口。 + +###设置断点 +与`Eclipse`十分相似, 在代码左侧位置点击一下即可, 圆点的颜色变了。 + +###Attach the debugger to a running process +在`debug`时不用每次都去重启应用程序。 我们可以对正在运行的程序进行`debug`: +- 点击`Attach debugger to Android proccess`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_3.png?raw=true)图标。 +- 设备选择窗口选择想要`debug`的设备。 +- 点击`Debug`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_2.png?raw=true)图标打开`Debug`工具栏。 +- 在`Debug`工具栏中有`Stop Over`, `Stop Into`等图标和快捷键,这些就不仔细说明了, 和`Eclipse`都差不多。 + +### View the system log +在`Android DDMS`中和`Debug`工具栏中都可以查看系统`log`日志, +- 在窗口底部栏点击`Android`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_4.png?raw=true) 图标打开`Android DDMS`工具栏。 +- 如果此时`Logcat`窗口中的日志是空得,点击`Restart`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_5.png?raw=true)图标。 +- 如果想要只显示当前某个进程的信息点击`Only Show Logcat from Selected Process`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_6.png?raw=true). 如果当时设备窗口不可见,点击右上角的`Restore Devices View`![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_7.png?raw=true)图标,该图标只有设备窗口不可见时才会显示。 + +删除`Project`及`Module` +--- + +很多人都在问`AndroidStudio`中如何删除`Project`,如何删除`Module`?怎么和`Eclipse`不同啊,找不到`delete`或`remove`选项。 + - 删除`Project` + 点击左侧`File`-->`Close project`,关闭当前工程, 然后直接找到工程所在本地文件进行删除(慎重啊), 删除完之后点击最近列表中的该项目就会提示不存在,我们把他从最近项目中移除即可.![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_11.png?raw=true)你会发现,点击`remove`之后没效果,以后估计会解决。 + - 删除`Module` + 在该`Module`邮件选择`Open Module Settings`。 + ![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_8.png?raw=true) + 进入设置页后选中要删除的`Module`点击左上角的删除图标`-`后点击确定。 + ![Image](https://github.com/CharonChui/Pictures/blob/master/AndroidStudio_6_9.png?raw=true) + + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" similarity index 98% rename from "Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" rename to "AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" index 09c7716b..23feeb94 100644 --- "a/Android\345\237\272\347\241\200/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" +++ "b/AndroidBasicPart/AndroidStudio\344\275\277\347\224\250\346\225\231\347\250\213(\347\254\254\345\233\233\345\274\271).md" @@ -1,130 +1,130 @@ -AndroidStudio使用教程(第四弹) -=== - -Gradle ---- - -讲解到这里我感觉有必要说明一下`Gradle`。 -`Gradle`是一个基于`Apache Ant`和`Apache Maven`概念的项目自动化建构工具。它使用一种基于`Groovy`的特定领域语言来声明项目设置,而不是传统的`XML`. -更多介绍请直接参考[Gradle](http://www.gradle.org/)或`Google`搜索。 - -以下是为什么Android Studio选择Gradle的主要原因: -- 使用领域专用语言(Domain Specific Language)来描述和处理构建逻辑。(以下简称DSL) -- 基于Groovy。DSL可以混合各种声明元素,用代码操控这些DSL元素达到逻辑自定义。 -- 支持已有的Maven或者Ivy仓库基础建设 -- 非常灵活,允许使用best practices,并不强制让你遵照它的原则来。 -- 其它插件时可以暴露自己的DSL和API来让Gradle构建文件使用。 -- 允许IDE集成,是很好的API工具 - -Overview ---- - -`AndroidStudio build`系统是一个你可以用来`build, test, run, package your apps`的工具。 `build`系统与`Android Studio`之间是独立的, -所以你可以在`Android Studio`中或者 -`command line`中去执行。写完自己的应用程序之后,你可以用`build`系统来做以下事情:      -- 自定义、配置和扩展`build`过程.。 -- 用同一个工程根据不同的特性来创建多个`APK`。 -- 重复利用代码和资源。 -`Android Studio`灵活的`build`系统能让你不用修改项目和核心文件而完成上面所有的工作。 - -Overview of the Build System ---- - -`Android Studio`的构建系统包含一个`Gradle`的`Android`插件,`Gradle`是一个管理依赖关系并且允许自定义构建逻辑的高级构建工具。 许多软件都用`Gradle`来进行构建。 -`Gradle`的`Android`插件并不依赖`Android Studio`, 虽然`Android Studio`全部集成了`Gralde`, 这就意味着:     -- 你可以用用命令行的方式去构建`Android`应用或者是在一些没有安装`Android Studio`的机器上。 -- 你可以用命令行构建时的配置和逻辑来在`Android Studio`中进行构建`Android`项目。 -不管你是通过命令行还是远程机器或者是`Android Studio`来进行构建的产出都是一致的。 - -Build configuration ---- - -项目的构建配置都在`Gralde build files`中, 都是些符合`Gradle`要求的选项和语法,`Android`插件并不依赖`Android -通过`build`文件来配置一下几个方面: -- `Build variants` 构建系统能对同一个项目通过不同的配置生成多个`APK`,当你想构建多个不同版本时这是非常有用的,因为不用为他们建立多个不同的项目。 -- `Dependencies` 构建系统管理项目的依赖关系,并且支持本地已经远程仓库的依赖关系。 这样你就不用再去搜索、下载,然后再拷贝相应的包到你工程的目录了。 -- `Manifest entries` 构建系统能够通过构建配置去指定清单文件中中某些元素的值。 当你想生成一些包名、最低`SDK`版本或目标`SDK`版本不同的`APK`时是非常有用的。 -- `Signing` 构建系统能在`build`配置中设置指定的签名, 在构建的构成会给`APK`进行签名。 -- `ProGuard` 构建系统允许根据不同的构建配置设置不同的混淆规则, 在构建的过程中会运行`ProGuard`来对`class`文件进行混淆。 -- `Testing` 构建系统会根据项目中的测试代码生成一个测试`APK`, 这样就不需要创建一个单独的测试工程了, 在构建的过程中会运行相应的测试功能。 -`Gradle`构建文件使用`Groovy`语法,`Groovy`是一门可以自定义构建逻辑并且能通过`Gradle`的`Android`插件与`Android`一些特定元素通信的语言。 - -Build by convention ---- - -`Android Studio`构建系统对项目结构和一些其他的构建选项做了一些很好的默认规范声明,如果你的项目符合这些条件,`Gradle`的构建文件就非常简单了。 如果你的项目不符合 -其中的一些要求, 灵活的构建系统也允许你去配置几乎所有的构建选项。例如你项目的源码没有放在默认的文件夹中,你可以通过`build`文件去配置它的位置。 - -Projects and modules ---- - -`Android Studio`中的`Project`代表了一个完整的`Android`应用,每个`Project`中可以有一个或多个`Module`。 `Module`是应用中可以单独`build, test`或`debug`的组件。 -`Module`中包含应用中的源码和资源文件, `Android Studio`中的`Project`包含以下三种`Module`: -- 包含可复用代码的`Java library modules`. 构建系统对`Java library module`会生成一个`JAR`包。 -- 有可复用`Android`代码和资源的`Android library modules`. 对该`library modules`构建系统会生成一个`AAR(Android ARchive)`包。 -- 有应用代码或者也可能是依赖其他`library modules`的`Android application modules`, 虽然很很多`Android`应用都只包含一个`application module`. -对于`application modules` -构建系统会生成一个`APK`包。 - -`Android Studio projects`在`project`的最外城都包含一个列出所有`modules`的`Gradle build file`, 每个`module`也都包含自己的`Gradle build file`. - -Dependencies ---- - -`Android Studio`的构建系统管理着依赖项目并且支持`module`依赖, 本地二进制文件依赖和远程二进制文件的依赖。 - -- `Module Dependencies` - 一个项目的`module`可以在构建未见中包含一系列所依赖的其他`modules`, 在你构建这个`module`的时候,系统回去组装这些所包含的`modules`. -- `Local Dependencies` - 如果本地文件系统中有`module`所依赖的二进制包如`JAR`包, 你可以在该`module`中的构建文件中声明这些依赖关系。 -- `Remote Dependencies` - 当你的依赖是在远程仓库中,你不需要去下载他们然后拷贝到自己的工程中。 `Android Studio`支持远程`Maven`依赖。 `Maven`是一个流行的项目管理工具, - 它可以使用仓库帮助组织项目依赖。 - - 许多优秀的软件类库和工具都在公共的`Maven`仓库中, 对于这些依赖只需按照远程仓库中不同元素的定义来指定他们的`Moven`位置即可。 - 构建系统使用的`Maven`位置格式是`group:name:version`. 例如`Google Guava`16.0.1版本类库的`Maven`坐标是 - `com.google.guava:guava:16.0.1`. - - ` Maven Central Repository`现在被广泛用于分发许多类库和工具。 - -下面分别为以上三种依赖关系的配置; -``` -dependencies { - compile project(":name") - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.google.guava:guava:16.0.1' -} -``` - -Build tasks ---- - -`Android Studio`构建系统定义了一些列的构建任务, 高级别的任务调用一些产出必要输出的任务。 构建系统提供了`project tasks`来构建`app`和`module tasks` -来独立的构建`modules`. - -可以通过`Andorid Studio`或者命令行看到当前可用任务的列表,并且执行里面的任务。 - -The Gradle wrapper ---- - -`Android Studio`项目包含了`Gradle wrapper`, 包括:      -- A JAR file -- A properties file -- A shell script for Windows platforms -- A shell script for Mac and Linux platforms -*声明:*需要把这些文件提交到代码控制系统。 - -使用`Gradle wrapper`(而不用本地安装的`Gradle`)能确保经常运行配置文件中配置的`Gralde`版本。 通过在配置文件中定义最新的版本来确保你的工程一直使用最新版的`Gradle`。 - -`Android Studio`从你项目中的`Gradle wrapper`目录读取配置文件,并且在该目录运行`wrapper`, 这样在处理多个需要不同`Gradle`版本的项目时就会游刃有余。 -*声明:*`Android Studio`不使用`shell`脚本,所以对于他们的任何改变在`IDE`构建时都不会生效,你应该在`Gradle build files`中去设置自定义的逻辑。 - -你可以在开发及其或者是一些没有安装`Android Studio`的及其上使用命令行运行`shell`脚本来构建项目。 - -直接上图: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_4_1.png?raw=true) - ---- - -- 邮箱 :charon.chui@gmail.com +AndroidStudio使用教程(第四弹) +=== + +Gradle +--- + +讲解到这里我感觉有必要说明一下`Gradle`。 +`Gradle`是一个基于`Apache Ant`和`Apache Maven`概念的项目自动化建构工具。它使用一种基于`Groovy`的特定领域语言来声明项目设置,而不是传统的`XML`. +更多介绍请直接参考[Gradle](http://www.gradle.org/)或`Google`搜索。 + +以下是为什么Android Studio选择Gradle的主要原因: +- 使用领域专用语言(Domain Specific Language)来描述和处理构建逻辑。(以下简称DSL) +- 基于Groovy。DSL可以混合各种声明元素,用代码操控这些DSL元素达到逻辑自定义。 +- 支持已有的Maven或者Ivy仓库基础建设 +- 非常灵活,允许使用best practices,并不强制让你遵照它的原则来。 +- 其它插件时可以暴露自己的DSL和API来让Gradle构建文件使用。 +- 允许IDE集成,是很好的API工具 + +Overview +--- + +`AndroidStudio build`系统是一个你可以用来`build, test, run, package your apps`的工具。 `build`系统与`Android Studio`之间是独立的, +所以你可以在`Android Studio`中或者 +`command line`中去执行。写完自己的应用程序之后,你可以用`build`系统来做以下事情:      +- 自定义、配置和扩展`build`过程.。 +- 用同一个工程根据不同的特性来创建多个`APK`。 +- 重复利用代码和资源。 +`Android Studio`灵活的`build`系统能让你不用修改项目和核心文件而完成上面所有的工作。 + +Overview of the Build System +--- + +`Android Studio`的构建系统包含一个`Gradle`的`Android`插件,`Gradle`是一个管理依赖关系并且允许自定义构建逻辑的高级构建工具。 许多软件都用`Gradle`来进行构建。 +`Gradle`的`Android`插件并不依赖`Android Studio`, 虽然`Android Studio`全部集成了`Gralde`, 这就意味着:     +- 你可以用用命令行的方式去构建`Android`应用或者是在一些没有安装`Android Studio`的机器上。 +- 你可以用命令行构建时的配置和逻辑来在`Android Studio`中进行构建`Android`项目。 +不管你是通过命令行还是远程机器或者是`Android Studio`来进行构建的产出都是一致的。 + +Build configuration +--- + +项目的构建配置都在`Gralde build files`中, 都是些符合`Gradle`要求的选项和语法,`Android`插件并不依赖`Android +通过`build`文件来配置一下几个方面: +- `Build variants` 构建系统能对同一个项目通过不同的配置生成多个`APK`,当你想构建多个不同版本时这是非常有用的,因为不用为他们建立多个不同的项目。 +- `Dependencies` 构建系统管理项目的依赖关系,并且支持本地已经远程仓库的依赖关系。 这样你就不用再去搜索、下载,然后再拷贝相应的包到你工程的目录了。 +- `Manifest entries` 构建系统能够通过构建配置去指定清单文件中中某些元素的值。 当你想生成一些包名、最低`SDK`版本或目标`SDK`版本不同的`APK`时是非常有用的。 +- `Signing` 构建系统能在`build`配置中设置指定的签名, 在构建的构成会给`APK`进行签名。 +- `ProGuard` 构建系统允许根据不同的构建配置设置不同的混淆规则, 在构建的过程中会运行`ProGuard`来对`class`文件进行混淆。 +- `Testing` 构建系统会根据项目中的测试代码生成一个测试`APK`, 这样就不需要创建一个单独的测试工程了, 在构建的过程中会运行相应的测试功能。 +`Gradle`构建文件使用`Groovy`语法,`Groovy`是一门可以自定义构建逻辑并且能通过`Gradle`的`Android`插件与`Android`一些特定元素通信的语言。 + +Build by convention +--- + +`Android Studio`构建系统对项目结构和一些其他的构建选项做了一些很好的默认规范声明,如果你的项目符合这些条件,`Gradle`的构建文件就非常简单了。 如果你的项目不符合 +其中的一些要求, 灵活的构建系统也允许你去配置几乎所有的构建选项。例如你项目的源码没有放在默认的文件夹中,你可以通过`build`文件去配置它的位置。 + +Projects and modules +--- + +`Android Studio`中的`Project`代表了一个完整的`Android`应用,每个`Project`中可以有一个或多个`Module`。 `Module`是应用中可以单独`build, test`或`debug`的组件。 +`Module`中包含应用中的源码和资源文件, `Android Studio`中的`Project`包含以下三种`Module`: +- 包含可复用代码的`Java library modules`. 构建系统对`Java library module`会生成一个`JAR`包。 +- 有可复用`Android`代码和资源的`Android library modules`. 对该`library modules`构建系统会生成一个`AAR(Android ARchive)`包。 +- 有应用代码或者也可能是依赖其他`library modules`的`Android application modules`, 虽然很很多`Android`应用都只包含一个`application module`. +对于`application modules` +构建系统会生成一个`APK`包。 + +`Android Studio projects`在`project`的最外城都包含一个列出所有`modules`的`Gradle build file`, 每个`module`也都包含自己的`Gradle build file`. + +Dependencies +--- + +`Android Studio`的构建系统管理着依赖项目并且支持`module`依赖, 本地二进制文件依赖和远程二进制文件的依赖。 + +- `Module Dependencies` + 一个项目的`module`可以在构建未见中包含一系列所依赖的其他`modules`, 在你构建这个`module`的时候,系统回去组装这些所包含的`modules`. +- `Local Dependencies` + 如果本地文件系统中有`module`所依赖的二进制包如`JAR`包, 你可以在该`module`中的构建文件中声明这些依赖关系。 +- `Remote Dependencies` + 当你的依赖是在远程仓库中,你不需要去下载他们然后拷贝到自己的工程中。 `Android Studio`支持远程`Maven`依赖。 `Maven`是一个流行的项目管理工具, + 它可以使用仓库帮助组织项目依赖。 + + 许多优秀的软件类库和工具都在公共的`Maven`仓库中, 对于这些依赖只需按照远程仓库中不同元素的定义来指定他们的`Moven`位置即可。 + 构建系统使用的`Maven`位置格式是`group:name:version`. 例如`Google Guava`16.0.1版本类库的`Maven`坐标是 + `com.google.guava:guava:16.0.1`. + + ` Maven Central Repository`现在被广泛用于分发许多类库和工具。 + +下面分别为以上三种依赖关系的配置; +``` +dependencies { + compile project(":name") + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.google.guava:guava:16.0.1' +} +``` + +Build tasks +--- + +`Android Studio`构建系统定义了一些列的构建任务, 高级别的任务调用一些产出必要输出的任务。 构建系统提供了`project tasks`来构建`app`和`module tasks` +来独立的构建`modules`. + +可以通过`Andorid Studio`或者命令行看到当前可用任务的列表,并且执行里面的任务。 + +The Gradle wrapper +--- + +`Android Studio`项目包含了`Gradle wrapper`, 包括:      +- A JAR file +- A properties file +- A shell script for Windows platforms +- A shell script for Mac and Linux platforms +*声明:*需要把这些文件提交到代码控制系统。 + +使用`Gradle wrapper`(而不用本地安装的`Gradle`)能确保经常运行配置文件中配置的`Gralde`版本。 通过在配置文件中定义最新的版本来确保你的工程一直使用最新版的`Gradle`。 + +`Android Studio`从你项目中的`Gradle wrapper`目录读取配置文件,并且在该目录运行`wrapper`, 这样在处理多个需要不同`Gradle`版本的项目时就会游刃有余。 +*声明:*`Android Studio`不使用`shell`脚本,所以对于他们的任何改变在`IDE`构建时都不会生效,你应该在`Gradle build files`中去设置自定义的逻辑。 + +你可以在开发及其或者是一些没有安装`Android Studio`的及其上使用命令行运行`shell`脚本来构建项目。 + +直接上图: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AndroidStudio_4_1.png?raw=true) + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" "b/AndroidBasicPart/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" similarity index 98% rename from "Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" rename to "AndroidBasicPart/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" index b0626692..ab179d5d 100644 --- "a/Android\345\237\272\347\241\200/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" +++ "b/AndroidBasicPart/Android\345\205\245\351\227\250\344\273\213\347\273\215.md" @@ -1,108 +1,108 @@ -Android入门介绍 -=== - -1. 3G、4G - - 第三代移动通信技术`(3rd - Generation)`,速率一般在几百`Kbps`,较之前的`2G`和`2.5G`在数据传输速度上有很大提升。 - - 第四代移动通信技术`(4th - Generation)`,速度可达到100`Mbps`以上,几乎可以满足人们的所有传输数据的需求。 - - 目前主流的`3G`技术标准有三种: - - `WCDMA`:全球80%以上的`3G`网络都是采用此种制式。中国联通运营。186 - - `CDMA2000`:目前日韩及北美使用较多。中国电信运营。 189 - - `TD-SCDMA`:中国自主知识产权的3G通信技术。中国移动运营。 188 - - 目前主流的`4G`技术为`LTE`,但还没有被广泛应用: - `GSM` → `GPRS` → `EDGE` → `WCDMA` → `HSDPA` → `HSDPA+` → `LTE` - -2. Android是什么 - 1. 手机设备的软件栈内存,包括 - - 一个完整的操作系统 - - 中间件 - - 关键的应用程序 - - 2. 底层是Linux内核 - - 安全管理 - - 内存管理 - - 进程管理 - - 电源管理 - - 硬件驱动 - -3. Android体系结构 - - Applications:桌面、电话、浏览器等应用程序 - - Applications Framework:ActivityManager、 WindowManager、ContentProvider、ResourceManager等 - - Libraries: SQLite库、SurfaceManager、WebKit、OppenGL等。 - - Android运行时 - - Core Libraries - - Dalvik Virtual Machine - - Linux Kernel: 硬件驱动、电源管理等 - -4. Dalvik VM和JVM的区别 - 1. 编译后文件的格式: - - JVM: .java->.class->.jar - - Dalvik: .java->.class->.dex->.odex - 2. 基于的架构不同 - - JVM基于栈的架构(栈内存) - - Dalvik基于寄存器的架构(CPU),执行效率比JVM要高 - 3. Dalvik专门针对移动平台进行优化 - JVM的jar包中会有很多class文件,每个class文件中都含有头信息、常量池、字段、方法等,而apk中只有一个dex,它里面包括了所有头信息、常量池、方法等。这样读取一个文件要比读取多个文件去找块。 - -5. CPU处理器架构 - 1. x86 - - intel - - AMD - 2. ARM - - 联发科 - - 高通 - - 海思 - - 三星 - -6. Android项目目录结构 - 1. src:源代码 - 2. gen:系统自动生成的文件,R.java 中记录了项目中各种资源ID - 3. res:系统资源,所有文件都会在R文件生成资源ID - - drawable:图片 - - layout:界面布局 - - values:数据 - - anim:定义动画的XML - - raw:原生文件 - 4. assets:资源路径,不会在R文件注册 - 5. project.properties:供Eclipse使用,读取该项目使用Android版本号,早期版本名为default.properties - 6. AndroidManifest.xml:清单文件,在软件安装的时候被读取 - Android中的四大组件(Activity、ContentProvider、BroadcastReceiver、Service)都需要在该文件中注册程序所需的权限也需要在此文件中声明,例如:电话、短信、互联网、访问SD卡 - 7. bin:二进制文件,包括class、资源文件、dex、apk等 - 8. proguard.cfg:用来混淆代码的配置文件,防止别人反编译 - -7. APK 安装过程 - 1. Eclipse将.java源文件编译成.class - 2. 使用dx工具将所有.class文件转换为.dex文件 - 3. 再将.dex文件和所有资源打包并且签名成.apk文件 - 4. 将.apk文件安装到虚拟机完成程序安装 - 5. 启动程序 – 开启进程 – 开启主线程 - 6. 创建Activity对象 – 执行OnCreate()方法 - 7. 按照main.xml文件初始化界面 - - 简单的来说软件的安装都是两个过程 - - 拷贝apk中的一些文件到系统的某个目录 - 1. `/data/app/`目录下 - 2. 创建一个文件夹 `/data/data/com.test.helloworld/`来保存数据 - - 在系统的packages.xml文件(类似于Windows的注册表)中里面配置应用权限等一些信息. `/data/system/packages.xml` - -8. Android安全学 - Android安全学中的一个重要的设计点是在默认情况下应用程序没有权限执行对其它应用程序、操作系统或用户有害的操作。 - 这些操作包括读/写用户的隐私数据(例如联系人或e-mail),读/写其它应用程序的文件,执行网络访问,保持设备活动,等等。 - 所有牵扯到付费或者可能与用户隐私相关的操作都要申请权限。 - -9. 测试分类 - 单元测试(Unit test) -> 功能测试( Function test) -> 集成测试(Intergation test) - -10. Android单元测试 - - AndroidManifest.xml中进行配置,导入android的junit环境 - - 编写测试类继承Android的测试父类,AndroidTestCase这个类( AndroidTestCase是为了去模拟一个手机的运行环境,这个类中有一个getContext方法能获取到当前测试类的应用上下文对象,所以这个方法必须要等到测试框架初始化完成后才可以去调用) - - 测试的方法名要求以小写的test开头,如不以test开头只能单独点这个方法运行,整体全部运行时没有这个方法,所有的测试方法都要抛出异常,要把异常抛给测试框架不能自己去捕获 - - 注意:测试的代码也是只能在手机上跑,它是在手机上测试完之后又将信息发送到了eclipse中 - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +Android入门介绍 +=== + +1. 3G、4G + - 第三代移动通信技术`(3rd - Generation)`,速率一般在几百`Kbps`,较之前的`2G`和`2.5G`在数据传输速度上有很大提升。 + - 第四代移动通信技术`(4th - Generation)`,速度可达到100`Mbps`以上,几乎可以满足人们的所有传输数据的需求。 + + 目前主流的`3G`技术标准有三种: + - `WCDMA`:全球80%以上的`3G`网络都是采用此种制式。中国联通运营。186 + - `CDMA2000`:目前日韩及北美使用较多。中国电信运营。 189 + - `TD-SCDMA`:中国自主知识产权的3G通信技术。中国移动运营。 188 + + 目前主流的`4G`技术为`LTE`,但还没有被广泛应用: + `GSM` → `GPRS` → `EDGE` → `WCDMA` → `HSDPA` → `HSDPA+` → `LTE` + +2. Android是什么 + 1. 手机设备的软件栈内存,包括 + - 一个完整的操作系统 + - 中间件 + - 关键的应用程序 + + 2. 底层是Linux内核 + - 安全管理 + - 内存管理 + - 进程管理 + - 电源管理 + - 硬件驱动 + +3. Android体系结构 + - Applications:桌面、电话、浏览器等应用程序 + - Applications Framework:ActivityManager、 WindowManager、ContentProvider、ResourceManager等 + - Libraries: SQLite库、SurfaceManager、WebKit、OppenGL等。 + - Android运行时 + - Core Libraries + - Dalvik Virtual Machine + - Linux Kernel: 硬件驱动、电源管理等 + +4. Dalvik VM和JVM的区别 + 1. 编译后文件的格式: + - JVM: .java->.class->.jar + - Dalvik: .java->.class->.dex->.odex + 2. 基于的架构不同 + - JVM基于栈的架构(栈内存) + - Dalvik基于寄存器的架构(CPU),执行效率比JVM要高 + 3. Dalvik专门针对移动平台进行优化 + JVM的jar包中会有很多class文件,每个class文件中都含有头信息、常量池、字段、方法等,而apk中只有一个dex,它里面包括了所有头信息、常量池、方法等。这样读取一个文件要比读取多个文件去找块。 + +5. CPU处理器架构 + 1. x86 + - intel + - AMD + 2. ARM + - 联发科 + - 高通 + - 海思 + - 三星 + +6. Android项目目录结构 + 1. src:源代码 + 2. gen:系统自动生成的文件,R.java 中记录了项目中各种资源ID + 3. res:系统资源,所有文件都会在R文件生成资源ID + - drawable:图片 + - layout:界面布局 + - values:数据 + - anim:定义动画的XML + - raw:原生文件 + 4. assets:资源路径,不会在R文件注册 + 5. project.properties:供Eclipse使用,读取该项目使用Android版本号,早期版本名为default.properties + 6. AndroidManifest.xml:清单文件,在软件安装的时候被读取 + Android中的四大组件(Activity、ContentProvider、BroadcastReceiver、Service)都需要在该文件中注册程序所需的权限也需要在此文件中声明,例如:电话、短信、互联网、访问SD卡 + 7. bin:二进制文件,包括class、资源文件、dex、apk等 + 8. proguard.cfg:用来混淆代码的配置文件,防止别人反编译 + +7. APK 安装过程 + 1. Eclipse将.java源文件编译成.class + 2. 使用dx工具将所有.class文件转换为.dex文件 + 3. 再将.dex文件和所有资源打包并且签名成.apk文件 + 4. 将.apk文件安装到虚拟机完成程序安装 + 5. 启动程序 – 开启进程 – 开启主线程 + 6. 创建Activity对象 – 执行OnCreate()方法 + 7. 按照main.xml文件初始化界面 + + 简单的来说软件的安装都是两个过程 + - 拷贝apk中的一些文件到系统的某个目录 + 1. `/data/app/`目录下 + 2. 创建一个文件夹 `/data/data/com.test.helloworld/`来保存数据 + - 在系统的packages.xml文件(类似于Windows的注册表)中里面配置应用权限等一些信息. `/data/system/packages.xml` + +8. Android安全学 + Android安全学中的一个重要的设计点是在默认情况下应用程序没有权限执行对其它应用程序、操作系统或用户有害的操作。 + 这些操作包括读/写用户的隐私数据(例如联系人或e-mail),读/写其它应用程序的文件,执行网络访问,保持设备活动,等等。 + 所有牵扯到付费或者可能与用户隐私相关的操作都要申请权限。 + +9. 测试分类 + 单元测试(Unit test) -> 功能测试( Function test) -> 集成测试(Intergation test) + +10. Android单元测试 + - AndroidManifest.xml中进行配置,导入android的junit环境 + - 编写测试类继承Android的测试父类,AndroidTestCase这个类( AndroidTestCase是为了去模拟一个手机的运行环境,这个类中有一个getContext方法能获取到当前测试类的应用上下文对象,所以这个方法必须要等到测试框架初始化完成后才可以去调用) + - 测试的方法名要求以小写的test开头,如不以test开头只能单独点这个方法运行,整体全部运行时没有这个方法,所有的测试方法都要抛出异常,要把异常抛给测试框架不能自己去捕获 + + 注意:测试的代码也是只能在手机上跑,它是在手机上测试完之后又将信息发送到了eclipse中 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" "b/AndroidBasicPart/Android\345\212\250\347\224\273.md" similarity index 100% rename from "Android\345\237\272\347\241\200/Android\345\212\250\347\224\273.md" rename to "AndroidBasicPart/Android\345\212\250\347\224\273.md" diff --git "a/Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" "b/AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" similarity index 97% rename from "Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" rename to "AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" index 4996bb4e..eb1ef070 100644 --- "a/Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" +++ "b/AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213ContentProvider.md" @@ -1,311 +1,311 @@ -Android四大组件之ContentProvider -=== - -ContentProvider ---- - -安卓应用程序默认是无法获取到其他程序的数据,这是安卓安全学的基石(沙盒原理)。但是经常我们需要给其他应用分享数据,内容提供者就是一个这种可以分享数据给其他应用的接口。 -可以简单的理解为,内容提供者就是一个可以在不同应用程序间共享数据的组件,相当于一个中间人,一个程序把数据暴露给这个中间人,另一个则通过这个中间人获取相应的数据. - -下面的这张图片能更直观的显示: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ContentProvider_1.png) - -- `ContentProvider`中的`getContext`和`AndroidTestCast`中的`getContext`方法一样,都是一个模拟的上下文,必须在该类初始化之后才会调用`setContext`方法将`context`设置成自己的成员变量中记录, - 所以对于获取`getContext`的时候只能放在方法内,不能放到成员位置,因为在成员上时是null,而在方法内调用时该类就会已经初始化完了 - -- `ContentProvider`中的`query()`后不能关闭数据库,因为其他的应用在调用该`query`方法时需要继续使用该返回值`Cursor`,所以不能关闭数据库,因为数据库关闭之后`Cursor`就不能用了, - `Cursor`中保存的数据其实是数据库的一个引用,如果数据库关了`Cursor`就不能找到里面的数据了,`Cursor.close()`只是释放·Cursor·用到的资源。说到这里就多数一句 - `According to Dianne Hackborn (Android framework engineer) there is no need to close the database in a content provider.`以为内容提供者是因为进程启动时便加载,之后就一直存在,当进程销毁 - 释放资源时会去关闭数据库。 - -- 如果数据是`SQLiteDatabase`,表中必须有一个`_id`的列,用来表示每条记录的唯一性。 - -1. 继承`ContentProvider`,并实现相应的方法。 - ```java - public class NoteProvider extends ContentProvider { - private static final int NOTES = 1; - private static final int NOTE_ID = 2; - - public static final String AUTHORITY = "com.charon.demo.provider.noteprovider"; - public static final String TABLE_NAME = "note"; - // 定义一个名为`CONTENT_URI`必须为其指定一个唯一的字符串值,最好的方案是以类的全名称 - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); - - // 声明一个路径的检查者,参数为Uri不匹配时的返回值 - // 虽然是中间人,但也不能谁要数据我们都给,所以要检查下,只有符合我们要求的人,我们才会给他数据。 - private static UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - private NoteSQLiteOpenHelper mSQLiteOpenHelper; - private SQLiteDatabase mSQLiteDatabase; - - static { - // 建立匹配规则,例如发现路径为ccom.charon.demo.provider.noteprovider/note/1表示要操作note表中id为1的记录 - sUriMatcher.addURI(AUTHORITY, TABLE_NAME, NOTES); - sUriMatcher.addURI(AUTHORITY, TABLE_NAME + "/#", NOTE_ID); - } - - /** - * Implement this to initialize your content provider on startup. - * This method is called for all registered content providers on the - * application main thread at application launch time. It must not perform - * lengthy operations, or application startup will be delayed. - *

- *

You should defer nontrivial initialization (such as opening, - * upgrading, and scanning databases) until the content provider is used - * (via {@link #query}, {@link #insert}, etc). Deferred initialization - * keeps application startup fast, avoids unnecessary work if the provider - * turns out not to be needed, and stops database errors (such as a full - * disk) from halting application launch. - *

- *

If you use SQLite, {@link android.database.sqlite.SQLiteOpenHelper} - * is a helpful utility class that makes it easy to manage databases, - * and will automatically defer opening until first use. If you do use - * SQLiteOpenHelper, make sure to avoid calling - * {@link android.database.sqlite.SQLiteOpenHelper#getReadableDatabase} or - * {@link android.database.sqlite.SQLiteOpenHelper#getWritableDatabase} - * from this method. (Instead, override - * {@link android.database.sqlite.SQLiteOpenHelper#onOpen} to initialize the - * database when it is first opened.) - * - * @return true if the provider was successfully loaded, false otherwise - */ - @Override - public boolean onCreate() { - mSQLiteOpenHelper = new NoteSQLiteOpenHelper(getContext()); - return true; - } - - /** - * 内容提供者暴露的查询的方法. - */ - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - mSQLiteDatabase = mSQLiteOpenHelper.getReadableDatabase(); - Cursor cursor; - // 1.重要的事情 ,检查 uri的路径. - switch (sUriMatcher.match(uri)) { - case NOTES: - break; - case NOTE_ID: - String id = uri.getLastPathSegment(); - if (TextUtils.isEmpty(selection)) { - selection = selection + "_id = " + id; - } else { - selection = selection + " and " + "_id = " + id; - } - break; - default: - throw new IllegalArgumentException("UnKnown Uri" + uri); - break; - } - cursor = mSQLiteDatabase.query(TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); - if (cursor != null) { - cursor.setNotificationUri(getContext().getContentResolver(), uri); - } - return cursor; - } - - /** - * Implement this to handle requests for the MIME type of the data at the - * given URI. The returned MIME type should start with - * vnd.android.cursor.item for a single record, - * or vnd.android.cursor.dir/ for multiple items. - * This method can be called from multiple threads, as described in - * Processes - * and Threads. - *

- *

Note that there are no permissions needed for an application to - * access this information; if your content provider requires read and/or - * write permissions, or is not exported, all applications can still call - * this method regardless of their access permissions. This allows them - * to retrieve the MIME type for a URI when dispatching intents. - * - * @param uri the URI to query. - * @return a MIME type string, or {@code null} if there is no type. - */ - @Override - public String getType(Uri uri) { - // 注释说的很清楚了,下面是常用的格式 - // 单个记录的IMEI类型 vnd.android.cursor.item/vnd.. - // 多个记录的IMEI类型 vnd.android.cursor.dir/vnd.. - switch (sUriMatcher.match(uri)) { - case NOTE_ID: - // 如果uri为 content://com.charon.demo.noteprovider/note/1 - return "vnd.android.cursor.item/vnd.charon.note"; - case NOTES: - return "vnd.android.cursor.dir/vnd.charon.note"; - default: - return null; - } - - // 这个MIME类型的作用是要匹配AndroidManifest.xml文件标签下标签的子标签的属性android:mimeType。 - // 如果不一致,则会导致对应的Activity无法启动。 - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - mSQLiteDatabase = mSQLiteOpenHelper.getWritableDatabase(); - switch (sUriMatcher.match(uri)) { - case NOTES: - break; - - case NOTE_ID: - break; - - default: - throw new IllegalArgumentException("UnKnown Uri" + uri); - break; - } - - long rowId = mSQLiteDatabase.insert(TABLE_NAME, null, values); - if (rowId > 0) { - Uri noteUri = ContentUris.withAppendedId(CONTENT_URI, rowId); - getContext().getContentResolver().notifyChange(noteUri, null); - return noteUri; - } - - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - mSQLiteDatabase = mSQLiteOpenHelper.getWritableDatabase(); - switch (sUriMatcher.match(uri)) { - case NOTES: - break; - - case NOTE_ID: - String id = uri.getLastPathSegment(); - if (TextUtils.isEmpty(selection)) { - selection = selection + "_id = " + id; - } else { - selection = selection + " and " + "_id = " + id; - } - break; - - default: - throw new IllegalArgumentException("UnKnown Uri" + uri); - break; - } - int count = mSQLiteDatabase.delete(TABLE_NAME, selection, selectionArgs); - getContext().getContentResolver().notifyChange(uri, null); - return count; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - switch (sUriMatcher.match(uri)) { - case NOTES: - - break; - - case NOTE_ID: - - break; - - default: - throw new IllegalArgumentException("UnKnown Uri" + uri); - break; - } - - mSQLiteDatabase = mSQLiteOpenHelper.getWritableDatabase(); - int update = mSQLiteDatabase.update(TABLE_NAME, values, selection, selectionArgs); - return update; - } - ``` - -2. 在清单文件中进行注册,并且指定其authorities - ```xml - - ``` - -3. 使用内容提供者获取数据,使用`ContentResolver`去操作`ContentProvider`, `ContentResolver`用于管理`ContentProvider`实例, - 并且可实现找到指定的`ContentProvider`并获取里面的数据 - ```java - public void query(View view){ - //得到内容提供者的解析器 中间人 - ContentResolver resolver = getContentResolver(); - Cursor cursor = resolver.query(NoteProvider.CONTENT_URI, null, null, null, null); - while(cursor.moveToNext()){ - String name = cursor.getString(cursor.getColumnIndex("name")); - int id = cursor.getInt(cursor.getColumnIndex("id")); - float money = cursor.getFloat(cursor.getColumnIndex("money")); - System.out.println("id="+id+",name="+name+",money="+money); - } - cursor.close(); - } - public void insert(View view){ - ContentResolver resolver = getContentResolver(); - ContentValues values = new ContentValues(); - values.put("name", "买洗头膏"); - values.put("money", 22.58f); - resolver.insert(NoteProvider.CONTENT_URI, values); - } - public void update(View view){ - ContentResolver resolver = getContentResolver(); - ContentValues values = new ContentValues(); - values.put("name", "买洗头膏"); - values.put("money", 42.58f); - resolver.update(NoteProvider.CONTENT_URI, values, "name=?", new String[]{"买洗头膏"}); - } - public void delete(View view){ - ContentResolver resolver = getContentResolver(); - resolver.delete(NoteProvider.CONTENT_URI, "name=?", new String[]{"买洗头膏"}); - } - ``` - -内容观察者 ---- - -内容观察者的原理: -`How a content provider actually stores its data under the covers is up to its designer. But all content providers implement a common interface for -querying the provider and returning results — as well as for adding, altering, and deleting data. -It's an interface that clients use indirectly, most generally through ContentResolver objects. -You get a ContentResolver by calling getContentResolver() from within the implementation of an Activity or other application component: -You can then use the ContentResolver's methods to interact with whatever content providers you're interested in.` - -1. 一方使用内容观察者去观察变化 - ```java - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - ContentResolver resolver = getContentResolver(); - resolver.registerContentObserver(NoteProvider.CONTENT_URI, true, new NoteObserver(new Handler())); - - } - - private class NoteObserver extends ContentObserver { - - public NoteObserver(Handler handler) { - super(handler); - - } - //当观察到数据发生变化的时候 会执行onchange方法. - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - Log.i(TAG,"发现有新的短信产生了..."); - //1.利用内容提供者 中间人 获取用户的短信数据. - ContentResolver resolver = getContentResolver(); - // .. 重新查询 - cursor = ...; - cursor.close(); - } - } - ``` - -2. 一方在发生变化的时候去发送改变的消息 - 对于一些系统的内容提供者内部都实现了该步骤,如果是自己写程序想要暴露就必须要加上该代码。 - - ```java - getContext().getContentResolver().notifyChange(uri, null); - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +Android四大组件之ContentProvider +=== + +ContentProvider +--- + +安卓应用程序默认是无法获取到其他程序的数据,这是安卓安全学的基石(沙盒原理)。但是经常我们需要给其他应用分享数据,内容提供者就是一个这种可以分享数据给其他应用的接口。 +可以简单的理解为,内容提供者就是一个可以在不同应用程序间共享数据的组件,相当于一个中间人,一个程序把数据暴露给这个中间人,另一个则通过这个中间人获取相应的数据. + +下面的这张图片能更直观的显示: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ContentProvider_1.png) + +- `ContentProvider`中的`getContext`和`AndroidTestCast`中的`getContext`方法一样,都是一个模拟的上下文,必须在该类初始化之后才会调用`setContext`方法将`context`设置成自己的成员变量中记录, + 所以对于获取`getContext`的时候只能放在方法内,不能放到成员位置,因为在成员上时是null,而在方法内调用时该类就会已经初始化完了 + +- `ContentProvider`中的`query()`后不能关闭数据库,因为其他的应用在调用该`query`方法时需要继续使用该返回值`Cursor`,所以不能关闭数据库,因为数据库关闭之后`Cursor`就不能用了, + `Cursor`中保存的数据其实是数据库的一个引用,如果数据库关了`Cursor`就不能找到里面的数据了,`Cursor.close()`只是释放·Cursor·用到的资源。说到这里就多数一句 + `According to Dianne Hackborn (Android framework engineer) there is no need to close the database in a content provider.`以为内容提供者是因为进程启动时便加载,之后就一直存在,当进程销毁 + 释放资源时会去关闭数据库。 + +- 如果数据是`SQLiteDatabase`,表中必须有一个`_id`的列,用来表示每条记录的唯一性。 + +1. 继承`ContentProvider`,并实现相应的方法。 + ```java + public class NoteProvider extends ContentProvider { + private static final int NOTES = 1; + private static final int NOTE_ID = 2; + + public static final String AUTHORITY = "com.charon.demo.provider.noteprovider"; + public static final String TABLE_NAME = "note"; + // 定义一个名为`CONTENT_URI`必须为其指定一个唯一的字符串值,最好的方案是以类的全名称 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); + + // 声明一个路径的检查者,参数为Uri不匹配时的返回值 + // 虽然是中间人,但也不能谁要数据我们都给,所以要检查下,只有符合我们要求的人,我们才会给他数据。 + private static UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + private NoteSQLiteOpenHelper mSQLiteOpenHelper; + private SQLiteDatabase mSQLiteDatabase; + + static { + // 建立匹配规则,例如发现路径为ccom.charon.demo.provider.noteprovider/note/1表示要操作note表中id为1的记录 + sUriMatcher.addURI(AUTHORITY, TABLE_NAME, NOTES); + sUriMatcher.addURI(AUTHORITY, TABLE_NAME + "/#", NOTE_ID); + } + + /** + * Implement this to initialize your content provider on startup. + * This method is called for all registered content providers on the + * application main thread at application launch time. It must not perform + * lengthy operations, or application startup will be delayed. + *

+ *

You should defer nontrivial initialization (such as opening, + * upgrading, and scanning databases) until the content provider is used + * (via {@link #query}, {@link #insert}, etc). Deferred initialization + * keeps application startup fast, avoids unnecessary work if the provider + * turns out not to be needed, and stops database errors (such as a full + * disk) from halting application launch. + *

+ *

If you use SQLite, {@link android.database.sqlite.SQLiteOpenHelper} + * is a helpful utility class that makes it easy to manage databases, + * and will automatically defer opening until first use. If you do use + * SQLiteOpenHelper, make sure to avoid calling + * {@link android.database.sqlite.SQLiteOpenHelper#getReadableDatabase} or + * {@link android.database.sqlite.SQLiteOpenHelper#getWritableDatabase} + * from this method. (Instead, override + * {@link android.database.sqlite.SQLiteOpenHelper#onOpen} to initialize the + * database when it is first opened.) + * + * @return true if the provider was successfully loaded, false otherwise + */ + @Override + public boolean onCreate() { + mSQLiteOpenHelper = new NoteSQLiteOpenHelper(getContext()); + return true; + } + + /** + * 内容提供者暴露的查询的方法. + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + mSQLiteDatabase = mSQLiteOpenHelper.getReadableDatabase(); + Cursor cursor; + // 1.重要的事情 ,检查 uri的路径. + switch (sUriMatcher.match(uri)) { + case NOTES: + break; + case NOTE_ID: + String id = uri.getLastPathSegment(); + if (TextUtils.isEmpty(selection)) { + selection = selection + "_id = " + id; + } else { + selection = selection + " and " + "_id = " + id; + } + break; + default: + throw new IllegalArgumentException("UnKnown Uri" + uri); + break; + } + cursor = mSQLiteDatabase.query(TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); + if (cursor != null) { + cursor.setNotificationUri(getContext().getContentResolver(), uri); + } + return cursor; + } + + /** + * Implement this to handle requests for the MIME type of the data at the + * given URI. The returned MIME type should start with + * vnd.android.cursor.item for a single record, + * or vnd.android.cursor.dir/ for multiple items. + * This method can be called from multiple threads, as described in + * Processes + * and Threads. + *

+ *

Note that there are no permissions needed for an application to + * access this information; if your content provider requires read and/or + * write permissions, or is not exported, all applications can still call + * this method regardless of their access permissions. This allows them + * to retrieve the MIME type for a URI when dispatching intents. + * + * @param uri the URI to query. + * @return a MIME type string, or {@code null} if there is no type. + */ + @Override + public String getType(Uri uri) { + // 注释说的很清楚了,下面是常用的格式 + // 单个记录的IMEI类型 vnd.android.cursor.item/vnd.. + // 多个记录的IMEI类型 vnd.android.cursor.dir/vnd.. + switch (sUriMatcher.match(uri)) { + case NOTE_ID: + // 如果uri为 content://com.charon.demo.noteprovider/note/1 + return "vnd.android.cursor.item/vnd.charon.note"; + case NOTES: + return "vnd.android.cursor.dir/vnd.charon.note"; + default: + return null; + } + + // 这个MIME类型的作用是要匹配AndroidManifest.xml文件标签下标签的子标签的属性android:mimeType。 + // 如果不一致,则会导致对应的Activity无法启动。 + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + mSQLiteDatabase = mSQLiteOpenHelper.getWritableDatabase(); + switch (sUriMatcher.match(uri)) { + case NOTES: + break; + + case NOTE_ID: + break; + + default: + throw new IllegalArgumentException("UnKnown Uri" + uri); + break; + } + + long rowId = mSQLiteDatabase.insert(TABLE_NAME, null, values); + if (rowId > 0) { + Uri noteUri = ContentUris.withAppendedId(CONTENT_URI, rowId); + getContext().getContentResolver().notifyChange(noteUri, null); + return noteUri; + } + + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + mSQLiteDatabase = mSQLiteOpenHelper.getWritableDatabase(); + switch (sUriMatcher.match(uri)) { + case NOTES: + break; + + case NOTE_ID: + String id = uri.getLastPathSegment(); + if (TextUtils.isEmpty(selection)) { + selection = selection + "_id = " + id; + } else { + selection = selection + " and " + "_id = " + id; + } + break; + + default: + throw new IllegalArgumentException("UnKnown Uri" + uri); + break; + } + int count = mSQLiteDatabase.delete(TABLE_NAME, selection, selectionArgs); + getContext().getContentResolver().notifyChange(uri, null); + return count; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + switch (sUriMatcher.match(uri)) { + case NOTES: + + break; + + case NOTE_ID: + + break; + + default: + throw new IllegalArgumentException("UnKnown Uri" + uri); + break; + } + + mSQLiteDatabase = mSQLiteOpenHelper.getWritableDatabase(); + int update = mSQLiteDatabase.update(TABLE_NAME, values, selection, selectionArgs); + return update; + } + ``` + +2. 在清单文件中进行注册,并且指定其authorities + ```xml + + ``` + +3. 使用内容提供者获取数据,使用`ContentResolver`去操作`ContentProvider`, `ContentResolver`用于管理`ContentProvider`实例, + 并且可实现找到指定的`ContentProvider`并获取里面的数据 + ```java + public void query(View view){ + //得到内容提供者的解析器 中间人 + ContentResolver resolver = getContentResolver(); + Cursor cursor = resolver.query(NoteProvider.CONTENT_URI, null, null, null, null); + while(cursor.moveToNext()){ + String name = cursor.getString(cursor.getColumnIndex("name")); + int id = cursor.getInt(cursor.getColumnIndex("id")); + float money = cursor.getFloat(cursor.getColumnIndex("money")); + System.out.println("id="+id+",name="+name+",money="+money); + } + cursor.close(); + } + public void insert(View view){ + ContentResolver resolver = getContentResolver(); + ContentValues values = new ContentValues(); + values.put("name", "买洗头膏"); + values.put("money", 22.58f); + resolver.insert(NoteProvider.CONTENT_URI, values); + } + public void update(View view){ + ContentResolver resolver = getContentResolver(); + ContentValues values = new ContentValues(); + values.put("name", "买洗头膏"); + values.put("money", 42.58f); + resolver.update(NoteProvider.CONTENT_URI, values, "name=?", new String[]{"买洗头膏"}); + } + public void delete(View view){ + ContentResolver resolver = getContentResolver(); + resolver.delete(NoteProvider.CONTENT_URI, "name=?", new String[]{"买洗头膏"}); + } + ``` + +内容观察者 +--- + +内容观察者的原理: +`How a content provider actually stores its data under the covers is up to its designer. But all content providers implement a common interface for +querying the provider and returning results — as well as for adding, altering, and deleting data. +It's an interface that clients use indirectly, most generally through ContentResolver objects. +You get a ContentResolver by calling getContentResolver() from within the implementation of an Activity or other application component: +You can then use the ContentResolver's methods to interact with whatever content providers you're interested in.` + +1. 一方使用内容观察者去观察变化 + ```java + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ContentResolver resolver = getContentResolver(); + resolver.registerContentObserver(NoteProvider.CONTENT_URI, true, new NoteObserver(new Handler())); + + } + + private class NoteObserver extends ContentObserver { + + public NoteObserver(Handler handler) { + super(handler); + + } + //当观察到数据发生变化的时候 会执行onchange方法. + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + Log.i(TAG,"发现有新的短信产生了..."); + //1.利用内容提供者 中间人 获取用户的短信数据. + ContentResolver resolver = getContentResolver(); + // .. 重新查询 + cursor = ...; + cursor.close(); + } + } + ``` + +2. 一方在发生变化的时候去发送改变的消息 + 对于一些系统的内容提供者内部都实现了该步骤,如果是自己写程序想要暴露就必须要加上该代码。 + + ```java + getContext().getContentResolver().notifyChange(uri, null); + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" "b/AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" similarity index 97% rename from "Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" rename to "AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" index fe270636..3f1d5a11 100644 --- "a/Android\345\237\272\347\241\200/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" +++ "b/AndroidBasicPart/Android\345\233\233\345\244\247\347\273\204\344\273\266\344\271\213Service.md" @@ -1,216 +1,216 @@ -Android四大组件之Service -=== - -服务的两种开启方式: ---- - -1. startService();开启服务. - 开启服务后 服务就会长期的后台运行,即使调用者退出了.服务仍然在后台继续运行.服务和调用者没有什么关系, 调用者是不可以访问服务里面的方法. -2. bindService();绑定服务. - 服务开启后,生命周期与调用者相关联.调用者挂了,服务也会跟着挂掉.不求同时生,但求同时死.调用者和服务绑定在一起,调用者可以间接的调用到服务里面的方法. - -AIDL ---- - -本地服务:服务代码在本应用中 -远程服务:服务在另外一个应用里面(另外一个进程里面) -aidl: android interface defination language -IPC implementation : inter process communication - -服务混合调用的生命周期 ---- - -开启服务后再去绑定服务然后再去停止服务,这时服务是无法停止了.必须先解绑服务然后再停止服务,在实际开发中会经常采用这种模式, -开启服务(保证服务长期后台运行) --> 绑定服务(调用服务的方法) --> 解绑服务(服务继续在后台运行) --> 停止服务(服务停止),服务只会被开启一次, -如果已经开启后再去执行开启操作是没有效果的。 - -绑定服务调用方法的原理 ---- - -1. 定义一个接口,里面定义一个方法 - ```java - public interface IService { - public void callMethodInService();//通过该类中提供一个方法,让自定 - 义的类实现这个接口 - } - ``` - -2. 在服务中自定义一个IBinder的实现类,让这个类继承Binder(Binder是IBinder的默认适配器),由于这个自定义类是私有的,为了其他类中能拿到该类, - 我们要定义一个接口,提供一个方法,让IBinder类去实现该接口,并在相应方法中调用自己要供别人调用的方法。 - ```java - public class TestService extends Service { - @Override - public IBinder onBind(Intent intent) { - System.out.println("onbind"); - return new MyBinder(); - } - - private class MyBinder extends Binder implements IService{ - public void callMethodInService(){ - //实现该方法,去调用服务中的方法 - methodInService(); - } - } - //服务中的方法 - public void methodInService(){ - Toast.makeText(this, "我是服务里面的春哥,巴拉布拉!", 0).show(); - } - } - ``` - -3. 服务的调用类中将`onServiceConnected`方法中的第二个参数强转成接口 - ```java - public class DemoActivity extends Activity { - private Intent intent; - private Myconn conn; - private IService iService; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - intent = new Intent(this,TestService.class); - setContentView(R.layout.main); - } - public void start(View view) { - startService(intent); - } - public void stop(View view) { - stopService(intent); - } - public void bind(View view) { - conn = new Myconn(); - //1.绑定服务 传递一个conn对象.这个conn就是一个回调接口 - bindService(intent, conn, Context.BIND_AUTO_CREATE); - } - public void unbind(View view) { - unbindService(conn); - } - //调用服务中的方法 - public void call(View view){ - iService.callMethodInService(); - } - private class Myconn implements ServiceConnection{ - //当服务被成功绑定的时候调用的方法. - @Override - public void onServiceConnected(ComponentName name, IBinder service) {//第二个参数就是服务中的onBind方法的返回值 - iService = (IService) service; - } - @Override - public void onServiceDisconnected(ComponentName name) { - } - } - } - ``` - -远程服务aidl ---- - -上面介绍了绑定服务调用服务中方法的原理,对于远程服务的绑定也是这样, -但是这个远程服务是在另外一个程序中的,在另外一个程序中定 义的这个接口, -在另外一个程序中是拿不到的,就算是我们在自己的应用 中也定义一个一模一样 -的接口,但是由于两个程序的报名不同,这两个接口也是不一样的,为了解决这个 -问题,谷歌的工程师给提供了aidl,我们将定义的这个接口的`.java`改成 `.aidl`, -然后将这个接口中的`权限修饰符`都**去掉**,在另一个程序中拷贝这个aidl文 -件,然后放到同一个包名中,由于`Android`中通过包名来区分应用程序,这两个 -`aidl`的包名一样,系统会认为两个程序中的接口是同一个,这样就能够在另一 -个程序中将参数强转成这个接口,在使用`aidl`文件拷贝到自己的工程之后会自动 -生成一个接口类,这个接口类中有 一个内部类`Stub`该类继承了`Binder`并实现了 -这个接口,所以我们在自定义 `IBinder的实现类`时只需让自定义的类继承`Stub类` -即可. - -1. 远程服务中定义一个接口,改成`aidl` - ```java - package com.seal.test.service; - - interface IService { - void callMethodInService(); - } - ``` - -2. 远程服务中定义一个`Ibinder的实现类`,让这个实现类继承上面接口的`Stub类`, -并在`onBind`方法中返回这个自定义类对象 - ```java - public class RemoteService extends Service { - @Override - public IBinder onBind(Intent intent) { - System.out.println("远程服务被绑定"); - return new MyBinder(); - } - private class MyBinder extends IService.Stub{ - @Override - public void callMethodInService() { - methodInService(); - } - } - public void methodInService(){ - System.out.println("我是远程服务里面的方法"); - } - } - ``` - -3. 在其他程序中想要绑定这个服务并且调用这个服务中的方法的时候首先要拷贝 -这个`aidl`文件到自己的工程,然后再`ServiceConnection`的实现类中将这个参数使 -用`asInterface`方法转成接口,通过这样来得到接口,从而调用接口中的方法 - ```java - public class CallRemoteActivity extends Activity { - private Intent service; - private IService iService; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main); - service = new Intent(); - service.setAction("com.itheima.xxxx"); - } - public void bind(View veiw){ - bindService(service, new MyConn(), BIND_AUTO_CREATE); - } - public void call(View view){ - try { - iService.callMethodInService(); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - private class MyConn implements ServiceConnection{ - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - iService = IService.Stub.asInterface(service); - } - @Override - public void onServiceDisconnected(ComponentName name) { - // TODO Auto-generated method stub - } - } - } - ``` - -最后说一下`IntentService`: - -`IntentService`是`Service`的子类,用来处理异步请求。客户端可以通过`startService(Intent)`方法将请求的`Intent`传递请求给`IntentService`, -`IntentService`会将该`Intent`加入到队列中,然后对每一个`Intent`开启一个`worker thread`来进行处理,执行完所有的工作之后自动停止`Service`。 -每一个请求都会在一个单独的`worker thread`中处理,不会阻塞应用程序的主线程。`IntentService` 实际上是`Looper`、`Handler`、`Service` 的集合体, -他不仅有服务的功能,还有处理和循环消息的功能. - -- Service: - 1. A Service is not a separate process. The Service object itself does not imply it is running in its own process; unless otherwise specified, - it runs in the same process as the application it is part of. - 2. A Service is not a thread. It is not a means itself to do work off of the main thread (to avoid Application Not Responding errors). -所以在`Service`中进行耗时的操作时必须要新开一个线程。 - -至于为什么要使用`Service`而不是`Thread`,这个主要的区别就是生命周期不同,`Service`是Android系统的一个组件,Android系统会尽量保持`Service`的长期后台运行, -即使内存不足杀死了该服务(很少会出现内存不足杀死服务的情况)也会在内存可用的时候去复活该服务,而`Thread`随后都会被杀死 -- IntentService - 1. IntentService is a base class for Services that handle asynchronous requests (expressed as Intents) on demand. - Clients send requests throughstartService(Intent) calls; - the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work. - 2. This "work queue processor" pattern is commonly used to offload tasks from an application's main thread. - The IntentService class exists to simplify this pattern and take care of the mechanics. - To use it, extend IntentService and implement onHandleIntent(Intent). IntentService will receive the Intents, launch a worker thread, - and stop the service as appropriate. - 3. All requests are handled on a single worker thread -- they may take as long as necessary (and will not block the application's main loop), - but only one request will be processed at a time. - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +Android四大组件之Service +=== + +服务的两种开启方式: +--- + +1. startService();开启服务. + 开启服务后 服务就会长期的后台运行,即使调用者退出了.服务仍然在后台继续运行.服务和调用者没有什么关系, 调用者是不可以访问服务里面的方法. +2. bindService();绑定服务. + 服务开启后,生命周期与调用者相关联.调用者挂了,服务也会跟着挂掉.不求同时生,但求同时死.调用者和服务绑定在一起,调用者可以间接的调用到服务里面的方法. + +AIDL +--- + +本地服务:服务代码在本应用中 +远程服务:服务在另外一个应用里面(另外一个进程里面) +aidl: android interface defination language +IPC implementation : inter process communication + +服务混合调用的生命周期 +--- + +开启服务后再去绑定服务然后再去停止服务,这时服务是无法停止了.必须先解绑服务然后再停止服务,在实际开发中会经常采用这种模式, +开启服务(保证服务长期后台运行) --> 绑定服务(调用服务的方法) --> 解绑服务(服务继续在后台运行) --> 停止服务(服务停止),服务只会被开启一次, +如果已经开启后再去执行开启操作是没有效果的。 + +绑定服务调用方法的原理 +--- + +1. 定义一个接口,里面定义一个方法 + ```java + public interface IService { + public void callMethodInService();//通过该类中提供一个方法,让自定 + 义的类实现这个接口 + } + ``` + +2. 在服务中自定义一个IBinder的实现类,让这个类继承Binder(Binder是IBinder的默认适配器),由于这个自定义类是私有的,为了其他类中能拿到该类, + 我们要定义一个接口,提供一个方法,让IBinder类去实现该接口,并在相应方法中调用自己要供别人调用的方法。 + ```java + public class TestService extends Service { + @Override + public IBinder onBind(Intent intent) { + System.out.println("onbind"); + return new MyBinder(); + } + + private class MyBinder extends Binder implements IService{ + public void callMethodInService(){ + //实现该方法,去调用服务中的方法 + methodInService(); + } + } + //服务中的方法 + public void methodInService(){ + Toast.makeText(this, "我是服务里面的春哥,巴拉布拉!", 0).show(); + } + } + ``` + +3. 服务的调用类中将`onServiceConnected`方法中的第二个参数强转成接口 + ```java + public class DemoActivity extends Activity { + private Intent intent; + private Myconn conn; + private IService iService; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + intent = new Intent(this,TestService.class); + setContentView(R.layout.main); + } + public void start(View view) { + startService(intent); + } + public void stop(View view) { + stopService(intent); + } + public void bind(View view) { + conn = new Myconn(); + //1.绑定服务 传递一个conn对象.这个conn就是一个回调接口 + bindService(intent, conn, Context.BIND_AUTO_CREATE); + } + public void unbind(View view) { + unbindService(conn); + } + //调用服务中的方法 + public void call(View view){ + iService.callMethodInService(); + } + private class Myconn implements ServiceConnection{ + //当服务被成功绑定的时候调用的方法. + @Override + public void onServiceConnected(ComponentName name, IBinder service) {//第二个参数就是服务中的onBind方法的返回值 + iService = (IService) service; + } + @Override + public void onServiceDisconnected(ComponentName name) { + } + } + } + ``` + +远程服务aidl +--- + +上面介绍了绑定服务调用服务中方法的原理,对于远程服务的绑定也是这样, +但是这个远程服务是在另外一个程序中的,在另外一个程序中定 义的这个接口, +在另外一个程序中是拿不到的,就算是我们在自己的应用 中也定义一个一模一样 +的接口,但是由于两个程序的报名不同,这两个接口也是不一样的,为了解决这个 +问题,谷歌的工程师给提供了aidl,我们将定义的这个接口的`.java`改成 `.aidl`, +然后将这个接口中的`权限修饰符`都**去掉**,在另一个程序中拷贝这个aidl文 +件,然后放到同一个包名中,由于`Android`中通过包名来区分应用程序,这两个 +`aidl`的包名一样,系统会认为两个程序中的接口是同一个,这样就能够在另一 +个程序中将参数强转成这个接口,在使用`aidl`文件拷贝到自己的工程之后会自动 +生成一个接口类,这个接口类中有 一个内部类`Stub`该类继承了`Binder`并实现了 +这个接口,所以我们在自定义 `IBinder的实现类`时只需让自定义的类继承`Stub类` +即可. + +1. 远程服务中定义一个接口,改成`aidl` + ```java + package com.seal.test.service; + + interface IService { + void callMethodInService(); + } + ``` + +2. 远程服务中定义一个`Ibinder的实现类`,让这个实现类继承上面接口的`Stub类`, +并在`onBind`方法中返回这个自定义类对象 + ```java + public class RemoteService extends Service { + @Override + public IBinder onBind(Intent intent) { + System.out.println("远程服务被绑定"); + return new MyBinder(); + } + private class MyBinder extends IService.Stub{ + @Override + public void callMethodInService() { + methodInService(); + } + } + public void methodInService(){ + System.out.println("我是远程服务里面的方法"); + } + } + ``` + +3. 在其他程序中想要绑定这个服务并且调用这个服务中的方法的时候首先要拷贝 +这个`aidl`文件到自己的工程,然后再`ServiceConnection`的实现类中将这个参数使 +用`asInterface`方法转成接口,通过这样来得到接口,从而调用接口中的方法 + ```java + public class CallRemoteActivity extends Activity { + private Intent service; + private IService iService; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + service = new Intent(); + service.setAction("com.itheima.xxxx"); + } + public void bind(View veiw){ + bindService(service, new MyConn(), BIND_AUTO_CREATE); + } + public void call(View view){ + try { + iService.callMethodInService(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + private class MyConn implements ServiceConnection{ + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + iService = IService.Stub.asInterface(service); + } + @Override + public void onServiceDisconnected(ComponentName name) { + // TODO Auto-generated method stub + } + } + } + ``` + +最后说一下`IntentService`: + +`IntentService`是`Service`的子类,用来处理异步请求。客户端可以通过`startService(Intent)`方法将请求的`Intent`传递请求给`IntentService`, +`IntentService`会将该`Intent`加入到队列中,然后对每一个`Intent`开启一个`worker thread`来进行处理,执行完所有的工作之后自动停止`Service`。 +每一个请求都会在一个单独的`worker thread`中处理,不会阻塞应用程序的主线程。`IntentService` 实际上是`Looper`、`Handler`、`Service` 的集合体, +他不仅有服务的功能,还有处理和循环消息的功能. + +- Service: + 1. A Service is not a separate process. The Service object itself does not imply it is running in its own process; unless otherwise specified, + it runs in the same process as the application it is part of. + 2. A Service is not a thread. It is not a means itself to do work off of the main thread (to avoid Application Not Responding errors). +所以在`Service`中进行耗时的操作时必须要新开一个线程。 + +至于为什么要使用`Service`而不是`Thread`,这个主要的区别就是生命周期不同,`Service`是Android系统的一个组件,Android系统会尽量保持`Service`的长期后台运行, +即使内存不足杀死了该服务(很少会出现内存不足杀死服务的情况)也会在内存可用的时候去复活该服务,而`Thread`随后都会被杀死 +- IntentService + 1. IntentService is a base class for Services that handle asynchronous requests (expressed as Intents) on demand. + Clients send requests throughstartService(Intent) calls; + the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work. + 2. This "work queue processor" pattern is commonly used to offload tasks from an application's main thread. + The IntentService class exists to simplify this pattern and take care of the mechanics. + To use it, extend IntentService and implement onHandleIntent(Intent). IntentService will receive the Intents, launch a worker thread, + and stop the service as appropriate. + 3. All requests are handled on a single worker thread -- they may take as long as necessary (and will not block the application's main loop), + but only one request will be processed at a time. + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" "b/AndroidBasicPart/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" similarity index 98% rename from "Android\345\237\272\347\241\200/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" rename to "AndroidBasicPart/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" index 0bd18a4d..f5a864c6 100644 --- "a/Android\345\237\272\347\241\200/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" +++ "b/AndroidBasicPart/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" @@ -1,622 +1,622 @@ -Android基础面试题 -=== - -没有删这套题,虽然都是网上找的,在刚开始找工作的时候这套题帮了我很多,那时候`Android`刚起步,很多家都是这一套面试题,我都是直接去了不看题画画一顿就写完了,哈哈 -现在估计没有公司会用这种笔试题了。还是留下来吧,回忆一下。 - -1. 下列哪些语句关于内存回收的说明是正确的? (b) - A、 程序员必须创建一个线程来释放内存 - B、 内存回收程序负责释放无用内存 - C、 内存回收程序允许程序员直接释放内存 - D、 内存回收程序可以在指定的时间释放内存对象 -2. 下面异常是属于Runtime Exception 的是(abcd)(多选) - A、ArithmeticException - B、IllegalArgumentException - C、NullPointerException - D、BufferUnderflowException -3. Math.round(11.5)等于多少(). Math.round(-11.5)等于多少(c) - A、11 ,-11 B、11 ,-12 C、12 ,-11 D、12 ,-12 -4. 下列程序段的输出结果是:(b) - ```java - void complicatedexpression_r(){ - int x=20, y=30; - boolean b; - b=x>50&&y>60||x>50&&y<-60||x<-50&&y>60||x<-50&&y<-60; - System.out.println(b); - } - ``` - A、true B、false C、1 D、011.activity -5. 对一些资源以及状态的操作保存,最好是保存在生命周期的哪个函数中进行(d) - A、onPause() B、onCreate() C、 onResume() D、onStart() -6. Intent传递数据时,下列的数据类型哪些可以被传递(abcd)(多选) - A、Serializable B、charsequence C、Parcelable D、Bundle -7. android 中下列属于Intent的作用的是(c) - A、实现应用程序间的数据共享 - B、是一段长的生命周期,没有用户界面的程序,可以保持应用在后台运行,而不会因为切换页面而消失 - C、可以实现界面间的切换,可以包含动作和动作数据,连接四大组件的纽带 - D、处理一个应用程序整体性的工作 -8. 下列属于SAX解析xml文件的优点的是(b) - A、将整个文档树在内存中,便于操作,支持删除,修改,重新排列等多种功能 - B、不用事先调入整个文档,占用资源少 - C、整个文档调入内存,浪费时间和空间 - D、不是长久驻留在内存,数据不是持久的,事件过后,若没有保存数据,数据就会消失 -9. 下面的对自定style的方式正确的是(a) - A、 - ```xml - - - - ``` - B、 - ```xml - - ``` - C、 - ```xml - - fill_parent - - ``` - D、 - ```xml - - - - ``` -10. 在android中使用Menu时可能需要重写的方法有(ac)。(多选) - A、onCreateOptionsMenu() - B、onCreateMenu() - C、onOptionsItemSelected() - D、onItemSelected() -11. 在SQL Server Management Studio 中运行下列T-SQL语句,其输出值(c) - `SELECT @@IDENTITY` - A、 可能为0.1 - B、 可能为3 - C、 不可能为-100 - D、 肯定为0 -12. 在SQL Server 2005中运行如下T-SQL语句,假定SALES表中有多行数据,执行查询之后的结果是(d)。 - ``` - BEGIN TRANSACTION A - Update SALES Set qty=30 WHERE qty<30 - BEGIN TRANSACTION B - Update SALES Set qty=40 WHERE qty<40 - Update SALES Set qty=50 WHERE qty<50 - Update SALES Set qty=60 WHERE qty<60 - COMMIT TRANSACTION B - COMMIT TRANSACTION A - ``` - A、SALES表中qty列最小值大于等于30 - B、SALES表中qty列最小值大于等于40 - C、SALES表中qty列的数据全部为50 - D、SALES表中qty列最小值大于等于60 -13. 在android中使用SQLiteOpenHelper这个辅助类时,可以生成一个数据库,并可以对数据库版本进行管理的方法可以是(ab) - A、getWriteableDatabase() - B、getReadableDatabase() - C、getDatabase() - D、getAbleDatabase() -14. android 关于service生命周期的onCreate()和onStart()说法正确的是(ad)(多选题) - A、当第一次启动的时候先后调用onCreate()和onStart()方法 - B、当第一次启动的时候只会调用onCreate()方法 - C、如果service已经启动,将先后调用onCreate()和onStart()方法 - D、如果service已经启动,只会执行onStart()方法,不在执行onCreate()方法 -15. 下面是属于GLSurFaceView特性的是(abc)(多选) - A、管理一个surface,这个surface就是一块特殊的内存,能直接排版到android的视图view上。 - B、管理一个EGL display,它能让opengl把内容渲染到上述的surface上。 - C、让渲染器在独立的线程里运作,和UI线程分离。 - D、可以直接从内存或者DMA等硬件接口取得图像数据 -16. 下面在AndroidManifest.xml文件中注册BroadcastReceiver方式正确的(a) - A、 - ```xml - - - - - - - ``` - B、 - ```xml - - - android:name="android.provider.action.NewBroad"/> - - - ``` - C、 - ```xml - - - - - ``` - D、 - ```xml - - - - android:name="android.provider.action.NewBroad"/> - - - - ``` -17. 关于ContenValues类说法正确的是(a) - A、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名是String类型,而值都是基本类型 - B、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名是任意类型,而值都是基本类型 - C、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名,可以为空,而值都是String类型 - D、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名是String类型,而值也是String类型 -18. 我们都知道Hanlder是线程与Activity通信的桥梁,如果线程处理不当,你的机器就会变得越慢,那么线程销毁的方法是(a) - A、onDestroy() - B、onClear() - C、onFinish() - D、onStop() -19. 下面退出Activity错误的方法是(c) - A、finish() - B、抛异常强制退出 - C、System.exit() - D、onStop() -20. 下面属于android的动画分类的有(ab)(多项) - A、Tween B、Frame C、Draw D、Animation -21. 下面关于Android dvm的进程和Linux的进程,应用程序的进程说法正确的是(d) - A、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,不一定拥有一个独立 的Dalvik虚拟机实例.而每一个DVM都是在Linux 中的一个进程, - 所以说可以认为是同一个概念. - B、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,不一定拥有一个独立的Dalvik虚拟机实例.而每一个DVM不一定都是在Linux 中的一个进程, - 所以说不是一个概念. - C、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例.而每一个DVM不一定都是在Linux 中的一个进程, - 所以说不是一个概念. - D、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的 Dalvik虚拟机实例.而每一个DVM都是在Linux 中的一个进程, - 所以说可以认为是同一个概念. -22. Android项目工程下面的assets目录的作用是什么(b) - A、放置应用到的图片资源。 - B、主要放置多媒体等数据文件 - C、放置字符串,颜色,数组等常量数据 - D、放置一些与UI相应的布局文件,都是xml文件 -23. 关于res/raw目录说法正确的是(a) - A、这里的文件是原封不动的存储到设备上不会转换为二进制的格式 - B、这里的文件是原封不动的存储到设备上会转换为二进制的格式 - C、这里的文件最终以二进制的格式存储到指定的包中 - D、这里的文件最终不会以二进制的格式存储到指定的包中 -24. 下列对android NDK的理解正确的是(abcd) - A、NDK是一系列工具的集合 - B、NDK 提供了一份稳定、功能有限的 API 头文件声明。 - C、使 “Java+C” 的开发方式终于转正,成为官方支持的开发方式 - D、NDK 将是 Android 平台支持 C 开发的开端 - -25. android中常用的四个布局是framlayout,linenarlayout,relativelayout和tablelayout。 -26. android 的四大组件是activiey,service,broadcast和contentprovide。 -27. java.io包中的objectinputstream和objectoutputstream类主要用于对对象(Object)的读写。 -28. android 中service的实现方法是:startservice和bindservice。 -29. activity一般会重载7个方法用来维护其生命周期,除了onCreate(),onStart(),onDestory() 外还有onrestart,onresume,onpause,onstop。 -30. android的数据存储的方式sharedpreference,文件,SQlite,contentprovider,网络。 -31. 当启动一个Activity并且新的Activity执行完后需要返回到启动它的Activity来执行 的回调函数是startActivityResult()。 -32. 请使用命令行的方式创建一个名字为myAvd,sdk版本为2.2,sd卡是在d盘的根目录下,名字为scard.img, 并指定屏幕大小HVGA.____________________________________。 -33. 程序运行的结果是:_____good and gbc__________。 - ```java - public class Example{ -   String str=new String("good"); -   char[]ch={'a','b','c'}; -   public static void main(String args[]){ -     Example ex=new Example(); -     ex.change(ex.str,ex.ch); -     System.out.print(ex.str+" and "); -     Sytem.out.print(ex.ch); -   } -   public void change(String str,char ch[]){ -     str="test ok"; -     ch[0]='g'; -   } - } - ``` -34. 在android中,请简述jni的调用过程。(8分) - 1)安装和下载Cygwin,下载 Android NDK - 2)在ndk项目中JNI接口的设计 - 3)使用C/C++实现本地方法 - 4)JNI生成动态链接库.so文件 - 5)将动态链接库复制到java工程,在java工程中调用,运行java工程即可 -35. 简述Android应用程序结构是哪些?(7分) - Android应用程序结构是: - Linux Kernel(Linux内核)、Libraries(系统运行库或者是c/c++核心库)、Application - Framework(开发框架包)、Applications (核心应用程序) -36. 请继承SQLiteOpenHelper实现:(10分) - 1).创建一个版本为1的“diaryOpenHelper.db”的数据库 - 2).同时创建一个 “diary” 表(包含一个_id主键并自增长,topic字符型100长度, content字符型1000长度) - 3).在数据库版本变化时请删除diary表,并重新创建出diary表。 - ```java - public class DBHelper extends SQLiteOpenHelper { - - public final static String DATABASENAME = "diaryOpenHelper.db"; - public final static int DATABASEVERSION = 1; - - //创建数据库 - public DBHelper(Context context,String name,CursorFactory factory,int version) - { - super(context, name, factory, version); - } - //创建表等机构性文件 - public void onCreate(SQLiteDatabase db) - { - String sql ="create table diary"+ - "("+ - "_id integer primary key autoincrement,"+ - "topic varchar(100),"+ - "content varchar(1000)"+ - ")"; - db.execSQL(sql); - } - //若数据库版本有更新,则调用此方法 - public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion) - { - - String sql = "drop table if exists diary"; - db.execSQL(sql); - this.onCreate(db); - } - } - ``` -37. 页面上现有ProgressBar控件progressBar,请用书写线程以10秒的的时间完成其进度显示工作。(10分) - ```java - public class ProgressBarStu extends Activity { - - private ProgressBar progressBar = null; - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.progressbar); - //从这到下是关键 - progressBar = (ProgressBar)findViewById(R.id.progressBar); - - Thread thread = new Thread(new Runnable() { - - @Override - public void run() { - int progressBarMax = progressBar.getMax(); - try { - while(progressBarMax!=progressBar.getProgress()) - { - - int stepProgress = progressBarMax/10; - int currentprogress = progressBar.getProgress(); - progressBar.setProgress(currentprogress+stepProgress); - Thread.sleep(1000); - } - - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - }); - - thread.start(); - //关键结束 - } - } - ``` -38. 如何退出Activity?如何安全退出已调用多个Activity的Application? - 对于单一Activity的应用来说,退出很简单,直接finish()即可。 - 当然,也可以用killProcess()和System.exit()这样的方法。 - 但是,对于多Activity的应用来说,在打开多个Activity后,如果想在最后打开的Activity直接退出,上边的方法都是没有用的,因为上边的方法都是结束一个Activity而已。 - 当然,网上也有人说可以。 - 就好像有人问,在应用里如何捕获Home键,有人就会说用keyCode比较KEYCODE_HOME即可,而事实上如果不修改framework,根本不可能做到这一点一样。 - 所以,最好还是自己亲自试一下。 - 那么,有没有办法直接退出整个应用呢? - 在2.1之前,可以使用ActivityManager的restartPackage方法。 - 它可以直接结束整个应用。在使用时需要权限android.permission.RESTART_PACKAGES。 - 注意不要被它的名字迷惑。 - 可是,在2.2,这个方法失效了。 - 在2.2添加了一个新的方法,killBackgroundProcesses(),需要权限 android.permission.KILL_BACKGROUND_PROCESSES。 - 可惜的是,它和2.2的restartPackage一样,根本起不到应有的效果。 - 另外还有一个方法,就是系统自带的应用程序管理里,强制结束程序的方法,forceStopPackage()。 - 它需要权限android.permission.FORCE_STOP_PACKAGES。 - 并且需要添加android:sharedUserId="android.uid.system"属性 - 同样可惜的是,该方法是非公开的,他只能运行在系统进程,第三方程序无法调用。 - 因为需要在Android.mk中添加LOCAL_CERTIFICATE := platform。 - 而Android.mk是用于在Android源码下编译程序用的。 - 从以上可以看出,在2.2,没有办法直接结束一个应用,而只能用自己的办法间接办到。 - 现提供几个方法,供参考: - - 抛异常强制退出: - 该方法通过抛异常,使程序Force Close。 - 验证可以,但是,需要解决的问题是,如何使程序结束掉,而不弹出Force Close的窗口。 - - 记录打开的Activity: - 每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。 - - 发送特定广播: - 在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。 - - 递归退出 - 在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。 - 除了第一个,都是想办法把每一个Activity都结束掉,间接达到目的。 - 但是这样做同样不完美。 - 你会发现,如果自己的应用程序对每一个Activity都设置了nosensor,在两个Activity结束的间隙,sensor可能有效了。 - 但至少,我们的目的达到了,而且没有影响用户使用。 - 为了编程方便,最好定义一个Activity基类,处理这些共通问题。 -39. 请介绍下Android中常用的五种布局。 - FrameLayout(框架布局),LinearLayout (线性布局),AbsoluteLayout(绝对布局),RelativeLayout(相对布局),TableLayout(表格布局) -40. 请介绍下Android的数据存储方式。 - - SharedPreferences方式 - - 文件存储方式 - - SQLite数据库方式 - - 内容提供器(Content provider)方式 - - 网络存储方式 -41. 请介绍下ContentProvider是如何实现数据共享的。 - 创建一个属于你自己的Content provider或者将你的数据添加到一个已经存在的Content provider中,前提是有相同数据类型并且有写入Content provider的权限。 -42. 如何启用Service,如何停用Service。 - Android中的service类似于windows中的service,service一般没有用户操作界面,它运行于系统中不容易被用户发觉, - 可以使用它开发如监控之类的程序。 - 一。步骤 - 第一步:继承Service类 - public class SMSService extends Service { } - 第二步:在AndroidManifest.xml文件中的节点里对服务进行配置: - - 二。Context.startService()和Context.bindService - 服务不能自己运行,需要通过调用Context.startService()或Context.bindService()方法启动服务。这两个方法都可 - 以启动Service,但是它们的使用场合有所不同。 - 1.使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。 - 使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止。 - 2.采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法, - 接着调用onStart()方法。如果调用startService()方法前服务已经被创建,多次调用startService()方法并 - 不会导致多次创建服务,但会导致多次调用onStart()方法。 - 采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用 - onDestroy()方法。 - - 3.采用Context.bindService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法, - 接着调用onBind()方法。这个时候调用者和服务绑定在一起,调用者退出了,系统就会先调用服务的onUnbind()方法, - 。接着调用onDestroy()方法。如果调用bindService()方法前服务已经被绑定,多次调用bindService()方法并不会 - 导致多次创建服务及绑定(也就是说onCreate()和onBind()方法并不会被多次调用)。如果调用者希望与正在绑定的服务 - 解除绑定,可以调用unbindService()方法,调用该方法也会导致系统调用服务的onUnbind()-->onDestroy()方法。 - 三。Service的生命周期 - 1. Service常用生命周期回调方法如下: - onCreate() 该方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次startService()或bindService()方法, - 服务也只被创建一次。 onDestroy()该方法在服务被终止时调用。 - 2. Context.startService()启动Service有关的生命周期方法 - onStart() 只有采用Context.startService()方法启动服务时才会回调该方法。该方法在服务开始运行时被调用。 - 多次调用startService()方法尽管不会多次创建服务,但onStart() 方法会被多次调用。 - 3. Context.bindService()启动Service有关的生命周期方法 - onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用, - 当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。 - onUnbind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务解除绑定时被调用。 - - 备注: - 1. 采用startService()启动服务 - Intent intent = new Intent(DemoActivity.this, DemoService.class); - startService(intent); - 2. Context.bindService()启动 - Intent intent = new Intent(DemoActivity.this, DemoService.class); - bindService(intent, conn, Context.BIND_AUTO_CREATE); - //unbindService(conn);//解除绑定 -43. 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。 - Android广播机制(两种注册方法) - 在android下,要想接受广播信息,那么这个广播接收器就得我们自己来实现了,我们可以继承BroadcastReceiver,就可以有一个广播接受器了。 - 有个接受器还不够,我们还得重写BroadcastReceiver里面的onReceiver方法,当来广播的时候我们要干什么,这就要我们自己来实现, - 不过我们可以搞一个信息防火墙。具体的代码: - ```java - public class SmsBroadCastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - Bundle bundle = intent.getExtras(); - Object[] object = (Object[])bundle.get("pdus"); - SmsMessage sms[]=new SmsMessage[object.length]; - for(int i=0;i - - - - - - - - - - - - - - - - - - - - - - - - ``` - 两种注册类型的区别是: - - 第一种不是常驻型广播,也就是说广播跟随程序的生命周期。 - - 第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。 -44. 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。 - Handler简介: - 一个Handler允许你发送和处理Message和Runable对象,这些对象和一个线程的MessageQueue相关联。每一个线程实例和一个单独的线程以及该线程的MessageQueue相关联。 - 当你创建一个新的Handler时,它就和创建它的线程绑定在一起了。这里,线程我们也可以理解为线程的MessageQueue。从这一点上来看, - Handler把Message和Runable对象传递给MessageQueue,而且在这些对象离开MessageQueue时,Handler负责执行他们。 - Handler有两个主要的用途:(1)确定在将来的某个时间点执行一个或者一些Message和Runnable对象。(2)在其他线程(不是Handler绑定线程)中排入一些要执行的动作。 - Scheduling Message,即(1),可以通过以下方法完成: - post(Runnable):Runnable在handler绑定的线程上执行,也就是说不创建新线程。 - postAtTime(Runnable,long): - postDelayed(Runnable,long): - sendEmptyMessage(int): - sendMessage(Message): - sendMessageAtTime(Message,long): - sendMessageDelayed(Message,long): - post这个动作让你把Runnable对象排入MessageQueue,MessageQueue受到这些消息的时候执行他们,当然以一定的排序。sendMessage这个动作允许你把Message对象排成队列, - 这些Message对象包含一些信息,Handler的hanlerMessage(Message)会处理这些Message.当然,handlerMessage(Message)必须由Handler的子类来重写。这是编程人员需要作的事。 - 当posting或者sending到一个Hanler时,你可以有三种行为:当MessageQueue准备好就处理,定义一个延迟时间,定义一个精确的时间去处理。 - 后两者允许你实现timeout,tick,和基于时间的行为。 - 当你的应用创建一个新的进程时,主线程(也就是UI线程)自带一个MessageQueue,这个MessageQueue管理顶层的应用对象(像activities,broadcast receivers等)和 - 主线程创建的窗体。你可以创建自己的线程,并通过一个Handler和主线程进行通信。这和之前一样,通过post和sendmessage来完成,差别在于在哪一个线程中执行这么方法。 - 在恰当的时候,给定的Runnable和Message将在Handler的MessageQueue中被Scheduled。 - Message简介: - Message类就是定义了一个信息,这个信息中包含一个描述符和任意的数据对象,这个信息被用来传递给Handler.Message对象提供额外的两个int域和一个Object域, - 这可以让你在大多数情况下不用作分配的动作。尽管Message的构造函数是public的,但是获取Message实例的最好方法是调用Message.obtain(), - 或者Handler.obtainMessage()方法,这些方法会从回收对象池中获取一个。 - MessageQueue简介: - 这是一个包含message列表的底层类。Looper负责分发这些message。Messages并不是直接加到一个MessageQueue中,而是通过MessageQueue.IdleHandler关联到Looper。 - 你可以通过Looper.myQueue()从当前线程中获取MessageQueue。 - Looper简介: - Looper类被用来执行一个线程中的message循环。默认情况,没有一个消息循环关联到线程。在线程中调用prepare()创建一个Looper,然后用loop()来处理messages,直到循环终止。 - 大多数和message loop的交互是通过Handler。 - 下面是一个典型的带有Looper的线程实现。 - ```java - class LooperThread extends Thread { - public Handler mHandler; - - public void run() { - Looper.prepare(); - - mHandler = new Handler() { - public void handleMessage(Message msg) { - // process incoming messages here - } - }; - - Looper.loop(); - } - } - ``` -45. AIDL的全称是什么?如何工作?能处理哪些类型的数据? - AIDL的英文全称是Android Interface Define Language - 当A进程要去调用B进程中的service时,并实现通信,我们通常都是通过AIDL来操作的 - A工程: - 首先我们在net.blogjava.mobile.aidlservice包中创建一个RemoteService.aidl文件,在里面我们自定义一个接口,含有方法get。ADT插件会在gen目录下自动生成一个RemoteService.java文件,该类中含有一个名为RemoteService.stub的内部类,该内部类中含有aidl文件接口的get方法。 - 说明一:aidl文件的位置不固定,可以任意 - 然后定义自己的MyService类,在MyService类中自定义一个内部类去继承RemoteService.stub这个内部类,实现get方法。在onBind方法中返回这个内部类的对象,系统会自动将这个对象封装成IBinder对象,传递给他的调用者。 - 其次需要在AndroidManifest.xml文件中配置MyService类,代码如下: - - - - - - - - 为什么要指定调用AIDL服务的ID,就是要告诉外界MyService这个类能够被别的进程访问,只要别的进程知道这个ID,正是有了这个ID,B工程才能找到A工程实现通信。 - 说明:AIDL并不需要权限 - B工程: - 首先我们要将A工程中生成的RemoteService.java文件拷贝到B工程中,在bindService方法中绑定aidl服务 - 绑定AIDL服务就是将RemoteService的ID作为intent的action参数。 - 说明:如果我们单独将RemoteService.aidl文件放在一个包里,那个在我们将gen目录下的该包拷贝到B工程中。如果我们将RemoteService.aidl文件和我们的其他类存放在一起,那么我们在B工程中就要建立相应的包,以保证RmoteService.java文件的报名正确,我们不能修改RemoteService.java文件 - bindService(new Inten("net.blogjava.mobile.aidlservice.RemoteService"), serviceConnection, Context.BIND_AUTO_CREATE); - ServiceConnection的onServiceConnected(ComponentName name, IBinder service)方法中的service参数就是A工程中MyService类中继承了RemoteService.stub类的内部类的对象。 -46. 请解释下Android程序运行时权限与文件系统权限的区别。 - 运行时权限Dalvik( android授权) - 文件系统 linux 内核授权 -47. 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。 - 通过直接发送Uri把参数带过去,或者通过manifest里的intentfilter里的data属性 -48. 你如何评价Android系统?优缺点。 - 答:Android平台手机 5大优势: - - 开放性 - 在优势方面,Android平台首先就是其开发性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者, - 随着用户和应用的日益丰富,一个崭新的平台也将很快走向成熟。开放性对于Android的发展而言,有利于积累人气,这里的人气包括消费者和厂商, - 而对于消费者来讲,随大的受益正是丰富的软件资源。开放的平台也会带来更大竞争,如此一来,消费者将可以用更低的价位购得心仪的手机。 - - 挣脱运营商的束缚 - 在过去很长的一段时间,特别是在欧美地区,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制。从去年iPhone 上市 , - 用户可以更加方便地连接网络,运营商的制约减少。随着EDGE、HSDPA这些2G至3G移动网络的逐步过渡和提升,手机随意接入网络已不是运营商口中的笑谈, - 当你可以通过手机IM软件方便地进行即时聊天时,再回想不久前天价的彩信和图铃下载业务,是不是像噩梦一样?互联网巨头Google推动的Android终端天生就有网络特色, - 将让用户离互联网更近。 - - 丰富的硬件选择 - 这一点还是与Android平台的开放性相关,由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色, - 却不会影响到数据同步、甚至软件的兼容,好比你从诺基亚 Symbian风格手机 一下改用苹果 iPhone ,同时还可将Symbian中优秀的软件带到iPhone上使用、 - 联系人等资料更是可以方便地转移,是不是非常方便呢? - - 不受任何限制的开发商 - Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰,可想而知,会有多少新颖别致的软件会诞生。但也有其两面性,血腥、暴力、情色方面的程序和游戏如可控制正是留给Android难题之一。 - - 无缝结合的Google应用 - 如今叱诧互联网的Google已经走过10年度历史,从搜索巨人到全面的互联网渗透,Google服务如地图、邮件、搜索等已经成为连接用户和互联网的重要纽带, - 而Android平台手机将无缝结合这些优秀的Google服务。 - - 再说Android的5大不足: - - 安全和隐私 - 由于手机 与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后, - 洞穿一切,因此,互联网的深入将会带来新一轮的隐私危机。 - - 首先开卖Android手机的不是最大运营商 - 众所周知,T-Mobile在23日,于美国纽约发布 了Android首款手机G1。但是在北美市场,最大的两家运营商乃AT&T和Verizon, - 而目前所知取得Android手机销售权的仅有 T-Mobile和Sprint,其中T-Mobile的3G网络相对于其他三家也要逊色不少,因此,用户可以买账购买G1, - 能否体验到最佳的3G网络服务则要另当别论了! - - 运营商仍然能够影响到Android手机 - 在国内市场,不少用户对购得移动定制机不满,感觉所购的手机被人涂画了广告一般。这样的情况在国外市场同样出现。 - Android手机的另一发售运营商Sprint就将在其机型中内置其手机商店程序。 - - 同类机型用户减少 - 在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富, - 产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。举个稍显不当的例子,现在山寨机泛滥,品种各异, - 就很少有专门针对某个型号山寨机的讨论和群组,除了哪些功能异常抢眼、颇受追捧的机型以外。 - - 过分依赖开发商缺少标准配置 - 在使用PC端的Windows Xp系统的时候,都会内置微软Windows Media Player这样一个浏览器程序,用户可以选择更多样的播放器, - 如Realplay或暴风影音等。但入手开始使用默认的程序同样可以应付多样的需要。在 Android平台中,由于其开放性,软件更多依赖第三方厂商, - 比如Android系统的SDK中就没有内置音乐 播放器,全部依赖第三方开发,缺少了产品的统一性。 -49. 什么是ANR 如何避免它? -  答:ANR:Application Not Responding,五秒 - 在Android中,活动管理器和窗口管理器这两个系统服务负责监视应用程序的响应。当出现下列情况时,Android就会显示ANR对话框了: - 对输入事件(如按键、触摸屏事件)的响应超过5秒 - 意向接受器(intentReceiver)超过10秒钟仍未执行完毕 - Android应用程序完全运行在一个独立的线程中(例如main)。这就意味着,任何在主线程中运行的,需要消耗大量时间的操作都会引发ANR。 - 因为此时,你的应用程序已经没有机会去响应输入事件和意向广播(Intent broadcast)。 - 因此,任何运行在主线程中的方法,都要尽可能的只做少量的工作。特别是活动生命周期中的重要方法如onCreate()和 onResume()等更应如此。 - 潜在的比较耗时的操作,如访问网络和数据库;或者是开销很大的计算,比如改变位图的大小,需要在一个单独的子线程中完成(或者是使用异步请求,如数据库操作)。 - 但这并不意味着你的主线程需要进入阻塞状态已等待子线程结束 -- 也不需要调用Therad.wait()或者Thread.sleep()方法。 - 取而代之的是,主线程为子线程提供一个句柄(Handler),让子线程在即将结束的时候调用它(xing:可以参看Snake的例子,这种方法与以前我们所接触的有所不同)。 - 使用这种方法涉及你的应用程序,能够保证你的程序对输入保持良好的响应,从而避免因为输入事件超过5秒钟不被处理而产生的ANR。 - 这种实践需要应用到所有显示用户界面的线程,因为他们都面临着同样的超时问题。 -50. 什么情况会导致Force Close ?如何避免?能否捕获导致其的异常? - 答:一般像空指针啊,可以看起logcat,然后对应到程序中 来解决错误 -   -51. 如何将SQLite数据库(dictionary.db文件)与apk文件一起发布? - 解答:可以将dictionary.db文件复制到Eclipse Android工程中的res aw目录中。所有在res aw目录中的文件不会被压缩,这样可以直接提取该目录中的文件。 - 可以将dictionary.db文件复制到res aw目录中 -52. 如何将打开res aw目录中的数据库文件? - 解答:在Android中不能直接打开res aw目录中的数据库文件,而需要在程序第一次启动时将该文件复制到手机内存或SD卡的某个目录中,然后再打开该数据库文件。复制的基本方法是使用getResources().openRawResource方法获得res aw目录中资源的 InputStream对象,然后将该InputStream对象中的数据写入其他的目录中相应文件中。 - 在Android SDK中可以使用SQLiteDatabase.openOrCreateDatabase方法来打开任意目录中的SQLite数据库文件。 -53. Android引入广播机制的用意? - - 从MVC的角度考虑(应用程序内) -  其实回答这个问题的时候还可以这样问,android为什么要有那4大组件,现在的移动开发模型基本上也是照搬的web那一套MVC架构,只不过是改了点嫁妆而已。 - android的四大组件本质上就是为了实现移动或者说嵌入式设备上的MVC架构,它们之间有时候是一种相互依存的关系,有时候又是一种补充关系, - 引入广播机制可以方便几大组件的信息和数据交互。 - - 程序间互通消息(例如在自己的应用程序内监听系统来电) - - 效率上(参考UDP的广播协议在局域网的方便性) - - 设计模式上(反转控制的一种应用,类似监听者模式) -54. Android dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念 - DVM指dalivk的虚拟机。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。而每一个DVM都是在Linux 中的一个进程, - 所以说可以认为是同一个概念。 -55. 说说mvc模式的原理,它在android中的运用 - MVC(Model_view_contraller)”模型_视图_控制器”。 MVC应用程序总是由这三个部分组成。Event(事件)导致Controller改变Model或View,或者同时改变两者。 - 只要 Controller改变了Models的数据或者属性,所有依赖的View都会自动更新。类似的,只要Contro -56. DDMS和TraceView的区别? - DDMS是一个程序执行查看器,在里面可以看见线程和堆栈等信息,TraceView是程序性能分析器 。 -57. java中如何引用本地语言 - 可以用JNI(java native interface java 本地接口)接口 。 -58. 谈谈Android的IPC(进程间通信)机制 - IPC是内部进程通信的简称, 是共享"命名管道"的资源。Android中的IPC机制是为了让Activity和Service之间可以随时的进行交互,故在Android中该机制, - 只适用于Activity和Service之间的通信,类似于远程方法调用,类似于C/S模式的访问。通过定义AIDL接口文件来定义IPC接口。Servier端实现IPC接口, - Client端调用IPC接口本地代理。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - +Android基础面试题 +=== + +没有删这套题,虽然都是网上找的,在刚开始找工作的时候这套题帮了我很多,那时候`Android`刚起步,很多家都是这一套面试题,我都是直接去了不看题画画一顿就写完了,哈哈 +现在估计没有公司会用这种笔试题了。还是留下来吧,回忆一下。 + +1. 下列哪些语句关于内存回收的说明是正确的? (b) + A、 程序员必须创建一个线程来释放内存 + B、 内存回收程序负责释放无用内存 + C、 内存回收程序允许程序员直接释放内存 + D、 内存回收程序可以在指定的时间释放内存对象 +2. 下面异常是属于Runtime Exception 的是(abcd)(多选) + A、ArithmeticException + B、IllegalArgumentException + C、NullPointerException + D、BufferUnderflowException +3. Math.round(11.5)等于多少(). Math.round(-11.5)等于多少(c) + A、11 ,-11 B、11 ,-12 C、12 ,-11 D、12 ,-12 +4. 下列程序段的输出结果是:(b) + ```java + void complicatedexpression_r(){ + int x=20, y=30; + boolean b; + b=x>50&&y>60||x>50&&y<-60||x<-50&&y>60||x<-50&&y<-60; + System.out.println(b); + } + ``` + A、true B、false C、1 D、011.activity +5. 对一些资源以及状态的操作保存,最好是保存在生命周期的哪个函数中进行(d) + A、onPause() B、onCreate() C、 onResume() D、onStart() +6. Intent传递数据时,下列的数据类型哪些可以被传递(abcd)(多选) + A、Serializable B、charsequence C、Parcelable D、Bundle +7. android 中下列属于Intent的作用的是(c) + A、实现应用程序间的数据共享 + B、是一段长的生命周期,没有用户界面的程序,可以保持应用在后台运行,而不会因为切换页面而消失 + C、可以实现界面间的切换,可以包含动作和动作数据,连接四大组件的纽带 + D、处理一个应用程序整体性的工作 +8. 下列属于SAX解析xml文件的优点的是(b) + A、将整个文档树在内存中,便于操作,支持删除,修改,重新排列等多种功能 + B、不用事先调入整个文档,占用资源少 + C、整个文档调入内存,浪费时间和空间 + D、不是长久驻留在内存,数据不是持久的,事件过后,若没有保存数据,数据就会消失 +9. 下面的对自定style的方式正确的是(a) + A、 + ```xml + + + + ``` + B、 + ```xml + + ``` + C、 + ```xml + + fill_parent + + ``` + D、 + ```xml + + + + ``` +10. 在android中使用Menu时可能需要重写的方法有(ac)。(多选) + A、onCreateOptionsMenu() + B、onCreateMenu() + C、onOptionsItemSelected() + D、onItemSelected() +11. 在SQL Server Management Studio 中运行下列T-SQL语句,其输出值(c) + `SELECT @@IDENTITY` + A、 可能为0.1 + B、 可能为3 + C、 不可能为-100 + D、 肯定为0 +12. 在SQL Server 2005中运行如下T-SQL语句,假定SALES表中有多行数据,执行查询之后的结果是(d)。 + ``` + BEGIN TRANSACTION A + Update SALES Set qty=30 WHERE qty<30 + BEGIN TRANSACTION B + Update SALES Set qty=40 WHERE qty<40 + Update SALES Set qty=50 WHERE qty<50 + Update SALES Set qty=60 WHERE qty<60 + COMMIT TRANSACTION B + COMMIT TRANSACTION A + ``` + A、SALES表中qty列最小值大于等于30 + B、SALES表中qty列最小值大于等于40 + C、SALES表中qty列的数据全部为50 + D、SALES表中qty列最小值大于等于60 +13. 在android中使用SQLiteOpenHelper这个辅助类时,可以生成一个数据库,并可以对数据库版本进行管理的方法可以是(ab) + A、getWriteableDatabase() + B、getReadableDatabase() + C、getDatabase() + D、getAbleDatabase() +14. android 关于service生命周期的onCreate()和onStart()说法正确的是(ad)(多选题) + A、当第一次启动的时候先后调用onCreate()和onStart()方法 + B、当第一次启动的时候只会调用onCreate()方法 + C、如果service已经启动,将先后调用onCreate()和onStart()方法 + D、如果service已经启动,只会执行onStart()方法,不在执行onCreate()方法 +15. 下面是属于GLSurFaceView特性的是(abc)(多选) + A、管理一个surface,这个surface就是一块特殊的内存,能直接排版到android的视图view上。 + B、管理一个EGL display,它能让opengl把内容渲染到上述的surface上。 + C、让渲染器在独立的线程里运作,和UI线程分离。 + D、可以直接从内存或者DMA等硬件接口取得图像数据 +16. 下面在AndroidManifest.xml文件中注册BroadcastReceiver方式正确的(a) + A、 + ```xml + + + + + + + ``` + B、 + ```xml + + + android:name="android.provider.action.NewBroad"/> + + + ``` + C、 + ```xml + + + + + ``` + D、 + ```xml + + + + android:name="android.provider.action.NewBroad"/> + + + + ``` +17. 关于ContenValues类说法正确的是(a) + A、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名是String类型,而值都是基本类型 + B、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名是任意类型,而值都是基本类型 + C、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名,可以为空,而值都是String类型 + D、他和Hashtable比较类似,也是负责存储一些名值对,但是他存储的名值对当中的名是String类型,而值也是String类型 +18. 我们都知道Hanlder是线程与Activity通信的桥梁,如果线程处理不当,你的机器就会变得越慢,那么线程销毁的方法是(a) + A、onDestroy() + B、onClear() + C、onFinish() + D、onStop() +19. 下面退出Activity错误的方法是(c) + A、finish() + B、抛异常强制退出 + C、System.exit() + D、onStop() +20. 下面属于android的动画分类的有(ab)(多项) + A、Tween B、Frame C、Draw D、Animation +21. 下面关于Android dvm的进程和Linux的进程,应用程序的进程说法正确的是(d) + A、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,不一定拥有一个独立 的Dalvik虚拟机实例.而每一个DVM都是在Linux 中的一个进程, + 所以说可以认为是同一个概念. + B、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,不一定拥有一个独立的Dalvik虚拟机实例.而每一个DVM不一定都是在Linux 中的一个进程, + 所以说不是一个概念. + C、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例.而每一个DVM不一定都是在Linux 中的一个进程, + 所以说不是一个概念. + D、DVM指dalivk的虚拟机.每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的 Dalvik虚拟机实例.而每一个DVM都是在Linux 中的一个进程, + 所以说可以认为是同一个概念. +22. Android项目工程下面的assets目录的作用是什么(b) + A、放置应用到的图片资源。 + B、主要放置多媒体等数据文件 + C、放置字符串,颜色,数组等常量数据 + D、放置一些与UI相应的布局文件,都是xml文件 +23. 关于res/raw目录说法正确的是(a) + A、这里的文件是原封不动的存储到设备上不会转换为二进制的格式 + B、这里的文件是原封不动的存储到设备上会转换为二进制的格式 + C、这里的文件最终以二进制的格式存储到指定的包中 + D、这里的文件最终不会以二进制的格式存储到指定的包中 +24. 下列对android NDK的理解正确的是(abcd) + A、NDK是一系列工具的集合 + B、NDK 提供了一份稳定、功能有限的 API 头文件声明。 + C、使 “Java+C” 的开发方式终于转正,成为官方支持的开发方式 + D、NDK 将是 Android 平台支持 C 开发的开端 + +25. android中常用的四个布局是framlayout,linenarlayout,relativelayout和tablelayout。 +26. android 的四大组件是activiey,service,broadcast和contentprovide。 +27. java.io包中的objectinputstream和objectoutputstream类主要用于对对象(Object)的读写。 +28. android 中service的实现方法是:startservice和bindservice。 +29. activity一般会重载7个方法用来维护其生命周期,除了onCreate(),onStart(),onDestory() 外还有onrestart,onresume,onpause,onstop。 +30. android的数据存储的方式sharedpreference,文件,SQlite,contentprovider,网络。 +31. 当启动一个Activity并且新的Activity执行完后需要返回到启动它的Activity来执行 的回调函数是startActivityResult()。 +32. 请使用命令行的方式创建一个名字为myAvd,sdk版本为2.2,sd卡是在d盘的根目录下,名字为scard.img, 并指定屏幕大小HVGA.____________________________________。 +33. 程序运行的结果是:_____good and gbc__________。 + ```java + public class Example{ +   String str=new String("good"); +   char[]ch={'a','b','c'}; +   public static void main(String args[]){ +     Example ex=new Example(); +     ex.change(ex.str,ex.ch); +     System.out.print(ex.str+" and "); +     Sytem.out.print(ex.ch); +   } +   public void change(String str,char ch[]){ +     str="test ok"; +     ch[0]='g'; +   } + } + ``` +34. 在android中,请简述jni的调用过程。(8分) + 1)安装和下载Cygwin,下载 Android NDK + 2)在ndk项目中JNI接口的设计 + 3)使用C/C++实现本地方法 + 4)JNI生成动态链接库.so文件 + 5)将动态链接库复制到java工程,在java工程中调用,运行java工程即可 +35. 简述Android应用程序结构是哪些?(7分) + Android应用程序结构是: + Linux Kernel(Linux内核)、Libraries(系统运行库或者是c/c++核心库)、Application + Framework(开发框架包)、Applications (核心应用程序) +36. 请继承SQLiteOpenHelper实现:(10分) + 1).创建一个版本为1的“diaryOpenHelper.db”的数据库 + 2).同时创建一个 “diary” 表(包含一个_id主键并自增长,topic字符型100长度, content字符型1000长度) + 3).在数据库版本变化时请删除diary表,并重新创建出diary表。 + ```java + public class DBHelper extends SQLiteOpenHelper { + + public final static String DATABASENAME = "diaryOpenHelper.db"; + public final static int DATABASEVERSION = 1; + + //创建数据库 + public DBHelper(Context context,String name,CursorFactory factory,int version) + { + super(context, name, factory, version); + } + //创建表等机构性文件 + public void onCreate(SQLiteDatabase db) + { + String sql ="create table diary"+ + "("+ + "_id integer primary key autoincrement,"+ + "topic varchar(100),"+ + "content varchar(1000)"+ + ")"; + db.execSQL(sql); + } + //若数据库版本有更新,则调用此方法 + public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion) + { + + String sql = "drop table if exists diary"; + db.execSQL(sql); + this.onCreate(db); + } + } + ``` +37. 页面上现有ProgressBar控件progressBar,请用书写线程以10秒的的时间完成其进度显示工作。(10分) + ```java + public class ProgressBarStu extends Activity { + + private ProgressBar progressBar = null; + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.progressbar); + //从这到下是关键 + progressBar = (ProgressBar)findViewById(R.id.progressBar); + + Thread thread = new Thread(new Runnable() { + + @Override + public void run() { + int progressBarMax = progressBar.getMax(); + try { + while(progressBarMax!=progressBar.getProgress()) + { + + int stepProgress = progressBarMax/10; + int currentprogress = progressBar.getProgress(); + progressBar.setProgress(currentprogress+stepProgress); + Thread.sleep(1000); + } + + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + }); + + thread.start(); + //关键结束 + } + } + ``` +38. 如何退出Activity?如何安全退出已调用多个Activity的Application? + 对于单一Activity的应用来说,退出很简单,直接finish()即可。 + 当然,也可以用killProcess()和System.exit()这样的方法。 + 但是,对于多Activity的应用来说,在打开多个Activity后,如果想在最后打开的Activity直接退出,上边的方法都是没有用的,因为上边的方法都是结束一个Activity而已。 + 当然,网上也有人说可以。 + 就好像有人问,在应用里如何捕获Home键,有人就会说用keyCode比较KEYCODE_HOME即可,而事实上如果不修改framework,根本不可能做到这一点一样。 + 所以,最好还是自己亲自试一下。 + 那么,有没有办法直接退出整个应用呢? + 在2.1之前,可以使用ActivityManager的restartPackage方法。 + 它可以直接结束整个应用。在使用时需要权限android.permission.RESTART_PACKAGES。 + 注意不要被它的名字迷惑。 + 可是,在2.2,这个方法失效了。 + 在2.2添加了一个新的方法,killBackgroundProcesses(),需要权限 android.permission.KILL_BACKGROUND_PROCESSES。 + 可惜的是,它和2.2的restartPackage一样,根本起不到应有的效果。 + 另外还有一个方法,就是系统自带的应用程序管理里,强制结束程序的方法,forceStopPackage()。 + 它需要权限android.permission.FORCE_STOP_PACKAGES。 + 并且需要添加android:sharedUserId="android.uid.system"属性 + 同样可惜的是,该方法是非公开的,他只能运行在系统进程,第三方程序无法调用。 + 因为需要在Android.mk中添加LOCAL_CERTIFICATE := platform。 + 而Android.mk是用于在Android源码下编译程序用的。 + 从以上可以看出,在2.2,没有办法直接结束一个应用,而只能用自己的办法间接办到。 + 现提供几个方法,供参考: + - 抛异常强制退出: + 该方法通过抛异常,使程序Force Close。 + 验证可以,但是,需要解决的问题是,如何使程序结束掉,而不弹出Force Close的窗口。 + - 记录打开的Activity: + 每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。 + - 发送特定广播: + 在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。 + - 递归退出 + 在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。 + 除了第一个,都是想办法把每一个Activity都结束掉,间接达到目的。 + 但是这样做同样不完美。 + 你会发现,如果自己的应用程序对每一个Activity都设置了nosensor,在两个Activity结束的间隙,sensor可能有效了。 + 但至少,我们的目的达到了,而且没有影响用户使用。 + 为了编程方便,最好定义一个Activity基类,处理这些共通问题。 +39. 请介绍下Android中常用的五种布局。 + FrameLayout(框架布局),LinearLayout (线性布局),AbsoluteLayout(绝对布局),RelativeLayout(相对布局),TableLayout(表格布局) +40. 请介绍下Android的数据存储方式。 + - SharedPreferences方式 + - 文件存储方式 + - SQLite数据库方式 + - 内容提供器(Content provider)方式 + - 网络存储方式 +41. 请介绍下ContentProvider是如何实现数据共享的。 + 创建一个属于你自己的Content provider或者将你的数据添加到一个已经存在的Content provider中,前提是有相同数据类型并且有写入Content provider的权限。 +42. 如何启用Service,如何停用Service。 + Android中的service类似于windows中的service,service一般没有用户操作界面,它运行于系统中不容易被用户发觉, + 可以使用它开发如监控之类的程序。 + 一。步骤 + 第一步:继承Service类 + public class SMSService extends Service { } + 第二步:在AndroidManifest.xml文件中的节点里对服务进行配置: + + 二。Context.startService()和Context.bindService + 服务不能自己运行,需要通过调用Context.startService()或Context.bindService()方法启动服务。这两个方法都可 + 以启动Service,但是它们的使用场合有所不同。 + 1.使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。 + 使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止。 + 2.采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法, + 接着调用onStart()方法。如果调用startService()方法前服务已经被创建,多次调用startService()方法并 + 不会导致多次创建服务,但会导致多次调用onStart()方法。 + 采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用 + onDestroy()方法。 + + 3.采用Context.bindService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法, + 接着调用onBind()方法。这个时候调用者和服务绑定在一起,调用者退出了,系统就会先调用服务的onUnbind()方法, + 。接着调用onDestroy()方法。如果调用bindService()方法前服务已经被绑定,多次调用bindService()方法并不会 + 导致多次创建服务及绑定(也就是说onCreate()和onBind()方法并不会被多次调用)。如果调用者希望与正在绑定的服务 + 解除绑定,可以调用unbindService()方法,调用该方法也会导致系统调用服务的onUnbind()-->onDestroy()方法。 + 三。Service的生命周期 + 1. Service常用生命周期回调方法如下: + onCreate() 该方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次startService()或bindService()方法, + 服务也只被创建一次。 onDestroy()该方法在服务被终止时调用。 + 2. Context.startService()启动Service有关的生命周期方法 + onStart() 只有采用Context.startService()方法启动服务时才会回调该方法。该方法在服务开始运行时被调用。 + 多次调用startService()方法尽管不会多次创建服务,但onStart() 方法会被多次调用。 + 3. Context.bindService()启动Service有关的生命周期方法 + onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用, + 当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。 + onUnbind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务解除绑定时被调用。 + + 备注: + 1. 采用startService()启动服务 + Intent intent = new Intent(DemoActivity.this, DemoService.class); + startService(intent); + 2. Context.bindService()启动 + Intent intent = new Intent(DemoActivity.this, DemoService.class); + bindService(intent, conn, Context.BIND_AUTO_CREATE); + //unbindService(conn);//解除绑定 +43. 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。 + Android广播机制(两种注册方法) + 在android下,要想接受广播信息,那么这个广播接收器就得我们自己来实现了,我们可以继承BroadcastReceiver,就可以有一个广播接受器了。 + 有个接受器还不够,我们还得重写BroadcastReceiver里面的onReceiver方法,当来广播的时候我们要干什么,这就要我们自己来实现, + 不过我们可以搞一个信息防火墙。具体的代码: + ```java + public class SmsBroadCastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Bundle bundle = intent.getExtras(); + Object[] object = (Object[])bundle.get("pdus"); + SmsMessage sms[]=new SmsMessage[object.length]; + for(int i=0;i + + + + + + + + + + + + + + + + + + + + + + + + ``` + 两种注册类型的区别是: + - 第一种不是常驻型广播,也就是说广播跟随程序的生命周期。 + - 第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。 +44. 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。 + Handler简介: + 一个Handler允许你发送和处理Message和Runable对象,这些对象和一个线程的MessageQueue相关联。每一个线程实例和一个单独的线程以及该线程的MessageQueue相关联。 + 当你创建一个新的Handler时,它就和创建它的线程绑定在一起了。这里,线程我们也可以理解为线程的MessageQueue。从这一点上来看, + Handler把Message和Runable对象传递给MessageQueue,而且在这些对象离开MessageQueue时,Handler负责执行他们。 + Handler有两个主要的用途:(1)确定在将来的某个时间点执行一个或者一些Message和Runnable对象。(2)在其他线程(不是Handler绑定线程)中排入一些要执行的动作。 + Scheduling Message,即(1),可以通过以下方法完成: + post(Runnable):Runnable在handler绑定的线程上执行,也就是说不创建新线程。 + postAtTime(Runnable,long): + postDelayed(Runnable,long): + sendEmptyMessage(int): + sendMessage(Message): + sendMessageAtTime(Message,long): + sendMessageDelayed(Message,long): + post这个动作让你把Runnable对象排入MessageQueue,MessageQueue受到这些消息的时候执行他们,当然以一定的排序。sendMessage这个动作允许你把Message对象排成队列, + 这些Message对象包含一些信息,Handler的hanlerMessage(Message)会处理这些Message.当然,handlerMessage(Message)必须由Handler的子类来重写。这是编程人员需要作的事。 + 当posting或者sending到一个Hanler时,你可以有三种行为:当MessageQueue准备好就处理,定义一个延迟时间,定义一个精确的时间去处理。 + 后两者允许你实现timeout,tick,和基于时间的行为。 + 当你的应用创建一个新的进程时,主线程(也就是UI线程)自带一个MessageQueue,这个MessageQueue管理顶层的应用对象(像activities,broadcast receivers等)和 + 主线程创建的窗体。你可以创建自己的线程,并通过一个Handler和主线程进行通信。这和之前一样,通过post和sendmessage来完成,差别在于在哪一个线程中执行这么方法。 + 在恰当的时候,给定的Runnable和Message将在Handler的MessageQueue中被Scheduled。 + Message简介: + Message类就是定义了一个信息,这个信息中包含一个描述符和任意的数据对象,这个信息被用来传递给Handler.Message对象提供额外的两个int域和一个Object域, + 这可以让你在大多数情况下不用作分配的动作。尽管Message的构造函数是public的,但是获取Message实例的最好方法是调用Message.obtain(), + 或者Handler.obtainMessage()方法,这些方法会从回收对象池中获取一个。 + MessageQueue简介: + 这是一个包含message列表的底层类。Looper负责分发这些message。Messages并不是直接加到一个MessageQueue中,而是通过MessageQueue.IdleHandler关联到Looper。 + 你可以通过Looper.myQueue()从当前线程中获取MessageQueue。 + Looper简介: + Looper类被用来执行一个线程中的message循环。默认情况,没有一个消息循环关联到线程。在线程中调用prepare()创建一个Looper,然后用loop()来处理messages,直到循环终止。 + 大多数和message loop的交互是通过Handler。 + 下面是一个典型的带有Looper的线程实现。 + ```java + class LooperThread extends Thread { + public Handler mHandler; + + public void run() { + Looper.prepare(); + + mHandler = new Handler() { + public void handleMessage(Message msg) { + // process incoming messages here + } + }; + + Looper.loop(); + } + } + ``` +45. AIDL的全称是什么?如何工作?能处理哪些类型的数据? + AIDL的英文全称是Android Interface Define Language + 当A进程要去调用B进程中的service时,并实现通信,我们通常都是通过AIDL来操作的 + A工程: + 首先我们在net.blogjava.mobile.aidlservice包中创建一个RemoteService.aidl文件,在里面我们自定义一个接口,含有方法get。ADT插件会在gen目录下自动生成一个RemoteService.java文件,该类中含有一个名为RemoteService.stub的内部类,该内部类中含有aidl文件接口的get方法。 + 说明一:aidl文件的位置不固定,可以任意 + 然后定义自己的MyService类,在MyService类中自定义一个内部类去继承RemoteService.stub这个内部类,实现get方法。在onBind方法中返回这个内部类的对象,系统会自动将这个对象封装成IBinder对象,传递给他的调用者。 + 其次需要在AndroidManifest.xml文件中配置MyService类,代码如下: + + + + + + + + 为什么要指定调用AIDL服务的ID,就是要告诉外界MyService这个类能够被别的进程访问,只要别的进程知道这个ID,正是有了这个ID,B工程才能找到A工程实现通信。 + 说明:AIDL并不需要权限 + B工程: + 首先我们要将A工程中生成的RemoteService.java文件拷贝到B工程中,在bindService方法中绑定aidl服务 + 绑定AIDL服务就是将RemoteService的ID作为intent的action参数。 + 说明:如果我们单独将RemoteService.aidl文件放在一个包里,那个在我们将gen目录下的该包拷贝到B工程中。如果我们将RemoteService.aidl文件和我们的其他类存放在一起,那么我们在B工程中就要建立相应的包,以保证RmoteService.java文件的报名正确,我们不能修改RemoteService.java文件 + bindService(new Inten("net.blogjava.mobile.aidlservice.RemoteService"), serviceConnection, Context.BIND_AUTO_CREATE); + ServiceConnection的onServiceConnected(ComponentName name, IBinder service)方法中的service参数就是A工程中MyService类中继承了RemoteService.stub类的内部类的对象。 +46. 请解释下Android程序运行时权限与文件系统权限的区别。 + 运行时权限Dalvik( android授权) + 文件系统 linux 内核授权 +47. 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。 + 通过直接发送Uri把参数带过去,或者通过manifest里的intentfilter里的data属性 +48. 你如何评价Android系统?优缺点。 + 答:Android平台手机 5大优势: + - 开放性 + 在优势方面,Android平台首先就是其开发性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者, + 随着用户和应用的日益丰富,一个崭新的平台也将很快走向成熟。开放性对于Android的发展而言,有利于积累人气,这里的人气包括消费者和厂商, + 而对于消费者来讲,随大的受益正是丰富的软件资源。开放的平台也会带来更大竞争,如此一来,消费者将可以用更低的价位购得心仪的手机。 + - 挣脱运营商的束缚 + 在过去很长的一段时间,特别是在欧美地区,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制。从去年iPhone 上市 , + 用户可以更加方便地连接网络,运营商的制约减少。随着EDGE、HSDPA这些2G至3G移动网络的逐步过渡和提升,手机随意接入网络已不是运营商口中的笑谈, + 当你可以通过手机IM软件方便地进行即时聊天时,再回想不久前天价的彩信和图铃下载业务,是不是像噩梦一样?互联网巨头Google推动的Android终端天生就有网络特色, + 将让用户离互联网更近。 + - 丰富的硬件选择 + 这一点还是与Android平台的开放性相关,由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色, + 却不会影响到数据同步、甚至软件的兼容,好比你从诺基亚 Symbian风格手机 一下改用苹果 iPhone ,同时还可将Symbian中优秀的软件带到iPhone上使用、 + 联系人等资料更是可以方便地转移,是不是非常方便呢? + - 不受任何限制的开发商 + Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰,可想而知,会有多少新颖别致的软件会诞生。但也有其两面性,血腥、暴力、情色方面的程序和游戏如可控制正是留给Android难题之一。 + - 无缝结合的Google应用 + 如今叱诧互联网的Google已经走过10年度历史,从搜索巨人到全面的互联网渗透,Google服务如地图、邮件、搜索等已经成为连接用户和互联网的重要纽带, + 而Android平台手机将无缝结合这些优秀的Google服务。 + + 再说Android的5大不足: + - 安全和隐私 + 由于手机 与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后, + 洞穿一切,因此,互联网的深入将会带来新一轮的隐私危机。 + - 首先开卖Android手机的不是最大运营商 + 众所周知,T-Mobile在23日,于美国纽约发布 了Android首款手机G1。但是在北美市场,最大的两家运营商乃AT&T和Verizon, + 而目前所知取得Android手机销售权的仅有 T-Mobile和Sprint,其中T-Mobile的3G网络相对于其他三家也要逊色不少,因此,用户可以买账购买G1, + 能否体验到最佳的3G网络服务则要另当别论了! + - 运营商仍然能够影响到Android手机 + 在国内市场,不少用户对购得移动定制机不满,感觉所购的手机被人涂画了广告一般。这样的情况在国外市场同样出现。 + Android手机的另一发售运营商Sprint就将在其机型中内置其手机商店程序。 + - 同类机型用户减少 + 在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富, + 产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。举个稍显不当的例子,现在山寨机泛滥,品种各异, + 就很少有专门针对某个型号山寨机的讨论和群组,除了哪些功能异常抢眼、颇受追捧的机型以外。 + - 过分依赖开发商缺少标准配置 + 在使用PC端的Windows Xp系统的时候,都会内置微软Windows Media Player这样一个浏览器程序,用户可以选择更多样的播放器, + 如Realplay或暴风影音等。但入手开始使用默认的程序同样可以应付多样的需要。在 Android平台中,由于其开放性,软件更多依赖第三方厂商, + 比如Android系统的SDK中就没有内置音乐 播放器,全部依赖第三方开发,缺少了产品的统一性。 +49. 什么是ANR 如何避免它? +  答:ANR:Application Not Responding,五秒 + 在Android中,活动管理器和窗口管理器这两个系统服务负责监视应用程序的响应。当出现下列情况时,Android就会显示ANR对话框了: + 对输入事件(如按键、触摸屏事件)的响应超过5秒 + 意向接受器(intentReceiver)超过10秒钟仍未执行完毕 + Android应用程序完全运行在一个独立的线程中(例如main)。这就意味着,任何在主线程中运行的,需要消耗大量时间的操作都会引发ANR。 + 因为此时,你的应用程序已经没有机会去响应输入事件和意向广播(Intent broadcast)。 + 因此,任何运行在主线程中的方法,都要尽可能的只做少量的工作。特别是活动生命周期中的重要方法如onCreate()和 onResume()等更应如此。 + 潜在的比较耗时的操作,如访问网络和数据库;或者是开销很大的计算,比如改变位图的大小,需要在一个单独的子线程中完成(或者是使用异步请求,如数据库操作)。 + 但这并不意味着你的主线程需要进入阻塞状态已等待子线程结束 -- 也不需要调用Therad.wait()或者Thread.sleep()方法。 + 取而代之的是,主线程为子线程提供一个句柄(Handler),让子线程在即将结束的时候调用它(xing:可以参看Snake的例子,这种方法与以前我们所接触的有所不同)。 + 使用这种方法涉及你的应用程序,能够保证你的程序对输入保持良好的响应,从而避免因为输入事件超过5秒钟不被处理而产生的ANR。 + 这种实践需要应用到所有显示用户界面的线程,因为他们都面临着同样的超时问题。 +50. 什么情况会导致Force Close ?如何避免?能否捕获导致其的异常? + 答:一般像空指针啊,可以看起logcat,然后对应到程序中 来解决错误 +   +51. 如何将SQLite数据库(dictionary.db文件)与apk文件一起发布? + 解答:可以将dictionary.db文件复制到Eclipse Android工程中的res aw目录中。所有在res aw目录中的文件不会被压缩,这样可以直接提取该目录中的文件。 + 可以将dictionary.db文件复制到res aw目录中 +52. 如何将打开res aw目录中的数据库文件? + 解答:在Android中不能直接打开res aw目录中的数据库文件,而需要在程序第一次启动时将该文件复制到手机内存或SD卡的某个目录中,然后再打开该数据库文件。复制的基本方法是使用getResources().openRawResource方法获得res aw目录中资源的 InputStream对象,然后将该InputStream对象中的数据写入其他的目录中相应文件中。 + 在Android SDK中可以使用SQLiteDatabase.openOrCreateDatabase方法来打开任意目录中的SQLite数据库文件。 +53. Android引入广播机制的用意? + - 从MVC的角度考虑(应用程序内) +  其实回答这个问题的时候还可以这样问,android为什么要有那4大组件,现在的移动开发模型基本上也是照搬的web那一套MVC架构,只不过是改了点嫁妆而已。 + android的四大组件本质上就是为了实现移动或者说嵌入式设备上的MVC架构,它们之间有时候是一种相互依存的关系,有时候又是一种补充关系, + 引入广播机制可以方便几大组件的信息和数据交互。 + - 程序间互通消息(例如在自己的应用程序内监听系统来电) + - 效率上(参考UDP的广播协议在局域网的方便性) + - 设计模式上(反转控制的一种应用,类似监听者模式) +54. Android dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念 + DVM指dalivk的虚拟机。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。而每一个DVM都是在Linux 中的一个进程, + 所以说可以认为是同一个概念。 +55. 说说mvc模式的原理,它在android中的运用 + MVC(Model_view_contraller)”模型_视图_控制器”。 MVC应用程序总是由这三个部分组成。Event(事件)导致Controller改变Model或View,或者同时改变两者。 + 只要 Controller改变了Models的数据或者属性,所有依赖的View都会自动更新。类似的,只要Contro +56. DDMS和TraceView的区别? + DDMS是一个程序执行查看器,在里面可以看见线程和堆栈等信息,TraceView是程序性能分析器 。 +57. java中如何引用本地语言 + 可以用JNI(java native interface java 本地接口)接口 。 +58. 谈谈Android的IPC(进程间通信)机制 + IPC是内部进程通信的简称, 是共享"命名管道"的资源。Android中的IPC机制是为了让Activity和Service之间可以随时的进行交互,故在Android中该机制, + 只适用于Activity和Service之间的通信,类似于远程方法调用,类似于C/S模式的访问。通过定义AIDL接口文件来定义IPC接口。Servier端实现IPC接口, + Client端调用IPC接口本地代理。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/Android\345\237\272\347\241\200/Android\347\274\226\347\240\201\350\247\204\350\214\203.md" "b/AndroidBasicPart/Android\347\274\226\347\240\201\350\247\204\350\214\203.md" similarity index 100% rename from "Android\345\237\272\347\241\200/Android\347\274\226\347\240\201\350\247\204\350\214\203.md" rename to "AndroidBasicPart/Android\347\274\226\347\240\201\350\247\204\350\214\203.md" diff --git "a/Android\345\237\272\347\241\200/Ant\346\211\223\345\214\205.md" "b/AndroidBasicPart/Ant\346\211\223\345\214\205.md" similarity index 98% rename from "Android\345\237\272\347\241\200/Ant\346\211\223\345\214\205.md" rename to "AndroidBasicPart/Ant\346\211\223\345\214\205.md" index 8869ab66..16c5733e 100644 --- "a/Android\345\237\272\347\241\200/Ant\346\211\223\345\214\205.md" +++ "b/AndroidBasicPart/Ant\346\211\223\345\214\205.md" @@ -1,38 +1,38 @@ -Ant打包 -=== - -使用步骤: -1. 对于已经存在的工程需要利用`Ant`命令更新一下: - `android update project -n Test -p D:/workspace/Test -s -t 1` -  -n (name) 后面跟的是这个工程的名子 -  -p (path)后面跟的是这个工程的目录路径 -  -t (target)后面是当前共有的`SDK`版本。表明我们的*目标版本*(如果有了`project.properties`就不用再跟`target`这个参数了). -  `android list target`这样就能够列出来所有的sdk版本 - -2. 将签名文件keystore复制到工程根目录下,并且在根目录下新建`ant.properties`内容如下(配置签名文件): - ``` -  key.store=keystore.keystore //把签名放到根目录中 -  key.alias=tencent -  key.store.password=1234 -  key.alias.password=1234 - ``` - -3. 刷新工程 - 在`eclipse`中的`Ant`视图中右键`add build files`选择工程中的`build.xml`,选择最下面的`release`或者是`debug`, - 注意`release`是生成带签名的`apk`包.生成的apk在`bin`目录中,名字为工程名`-release.apk`. - -4. 常见错误: - 有时候在用`ant`打包的时候会报一些错误,一般按照错误的提示进行修改即可,如文件的非法字符等。 - ```java - Error occurred during initialization of VM - Could not reserve enough space for object heap - Error: Could not create the Java Virtual Machine. - Error: A fatal exception has occurred. Program will exit. - ``` - 如果发现以上错误,就是说明栈内存不足了,一种是内存设置的太小,还有一种情况就是你设置的内存大小已经超过了当前系统限制的大小。 - 打开`D:\Java\adt-bundle-windows\sdk\build-tools\android-4.4\dx.bat`将`set defaultXmx=-Xmx1024M`改为`set defaultXmx=-Xmx512M`即可。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +Ant打包 +=== + +使用步骤: +1. 对于已经存在的工程需要利用`Ant`命令更新一下: + `android update project -n Test -p D:/workspace/Test -s -t 1` +  -n (name) 后面跟的是这个工程的名子 +  -p (path)后面跟的是这个工程的目录路径 +  -t (target)后面是当前共有的`SDK`版本。表明我们的*目标版本*(如果有了`project.properties`就不用再跟`target`这个参数了). +  `android list target`这样就能够列出来所有的sdk版本 + +2. 将签名文件keystore复制到工程根目录下,并且在根目录下新建`ant.properties`内容如下(配置签名文件): + ``` +  key.store=keystore.keystore //把签名放到根目录中 +  key.alias=tencent +  key.store.password=1234 +  key.alias.password=1234 + ``` + +3. 刷新工程 + 在`eclipse`中的`Ant`视图中右键`add build files`选择工程中的`build.xml`,选择最下面的`release`或者是`debug`, + 注意`release`是生成带签名的`apk`包.生成的apk在`bin`目录中,名字为工程名`-release.apk`. + +4. 常见错误: + 有时候在用`ant`打包的时候会报一些错误,一般按照错误的提示进行修改即可,如文件的非法字符等。 + ```java + Error occurred during initialization of VM + Could not reserve enough space for object heap + Error: Could not create the Java Virtual Machine. + Error: A fatal exception has occurred. Program will exit. + ``` + 如果发现以上错误,就是说明栈内存不足了,一种是内存设置的太小,还有一种情况就是你设置的内存大小已经超过了当前系统限制的大小。 + 打开`D:\Java\adt-bundle-windows\sdk\build-tools\android-4.4\dx.bat`将`set defaultXmx=-Xmx1024M`改为`set defaultXmx=-Xmx512M`即可。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/Bitmap\344\274\230\345\214\226.md" "b/AndroidBasicPart/Bitmap\344\274\230\345\214\226.md" similarity index 97% rename from "Android\345\237\272\347\241\200/Bitmap\344\274\230\345\214\226.md" rename to "AndroidBasicPart/Bitmap\344\274\230\345\214\226.md" index 332c1b81..c127bf68 100644 --- "a/Android\345\237\272\347\241\200/Bitmap\344\274\230\345\214\226.md" +++ "b/AndroidBasicPart/Bitmap\344\274\230\345\214\226.md" @@ -1,137 +1,137 @@ -Bitmap优化 -=== - -1. 一个进程的内存可以由2个部分组成:`native和dalvik` - `dalvik`就是我们平常说的`java`堆,我们创建的对象是在这里面分配的,而`bitmap`是直接在`native`上分配的。 - 一旦内存分配给`Java`后,以后这块内存即使释放后,也只能给`Java`的使用,所以如果`Java`突然占用了一个大块内存, - 即使很快释放了,`C`能用的内存也是16M减去`Java`最大占用的内存数。 - 而`Bitmap`的生成是通过`malloc`进行内存分配的,占用的是`C`的内存,这个也就说明了,上述的`4MBitmap`无法生成的原因, - 因为在`13M`被`Java`用过后,剩下`C`能用的只有`3M`了。 - -2. 在`Android`应用里,最耗费内存的就是图片资源。 - 在`Android`系统中,读取位图`Bitmap`时,分给虚拟机中的图片的堆栈大小只有8M,如果超出了,就会出现`OutOfMemory`异常。 - -3. 及时回收Bitmap的内存 - ```java - // 先判断是否已经回收 - if(bitmap != null && !bitmap.isRecycled()){ - // 回收并且置为null - bitmap.recycle(); - bitmap = null; - } - System.gc(); - ``` - -4. 捕获异常 - 在实例化`Bitmap`的代码中,一定要对`OutOfMemory`异常进行捕获。下面对初始化`Bitmap`对象过程中可能发生的`OutOfMemory`异常进行了捕获。 - 如果发生了异常,应用不会崩溃,而是得到了一个默认的图片。 - ```java - Bitmap bitmap = null; - try { - // 实例化Bitmap - bitmap = BitmapFactory.decodeFile(path); - } catch (OutOfMemoryError e) { - // - } - if (bitmap == null) { - // 如果实例化失败 返回默认的Bitmap对象 - return defaultBitmapMap; - } - ``` - -5. 缓存通用的Bitmap对象 - -6. 压缩图片 - 如果图片像素过大可以将图片缩小,以减少载入图片过程中的内存的使用,避免异常发生。 - 使用`BitmapFactory.Options.inSampleSize`就可以缩小图片。属性值`inSampleSize`表示缩略图大小为原始图片大小的几分之一。 - 即如果这个值为2,则取出的缩略图的宽和高都是原始图片的1/2,图片的大小就为原始大小的1/4。 - 如果知道图片的像素过大,就可以对其进行缩小。那么如何才知道图片过大呢? - 使用`BitmapFactory.Options`设置`inJustDecodeBounds`为`true`后,并不会真正的分配空间,即解码出来的`Bitmap`为`null`, - 但是可计算出原始图片的宽度和高度,即`options.outWidth`和`options.outHeight`。 - 通过这两个值,就可以知道图片是否过大了。 - ```java - BitmapFactory.Options opts = new BitmapFactory.Options(); - // 设置inJustDecodeBounds为true - opts.inJustDecodeBounds = true; - // 使用decodeFile方法得到图片的宽和高 - BitmapFactory.decodeFile(path, opts); - // 打印出图片的宽和高 - Log.d("example", opts.outWidth + "," + opts.outHeight); - ``` - 在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要跑缩小。如果不需要缩小,设置inSampleSize的值为1。如果需要缩小,则动态计算并设置inSampleSize的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法实例化Bitmap对象前,别忘记将opts.inJustDecodeBound设置回false。否则获取的bitmap对象还是null。 - - 以从Gallery获取一个图片为例讲解缩放: - ```java - public class MainActivity extends Activity { - private ImageView iv; - private WindowManager wm; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - wm = getWindowManager(); - iv = (ImageView) findViewById(R.id.iv); - } - - // 从系统的图库里面 获取一张照片 - public void click(View view) { - Intent intent = new Intent(); - intent.setAction("android.intent.action.PICK"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.setType("image/*"); - startActivityForResult(intent, 0); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (data != null) { - // 获取到系统图库返回回来图片的uri - Uri uri = data.getData(); - System.out.println(uri.toString()); - - try { - InputStream is = getContentResolver().openInputStream(uri); - // 1.计算出来屏幕的宽高. - int windowWidth = wm.getDefaultDisplay().getWidth(); - int windowHeight = wm.getDefaultDisplay().getHeight(); - //2. 计算图片的宽高. - BitmapFactory.Options opts = new Options(); - // 设置 不去真正的解析位图 不把他加载到内存 只是获取这个图片的宽高信息 - opts.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, opts); - int bitmapHeight = opts.outHeight; - int bitmapWidth = opts.outWidth; - - if (bitmapHeight > windowHeight || bitmapWidth > windowWidth) { - int scaleX = bitmapWidth/windowWidth; - int scaleY = bitmapHeight/windowHeight; - if(scaleX>scaleY){//按照水平方向的比例缩放 - opts.inSampleSize = scaleX; - }else{//按照竖直方向的比例缩放 - opts.inSampleSize = scaleY; - } - - }else{//如果图片比手机屏幕小 不去缩放了. - opts.inSampleSize = 1; - } - //让位图工厂真正的去解析图片 - opts.inJustDecodeBounds = false; - //注意: 流的操作 - is = getContentResolver().openInputStream(uri); - Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts); - iv.setImageBitmap(bitmap); - - } catch (Exception e) { - e.printStackTrace(); - } - } - super.onActivityResult(requestCode, resultCode, data); - } - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +Bitmap优化 +=== + +1. 一个进程的内存可以由2个部分组成:`native和dalvik` + `dalvik`就是我们平常说的`java`堆,我们创建的对象是在这里面分配的,而`bitmap`是直接在`native`上分配的。 + 一旦内存分配给`Java`后,以后这块内存即使释放后,也只能给`Java`的使用,所以如果`Java`突然占用了一个大块内存, + 即使很快释放了,`C`能用的内存也是16M减去`Java`最大占用的内存数。 + 而`Bitmap`的生成是通过`malloc`进行内存分配的,占用的是`C`的内存,这个也就说明了,上述的`4MBitmap`无法生成的原因, + 因为在`13M`被`Java`用过后,剩下`C`能用的只有`3M`了。 + +2. 在`Android`应用里,最耗费内存的就是图片资源。 + 在`Android`系统中,读取位图`Bitmap`时,分给虚拟机中的图片的堆栈大小只有8M,如果超出了,就会出现`OutOfMemory`异常。 + +3. 及时回收Bitmap的内存 + ```java + // 先判断是否已经回收 + if(bitmap != null && !bitmap.isRecycled()){ + // 回收并且置为null + bitmap.recycle(); + bitmap = null; + } + System.gc(); + ``` + +4. 捕获异常 + 在实例化`Bitmap`的代码中,一定要对`OutOfMemory`异常进行捕获。下面对初始化`Bitmap`对象过程中可能发生的`OutOfMemory`异常进行了捕获。 + 如果发生了异常,应用不会崩溃,而是得到了一个默认的图片。 + ```java + Bitmap bitmap = null; + try { + // 实例化Bitmap + bitmap = BitmapFactory.decodeFile(path); + } catch (OutOfMemoryError e) { + // + } + if (bitmap == null) { + // 如果实例化失败 返回默认的Bitmap对象 + return defaultBitmapMap; + } + ``` + +5. 缓存通用的Bitmap对象 + +6. 压缩图片 + 如果图片像素过大可以将图片缩小,以减少载入图片过程中的内存的使用,避免异常发生。 + 使用`BitmapFactory.Options.inSampleSize`就可以缩小图片。属性值`inSampleSize`表示缩略图大小为原始图片大小的几分之一。 + 即如果这个值为2,则取出的缩略图的宽和高都是原始图片的1/2,图片的大小就为原始大小的1/4。 + 如果知道图片的像素过大,就可以对其进行缩小。那么如何才知道图片过大呢? + 使用`BitmapFactory.Options`设置`inJustDecodeBounds`为`true`后,并不会真正的分配空间,即解码出来的`Bitmap`为`null`, + 但是可计算出原始图片的宽度和高度,即`options.outWidth`和`options.outHeight`。 + 通过这两个值,就可以知道图片是否过大了。 + ```java + BitmapFactory.Options opts = new BitmapFactory.Options(); + // 设置inJustDecodeBounds为true + opts.inJustDecodeBounds = true; + // 使用decodeFile方法得到图片的宽和高 + BitmapFactory.decodeFile(path, opts); + // 打印出图片的宽和高 + Log.d("example", opts.outWidth + "," + opts.outHeight); + ``` + 在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要跑缩小。如果不需要缩小,设置inSampleSize的值为1。如果需要缩小,则动态计算并设置inSampleSize的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法实例化Bitmap对象前,别忘记将opts.inJustDecodeBound设置回false。否则获取的bitmap对象还是null。 + + 以从Gallery获取一个图片为例讲解缩放: + ```java + public class MainActivity extends Activity { + private ImageView iv; + private WindowManager wm; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + wm = getWindowManager(); + iv = (ImageView) findViewById(R.id.iv); + } + + // 从系统的图库里面 获取一张照片 + public void click(View view) { + Intent intent = new Intent(); + intent.setAction("android.intent.action.PICK"); + intent.addCategory("android.intent.category.DEFAULT"); + intent.setType("image/*"); + startActivityForResult(intent, 0); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null) { + // 获取到系统图库返回回来图片的uri + Uri uri = data.getData(); + System.out.println(uri.toString()); + + try { + InputStream is = getContentResolver().openInputStream(uri); + // 1.计算出来屏幕的宽高. + int windowWidth = wm.getDefaultDisplay().getWidth(); + int windowHeight = wm.getDefaultDisplay().getHeight(); + //2. 计算图片的宽高. + BitmapFactory.Options opts = new Options(); + // 设置 不去真正的解析位图 不把他加载到内存 只是获取这个图片的宽高信息 + opts.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, opts); + int bitmapHeight = opts.outHeight; + int bitmapWidth = opts.outWidth; + + if (bitmapHeight > windowHeight || bitmapWidth > windowWidth) { + int scaleX = bitmapWidth/windowWidth; + int scaleY = bitmapHeight/windowHeight; + if(scaleX>scaleY){//按照水平方向的比例缩放 + opts.inSampleSize = scaleX; + }else{//按照竖直方向的比例缩放 + opts.inSampleSize = scaleY; + } + + }else{//如果图片比手机屏幕小 不去缩放了. + opts.inSampleSize = 1; + } + //让位图工厂真正的去解析图片 + opts.inJustDecodeBounds = false; + //注意: 流的操作 + is = getContentResolver().openInputStream(uri); + Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts); + iv.setImageBitmap(bitmap); + + } catch (Exception e) { + e.printStackTrace(); + } + } + super.onActivityResult(requestCode, resultCode, data); + } + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/DLNA\347\256\200\344\273\213.md" "b/AndroidBasicPart/DLNA\347\256\200\344\273\213.md" similarity index 100% rename from "Android\345\237\272\347\241\200/DLNA\347\256\200\344\273\213.md" rename to "AndroidBasicPart/DLNA\347\256\200\344\273\213.md" diff --git "a/Android\345\237\272\347\241\200/Fragment\344\270\223\351\242\230.md" "b/AndroidBasicPart/Fragment\344\270\223\351\242\230.md" similarity index 97% rename from "Android\345\237\272\347\241\200/Fragment\344\270\223\351\242\230.md" rename to "AndroidBasicPart/Fragment\344\270\223\351\242\230.md" index 440ad746..1259b0dc 100644 --- "a/Android\345\237\272\347\241\200/Fragment\344\270\223\351\242\230.md" +++ "b/AndroidBasicPart/Fragment\344\270\223\351\242\230.md" @@ -1,281 +1,281 @@ -Fragment专题 -=== - -##简介 - -A Fragment is a piece of an application's user interface or behavior that can be placed in an Activity. -Interaction with fragments is done through FragmentManager, -which can be obtained via Activity.getFragmentManager() and Fragment.getFragmentManager(). - -The Fragment class can be used many ways to achieve a wide variety of results. -In its core, it represents a particular operation or interface that is running within a larger Activity. -A Fragment is closely tied to the Activity it is in, and can not be used apart from one. -Though Fragment defines its own lifecycle, that lifecycle is dependent on its activity: if the activity is stopped, -no fragments inside of it can be started; when the activity is destroyed, all fragments will be destroyed. - -All subclasses of Fragment must include a public no-argument constructor. -The framework will often re-instantiate a fragment class when needed, in particular during state restore, -and needs to be able to find this constructor to instantiate it. -If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore. - - -##生命周期 - -`onAttach()`(`Fragment`被绑定到`Activity`时调用) ---> `onCreate()`(`Fragment`创建) --> -`onCreateView()`(创建和`Fragment`关联的`View Hierarchy`时调用) --> `onActivityCreated()`(`Activity`的`onCreate()`方法返回时调用) ---> `onStart()` --> `onResume()` --> `onPause()` --> `onStop()` --> `onDestroyView()`当和`Fragment`关联的`view hierarchy`正在被移除时调用. ---> `onDestroy()`(`Activity`的`onDestroy`执行后的回调), --> `onDetach()`(当`Fragment`从`Activity`解除关联时被调用) - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/complete_android_fragment_lifecycle.png) - -##使用 - -1. 布局添加 - ```xml - - - - - - - - - import android.support.v4.app.FragmentActivity; - - public class MainActivity extends FragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.fragment_layout); - } - } - ``` - **每一个fragment 都需要一个唯一的标识,如果activity重启,系统可以用来恢复fragment(并且你也可以用来捕获fragment 来处理事务,例如移除它.)** - 有3 种方法来为一个`fragment` 提供一个标识: - 1. 为`android:id`属性提供一个唯一ID. - 2. 为`android:tag`属性提供一个唯一字符串. - 3. 如果以上2个你都没有提供,系统使用容器`view`的`ID`. - - ```java - public static ArticleFragment newInstance(int index) { - ArticleFragment f = new ArticleFragment(); - - // Supply index input as an argument. - Bundle args = new Bundle(); - args.putInt("index", index); - f.setArguments(args); - - return f; - } - ``` - -2. 通过代码添加 - 只需简单的指定一个需要放置`Fragment`的`ViewGroup`.为了在`Activity`中操作`Fragment`事务(例如添加、移除或代替),必须使用来自`FragmentTransaction`. - ```java - public class MainActivity extends FragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.fragment_layout); - - // Check that the activity is using the layout version with - // the fragment_container FrameLayout - if (findViewById(R.id.fragment_container) != null) { - - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (savedInstanceState != null) { - return; - } - - // Create a new Fragment to be placed in the activity layout - HeadlinesFragment firstFragment = new HeadlinesFragment(); - - // In case this activity was started with special instructions from an - // Intent, pass the Intent's extras to the fragment as arguments - firstFragment.setArguments(getIntent().getExtras()); - - // Add the fragment to the 'fragment_container' FrameLayout - getSupportFragmentManager().beginTransaction() - .add(R.id.fragment_container, firstFragment).commit(); - } - } - } - ``` - -##管理Fragment - -要在`activity`中管理`fragment`,需要使用`FragmentManager`. 通过调用`activity`的`getFragmentManager()`取得它的实例. -可以通过`FragmentManager`做一些事情, 包括: - 1. 使用findFragmentById() (用于在activitylayout 中提供一个UI 的fragment)或findFragmentByTag()(适用于有或没有UI 的fragment)获取activity 中存在的fragment - 2. 将fragment 从后台堆栈中弹出, 使用popBackStack() (模拟用户按下BACK 命令). - 3. 使用addOnBackStackChangeListener()注册一个监听后台堆栈变化的listener. - -## 处理Fragment事务 - -每一个事务都是同时要执行的一套变化.可以在一个给定的事务中设置你想执行的所有变化,使用诸如`add()`, `remove()`,和`replace()`. -要给`activity`应用事务, 必须调用`commit()`.在调用`commit()`之前, -你可能想调用`addToBackStack()`,将事务添加到一个fragment 事务的`backstack` -将一个fragment 替换为另一个, 并在后台堆栈中保留之前的状态: -```java -// Create new fragment and transaction -Fragment newFragment = new ExampleFragment(); -FragmentTransaction transaction = getFragmentManager().beginTransaction(); -// Replace whatever is in the fragment_container view with this fragment, -// and add the transaction to the back stack -transaction.replace(R.id.fragment_container, newFragment); -transaction.addToBackStack(null); -// Commit the transaction -transaction.commit(); -``` - -`Fragment`通过调用`addToBackStack()`, `replace`事务被保存到`back stack`,因此用户可以回退事务,并通过按下`BACK`按键带回前一个`Fragment`. - -## Fragment真正的onPause以及onResume - -`Fragment`虽然有`onResume()`和`onPause()`方法,但是这两个方法是`Activity`的方法调用时机也与`Activity`相同, -和`ViewPager`搭配使用这个方法就很鸡肋了,根本不是你想要的效果,这里介绍一种方法。 -```java -@Override -public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (isVisibleToUser) { - //相当于Fragment的onResume - } else { - //相当于Fragment的onPause - } -} -``` - -通过阅读`ViewPager`和`PageAdapter`相关的代码,切换`Fragment`实际上就是通过设置`setUserVisibleHint`和`setMenuVisibility`来实现的, -调用这个方法时并不会释放掉`Fragment`(即不会执行`onDestoryView`)。 - -##Fragment与ViewPager搭配 -`FragmentStatePagerAdapter`,会自动保存和恢复`Fragment`。 -```java -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -... -public class ScreenSlidePagerActivity extends FragmentActivity { - /** - * The number of pages (wizard steps) to show in this demo. - */ - private static final int NUM_PAGES = 5; - - /** - * The pager widget, which handles animation and allows swiping horizontally to access previous - * and next wizard steps. - */ - private ViewPager mPager; - - /** - * The pager adapter, which provides the pages to the view pager widget. - */ - private PagerAdapter mPagerAdapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_screen_slide); - - // Instantiate a ViewPager and a PagerAdapter. - mPager = (ViewPager) findViewById(R.id.pager); - mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); - mPager.setAdapter(mPagerAdapter); - } - - @Override - public void onBackPressed() { - if (mPager.getCurrentItem() == 0) { - // If the user is currently looking at the first step, allow the system to handle the - // Back button. This calls finish() on this activity and pops the back stack. - super.onBackPressed(); - } else { - // Otherwise, select the previous step. - mPager.setCurrentItem(mPager.getCurrentItem() - 1); - } - } - - /** - * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in - * sequence. - */ - private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { - public ScreenSlidePagerAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public Fragment getItem(int position) { - return new ScreenSlidePageFragment(); - } - - @Override - public int getCount() { - return NUM_PAGES; - } - } -} -``` - -如何给`ViewPager`切换时增加动画. -```java -ViewPager mPager = (ViewPager) findViewById(R.id.pager); -mPager.setPageTransformer(true, new DepthPageTransformer ()); -``` - -```java -public class DepthPageTransformer implements ViewPager.PageTransformer { - private static final float MIN_SCALE = 0.75f; - - public void transformPage(View view, float position) { - int pageWidth = view.getWidth(); - - if (position < -1) { // [-Infinity,-1) - // This page is way off-screen to the left. - view.setAlpha(0); - - } else if (position <= 0) { // [-1,0] - // Use the default slide transition when moving to the left page - view.setAlpha(1); - view.setTranslationX(0); - view.setScaleX(1); - view.setScaleY(1); - - } else if (position <= 1) { // (0,1] - // Fade the page out. - view.setAlpha(1 - position); - - // Counteract the default slide transition - view.setTranslationX(pageWidth * -position); - - // Scale the page down (between MIN_SCALE and 1) - float scaleFactor = MIN_SCALE - + (1 - MIN_SCALE) * (1 - Math.abs(position)); - view.setScaleX(scaleFactor); - view.setScaleY(scaleFactor); - - } else { // (1,+Infinity] - // This page is way off-screen to the right. - view.setAlpha(0); - } - } -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com +Fragment专题 +=== + +##简介 + +A Fragment is a piece of an application's user interface or behavior that can be placed in an Activity. +Interaction with fragments is done through FragmentManager, +which can be obtained via Activity.getFragmentManager() and Fragment.getFragmentManager(). + +The Fragment class can be used many ways to achieve a wide variety of results. +In its core, it represents a particular operation or interface that is running within a larger Activity. +A Fragment is closely tied to the Activity it is in, and can not be used apart from one. +Though Fragment defines its own lifecycle, that lifecycle is dependent on its activity: if the activity is stopped, +no fragments inside of it can be started; when the activity is destroyed, all fragments will be destroyed. + +All subclasses of Fragment must include a public no-argument constructor. +The framework will often re-instantiate a fragment class when needed, in particular during state restore, +and needs to be able to find this constructor to instantiate it. +If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore. + + +##生命周期 + +`onAttach()`(`Fragment`被绑定到`Activity`时调用) ---> `onCreate()`(`Fragment`创建) --> +`onCreateView()`(创建和`Fragment`关联的`View Hierarchy`时调用) --> `onActivityCreated()`(`Activity`的`onCreate()`方法返回时调用) +--> `onStart()` --> `onResume()` --> `onPause()` --> `onStop()` --> `onDestroyView()`当和`Fragment`关联的`view hierarchy`正在被移除时调用. +--> `onDestroy()`(`Activity`的`onDestroy`执行后的回调), --> `onDetach()`(当`Fragment`从`Activity`解除关联时被调用) + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/complete_android_fragment_lifecycle.png) + +##使用 + +1. 布局添加 + ```xml + + + + + + + + + import android.support.v4.app.FragmentActivity; + + public class MainActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fragment_layout); + } + } + ``` + **每一个fragment 都需要一个唯一的标识,如果activity重启,系统可以用来恢复fragment(并且你也可以用来捕获fragment 来处理事务,例如移除它.)** + 有3 种方法来为一个`fragment` 提供一个标识: + 1. 为`android:id`属性提供一个唯一ID. + 2. 为`android:tag`属性提供一个唯一字符串. + 3. 如果以上2个你都没有提供,系统使用容器`view`的`ID`. + + ```java + public static ArticleFragment newInstance(int index) { + ArticleFragment f = new ArticleFragment(); + + // Supply index input as an argument. + Bundle args = new Bundle(); + args.putInt("index", index); + f.setArguments(args); + + return f; + } + ``` + +2. 通过代码添加 + 只需简单的指定一个需要放置`Fragment`的`ViewGroup`.为了在`Activity`中操作`Fragment`事务(例如添加、移除或代替),必须使用来自`FragmentTransaction`. + ```java + public class MainActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fragment_layout); + + // Check that the activity is using the layout version with + // the fragment_container FrameLayout + if (findViewById(R.id.fragment_container) != null) { + + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Create a new Fragment to be placed in the activity layout + HeadlinesFragment firstFragment = new HeadlinesFragment(); + + // In case this activity was started with special instructions from an + // Intent, pass the Intent's extras to the fragment as arguments + firstFragment.setArguments(getIntent().getExtras()); + + // Add the fragment to the 'fragment_container' FrameLayout + getSupportFragmentManager().beginTransaction() + .add(R.id.fragment_container, firstFragment).commit(); + } + } + } + ``` + +##管理Fragment + +要在`activity`中管理`fragment`,需要使用`FragmentManager`. 通过调用`activity`的`getFragmentManager()`取得它的实例. +可以通过`FragmentManager`做一些事情, 包括: + 1. 使用findFragmentById() (用于在activitylayout 中提供一个UI 的fragment)或findFragmentByTag()(适用于有或没有UI 的fragment)获取activity 中存在的fragment + 2. 将fragment 从后台堆栈中弹出, 使用popBackStack() (模拟用户按下BACK 命令). + 3. 使用addOnBackStackChangeListener()注册一个监听后台堆栈变化的listener. + +## 处理Fragment事务 + +每一个事务都是同时要执行的一套变化.可以在一个给定的事务中设置你想执行的所有变化,使用诸如`add()`, `remove()`,和`replace()`. +要给`activity`应用事务, 必须调用`commit()`.在调用`commit()`之前, +你可能想调用`addToBackStack()`,将事务添加到一个fragment 事务的`backstack` +将一个fragment 替换为另一个, 并在后台堆栈中保留之前的状态: +```java +// Create new fragment and transaction +Fragment newFragment = new ExampleFragment(); +FragmentTransaction transaction = getFragmentManager().beginTransaction(); +// Replace whatever is in the fragment_container view with this fragment, +// and add the transaction to the back stack +transaction.replace(R.id.fragment_container, newFragment); +transaction.addToBackStack(null); +// Commit the transaction +transaction.commit(); +``` + +`Fragment`通过调用`addToBackStack()`, `replace`事务被保存到`back stack`,因此用户可以回退事务,并通过按下`BACK`按键带回前一个`Fragment`. + +## Fragment真正的onPause以及onResume + +`Fragment`虽然有`onResume()`和`onPause()`方法,但是这两个方法是`Activity`的方法调用时机也与`Activity`相同, +和`ViewPager`搭配使用这个方法就很鸡肋了,根本不是你想要的效果,这里介绍一种方法。 +```java +@Override +public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isVisibleToUser) { + //相当于Fragment的onResume + } else { + //相当于Fragment的onPause + } +} +``` + +通过阅读`ViewPager`和`PageAdapter`相关的代码,切换`Fragment`实际上就是通过设置`setUserVisibleHint`和`setMenuVisibility`来实现的, +调用这个方法时并不会释放掉`Fragment`(即不会执行`onDestoryView`)。 + +##Fragment与ViewPager搭配 +`FragmentStatePagerAdapter`,会自动保存和恢复`Fragment`。 +```java +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +... +public class ScreenSlidePagerActivity extends FragmentActivity { + /** + * The number of pages (wizard steps) to show in this demo. + */ + private static final int NUM_PAGES = 5; + + /** + * The pager widget, which handles animation and allows swiping horizontally to access previous + * and next wizard steps. + */ + private ViewPager mPager; + + /** + * The pager adapter, which provides the pages to the view pager widget. + */ + private PagerAdapter mPagerAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_screen_slide); + + // Instantiate a ViewPager and a PagerAdapter. + mPager = (ViewPager) findViewById(R.id.pager); + mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + mPager.setAdapter(mPagerAdapter); + } + + @Override + public void onBackPressed() { + if (mPager.getCurrentItem() == 0) { + // If the user is currently looking at the first step, allow the system to handle the + // Back button. This calls finish() on this activity and pops the back stack. + super.onBackPressed(); + } else { + // Otherwise, select the previous step. + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } + } + + /** + * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in + * sequence. + */ + private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + public ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + return new ScreenSlidePageFragment(); + } + + @Override + public int getCount() { + return NUM_PAGES; + } + } +} +``` + +如何给`ViewPager`切换时增加动画. +```java +ViewPager mPager = (ViewPager) findViewById(R.id.pager); +mPager.setPageTransformer(true, new DepthPageTransformer ()); +``` + +```java +public class DepthPageTransformer implements ViewPager.PageTransformer { + private static final float MIN_SCALE = 0.75f; + + public void transformPage(View view, float position) { + int pageWidth = view.getWidth(); + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + view.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + // Use the default slide transition when moving to the left page + view.setAlpha(1); + view.setTranslationX(0); + view.setScaleX(1); + view.setScaleY(1); + + } else if (position <= 1) { // (0,1] + // Fade the page out. + view.setAlpha(1 - position); + + // Counteract the default slide transition + view.setTranslationX(pageWidth * -position); + + // Scale the page down (between MIN_SCALE and 1) + float scaleFactor = MIN_SCALE + + (1 - MIN_SCALE) * (1 - Math.abs(position)); + view.setScaleX(scaleFactor); + view.setScaleY(scaleFactor); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + view.setAlpha(0); + } + } +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/Home\351\224\256\347\233\221\345\220\254.md" "b/AndroidBasicPart/Home\351\224\256\347\233\221\345\220\254.md" similarity index 100% rename from "Android\345\237\272\347\241\200/Home\351\224\256\347\233\221\345\220\254.md" rename to "AndroidBasicPart/Home\351\224\256\347\233\221\345\220\254.md" diff --git "a/Android\345\237\272\347\241\200/HttpClient\346\211\247\350\241\214Get\345\222\214Post\350\257\267\346\261\202.md" "b/AndroidBasicPart/HttpClient\346\211\247\350\241\214Get\345\222\214Post\350\257\267\346\261\202.md" similarity index 100% rename from "Android\345\237\272\347\241\200/HttpClient\346\211\247\350\241\214Get\345\222\214Post\350\257\267\346\261\202.md" rename to "AndroidBasicPart/HttpClient\346\211\247\350\241\214Get\345\222\214Post\350\257\267\346\261\202.md" diff --git "a/Android\345\237\272\347\241\200/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" "b/AndroidBasicPart/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" similarity index 97% rename from "Android\345\237\272\347\241\200/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" rename to "AndroidBasicPart/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" index 5131fe88..3bd5e206 100644 --- "a/Android\345\237\272\347\241\200/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" +++ "b/AndroidBasicPart/JNI_C\350\257\255\350\250\200\345\237\272\347\241\200.md" @@ -1,467 +1,467 @@ -JNI_C语言基础 -=== - -1. JNI(java native interface) - `Java`本地开发接口,`JNI`是一个协议,这个协议用来沟通`Java`代码和外部的本地代码(`c/c++`). - 通过这个协议`Java`代码就可以调用外部的`c/c++`代码,外部的`c/c++`代码也可以调用java代码, - 使用JNI技术,其实就是在Java程序中,调用C语言的函数库中提供的函数,来完成一些Java语言无法完成的任务。由于Java语言和C语言结构完全不相同,因此若想让它们二者交互,则需要制定一系列的规范。 - JNI就是这组规范,此时 Java只和JNI交互,而由JNI去和C语言交互。 - - JNI技术分为两部分:Java端和C语言端。且以Java端为主导。 - - 首先,Java程序员在Java端定义一些native方法,并将这些方法以C语言头文件的方式提供给C程序员。 - - 然后,C程序员使用C语言,来实现Java程序员提供的头文件中定义的函数。 - - 接着,C程序员将函数打包成一个库文件,并将库文件交给Java程序员。 - - 最后,Java程序员在Java程序中导入库文件,然后调用native方法。 - 在Java程序执行的时候,若在某个类中调用了native方法,则虚拟机会通过JNI来转调用库文件中的C语言代码。提示:C代码最终是在Linux进程中执行的,而不是在虚拟机中。 - -2. 为什么要用JNI - - 首先,Java语言提供的类库无法满足要求(驱动开发 wifi等),且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。 - - 然后,Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。 - - 接着,使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。Opencore - - 接着,特殊的业务场景,java能反编译但是c不能,因此对于一些不想让别人知道的东西可以用c,加密等 - -3. 怎么用JNI - 1. C/C++语言 - 2. 掌握java jni流程 - 3. NDK (native develop kits ) - -4. 指针和指针变量的关系 - 指针就是地址,地址就是指针 - 地址就是内存单元的编号 - 指针变量是存放地址(指针)的变量 - 指针和指针变量是两个不同的概念 - 但是要注意: 通常我们叙述时会把指针变量简称为指针,实际它们含义并不一样 - - - 未经过初始化的指针变量,不能够直接使用 - - 指针变量的类型 不能够相互转换 - - 函数的变量(静态)不能够跨函数访问,失去了函数变量的访问范围(生命周期),因为方法执行完之后会释放内存,所以方法中的变量就没有了, - 但是地址值还是能拿到的,因为地址值是内存中真实存在的地址位置 - - 指针声明的三种方式 - int * p; //p 是变量的名字, int * 是一个类型,这个变量存放的是int类型变量的地址。 - int* p int * p int *p - -5. *号的三种含义 - 1. 乘法 3*5 - 2. 定义指针变量 int * p; - 3. 指针运算符,如果p是一个已经定义好的指针变量,则*p表示以p的内容为地址的变量 - -6. 为什么要使用指针(指针的重要性) - 直接访问硬件 (opengl 显卡绘图) - 快速传递数据(指针表示地址) - 返回一个以上的值(返回一个数组或者结构体的指针) - 表示复杂的数据结构(结构体) - 方便处理字符串 - 指针有助于理解面向对象 - -7. 指针和数组的关系 - 一维数组的数组名是个指针常量,它存放的是一维数组第一个元素的地址 - C中数组的定义比较死板,中括号必须放到名字的后面 - int a[5]; - printf("%#X\n",&a[0]); - printf("%#X\n",&a); - - 如果p是一维数组 则p[i] 等价于 *(p+i),都是得到一维数组中的第i个元素。 - 在c语言中不会检查角标越界如int a[5]写a[5]不会报错 - -8. 动态分配内存Malloc - ```c - 采用malloc在椎内存中申请空间 - #include //不能省 malloc 是 memory(内存) allocate(分配)的缩写 - #include - - main(){ - //malloc() 在堆空间中动态的申请一块连续的内存空间(数组) - // 参数: 指定申请的内存空间的大小(字节) - // 返回值: 所申请空间的首地址(数组的第一个元素的地址),返回值是Void数据类型 - - int* p = (int*) malloc( sizeof(int) ); //因为返回值是一个Void类型,所以要强转 - *p = 99; - - //free()释放已经分配的内存块 - //参数: 指定释放哪块内存空间(地址) - - free(p); - printf("内容是 %d\n", *p); //上面的free只是释放内存块中的内容,但是打印这个内存块的地址还是能够打印出来的,因为这个内存块的地址是内存上的地址是真实存在的 - system("pause"); - } - - 如果动态申请的内存不够用,那么可以继续申请内存 - 用realloc - /* - 1\创建数组 - 2、赋值 - 3、打印 - */ - #include - #include - - void printArr(int* arr, int len){ - int i; - for( i = 0 ; i < len; i++){ - printf("arr[ %d ]= %d\n", i, *(arr+i)); - } - - } - - main(){ - printf("请您输入所要创建的数组大小: \n"); - int len ; - scanf("%d", &len); &是取地址符 - - //动态数组创建 - int* arr = (int*) malloc( sizeof(int) * len); - - printf("请您为每个元素赋值: \n"); - - int i; - for(i = 0; i < len; i++){ - int temp; - scanf("%d", &temp); - - *(arr + i) = temp; - } - - //打印 - printf("数组元素的值为: \n"); - printArr(arr, len); - - //----------------------------------------- - - printf("请输入增加的元素个数: \n"); - int count; - scanf("%d", &count); - - //更改数组大小 - //realloc() - //参数1: 指定所需修改的数组 - //参数2: 指定修改后的数组的大小 - //返回值:修改后数组的首地址 (VOID) - arr = (int*) realloc(arr, len + count); - - printf("请为新增加的元素赋值: \n"); - - int j; - for(j = len; j < len + count; j++){ - int temp; - scanf("%d", &temp); - - *(arr + j) = temp; - } - - //打印 - printf("数组元素的值为: \n"); - printArr(arr, len + count); - - system("pause"); - } - 或者有个简单的写法 - main() - { - int* arr =(int* ) malloc(sizeof(int)*len) ; //动态申请的内存 - int i=0; - for(;i char str[] ={'h','e','l','l','o','\0'}; - char cc[20] = "heima 15"; - char cc[20] = {'h','e','i','m','a'}; - -11. c文件的后缀是.c - ```c - 示例代码: - c中的打印语句中要有类似占位符,在后面的参数对占位符的内容进行声明 - #include - main() { - printf("%d\n",sizeof(int)); //sizeof() 得到制定数据类型的长度(占用字节数)参数 接受一个数据类型 - printf("%d\n",sizeof(char)); - system("pause");//可以执行命令行中的命令如 system("shutdown -s -t 60");如果不加pause,运行窗口会一闪而过,因为会释放内存,把dos关闭了,所以要加上pause - } - ``` - -12. C中的输入输出 - %d - int - %ld – long int - %c - char - %f - float - %lf – double - %x – 十六进制输出 int 或者long int 或者short int - %#x – 以0x开头 十六进制输出 int 或者long int 或者short int - %o - 八进制输出 - %s – 字符串 - -13. c语言从键盘输入一个字符串 - ```c - //scanf() 接收键盘输入的数据,参数1: 指定接收的数据的数据类型参数 2: 指定接收的数据存放的位置 - #include - main() { - char c[20]; - scanf("%s", c); - printf("%s", c); - system("pause"); - } - ``` - -14. 取地址符 &(能得到一个对象的地址) - -15. C中两个数的交换 - ```c - #include - void swap(int* i, int* j) { - int temp = *i; - *i = *j; - *j = temp; - } - main() { - int i = 3; - int j = 5; - swap(&i, &j); - printf("%d",i); - printf("%d",j); - system("pause"); - } - ``` - -16. C中的for循环 - ```c - #include - - /** - 打印输出数组的每一个元素 - */ - void printArr(int* arr, int len){ - int i; - for(i=0;i - int add(int x,int y){ - return x+y; - } - main() - { - int (*pf)(int x, int y); //定义一个函数的指针,就是讲函数的名字改了,其它的都和函数的定义一样 - pf = add; //将pf指向add - printf("result=%d\n", pf(3,5)); //使用pf - - system("pause"); - } - - 内存的四个部分 Stack Heap CodeSegment DataSegment - 函数是存放到CodeSegment中的,这个函数的地址就是CodeSegment中的这个函数的地址,我们得到函数的地址如果去访问这个地址就相当于调用了这个函数 - ``` - -19. 结构体(类似于java中的类) - ```c - #include - struct Student - { - int age; //4 - float score; //4 - long id; //4 - char sex; //1 - }; - int main(void) - { - struct Student st={80,55.6f,10001,'F'}; - printf("st.age=%d\n",st.age); - - printf("结构体的长度为%d\n",sizeof(st));//打印出来的结果是16为什么呢? 编译器为了方便起见 做了处理,它将所有的变量的长度都统一成最大的长度 - - struct Student* pst = &st;//结构体的指针 - - printf("st.age=%d\n",(*pst).age);//(*pst)就是得到结构体,由于*的优先级比较低,通常要用括号括起来。 - - printf("age=%d\n",pst->age);//这一行是上一行的简单写法,pst->age 在计算机内部会被转换为 (*pst).age pst->age的含义: pst所指向的结构体变量中的age这个成员 - system("pause"); - } - ``` - 结构体的三种写法 - 第一种 - ```c - struct Student - { - int age; - float score; - char sex; - } - ``` - 第二种 - ```c - struct Student2 - { - int age; - float score; - char sex; - } st2;//相当于java中直接弄了一个对象 - ``` - 第三种 - ```c - struct - { - int age; - float score; - char sex; - } st3; - ``` - -20. Union联合体 - ```c - #include - main() { - struct date { int year, month, day; }today; - union { long i; int k; char ii; double d; } mix; - - printf("date:%d\n",sizeof(struct date)); - printf("mix:%d\n",sizeof(mix)); - mix.i = 33; - mix.ii = 'a'; - printf("i=%d\n",mix.i); //结果是96,因为联合体是一个公用的内存空间,在存ii的时候将i的值给覆盖了 - //110100000101 - - system("pause"); - } - 联合体是一个公用的内存空间,联合体长度为: 占有字节数最大的元素的长度(字节数) - ``` - -21. 枚举 - ```c - enum WeekDay { - Monday=8,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday - };//这里一定要加分号 - - int main(void) - { - enum WeekDay day = Sunday; - printf("%d\n",day);//打印出来是14,因为是从8开始往后逐个加1 - system("pause"); - return 0; - } - 默认的情况是从0开始往后递加 - ``` - -22. typedef - 定义别名 - 声明自定义数据类型,配合各种原有数据类型来达到简化编程的目的的类型定义关键字。 - ```c - typedef int haha; //定义数据类型的别名. - int main(void) - { - haha i = 3;这里haha就相当于int - printf("i=%d\n",i); - } - ``` - 每一个指针占四个字节 - -23. 多级指针 - ```c - #include - main() { - int i = 88; - int* p = &i; - int** q = &p; //指针的指针前面要加两个* - int*** r = &q; - printf("i=%d\n",***r); - system("pause"); - } - ``` - -C语言常见术语: -库函数: -- 为了代码重用,在C语言中提供了一些常用的、用于执行一些标准任务(如输入/出)的函数,这些函数事先被编译,并生成目标代码,然后将生成的目标代码打包成一个库文件, -以供再次使用。 库文件中的函数被称为库函数,库文件被称为函数库。 -通过头文件的方式 把函数库里面所有的函数暴露 .h -- 在Windows中C语言库函数中的目标代码都是以.obj为后缀的,Linux中是以 .o为后缀。 -提示:单个目标代码是无法直接执行的,目标代码在运行之前需要使用连接程序将目标代码和其他库函数连接在一起后生成可执行的文件。Windows ->.exe .dll -Linux -> .so 动态库 - .a 静态库 -头文件: -- 头文件中存放的是对某个库中所定义的函数、宏、类型、全局变量等进行声明,它类似于一份仓库清单。若用户程序中需要使用某个库中的函数, - 则只需要将该库所对应的头文件include到程序中即可。 -- 头文件中定义的是库中所有函数的函数原型。而函数的具体实现则是在库文件中。 -- 简单的说:头文件是给编译器用的,库文件是给连接器用的。 -- 在连接器连接程序时,会依据用户程序中导入的头文件,将对应的库函数导入到程序中。头文件以.h为后缀名。 -函数库: -- 动态库:在编译用户程序时不会将用户程序内使用的库函数连接到用户程序的目标代码中,只有在运行时,且用户程序执行到相关函数时才会调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。 .so 动态库 -- 静态库:在编译用户程序时会将其内使用的库函数连接到目标代码中,程序运行时不再需要动态库。使用静态库生成可执行文件比较大。 -在Linux中: -- 静态库命名一般为:lib+库名+.a 。 -- 如:libcxy.a 其中lib说明此文件是一个库文件,cxy是库的名称,.a说明是静态的。 -- 动态库命名一般为:lib+库名+.so 。.so说明是动态的。 -Windows 下的动态库 .dll - ---- - -- 邮箱 :charon.chui@gmail.com +JNI_C语言基础 +=== + +1. JNI(java native interface) + `Java`本地开发接口,`JNI`是一个协议,这个协议用来沟通`Java`代码和外部的本地代码(`c/c++`). + 通过这个协议`Java`代码就可以调用外部的`c/c++`代码,外部的`c/c++`代码也可以调用java代码, + 使用JNI技术,其实就是在Java程序中,调用C语言的函数库中提供的函数,来完成一些Java语言无法完成的任务。由于Java语言和C语言结构完全不相同,因此若想让它们二者交互,则需要制定一系列的规范。 + JNI就是这组规范,此时 Java只和JNI交互,而由JNI去和C语言交互。 + + JNI技术分为两部分:Java端和C语言端。且以Java端为主导。 + - 首先,Java程序员在Java端定义一些native方法,并将这些方法以C语言头文件的方式提供给C程序员。 + - 然后,C程序员使用C语言,来实现Java程序员提供的头文件中定义的函数。 + - 接着,C程序员将函数打包成一个库文件,并将库文件交给Java程序员。 + - 最后,Java程序员在Java程序中导入库文件,然后调用native方法。 + 在Java程序执行的时候,若在某个类中调用了native方法,则虚拟机会通过JNI来转调用库文件中的C语言代码。提示:C代码最终是在Linux进程中执行的,而不是在虚拟机中。 + +2. 为什么要用JNI + - 首先,Java语言提供的类库无法满足要求(驱动开发 wifi等),且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。 + - 然后,Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。 + - 接着,使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。Opencore + - 接着,特殊的业务场景,java能反编译但是c不能,因此对于一些不想让别人知道的东西可以用c,加密等 + +3. 怎么用JNI + 1. C/C++语言 + 2. 掌握java jni流程 + 3. NDK (native develop kits ) + +4. 指针和指针变量的关系 + 指针就是地址,地址就是指针 + 地址就是内存单元的编号 + 指针变量是存放地址(指针)的变量 + 指针和指针变量是两个不同的概念 + 但是要注意: 通常我们叙述时会把指针变量简称为指针,实际它们含义并不一样 + + - 未经过初始化的指针变量,不能够直接使用 + - 指针变量的类型 不能够相互转换 + - 函数的变量(静态)不能够跨函数访问,失去了函数变量的访问范围(生命周期),因为方法执行完之后会释放内存,所以方法中的变量就没有了, + 但是地址值还是能拿到的,因为地址值是内存中真实存在的地址位置 + - 指针声明的三种方式 + int * p; //p 是变量的名字, int * 是一个类型,这个变量存放的是int类型变量的地址。 + int* p int * p int *p + +5. *号的三种含义 + 1. 乘法 3*5 + 2. 定义指针变量 int * p; + 3. 指针运算符,如果p是一个已经定义好的指针变量,则*p表示以p的内容为地址的变量 + +6. 为什么要使用指针(指针的重要性) + 直接访问硬件 (opengl 显卡绘图) + 快速传递数据(指针表示地址) + 返回一个以上的值(返回一个数组或者结构体的指针) + 表示复杂的数据结构(结构体) + 方便处理字符串 + 指针有助于理解面向对象 + +7. 指针和数组的关系 + 一维数组的数组名是个指针常量,它存放的是一维数组第一个元素的地址 + C中数组的定义比较死板,中括号必须放到名字的后面 + int a[5]; + printf("%#X\n",&a[0]); + printf("%#X\n",&a); + + 如果p是一维数组 则p[i] 等价于 *(p+i),都是得到一维数组中的第i个元素。 + 在c语言中不会检查角标越界如int a[5]写a[5]不会报错 + +8. 动态分配内存Malloc + ```c + 采用malloc在椎内存中申请空间 + #include //不能省 malloc 是 memory(内存) allocate(分配)的缩写 + #include + + main(){ + //malloc() 在堆空间中动态的申请一块连续的内存空间(数组) + // 参数: 指定申请的内存空间的大小(字节) + // 返回值: 所申请空间的首地址(数组的第一个元素的地址),返回值是Void数据类型 + + int* p = (int*) malloc( sizeof(int) ); //因为返回值是一个Void类型,所以要强转 + *p = 99; + + //free()释放已经分配的内存块 + //参数: 指定释放哪块内存空间(地址) + + free(p); + printf("内容是 %d\n", *p); //上面的free只是释放内存块中的内容,但是打印这个内存块的地址还是能够打印出来的,因为这个内存块的地址是内存上的地址是真实存在的 + system("pause"); + } + + 如果动态申请的内存不够用,那么可以继续申请内存 + 用realloc + /* + 1\创建数组 + 2、赋值 + 3、打印 + */ + #include + #include + + void printArr(int* arr, int len){ + int i; + for( i = 0 ; i < len; i++){ + printf("arr[ %d ]= %d\n", i, *(arr+i)); + } + + } + + main(){ + printf("请您输入所要创建的数组大小: \n"); + int len ; + scanf("%d", &len); &是取地址符 + + //动态数组创建 + int* arr = (int*) malloc( sizeof(int) * len); + + printf("请您为每个元素赋值: \n"); + + int i; + for(i = 0; i < len; i++){ + int temp; + scanf("%d", &temp); + + *(arr + i) = temp; + } + + //打印 + printf("数组元素的值为: \n"); + printArr(arr, len); + + //----------------------------------------- + + printf("请输入增加的元素个数: \n"); + int count; + scanf("%d", &count); + + //更改数组大小 + //realloc() + //参数1: 指定所需修改的数组 + //参数2: 指定修改后的数组的大小 + //返回值:修改后数组的首地址 (VOID) + arr = (int*) realloc(arr, len + count); + + printf("请为新增加的元素赋值: \n"); + + int j; + for(j = len; j < len + count; j++){ + int temp; + scanf("%d", &temp); + + *(arr + j) = temp; + } + + //打印 + printf("数组元素的值为: \n"); + printArr(arr, len + count); + + system("pause"); + } + 或者有个简单的写法 + main() + { + int* arr =(int* ) malloc(sizeof(int)*len) ; //动态申请的内存 + int i=0; + for(;i char str[] ={'h','e','l','l','o','\0'}; + char cc[20] = "heima 15"; + char cc[20] = {'h','e','i','m','a'}; + +11. c文件的后缀是.c + ```c + 示例代码: + c中的打印语句中要有类似占位符,在后面的参数对占位符的内容进行声明 + #include + main() { + printf("%d\n",sizeof(int)); //sizeof() 得到制定数据类型的长度(占用字节数)参数 接受一个数据类型 + printf("%d\n",sizeof(char)); + system("pause");//可以执行命令行中的命令如 system("shutdown -s -t 60");如果不加pause,运行窗口会一闪而过,因为会释放内存,把dos关闭了,所以要加上pause + } + ``` + +12. C中的输入输出 + %d - int + %ld – long int + %c - char + %f - float + %lf – double + %x – 十六进制输出 int 或者long int 或者short int + %#x – 以0x开头 十六进制输出 int 或者long int 或者short int + %o - 八进制输出 + %s – 字符串 + +13. c语言从键盘输入一个字符串 + ```c + //scanf() 接收键盘输入的数据,参数1: 指定接收的数据的数据类型参数 2: 指定接收的数据存放的位置 + #include + main() { + char c[20]; + scanf("%s", c); + printf("%s", c); + system("pause"); + } + ``` + +14. 取地址符 &(能得到一个对象的地址) + +15. C中两个数的交换 + ```c + #include + void swap(int* i, int* j) { + int temp = *i; + *i = *j; + *j = temp; + } + main() { + int i = 3; + int j = 5; + swap(&i, &j); + printf("%d",i); + printf("%d",j); + system("pause"); + } + ``` + +16. C中的for循环 + ```c + #include + + /** + 打印输出数组的每一个元素 + */ + void printArr(int* arr, int len){ + int i; + for(i=0;i + int add(int x,int y){ + return x+y; + } + main() + { + int (*pf)(int x, int y); //定义一个函数的指针,就是讲函数的名字改了,其它的都和函数的定义一样 + pf = add; //将pf指向add + printf("result=%d\n", pf(3,5)); //使用pf + + system("pause"); + } + + 内存的四个部分 Stack Heap CodeSegment DataSegment + 函数是存放到CodeSegment中的,这个函数的地址就是CodeSegment中的这个函数的地址,我们得到函数的地址如果去访问这个地址就相当于调用了这个函数 + ``` + +19. 结构体(类似于java中的类) + ```c + #include + struct Student + { + int age; //4 + float score; //4 + long id; //4 + char sex; //1 + }; + int main(void) + { + struct Student st={80,55.6f,10001,'F'}; + printf("st.age=%d\n",st.age); + + printf("结构体的长度为%d\n",sizeof(st));//打印出来的结果是16为什么呢? 编译器为了方便起见 做了处理,它将所有的变量的长度都统一成最大的长度 + + struct Student* pst = &st;//结构体的指针 + + printf("st.age=%d\n",(*pst).age);//(*pst)就是得到结构体,由于*的优先级比较低,通常要用括号括起来。 + + printf("age=%d\n",pst->age);//这一行是上一行的简单写法,pst->age 在计算机内部会被转换为 (*pst).age pst->age的含义: pst所指向的结构体变量中的age这个成员 + system("pause"); + } + ``` + 结构体的三种写法 + 第一种 + ```c + struct Student + { + int age; + float score; + char sex; + } + ``` + 第二种 + ```c + struct Student2 + { + int age; + float score; + char sex; + } st2;//相当于java中直接弄了一个对象 + ``` + 第三种 + ```c + struct + { + int age; + float score; + char sex; + } st3; + ``` + +20. Union联合体 + ```c + #include + main() { + struct date { int year, month, day; }today; + union { long i; int k; char ii; double d; } mix; + + printf("date:%d\n",sizeof(struct date)); + printf("mix:%d\n",sizeof(mix)); + mix.i = 33; + mix.ii = 'a'; + printf("i=%d\n",mix.i); //结果是96,因为联合体是一个公用的内存空间,在存ii的时候将i的值给覆盖了 + //110100000101 + + system("pause"); + } + 联合体是一个公用的内存空间,联合体长度为: 占有字节数最大的元素的长度(字节数) + ``` + +21. 枚举 + ```c + enum WeekDay { + Monday=8,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday + };//这里一定要加分号 + + int main(void) + { + enum WeekDay day = Sunday; + printf("%d\n",day);//打印出来是14,因为是从8开始往后逐个加1 + system("pause"); + return 0; + } + 默认的情况是从0开始往后递加 + ``` + +22. typedef + 定义别名 + 声明自定义数据类型,配合各种原有数据类型来达到简化编程的目的的类型定义关键字。 + ```c + typedef int haha; //定义数据类型的别名. + int main(void) + { + haha i = 3;这里haha就相当于int + printf("i=%d\n",i); + } + ``` + 每一个指针占四个字节 + +23. 多级指针 + ```c + #include + main() { + int i = 88; + int* p = &i; + int** q = &p; //指针的指针前面要加两个* + int*** r = &q; + printf("i=%d\n",***r); + system("pause"); + } + ``` + +C语言常见术语: +库函数: +- 为了代码重用,在C语言中提供了一些常用的、用于执行一些标准任务(如输入/出)的函数,这些函数事先被编译,并生成目标代码,然后将生成的目标代码打包成一个库文件, +以供再次使用。 库文件中的函数被称为库函数,库文件被称为函数库。 +通过头文件的方式 把函数库里面所有的函数暴露 .h +- 在Windows中C语言库函数中的目标代码都是以.obj为后缀的,Linux中是以 .o为后缀。 +提示:单个目标代码是无法直接执行的,目标代码在运行之前需要使用连接程序将目标代码和其他库函数连接在一起后生成可执行的文件。Windows ->.exe .dll +Linux -> .so 动态库 + .a 静态库 +头文件: +- 头文件中存放的是对某个库中所定义的函数、宏、类型、全局变量等进行声明,它类似于一份仓库清单。若用户程序中需要使用某个库中的函数, + 则只需要将该库所对应的头文件include到程序中即可。 +- 头文件中定义的是库中所有函数的函数原型。而函数的具体实现则是在库文件中。 +- 简单的说:头文件是给编译器用的,库文件是给连接器用的。 +- 在连接器连接程序时,会依据用户程序中导入的头文件,将对应的库函数导入到程序中。头文件以.h为后缀名。 +函数库: +- 动态库:在编译用户程序时不会将用户程序内使用的库函数连接到用户程序的目标代码中,只有在运行时,且用户程序执行到相关函数时才会调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。 .so 动态库 +- 静态库:在编译用户程序时会将其内使用的库函数连接到目标代码中,程序运行时不再需要动态库。使用静态库生成可执行文件比较大。 +在Linux中: +- 静态库命名一般为:lib+库名+.a 。 +- 如:libcxy.a 其中lib说明此文件是一个库文件,cxy是库的名称,.a说明是静态的。 +- 动态库命名一般为:lib+库名+.so 。.so说明是动态的。 +Windows 下的动态库 .dll + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/JNI\345\237\272\347\241\200.md" "b/AndroidBasicPart/JNI\345\237\272\347\241\200.md" similarity index 97% rename from "Android\345\237\272\347\241\200/JNI\345\237\272\347\241\200.md" rename to "AndroidBasicPart/JNI\345\237\272\347\241\200.md" index 4cb61518..8de0b638 100644 --- "a/Android\345\237\272\347\241\200/JNI\345\237\272\347\241\200.md" +++ "b/AndroidBasicPart/JNI\345\237\272\347\241\200.md" @@ -1,410 +1,410 @@ -JNI基础 -=== - -1. 将java中的字符串转换成C中字符串的工具方法 - ```c - char* Jstring2CStr(JNIEnv* env, jstring jstr){ - char* rtn = NULL; - jclass clsstring = (*env)->FindClass(env,"java/lang/String"); - jstring strencode = (*env)->NewStringUTF(env,"GB2312"); - jmethodID mid = (*env)->GetMethodID(env,clsstring, "getBytes", "(Ljava/lang/String;)[B"); - jbyteArray barr= (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312"); - jsize alen = (*env)->GetArrayLength(env,barr); - jbyte* ba = (*env)->GetByteArrayElements(env,barr,JNI_FALSE); - if(alen > 0) - { - rtn = (char*)malloc(alen+1); //"\0" - memcpy(rtn,ba,alen); - rtn[alen]=0; - } - (*env)->ReleaseByteArrayElements(env,barr,ba,0); // - return rtn; - } - ``` - -2. 程序被运行要经历两个步骤(1.编译 2.链接) - 编译就是将源文件编译成二进制代码,而链接则是将二进制代码转换成可执行的文件如.exe等头文件.(函数的声明,函数的清单文件)作用: 给编译器看的. - 库函数: 头文件里面函数的实现. - 作用:给连接器看的. - -3. jni开发的常见错误: - 错误1: 忘记编写android.mk文件 unknown file: ./jni/Android.mk - Android NDK: Your APP_BUILD_SCRIPT points to an unknown file: ./jni/Android.mk - /cygdrive/c/android-ndk-r7b/build/core/add-application.mk:133: Android NDK: Aborting... 。 停止。 - - 错误2: ndk-build 没有任何反应. - 忘记配置android.mk脚本 - - 错误3: $ ndk-build jni/Android.mk:4: 遗漏分隔符 。 停止。 -中文的回车或者换行 - - 错误4:java.lang.UnsatisfiedLinkError: hello - 忘记加载了c代码的.so库 或者 函数的签名不正确,没有找到与之对应的c代码 - - 错误5:07-30 java.lang.UnsatisfiedLinkError: Library Hel1o not found - 没有找到对应的c代码库 - - 错误6: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - 07-30 11:53:17.898: INFO/DEBUG(31): Build fingerprint: - 'generic/sdk/generic/:2.2/FRF91/43546:eng/test-keys' - c代码里面有严重的逻辑错误,产生内存的泄露. - - 错误7: - make: *** [obj/local/armeabi/objs/Hello/Hello.o] Error 1 - 编译的时候 程序出现了问题,c语言的语法有问题 - c语言代码编译错误的时候 先去解决第一个错误. - -4. Java调用JNI的前提 - 开发所使用的电脑(windows系统, x86的CPU) - 目标代码: android手机上运行的.( linux系统, arm的CPU) - 所以我们要模拟手机的系统,手机的处理器,生成手机上可以运行的二进制代码这就要用到交叉编译; - 根据运行的设备的不同,可以将cpu分为: - - arm结构 :主要在移动手持、嵌入式设备上。 - - x86结构 : 主要在台式机、笔记本上使用。如Intel和AMD的CPU 。 - 交叉编译: 在一种操作系统平台或者cpu平台下 编译生成 另外一个平台(cpu)可以运行的二进制代码.(使用NDK中的ndk-build命令) - - -- 工具一: 交叉编译的工具链: NDK - NDK全称:Native Development Kit 。 - - NDK是一系列工具的集合,它有很多作用。 - - 首先,NDK可以帮助开发者快速开发C(或C++)的动态库。 - - 其次,NDK集成了交叉编译器。使用NDK,我们可以将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。 - - NDK工具是提供给Linux系统用的(随着版本的升级也可以直接在Windows下使用,但是现在仍不完善有bug), - 所以要在windows下使用ndk的工具,必须要提供一个工具(linux环境的模拟器) - -- 工具二: cygwin(windows下linux系统环境的模拟器, 主要是为了能够运行ndk的工具) - 安装 devel shell - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jni_cygwin.png?raw=true) - linux 特点:所有的设备 硬件 都是以文件的方式定义的. - 安装完后进入`cygwin`打印`make -v`命令如果能打印出`GNU Make ...`就说明安装木问题了。 - -- 工具三: cdt(c/c++ develop tools) eclipse 的一个插件 用来让c\c++代码 语法高亮显示. - adt(android develop tools) - -- 工具四: - 为了不用每次使用ndk-build命令都要进入到ndk的安装目录,这里要进行Path变量的配置。 - 配置cygwin的环境变量: 在cygwin安装目录,etc目录,profile的文件 32行 添加ndk工具所在的目录. - `PATH="/usr/local/bin:/usr/bin:/cygdrive/d/android-ndk-r7b:${PATH}"`在这个后面加上:ndk-build的路径(注意:在linux中路径的分隔符不是分号而是冒号), - 改成这样 - `PATH="/usr/local/bin:/usr/bin:${PATH}:/cygdrive/d/android-ndk-r7b"`//注意这里的路径是在linux系统下的ndk路径而不是windows下的路径,/cygdrive/d/是在linux下看到的d盘。 - -###JNI开发步骤: - -1. 创建一个android工程 -2. JAVA代码中写声明native 方法 public native String helloFromJNI(); -3. 用javah工具生成头文件 -4. 创建jni目录,引入头文件,根据头文件实现c代码 -5. 编写Android.mk文件 -6. Ndk编译生成动态库 -7. Java代码load 动态库.调用native代码 - -###JNI开发之Java中调用C代码步骤 - -1. 在java中定义一个要调用的C的方法(本地方法) - //1.定义一个native的方法 - `public native String helloFromC();` - -2. 在工程中新建一个jni文件夹(然后在这个文件夹中写c代码,在C中实现java里面定义的c方法默认的时候是自己手写c的方法名, - 但是很麻烦这里要参考七里面提供的方式,用javah编译后,然后拷贝h的头文件到jni文件夹中,在从h文件拷贝方法的名字,然后实现该方法). - ``` - C:\Users\Administrator>javah -help - 用法: - javah [options] - 其中, [options] 包括: - -o 输出文件 (只能使用 -d 或 -o 之一) - -d

输出目录 - -v -verbose 启用详细输出 - -h --help -? 输出此消息 - -version 输出版本信息 - -jni 生成 JNI 样式的标头文件 (默认值) - -force 始终写入输出文件 - -classpath 从中加载类的路径 - -cp 从中加载类的路径 - -bootclasspath 从中加载引导类的路径 - ``` - `#include ` - `#include ` - //这个方法的名字的写法固定Java_表示这个方法由Java调用,cn_itcast_ndk表示java的包名DemoActivity表示 - //java中调用这个方法的类名helloFromC表示java中调用这个方法的方法名字 - ```java - jstring Java_cn_itcast_ndk_DemoActivity_helloFromC (JNIEnv* env , jobject obj){//这两个参数是固定必不可少的 - //return (*(*env)).NewStringUTF(env,"hello from c!");//调用NewStringUTF这个方法new出来一个java中的String类型的字符串 - return (*env)->NewStringUTF(env,"hello from c!" );//这个写法和上面这个注释掉的一样,只是更简洁一点 - } - ``` - -3. 在jni文件夹中编写android.mk文件,在这个文件夹中声明要编译的c文件名以后编译后生成的文件名 - ```c - LOCAL_PATH := $(call my-dir) //将jni所在的目录返回去到LOCAL_PATH - #clear_vars 一个函数 初始化编译工具链的所有的变量. - #特点:清空所有的以LOCAL_开头的变量,但是不会清空LOCAL_PATH的变量 - include $(CLEAR_VARS) - #指定编译后的文件的名称 符合linux系统下makefile的语法. - LOCAL_MODULE := Hello - #指定编译的源文件的名称 ,编译器非常智能 - LOCAL_SRC_FILES := Hello.c - #指定编译后的文件的类型. 默认编译成动态库 BUILD_SHARED_LIBRARY 扩展名.so .so代码体积很小 - # 静态库 BUILD_STATIC_LIBRARY 扩展名.a .a代码体积很大 - include $(BUILD_SHARED_LIBRARY) - ``` -4. cmd进入到当前的工程的文件夹中(也可以进入到当前工程的jni目录中),然后运行ndk-build工具就能将c文件编译成一个可执行的二进制文件. ->.so, - 注意用ndk-build编译之后一定要刷新,不然eclipse会缓存旧的不加载新的进来 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ndk_build.png?raw=true) - - -5. 刷新工程,就能看到多出了两个文件夹 - -6. 在java中将要调用的c代码加载到java虚拟机中,通过静态代码块的方式 - ```java - public class DemoActivity extends Activity { - //1.定义一个native的方法 - public native String helloFromC(); - - static{ - //5.把要调用的c代码 给加载到java虚拟机里面 - System. loadLibrary("Hello");//注意写的是Hello不要加后缀 - } - } - ``` - -7. 调用c代码 - ```java - public void click(View view){ - //调用c代码 - Toast.makeText(this, helloFromC(), 1).show(); - - } - ``` - -7. 利用jdk的工具javah动态生成c方法名 - 在上面的调用c中的方法的时候,在c中区实现这个方法的时候的方法名字写起来很复杂,而且容易出去,在java在jdk中提供了一个工具javah, - 我们只要在windows的dos窗口cmd到classes目录下去执行javah 包名.类名就能够由class文件动态的生成一个c的h文件,在这个h文件中有该class文件中的native方法的名字 - 我们只要拷贝这个h文件到自己工程的jni目录中,然后在c文件中引入这个h文件,并拷贝这个h文件中的方法去实现就可以了 - ```java - #include //这个<>是引入工具的h文件 - #include "cn_itcast_ndk2_DemoActivity.h" //对于自己工程中的h文件用""来引入或者引入#include 也可以 - - JNIEXPORT jstring JNICALL Java_cn_itcast_ndk2_DemoActivity_hello_1_1_1from_1_1_1c //拷贝h文件中生成的方法名 - (JNIEnv * env, jobject obj){ - return (*env)->NewStringUTF(env,"hello_from_c 2!" ); - - } - ``` - 然后就和上面的步骤一样了 - - 注意上面的这个javah的用法师在jdk1.6中用的,如果在jdk1.7中就不能这样用了 - 对于jdk1.7在使用javah的工具的时候就不能够直接进入到classes目录下直接运行命令了, - 而是要将sdk中的platforms下的android版本中的android.jar这个路径加载到classPath的环境变量中(麻烦),或者是直接进入到src目录下用javah包名.类名(简单常用) - -8. 如何在c中向logcat中打印日志 - 如果想像logcat打印日志就要用到谷歌在ndk中提供的一个工具log.h的头文件 - 步骤: - 1. 在c文件的头上面导入文件,加入下面的这四行代码 - ```c - #include //导入log.h - #define LOG_TAG "clog" //指定打印到logcat的Tag - #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) //对后面的这个打印日志的方法起一个别名是LOGD - #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) - ``` - 2. 在android.mk中加载文件 - ``` - LOCAL_PATH := $(call my-dir) - include $(CLEAR_VARS) - - LOCAL_MODULE := Hello - LOCAL_SRC_FILES := Hello.c - LOCAL_LDLIBS += -llog //新增加这一句,作用是 #把c语言调用的log函数对应的函数库加入到编译的运行时里面 #liblog.so,如果还要加载其他的就在后面继续 -lXXX - include $(BUILD_SHARED_LIBRARY) - ``` - - 3. 在c的代码中直接使用LOGD或者LOGI就能向logcat中输入打印信息 - ```c - JNIEXPORT jint JNICALL Java_cn_itcast_ndk3_DataProvider_add - (JNIEnv * env, jobject obj, jint x, jint y){ - LOGI( "x=%d",x); - LOGD( "y=%d",y); - int result = x+y; - LOGD( "result=%d",result); - return result; - } - ``` - -9. 如何将java的数据传递给c语言 - 就是java在方法中传值,然后c通过参数得到数据处理后返回和上面的一样 - -10. 将c中的字符串数组转成java中的string用到jni.h中的一个方法 - `jstring (*NewStringUTF)(JNIEnv*, const char*);` - -11. C中调用java - c语言回调java的场景. - 1. 如果有一个操作已经有方便的java实现,才用c调用java可以避免重复发明一个轮子. - 2. 想在c代码里面通知界面更新ui. - - C调用java的 思想类似于java中的反射,我们在c中就是通过反射的c实现来找到java中的这个方法, - 在getMethodID的第二个参数是一个方法的签名,这里我们可以通过jdk提供的一个工具javap,来到classes目录下, - 然后用 javap -s 类名.方法名 来得到一个方法的签名,这样就能列出来所有方法的签名 - - ```c - /** - * env JNIEnv* java虚拟机环境的指针. - * - *jobject obj ,哪个对象调用的这个native的方法 , obj就代表的是哪个对象 - */ - JNIEXPORT void JNICALL Java_cn_itcast_ndk4_DataProvider_callmethod1 - (JNIEnv * env, jobject obj){ - //思考 java中的反射 - //1.找到某一个类的字节码 - // jclass (*FindClass)(JNIEnv*, const char*); - jclass jclazz = (*env)->FindClass(env,"cn/itcast/ndk4/DataProvider" ); - if(jclazz==0){ - LOGI( "LOAD CLAZZ ERROR"); - } else{ - LOGI( "LOAD CLAZZ success" ); - } - //2.找到类的字节码里面的方法. - // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); - - jmethodID methodid = (*env)->GetMethodID(env,jclazz,"helloFromJava", "()V"); //最后一个参数是方法的签名 - if(methodid==0){ - LOGI( "LOAD methodid ERROR" ); - } else{ - LOGI( "LOAD methodid success" ); - } - - //3.调用方法 - //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); - (*env)->CallVoidMethod(env,obj,methodid); - } - ``` - -12. 小知识 - 1. Android的API提供了SystemClock类,这个类中有一个方法 - public static void sleep(long ms),这个方法内部对Thread.sleep进行了封装对异常进行了try catch,平时用Thread.sleep还要自己进行捕捉, - 所以可以使用SystemColock.sleep()还简单 - - 2. Java中通过java虚拟机来调用c的代码,首先将c的库加载到虚拟机中,但是其实这个c代码并不是运行在java虚拟机中的,而是运行在虚拟机之外的一个单独的进程中 - -13. 自定义一个View控件(用于表示锅炉的压力大小) - 1. 写一个类继承View(View是Android中所有能显示到界面上的东西全的父类) - 2. 重写onDraw()方法,这个方法是该控件被画到桌面上的时候调用的方法。 - -14. C++与C代码的不同 - C++文件的后缀是cpp - C++与C的不同就是C++提供了模板、继承、抽象等 - ```c - //将java字符串转成C++字符串的工具方法 - char* Jstring2CStr(JNIEnv* env, jstring jstr) - { - char* rtn = NULL; - jclass clsstring = (env)->FindClass("java/lang/String"); - jstring strencode = (env)->NewStringUTF("GB2312"); - jmethodID mid = (env)->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B"); - jbyteArray barr= (jbyteArray)(env)->CallObjectMethod(jstr,mid,strencode); // String .getByte("GB2312"); - jsize alen = (env)->GetArrayLength(barr); - jbyte* ba = (env)->GetByteArrayElements(barr,JNI_FALSE); - if(alen > 0) - { - rtn = (char*)malloc(alen+1); //"\0" - memcpy(rtn,ba,alen); - rtn[alen]=0; - } - (env)->ReleaseByteArrayElements(barr,ba,0); // - return rtn; - } - JNIEXPORT jstring JNICALL Java_cn_itcast_cpp_DemoActivity_HelloFromC - (JNIEnv * env, jobject obj){ - //C代码 - //return (*env)->NewStringUTF(env,"haha from c"); 在C中env代表的是C中结构体的指针的指针 - //c++代码 - return env->NewStringUTF("haha from cpp");//在C++中env代表的是C++中结构体的指针 - } - ``` - -15. 对于JNI中的中文乱码问题 - 老版本的ndk r7之前 r6 r5 r5 crystal r4(编译的时候 语言集 是iso-8859-1) - 在使用老版本ndk 编译出来的so文件的时候 要手动的进行转码.先用iso8859-1解码,再用utf-8编码,在r7(包括)之后的我们只要将C文件的格式改为UTF-8就可以了 - -16. 文件的格式及格式转换 - 格式转换的原理: - 1. 读取一段数据到内存 - 2. 分析这一段数据 - 3. 修改里面的内容 - 4. 写到文件里面 - - 文件的格式: - 文件的存储方式是二进制0101这样 - 那么怎么设别文件的类型呢? - 1. 根据扩展名 - 2. 根据文件的头信息(头信息才是一个文件的真正的格式),有些文件我们修改了扩展名也可以打开, - 这是因为打开文件的程序区扫描了文件的头信息,并用头信息中的类型来打开了这个文件 - -17. C中读取数据 - ```c - #include - main(){ - //用 法: FILE *fopen(char *filename, char *type); //第二个参数是打开的方式 rt就是读文件, rb就是读二进制 - FILE* fp = fopen("1.txt","rt"); - //用 法: int fread (void *ptr, int size, int nitems, FILE *stream); - //ptr要读的数据 放在哪一块内存空间里面. - //size 一次读的数据的长度. - //nitems 读多少次 - //stream 从哪个文件里面 读 - - char* buffer = malloc(sizeof(char)*12); - int len= fread(buffer,sizeof(char),12,fp); - printf("读了%d个char\n",len); - printf("str=%s\n",buffer); - fclose(fp); //关闭掉流 - - system("pause"); - } - ``` - -18. C中写文件 - ```c - #include - main(){ - //用 法: FILE *fopen(char *filename, char *type); - FILE* fp = fopen("1.txt","wt"); - //用 法: int fwrite(void *ptr, int size, int nitems, FILE *stream); - //ptr要向文件写的是哪一块内存里面的数据 - //size 一次写的数据的长度. - //nitems 写多少次 - //stream 写到哪个文件里面 - char* str="hello from c"; - int len = fwrite(str,sizeof(char),12,fp); - printf("写了%d个char\n",len); - fclose(fp); //关闭掉流 - - system("pause"); - } - ``` - -19. C语言文件操作模式 - “rt” 只读打开一个文本文件,只允许读数据 - “wt” 只写打开或建立一个文本文件,只允许写数据 - “at” 追加打开一个文本文件,并在文件末尾写数据 - “rb” 只读打开一个二进制文件,只允许读数据 - “wb” 只写打开或建立一个二进制文件,只允许写数据 - “ab” 追加打开一个二进制文件,并在文件末尾写数据 - “rt+” 读写打开一个文本文件,允许读和写 - “wt+” 读写打开或建立一个文本文件,允许读写 - “at+” 读写打开一个文本文件,允许读,或在文件末追加数据 - “rb+” 读写打开一个二进制文件,允许读和写 - “wb+” 读写打开或建立一个二进制文件,允许读和写 - “ab+” 读写打开一个二进制文件,允许读,或在文件末追加数据 - - 对于文件使用方式有以下几点说明: - 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是: - - r(read): 读 - w(write): 写 - a(append): 追加 - t(text): 文本文件,可省略不写 - b(banary): 二进制文件 - ---- - -- 邮箱 :charon.chui@gmail.com +JNI基础 +=== + +1. 将java中的字符串转换成C中字符串的工具方法 + ```c + char* Jstring2CStr(JNIEnv* env, jstring jstr){ + char* rtn = NULL; + jclass clsstring = (*env)->FindClass(env,"java/lang/String"); + jstring strencode = (*env)->NewStringUTF(env,"GB2312"); + jmethodID mid = (*env)->GetMethodID(env,clsstring, "getBytes", "(Ljava/lang/String;)[B"); + jbyteArray barr= (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312"); + jsize alen = (*env)->GetArrayLength(env,barr); + jbyte* ba = (*env)->GetByteArrayElements(env,barr,JNI_FALSE); + if(alen > 0) + { + rtn = (char*)malloc(alen+1); //"\0" + memcpy(rtn,ba,alen); + rtn[alen]=0; + } + (*env)->ReleaseByteArrayElements(env,barr,ba,0); // + return rtn; + } + ``` + +2. 程序被运行要经历两个步骤(1.编译 2.链接) + 编译就是将源文件编译成二进制代码,而链接则是将二进制代码转换成可执行的文件如.exe等头文件.(函数的声明,函数的清单文件)作用: 给编译器看的. + 库函数: 头文件里面函数的实现. + 作用:给连接器看的. + +3. jni开发的常见错误: + 错误1: 忘记编写android.mk文件 unknown file: ./jni/Android.mk + Android NDK: Your APP_BUILD_SCRIPT points to an unknown file: ./jni/Android.mk + /cygdrive/c/android-ndk-r7b/build/core/add-application.mk:133: Android NDK: Aborting... 。 停止。 + + 错误2: ndk-build 没有任何反应. + 忘记配置android.mk脚本 + + 错误3: $ ndk-build jni/Android.mk:4: 遗漏分隔符 。 停止。 +中文的回车或者换行 + + 错误4:java.lang.UnsatisfiedLinkError: hello + 忘记加载了c代码的.so库 或者 函数的签名不正确,没有找到与之对应的c代码 + + 错误5:07-30 java.lang.UnsatisfiedLinkError: Library Hel1o not found + 没有找到对应的c代码库 + + 错误6: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + 07-30 11:53:17.898: INFO/DEBUG(31): Build fingerprint: + 'generic/sdk/generic/:2.2/FRF91/43546:eng/test-keys' + c代码里面有严重的逻辑错误,产生内存的泄露. + + 错误7: + make: *** [obj/local/armeabi/objs/Hello/Hello.o] Error 1 + 编译的时候 程序出现了问题,c语言的语法有问题 + c语言代码编译错误的时候 先去解决第一个错误. + +4. Java调用JNI的前提 + 开发所使用的电脑(windows系统, x86的CPU) + 目标代码: android手机上运行的.( linux系统, arm的CPU) + 所以我们要模拟手机的系统,手机的处理器,生成手机上可以运行的二进制代码这就要用到交叉编译; + 根据运行的设备的不同,可以将cpu分为: + - arm结构 :主要在移动手持、嵌入式设备上。 + - x86结构 : 主要在台式机、笔记本上使用。如Intel和AMD的CPU 。 + 交叉编译: 在一种操作系统平台或者cpu平台下 编译生成 另外一个平台(cpu)可以运行的二进制代码.(使用NDK中的ndk-build命令) + + +- 工具一: 交叉编译的工具链: NDK + NDK全称:Native Development Kit 。 + - NDK是一系列工具的集合,它有很多作用。 + - 首先,NDK可以帮助开发者快速开发C(或C++)的动态库。 + - 其次,NDK集成了交叉编译器。使用NDK,我们可以将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。 + + NDK工具是提供给Linux系统用的(随着版本的升级也可以直接在Windows下使用,但是现在仍不完善有bug), + 所以要在windows下使用ndk的工具,必须要提供一个工具(linux环境的模拟器) + +- 工具二: cygwin(windows下linux系统环境的模拟器, 主要是为了能够运行ndk的工具) + 安装 devel shell + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jni_cygwin.png?raw=true) + linux 特点:所有的设备 硬件 都是以文件的方式定义的. + 安装完后进入`cygwin`打印`make -v`命令如果能打印出`GNU Make ...`就说明安装木问题了。 + +- 工具三: cdt(c/c++ develop tools) eclipse 的一个插件 用来让c\c++代码 语法高亮显示. + adt(android develop tools) + +- 工具四: + 为了不用每次使用ndk-build命令都要进入到ndk的安装目录,这里要进行Path变量的配置。 + 配置cygwin的环境变量: 在cygwin安装目录,etc目录,profile的文件 32行 添加ndk工具所在的目录. + `PATH="/usr/local/bin:/usr/bin:/cygdrive/d/android-ndk-r7b:${PATH}"`在这个后面加上:ndk-build的路径(注意:在linux中路径的分隔符不是分号而是冒号), + 改成这样 + `PATH="/usr/local/bin:/usr/bin:${PATH}:/cygdrive/d/android-ndk-r7b"`//注意这里的路径是在linux系统下的ndk路径而不是windows下的路径,/cygdrive/d/是在linux下看到的d盘。 + +###JNI开发步骤: + +1. 创建一个android工程 +2. JAVA代码中写声明native 方法 public native String helloFromJNI(); +3. 用javah工具生成头文件 +4. 创建jni目录,引入头文件,根据头文件实现c代码 +5. 编写Android.mk文件 +6. Ndk编译生成动态库 +7. Java代码load 动态库.调用native代码 + +###JNI开发之Java中调用C代码步骤 + +1. 在java中定义一个要调用的C的方法(本地方法) + //1.定义一个native的方法 + `public native String helloFromC();` + +2. 在工程中新建一个jni文件夹(然后在这个文件夹中写c代码,在C中实现java里面定义的c方法默认的时候是自己手写c的方法名, + 但是很麻烦这里要参考七里面提供的方式,用javah编译后,然后拷贝h的头文件到jni文件夹中,在从h文件拷贝方法的名字,然后实现该方法). + ``` + C:\Users\Administrator>javah -help + 用法: + javah [options] + 其中, [options] 包括: + -o 输出文件 (只能使用 -d 或 -o 之一) + -d 输出目录 + -v -verbose 启用详细输出 + -h --help -? 输出此消息 + -version 输出版本信息 + -jni 生成 JNI 样式的标头文件 (默认值) + -force 始终写入输出文件 + -classpath 从中加载类的路径 + -cp 从中加载类的路径 + -bootclasspath 从中加载引导类的路径 + ``` + `#include ` + `#include ` + //这个方法的名字的写法固定Java_表示这个方法由Java调用,cn_itcast_ndk表示java的包名DemoActivity表示 + //java中调用这个方法的类名helloFromC表示java中调用这个方法的方法名字 + ```java + jstring Java_cn_itcast_ndk_DemoActivity_helloFromC (JNIEnv* env , jobject obj){//这两个参数是固定必不可少的 + //return (*(*env)).NewStringUTF(env,"hello from c!");//调用NewStringUTF这个方法new出来一个java中的String类型的字符串 + return (*env)->NewStringUTF(env,"hello from c!" );//这个写法和上面这个注释掉的一样,只是更简洁一点 + } + ``` + +3. 在jni文件夹中编写android.mk文件,在这个文件夹中声明要编译的c文件名以后编译后生成的文件名 + ```c + LOCAL_PATH := $(call my-dir) //将jni所在的目录返回去到LOCAL_PATH + #clear_vars 一个函数 初始化编译工具链的所有的变量. + #特点:清空所有的以LOCAL_开头的变量,但是不会清空LOCAL_PATH的变量 + include $(CLEAR_VARS) + #指定编译后的文件的名称 符合linux系统下makefile的语法. + LOCAL_MODULE := Hello + #指定编译的源文件的名称 ,编译器非常智能 + LOCAL_SRC_FILES := Hello.c + #指定编译后的文件的类型. 默认编译成动态库 BUILD_SHARED_LIBRARY 扩展名.so .so代码体积很小 + # 静态库 BUILD_STATIC_LIBRARY 扩展名.a .a代码体积很大 + include $(BUILD_SHARED_LIBRARY) + ``` +4. cmd进入到当前的工程的文件夹中(也可以进入到当前工程的jni目录中),然后运行ndk-build工具就能将c文件编译成一个可执行的二进制文件. ->.so, + 注意用ndk-build编译之后一定要刷新,不然eclipse会缓存旧的不加载新的进来 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ndk_build.png?raw=true) + + +5. 刷新工程,就能看到多出了两个文件夹 + +6. 在java中将要调用的c代码加载到java虚拟机中,通过静态代码块的方式 + ```java + public class DemoActivity extends Activity { + //1.定义一个native的方法 + public native String helloFromC(); + + static{ + //5.把要调用的c代码 给加载到java虚拟机里面 + System. loadLibrary("Hello");//注意写的是Hello不要加后缀 + } + } + ``` + +7. 调用c代码 + ```java + public void click(View view){ + //调用c代码 + Toast.makeText(this, helloFromC(), 1).show(); + + } + ``` + +7. 利用jdk的工具javah动态生成c方法名 + 在上面的调用c中的方法的时候,在c中区实现这个方法的时候的方法名字写起来很复杂,而且容易出去,在java在jdk中提供了一个工具javah, + 我们只要在windows的dos窗口cmd到classes目录下去执行javah 包名.类名就能够由class文件动态的生成一个c的h文件,在这个h文件中有该class文件中的native方法的名字 + 我们只要拷贝这个h文件到自己工程的jni目录中,然后在c文件中引入这个h文件,并拷贝这个h文件中的方法去实现就可以了 + ```java + #include //这个<>是引入工具的h文件 + #include "cn_itcast_ndk2_DemoActivity.h" //对于自己工程中的h文件用""来引入或者引入#include 也可以 + + JNIEXPORT jstring JNICALL Java_cn_itcast_ndk2_DemoActivity_hello_1_1_1from_1_1_1c //拷贝h文件中生成的方法名 + (JNIEnv * env, jobject obj){ + return (*env)->NewStringUTF(env,"hello_from_c 2!" ); + + } + ``` + 然后就和上面的步骤一样了 + + 注意上面的这个javah的用法师在jdk1.6中用的,如果在jdk1.7中就不能这样用了 + 对于jdk1.7在使用javah的工具的时候就不能够直接进入到classes目录下直接运行命令了, + 而是要将sdk中的platforms下的android版本中的android.jar这个路径加载到classPath的环境变量中(麻烦),或者是直接进入到src目录下用javah包名.类名(简单常用) + +8. 如何在c中向logcat中打印日志 + 如果想像logcat打印日志就要用到谷歌在ndk中提供的一个工具log.h的头文件 + 步骤: + 1. 在c文件的头上面导入文件,加入下面的这四行代码 + ```c + #include //导入log.h + #define LOG_TAG "clog" //指定打印到logcat的Tag + #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) //对后面的这个打印日志的方法起一个别名是LOGD + #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + ``` + 2. 在android.mk中加载文件 + ``` + LOCAL_PATH := $(call my-dir) + include $(CLEAR_VARS) + + LOCAL_MODULE := Hello + LOCAL_SRC_FILES := Hello.c + LOCAL_LDLIBS += -llog //新增加这一句,作用是 #把c语言调用的log函数对应的函数库加入到编译的运行时里面 #liblog.so,如果还要加载其他的就在后面继续 -lXXX + include $(BUILD_SHARED_LIBRARY) + ``` + + 3. 在c的代码中直接使用LOGD或者LOGI就能向logcat中输入打印信息 + ```c + JNIEXPORT jint JNICALL Java_cn_itcast_ndk3_DataProvider_add + (JNIEnv * env, jobject obj, jint x, jint y){ + LOGI( "x=%d",x); + LOGD( "y=%d",y); + int result = x+y; + LOGD( "result=%d",result); + return result; + } + ``` + +9. 如何将java的数据传递给c语言 + 就是java在方法中传值,然后c通过参数得到数据处理后返回和上面的一样 + +10. 将c中的字符串数组转成java中的string用到jni.h中的一个方法 + `jstring (*NewStringUTF)(JNIEnv*, const char*);` + +11. C中调用java + c语言回调java的场景. + 1. 如果有一个操作已经有方便的java实现,才用c调用java可以避免重复发明一个轮子. + 2. 想在c代码里面通知界面更新ui. + + C调用java的 思想类似于java中的反射,我们在c中就是通过反射的c实现来找到java中的这个方法, + 在getMethodID的第二个参数是一个方法的签名,这里我们可以通过jdk提供的一个工具javap,来到classes目录下, + 然后用 javap -s 类名.方法名 来得到一个方法的签名,这样就能列出来所有方法的签名 + + ```c + /** + * env JNIEnv* java虚拟机环境的指针. + * + *jobject obj ,哪个对象调用的这个native的方法 , obj就代表的是哪个对象 + */ + JNIEXPORT void JNICALL Java_cn_itcast_ndk4_DataProvider_callmethod1 + (JNIEnv * env, jobject obj){ + //思考 java中的反射 + //1.找到某一个类的字节码 + // jclass (*FindClass)(JNIEnv*, const char*); + jclass jclazz = (*env)->FindClass(env,"cn/itcast/ndk4/DataProvider" ); + if(jclazz==0){ + LOGI( "LOAD CLAZZ ERROR"); + } else{ + LOGI( "LOAD CLAZZ success" ); + } + //2.找到类的字节码里面的方法. + // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); + + jmethodID methodid = (*env)->GetMethodID(env,jclazz,"helloFromJava", "()V"); //最后一个参数是方法的签名 + if(methodid==0){ + LOGI( "LOAD methodid ERROR" ); + } else{ + LOGI( "LOAD methodid success" ); + } + + //3.调用方法 + //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); + (*env)->CallVoidMethod(env,obj,methodid); + } + ``` + +12. 小知识 + 1. Android的API提供了SystemClock类,这个类中有一个方法 + public static void sleep(long ms),这个方法内部对Thread.sleep进行了封装对异常进行了try catch,平时用Thread.sleep还要自己进行捕捉, + 所以可以使用SystemColock.sleep()还简单 + + 2. Java中通过java虚拟机来调用c的代码,首先将c的库加载到虚拟机中,但是其实这个c代码并不是运行在java虚拟机中的,而是运行在虚拟机之外的一个单独的进程中 + +13. 自定义一个View控件(用于表示锅炉的压力大小) + 1. 写一个类继承View(View是Android中所有能显示到界面上的东西全的父类) + 2. 重写onDraw()方法,这个方法是该控件被画到桌面上的时候调用的方法。 + +14. C++与C代码的不同 + C++文件的后缀是cpp + C++与C的不同就是C++提供了模板、继承、抽象等 + ```c + //将java字符串转成C++字符串的工具方法 + char* Jstring2CStr(JNIEnv* env, jstring jstr) + { + char* rtn = NULL; + jclass clsstring = (env)->FindClass("java/lang/String"); + jstring strencode = (env)->NewStringUTF("GB2312"); + jmethodID mid = (env)->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B"); + jbyteArray barr= (jbyteArray)(env)->CallObjectMethod(jstr,mid,strencode); // String .getByte("GB2312"); + jsize alen = (env)->GetArrayLength(barr); + jbyte* ba = (env)->GetByteArrayElements(barr,JNI_FALSE); + if(alen > 0) + { + rtn = (char*)malloc(alen+1); //"\0" + memcpy(rtn,ba,alen); + rtn[alen]=0; + } + (env)->ReleaseByteArrayElements(barr,ba,0); // + return rtn; + } + JNIEXPORT jstring JNICALL Java_cn_itcast_cpp_DemoActivity_HelloFromC + (JNIEnv * env, jobject obj){ + //C代码 + //return (*env)->NewStringUTF(env,"haha from c"); 在C中env代表的是C中结构体的指针的指针 + //c++代码 + return env->NewStringUTF("haha from cpp");//在C++中env代表的是C++中结构体的指针 + } + ``` + +15. 对于JNI中的中文乱码问题 + 老版本的ndk r7之前 r6 r5 r5 crystal r4(编译的时候 语言集 是iso-8859-1) + 在使用老版本ndk 编译出来的so文件的时候 要手动的进行转码.先用iso8859-1解码,再用utf-8编码,在r7(包括)之后的我们只要将C文件的格式改为UTF-8就可以了 + +16. 文件的格式及格式转换 + 格式转换的原理: + 1. 读取一段数据到内存 + 2. 分析这一段数据 + 3. 修改里面的内容 + 4. 写到文件里面 + + 文件的格式: + 文件的存储方式是二进制0101这样 + 那么怎么设别文件的类型呢? + 1. 根据扩展名 + 2. 根据文件的头信息(头信息才是一个文件的真正的格式),有些文件我们修改了扩展名也可以打开, + 这是因为打开文件的程序区扫描了文件的头信息,并用头信息中的类型来打开了这个文件 + +17. C中读取数据 + ```c + #include + main(){ + //用 法: FILE *fopen(char *filename, char *type); //第二个参数是打开的方式 rt就是读文件, rb就是读二进制 + FILE* fp = fopen("1.txt","rt"); + //用 法: int fread (void *ptr, int size, int nitems, FILE *stream); + //ptr要读的数据 放在哪一块内存空间里面. + //size 一次读的数据的长度. + //nitems 读多少次 + //stream 从哪个文件里面 读 + + char* buffer = malloc(sizeof(char)*12); + int len= fread(buffer,sizeof(char),12,fp); + printf("读了%d个char\n",len); + printf("str=%s\n",buffer); + fclose(fp); //关闭掉流 + + system("pause"); + } + ``` + +18. C中写文件 + ```c + #include + main(){ + //用 法: FILE *fopen(char *filename, char *type); + FILE* fp = fopen("1.txt","wt"); + //用 法: int fwrite(void *ptr, int size, int nitems, FILE *stream); + //ptr要向文件写的是哪一块内存里面的数据 + //size 一次写的数据的长度. + //nitems 写多少次 + //stream 写到哪个文件里面 + char* str="hello from c"; + int len = fwrite(str,sizeof(char),12,fp); + printf("写了%d个char\n",len); + fclose(fp); //关闭掉流 + + system("pause"); + } + ``` + +19. C语言文件操作模式 + “rt” 只读打开一个文本文件,只允许读数据 + “wt” 只写打开或建立一个文本文件,只允许写数据 + “at” 追加打开一个文本文件,并在文件末尾写数据 + “rb” 只读打开一个二进制文件,只允许读数据 + “wb” 只写打开或建立一个二进制文件,只允许写数据 + “ab” 追加打开一个二进制文件,并在文件末尾写数据 + “rt+” 读写打开一个文本文件,允许读和写 + “wt+” 读写打开或建立一个文本文件,允许读写 + “at+” 读写打开一个文本文件,允许读,或在文件末追加数据 + “rb+” 读写打开一个二进制文件,允许读和写 + “wb+” 读写打开或建立一个二进制文件,允许读和写 + “ab+” 读写打开一个二进制文件,允许读,或在文件末追加数据 + + 对于文件使用方式有以下几点说明: + 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是: + + r(read): 读 + w(write): 写 + a(append): 追加 + t(text): 文本文件,可省略不写 + b(banary): 二进制文件 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/ListView\344\270\223\351\242\230.md" "b/AndroidBasicPart/ListView\344\270\223\351\242\230.md" similarity index 97% rename from "Android\345\237\272\347\241\200/ListView\344\270\223\351\242\230.md" rename to "AndroidBasicPart/ListView\344\270\223\351\242\230.md" index 2daa8eba..f790ac27 100644 --- "a/Android\345\237\272\347\241\200/ListView\344\270\223\351\242\230.md" +++ "b/AndroidBasicPart/ListView\344\270\223\351\242\230.md" @@ -1,120 +1,120 @@ -ListView专题 -=== - -1.`ListView`属性: ---- - -1. `fadingEdge`属性 - `ListView`上边和下边有黑色的阴影,`android : fadingEdge = "none"`后就不会有阴影了 - -2. `scrollbars`属性,隐藏滚动条 - `android : scrollbars = "none"` - `setVerticalScrollBarEnabled(true);` - -3. `fadeScrollbars`属性 - `android : fadeScrollbars = "true"` - 设置此值为true就可以实现滚动条的自动隐藏和显示。 - -4. `fastScrollEnabled`属性 - 快速滚动滑块 - `android : fastScrollEnabled = "true"` - `mListView.setFastScrollEnabled(true);` - -5. `drawSelectorOnTop`属性 - When set to true, the selector will be drawn over the selecteditem. Otherwise the selector is drawn behind the selected item. Thedefault value is false. - `android:drawSelectorOnTop = "false"` 点击某条记录不放,颜色会在记录的后面,成为背景色,但是记录内容的文字是可见的 - -2.`ListView.setEmptyView()`没有效果 ---- - -有时调用`setEmptyView`没有效果,这是因为我们设置的这个`EmptyView`必须和该`ListView`在同一个**布局体系中** -如:下面这样的代码有些时候会没有效果 -```java -View loadingView = View.inflate(getActivity(), R.layout.loading, null); -mPullLoadListView.setEmptyView(loadingView); -mPullLoadListView.setAdapter(adapter); -``` - -- `Fragment`中添加下面代码就可以了。 - ```java - View loadingView = View.inflate(getActivity(), R.layout.loading, null); - //添加到同一布局体系中 - getActivity().addContentView(loadingView, - new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT )); - mPullLoadListView.setEmptyView(loadingView); - mPullLoadListView.setAdapter(adapter); - ``` - -- `Activity`中 - ```java - View empty = getLayoutInflater().inflate(R.layout.empty_list_item, null, false); - addContentView(empty, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - mPullLoadListView.setEmptyView(empty); - ``` - -3.`ListView`调用`addHeaderView`后,`onItemClick`时位置不正确 ---- - -`addHeaderView()`以及`addFooterView()`一定要在调用`setAdapter()`方法之前调用,不然会报错。 -当`ListView`通过`addHeaderView`添后,在o`nItemClick`中的`position`会加上`Header`的个数,所以这时候在获取数据的时候要对位置进行处理。 - -下面两种方法都可以: - -1. 第一种 - ```java - public void onItemClick(AdapterView parent, View v, int position, long id) { - //parent.getAdapter().getItem(position)能得到真正位置的数据 - doSomething(parent.getAdapter().getItem(position)); - } - ``` - -2. 第二种 - ```java - mListView.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - - int headerViewCount = mListView.getHeaderViewsCount(); - int realPos = position - mListView.getHeaderViewsCount(); - if (realPos < 0) - return; - ......这样realPos就是真是的位置 - - } - }); - ``` - -4.`ListView.addHeadrView()`添加`ViewPager`不显示的问题 ---- - -`addHeaderView()`添加`ViewPager`后不能显示出来的问题: -```xml - - - - - - -``` -```java -mHeaderView = View.inflate(this, R.layout.auto_circle_viewpager, null); -mAutoCircleViewPager = (ViewPager) mHeaderView.findViewById(R.id.vp_auto_circle); -//addHeaderView要在ListView的setAdapter前添加 -mListView.addHeaderView(mHeaderView); -``` -**注意**ViewPager的布局中宽高不能够使用`wrap_content`可以使用`match_parent`但是上面显示不出来也是由于match_parent的问题, -如果我们将布局中的`layout_height="200dip"`,这样就能够显示出来`ViewPager` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +ListView专题 +=== + +1.`ListView`属性: +--- + +1. `fadingEdge`属性 + `ListView`上边和下边有黑色的阴影,`android : fadingEdge = "none"`后就不会有阴影了 + +2. `scrollbars`属性,隐藏滚动条 + `android : scrollbars = "none"` + `setVerticalScrollBarEnabled(true);` + +3. `fadeScrollbars`属性 + `android : fadeScrollbars = "true"` + 设置此值为true就可以实现滚动条的自动隐藏和显示。 + +4. `fastScrollEnabled`属性 + 快速滚动滑块 + `android : fastScrollEnabled = "true"` + `mListView.setFastScrollEnabled(true);` + +5. `drawSelectorOnTop`属性 + When set to true, the selector will be drawn over the selecteditem. Otherwise the selector is drawn behind the selected item. Thedefault value is false. + `android:drawSelectorOnTop = "false"` 点击某条记录不放,颜色会在记录的后面,成为背景色,但是记录内容的文字是可见的 + +2.`ListView.setEmptyView()`没有效果 +--- + +有时调用`setEmptyView`没有效果,这是因为我们设置的这个`EmptyView`必须和该`ListView`在同一个**布局体系中** +如:下面这样的代码有些时候会没有效果 +```java +View loadingView = View.inflate(getActivity(), R.layout.loading, null); +mPullLoadListView.setEmptyView(loadingView); +mPullLoadListView.setAdapter(adapter); +``` + +- `Fragment`中添加下面代码就可以了。 + ```java + View loadingView = View.inflate(getActivity(), R.layout.loading, null); + //添加到同一布局体系中 + getActivity().addContentView(loadingView, + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT )); + mPullLoadListView.setEmptyView(loadingView); + mPullLoadListView.setAdapter(adapter); + ``` + +- `Activity`中 + ```java + View empty = getLayoutInflater().inflate(R.layout.empty_list_item, null, false); + addContentView(empty, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + mPullLoadListView.setEmptyView(empty); + ``` + +3.`ListView`调用`addHeaderView`后,`onItemClick`时位置不正确 +--- + +`addHeaderView()`以及`addFooterView()`一定要在调用`setAdapter()`方法之前调用,不然会报错。 +当`ListView`通过`addHeaderView`添后,在o`nItemClick`中的`position`会加上`Header`的个数,所以这时候在获取数据的时候要对位置进行处理。 + +下面两种方法都可以: + +1. 第一种 + ```java + public void onItemClick(AdapterView parent, View v, int position, long id) { + //parent.getAdapter().getItem(position)能得到真正位置的数据 + doSomething(parent.getAdapter().getItem(position)); + } + ``` + +2. 第二种 + ```java + mListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + + int headerViewCount = mListView.getHeaderViewsCount(); + int realPos = position - mListView.getHeaderViewsCount(); + if (realPos < 0) + return; + ......这样realPos就是真是的位置 + + } + }); + ``` + +4.`ListView.addHeadrView()`添加`ViewPager`不显示的问题 +--- + +`addHeaderView()`添加`ViewPager`后不能显示出来的问题: +```xml + + + + + + +``` +```java +mHeaderView = View.inflate(this, R.layout.auto_circle_viewpager, null); +mAutoCircleViewPager = (ViewPager) mHeaderView.findViewById(R.id.vp_auto_circle); +//addHeaderView要在ListView的setAdapter前添加 +mListView.addHeaderView(mHeaderView); +``` +**注意**ViewPager的布局中宽高不能够使用`wrap_content`可以使用`match_parent`但是上面显示不出来也是由于match_parent的问题, +如果我们将布局中的`layout_height="200dip"`,这样就能够显示出来`ViewPager` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" "b/AndroidBasicPart/Parcelable\345\217\212Serializable.md" similarity index 100% rename from "Android\345\237\272\347\241\200/Parcelable\345\217\212Serializable.md" rename to "AndroidBasicPart/Parcelable\345\217\212Serializable.md" diff --git "a/Android\345\237\272\347\241\200/PopupWindow\347\273\206\350\212\202.md" "b/AndroidBasicPart/PopupWindow\347\273\206\350\212\202.md" similarity index 97% rename from "Android\345\237\272\347\241\200/PopupWindow\347\273\206\350\212\202.md" rename to "AndroidBasicPart/PopupWindow\347\273\206\350\212\202.md" index 22a20c54..328adf4a 100644 --- "a/Android\345\237\272\347\241\200/PopupWindow\347\273\206\350\212\202.md" +++ "b/AndroidBasicPart/PopupWindow\347\273\206\350\212\202.md" @@ -1,87 +1,87 @@ -PopupWindow细节 -=== - -##简介 - -`A popup window that can be used to display an arbitrary view. The popup windows is a floating container that appears on top of the current activity.` - -1. 显示 - ```java - View contentView = View.inflate(getApplicationContext(),R.layout.popup_appmanger, null); - //这里最后一个参数是指定是否能够获取焦点,如果为false那么点击弹出来之后就不消失了,但是设置为true之后点击一个条目它弹出来了, - 再点击别的条目的时候这个popupWindow窗口没有焦点了就自己消失了 - PopupWindow popwindow = new PopupWindow(contentView ,LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,true); - popwindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - int[] location = new int[2]; - //得到当前组件的位置 - view.getLocationInWindow(location); - //显示popupWindow - popwindow.showAtLocation(parent,Gravity.LEFT | Gravity.TOP, DensityUtil.dip2px(getApplicationContext(), location[0] + 70),location[1]); - ``` - -2. 取消 - ```java - if (popwindow != null && popwindow.isShowing()) { - popwindow.dismiss(); - popwindow = null; - } - ``` - -3. 细节 - `PopupWindow`是存在到`Activity`上的,如果`PopupWindow`在显示的时候按退出键的时候该`Activity`已经销毁,但是`PopupWindow`没有销毁, - 所以就报错了(`Logcat`有报错信息但是程序不会崩溃),所以我们在`Activity`的`onDestroy`方法中要判断一下`PopupWindow`是否在显示,如果在显示就取消显示。 - `PopupWindow`默认是没有背景的,如果想让它播放动画就没有效果了,因为没有背景就什么也播放不了,所以我们在用这个`PopupWindow`的时候必须要给它设置一个背景, - 通常可以给它设置为透明色,这样再播放动画就有效果了 - `popwindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));` - -##让`PopupWindow`响应`Back`键后关闭。 - -- 最简单 - 在`new`的时候,使用下面的方法: - ```java - new PopupWindow(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true); - ``` - - 当然你也可以手动设置它: - ```java - mPopupWindow.setFocusable(true); - mPopupWindow.setFocusableInTouchMode(true); - ``` - - 此时实际上还是不能起作用的,必须加入下面这行作用未知的语句才能发挥作用: - ```java - mPopupWindow.setBackgroundDrawable(new BitmapDrawable()); - ``` - 设置 `BackgroundDrawable`并不会改变你在配置文件中设置的背景颜色或图像。 - -- 最通用 - 首先在布局文件`(*.xml)`中随意选取一个不影响任何操作的`View`,推荐使用最外层的`Layout`。 - 然后设置该`Layout`的`Focusable`和`FocusableInTouchMode`都为`true`。 - 接着回到代码中对该`View`重写`OnKeyListener()`事件了。捕获`KEYCODE_BACK`给对话框`dismiss()`。 - - 给出一段示例: - ```java - private PopupWindow mPopupWindow; - private View view; - private LinearLayout layMenu; - - LayoutInflater inflater = (LayoutInflater) main.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflater.inflate(R.layout.popup_main_menu, null, false); - layMenu = (LinearLayout) view.findViewById(R.id.layMenu); - mPopupWindow = new PopupWindow(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true); - - layMenu.setOnKeyListener(new OnKeyListener() { - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { - mPopupWindow.dismiss(); - } - - return false; - } - }); - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +PopupWindow细节 +=== + +##简介 + +`A popup window that can be used to display an arbitrary view. The popup windows is a floating container that appears on top of the current activity.` + +1. 显示 + ```java + View contentView = View.inflate(getApplicationContext(),R.layout.popup_appmanger, null); + //这里最后一个参数是指定是否能够获取焦点,如果为false那么点击弹出来之后就不消失了,但是设置为true之后点击一个条目它弹出来了, + 再点击别的条目的时候这个popupWindow窗口没有焦点了就自己消失了 + PopupWindow popwindow = new PopupWindow(contentView ,LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,true); + popwindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + int[] location = new int[2]; + //得到当前组件的位置 + view.getLocationInWindow(location); + //显示popupWindow + popwindow.showAtLocation(parent,Gravity.LEFT | Gravity.TOP, DensityUtil.dip2px(getApplicationContext(), location[0] + 70),location[1]); + ``` + +2. 取消 + ```java + if (popwindow != null && popwindow.isShowing()) { + popwindow.dismiss(); + popwindow = null; + } + ``` + +3. 细节 + `PopupWindow`是存在到`Activity`上的,如果`PopupWindow`在显示的时候按退出键的时候该`Activity`已经销毁,但是`PopupWindow`没有销毁, + 所以就报错了(`Logcat`有报错信息但是程序不会崩溃),所以我们在`Activity`的`onDestroy`方法中要判断一下`PopupWindow`是否在显示,如果在显示就取消显示。 + `PopupWindow`默认是没有背景的,如果想让它播放动画就没有效果了,因为没有背景就什么也播放不了,所以我们在用这个`PopupWindow`的时候必须要给它设置一个背景, + 通常可以给它设置为透明色,这样再播放动画就有效果了 + `popwindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));` + +##让`PopupWindow`响应`Back`键后关闭。 + +- 最简单 + 在`new`的时候,使用下面的方法: + ```java + new PopupWindow(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true); + ``` + + 当然你也可以手动设置它: + ```java + mPopupWindow.setFocusable(true); + mPopupWindow.setFocusableInTouchMode(true); + ``` + + 此时实际上还是不能起作用的,必须加入下面这行作用未知的语句才能发挥作用: + ```java + mPopupWindow.setBackgroundDrawable(new BitmapDrawable()); + ``` + 设置 `BackgroundDrawable`并不会改变你在配置文件中设置的背景颜色或图像。 + +- 最通用 + 首先在布局文件`(*.xml)`中随意选取一个不影响任何操作的`View`,推荐使用最外层的`Layout`。 + 然后设置该`Layout`的`Focusable`和`FocusableInTouchMode`都为`true`。 + 接着回到代码中对该`View`重写`OnKeyListener()`事件了。捕获`KEYCODE_BACK`给对话框`dismiss()`。 + + 给出一段示例: + ```java + private PopupWindow mPopupWindow; + private View view; + private LinearLayout layMenu; + + LayoutInflater inflater = (LayoutInflater) main.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.popup_main_menu, null, false); + layMenu = (LinearLayout) view.findViewById(R.id.layMenu); + mPopupWindow = new PopupWindow(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true); + + layMenu.setOnKeyListener(new OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { + mPopupWindow.dismiss(); + } + + return false; + } + }); + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/SDK Manager\346\227\240\346\263\225\346\233\264\346\226\260\347\232\204\351\227\256\351\242\230.md" "b/AndroidBasicPart/SDK Manager\346\227\240\346\263\225\346\233\264\346\226\260\347\232\204\351\227\256\351\242\230.md" similarity index 100% rename from "Android\345\237\272\347\241\200/SDK Manager\346\227\240\346\263\225\346\233\264\346\226\260\347\232\204\351\227\256\351\242\230.md" rename to "AndroidBasicPart/SDK Manager\346\227\240\346\263\225\346\233\264\346\226\260\347\232\204\351\227\256\351\242\230.md" diff --git "a/Android\345\237\272\347\241\200/Scroller\347\256\200\344\273\213.md" "b/AndroidBasicPart/Scroller\347\256\200\344\273\213.md" similarity index 98% rename from "Android\345\237\272\347\241\200/Scroller\347\256\200\344\273\213.md" rename to "AndroidBasicPart/Scroller\347\256\200\344\273\213.md" index 79f27c69..8ed68bfa 100644 --- "a/Android\345\237\272\347\241\200/Scroller\347\256\200\344\273\213.md" +++ "b/AndroidBasicPart/Scroller\347\256\200\344\273\213.md" @@ -1,53 +1,53 @@ -Scroller简介 -=== - -在`SlidingMenu`项目中为了实现控件的滑动,需要用到`Scroller`类来实现缓慢的滑动过程,至于有人说`View`类可以直接调用`scrollTo()`方法, -这里`scrollTo()`方法也能实现移动,但是它的移动是很快一下子就移过去了,就像穿越一样,直接从现实回到了过去,而`Scroller`类能够实现过程的移动。 -可以理解为一步步的走。 - -1. 查看Scroller源码 - ```java - public class Scroller { - //... - } - ``` - 发现`Scroller`类并不是`View`的子类,只是一个普通的类,这个类中封装了滚动的操作,记录了滚动的位置以及时间等。 -该类有两个重要的方法: - - `computeScrollOffset()`: - 文档的说明为`Call this when you want to know the new location.`查看源码可以发现,如果在移动到指定位置后就会返回false.正在移动的过程中返回true。 - - `startScroll()`: - 该方法的内部实现,并没有具体的移动方法,而是设置了一些移动所需的数据,包括移动持续的时间、开始位置、结束位置等。从而我们可以知道调用`Scroller.startScroll()`方法并没有真正的移动,而是设置了一些数据。 - -2. `Scroller.startScoll()`是如何与`View`的移动相关联呢?在`View`的源码中: - ```java - /** - * Called by a parent to request that a child update its values for mScrollX - * and mScrollY if necessary. This will typically be done if the child is - * animating a scroll using a {@link android.widget.Scroller Scroller} - * object. - */ - public void computeScroll() { - } - ``` - 通过注释我们可以看到该方法又父类调用根据滚动的值去更新`View`,在使用`Scroller`的时候通常都要实现该方法。来达到子`View`的滚动效果。 - 继续往下跟发现在`draw()`方法中回去调用`computeScroll()`,而`draw()`方法会在父布局调用`drawChild()`的时候使用。 - -3. 具体关联 - 通过上面两步大体能得到`Scroller`与`View`的移动要通过`computeScroll()`来完成,但是在究竟如何进行代码实现。 - `Scroller.startScroll()`方法被调用后会储存要滚动的起始位置、结束位置、持续时间。所以我们可以在`computeScroll()`方法中去判断一下当前是否已经滚动完成,如果没有滚动完成, - 我们就去不断的获取当前`Scroller的位置`,根据这个位置,来把相应的`View`移动到这里。 - ```java - public void computeScroll() { - if (mScroller.computeScrollOffset()) { - //如果还没有滚动完成,我们就去让当前的View移动到指定位置去 - mCenterView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - //移动完后,我们应该继续调用computeScoll方法去获取并且移动当前View。所以我们调用invalidate方法去请求重绘,这样父类就会调用computeScroll - postInvalidate(); - } - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +Scroller简介 +=== + +在`SlidingMenu`项目中为了实现控件的滑动,需要用到`Scroller`类来实现缓慢的滑动过程,至于有人说`View`类可以直接调用`scrollTo()`方法, +这里`scrollTo()`方法也能实现移动,但是它的移动是很快一下子就移过去了,就像穿越一样,直接从现实回到了过去,而`Scroller`类能够实现过程的移动。 +可以理解为一步步的走。 + +1. 查看Scroller源码 + ```java + public class Scroller { + //... + } + ``` + 发现`Scroller`类并不是`View`的子类,只是一个普通的类,这个类中封装了滚动的操作,记录了滚动的位置以及时间等。 +该类有两个重要的方法: + - `computeScrollOffset()`: + 文档的说明为`Call this when you want to know the new location.`查看源码可以发现,如果在移动到指定位置后就会返回false.正在移动的过程中返回true。 + - `startScroll()`: + 该方法的内部实现,并没有具体的移动方法,而是设置了一些移动所需的数据,包括移动持续的时间、开始位置、结束位置等。从而我们可以知道调用`Scroller.startScroll()`方法并没有真正的移动,而是设置了一些数据。 + +2. `Scroller.startScoll()`是如何与`View`的移动相关联呢?在`View`的源码中: + ```java + /** + * Called by a parent to request that a child update its values for mScrollX + * and mScrollY if necessary. This will typically be done if the child is + * animating a scroll using a {@link android.widget.Scroller Scroller} + * object. + */ + public void computeScroll() { + } + ``` + 通过注释我们可以看到该方法又父类调用根据滚动的值去更新`View`,在使用`Scroller`的时候通常都要实现该方法。来达到子`View`的滚动效果。 + 继续往下跟发现在`draw()`方法中回去调用`computeScroll()`,而`draw()`方法会在父布局调用`drawChild()`的时候使用。 + +3. 具体关联 + 通过上面两步大体能得到`Scroller`与`View`的移动要通过`computeScroll()`来完成,但是在究竟如何进行代码实现。 + `Scroller.startScroll()`方法被调用后会储存要滚动的起始位置、结束位置、持续时间。所以我们可以在`computeScroll()`方法中去判断一下当前是否已经滚动完成,如果没有滚动完成, + 我们就去不断的获取当前`Scroller的位置`,根据这个位置,来把相应的`View`移动到这里。 + ```java + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + //如果还没有滚动完成,我们就去让当前的View移动到指定位置去 + mCenterView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + //移动完后,我们应该继续调用computeScoll方法去获取并且移动当前View。所以我们调用invalidate方法去请求重绘,这样父类就会调用computeScroll + postInvalidate(); + } + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/ScrollingTabs.md" b/AndroidBasicPart/ScrollingTabs.md similarity index 96% rename from "Android\345\237\272\347\241\200/ScrollingTabs.md" rename to AndroidBasicPart/ScrollingTabs.md index 2cbf8999..f543925a 100644 --- "a/Android\345\237\272\347\241\200/ScrollingTabs.md" +++ b/AndroidBasicPart/ScrollingTabs.md @@ -1,165 +1,165 @@ -ScrollingTabs -=== - -自定义ScrollingTabs结合ViewPager实现指引的效果。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ScrollingTabs.png?raw=true) - -**原理:** -由于`ScrollingTabs`即可以点击又可以实现左右滑动,首先想到的就是继承`HorizontalScrollView`来实现滑动,至于点击的实现需要通过对`View` -设置点击。 -通过对`ViewPager`设置`OnPageChangeListener`来监听页面变化,从而实现对`ScrollingTabs`的改变,而在每个`Tab`上设置 -点击事件,当点击的时候就去设置`ViewPager`的当前页面 - -1. 继承HorizontalScrollView,并且添加一个水平方向的线性布局,作为Tab的父布局 - ```java - public class ScrollingTabs extends HorizontalScrollView { - - private LinearLayout mContainer; - - public ScrollingTabs(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - public ScrollingTabs(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public ScrollingTabs(Context context) { - super(context); - init(context); - } - - private void init(Context context) { - this.setHorizontalScrollBarEnabled(false); - this.setHorizontalFadingEdgeEnabled(false); - - mContainer = new LinearLayout(context); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - android.view.ViewGroup.LayoutParams.MATCH_PARENT, - android.view.ViewGroup.LayoutParams.MATCH_PARENT); - mContainer.setLayoutParams(params); - mContainer.setOrientation(LinearLayout.HORIZONTAL); - - addView(mContainer); - } - } - ``` - -2. 提供接口供调用者设置每个Tab的视图。 - ```java - public interface TabAdapter { - /** - * 每个Tab的视图 - */ - public View getView(int position); - /** - * Tab之间的分割线 - */ - public View getSeparator(); - } - ``` - -3. 暴露方法,初始化Tab。 - ```java - public void setTabAdapter(TabAdapter adapter) { - this.mTabAdapter = adapter; - initTabView(); - } - - public void setViewPager(ViewPager pager) { - this.mViewPager = pager; - mViewPager.setOnPageChangeListener(this); - initTabView(); - } - - /** - * 必须等到ViewPager和TabAdapter都设置完成后才可以调用 - */ - private void initTabView() { - if (mViewPager != null && mTabAdapter != null) { - //清空父布局,保险起见 - mContainer.removeAllViews(); - //根据ViewPager的页数去设置Tab - for (int i = 0; i < mViewPager.getAdapter().getCount(); i++) { - final View tab = mTabAdapter.getView(i); - tab.setTag(i); - - mContainer.addView(tab); - - // Segmentation view - if (mTabAdapter.getSeparator() != null - && i != mViewPager.getAdapter().getCount() - 1) { - //Tabs之间使用分割线 - isUseSeperator = true; - mContainer.addView(mTabAdapter.getSeparator()); - } - - // 对每个Tab设置点击事件 - tab.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - int index = (Integer) tab.getTag(); - if (mTabClickListener != null) { - //暴露接口 - mTabClickListener.onClick(index); - } else { - if (mViewPager.getCurrentItem() == index) { - //如果当前ViewPager已经显示到了该Tab也,就直接让其选中 - selectTab(index); - } else { - //当前ViewPager并没有显示该Tab页,要让ViewPager去显示相应的Tab页 - mViewPager.setCurrentItem(index, true); - } - } - } - }); - - } - - // 初始化时核对一下Tab - selectTab(mViewPager.getCurrentItem()); - } - } - ``` - -4. selectTab的实现,选中相应的Tab,并且实现滑动到屏幕中间位置 - ```java - private void selectTab(int position) { - if (!isUseSeperator) { - //没有分割线 - for (int i = 0; i < mContainer.getChildCount(); i++) { - View tab = mContainer.getChildAt(i); - tab.setSelected(i == position); - } - } else { - //有分割线 - for (int i = 0, pos = 0; i < mContainer.getChildCount(); i += 2, pos++) { - View tab = mContainer.getChildAt(i); - tab.setSelected(pos == position); - } - } - //得到当前的Tab - View selectedView = null; - if (!isUseSeperator) { - selectedView = mContainer.getChildAt(position); - } else { - selectedView = mContainer.getChildAt(position * 2); - } - - int tabWidth = selectedView.getMeasuredWidth(); - int tabLeft = selectedView.getLeft(); - - //距离左边屏幕的位置加上该Tab宽度的一半正好是该Tab中心点的位置。我们需要让该Tab的中心点移动到屏幕的中心点。 - int distance = (tabLeft + tabWidth / 2) - mWindowWidth / 2; - //移动 - smoothScrollTo(distance, this.getScrollY()); - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +ScrollingTabs +=== + +自定义ScrollingTabs结合ViewPager实现指引的效果。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ScrollingTabs.png?raw=true) + +**原理:** +由于`ScrollingTabs`即可以点击又可以实现左右滑动,首先想到的就是继承`HorizontalScrollView`来实现滑动,至于点击的实现需要通过对`View` +设置点击。 +通过对`ViewPager`设置`OnPageChangeListener`来监听页面变化,从而实现对`ScrollingTabs`的改变,而在每个`Tab`上设置 +点击事件,当点击的时候就去设置`ViewPager`的当前页面 + +1. 继承HorizontalScrollView,并且添加一个水平方向的线性布局,作为Tab的父布局 + ```java + public class ScrollingTabs extends HorizontalScrollView { + + private LinearLayout mContainer; + + public ScrollingTabs(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public ScrollingTabs(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ScrollingTabs(Context context) { + super(context); + init(context); + } + + private void init(Context context) { + this.setHorizontalScrollBarEnabled(false); + this.setHorizontalFadingEdgeEnabled(false); + + mContainer = new LinearLayout(context); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT); + mContainer.setLayoutParams(params); + mContainer.setOrientation(LinearLayout.HORIZONTAL); + + addView(mContainer); + } + } + ``` + +2. 提供接口供调用者设置每个Tab的视图。 + ```java + public interface TabAdapter { + /** + * 每个Tab的视图 + */ + public View getView(int position); + /** + * Tab之间的分割线 + */ + public View getSeparator(); + } + ``` + +3. 暴露方法,初始化Tab。 + ```java + public void setTabAdapter(TabAdapter adapter) { + this.mTabAdapter = adapter; + initTabView(); + } + + public void setViewPager(ViewPager pager) { + this.mViewPager = pager; + mViewPager.setOnPageChangeListener(this); + initTabView(); + } + + /** + * 必须等到ViewPager和TabAdapter都设置完成后才可以调用 + */ + private void initTabView() { + if (mViewPager != null && mTabAdapter != null) { + //清空父布局,保险起见 + mContainer.removeAllViews(); + //根据ViewPager的页数去设置Tab + for (int i = 0; i < mViewPager.getAdapter().getCount(); i++) { + final View tab = mTabAdapter.getView(i); + tab.setTag(i); + + mContainer.addView(tab); + + // Segmentation view + if (mTabAdapter.getSeparator() != null + && i != mViewPager.getAdapter().getCount() - 1) { + //Tabs之间使用分割线 + isUseSeperator = true; + mContainer.addView(mTabAdapter.getSeparator()); + } + + // 对每个Tab设置点击事件 + tab.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + int index = (Integer) tab.getTag(); + if (mTabClickListener != null) { + //暴露接口 + mTabClickListener.onClick(index); + } else { + if (mViewPager.getCurrentItem() == index) { + //如果当前ViewPager已经显示到了该Tab也,就直接让其选中 + selectTab(index); + } else { + //当前ViewPager并没有显示该Tab页,要让ViewPager去显示相应的Tab页 + mViewPager.setCurrentItem(index, true); + } + } + } + }); + + } + + // 初始化时核对一下Tab + selectTab(mViewPager.getCurrentItem()); + } + } + ``` + +4. selectTab的实现,选中相应的Tab,并且实现滑动到屏幕中间位置 + ```java + private void selectTab(int position) { + if (!isUseSeperator) { + //没有分割线 + for (int i = 0; i < mContainer.getChildCount(); i++) { + View tab = mContainer.getChildAt(i); + tab.setSelected(i == position); + } + } else { + //有分割线 + for (int i = 0, pos = 0; i < mContainer.getChildCount(); i += 2, pos++) { + View tab = mContainer.getChildAt(i); + tab.setSelected(pos == position); + } + } + //得到当前的Tab + View selectedView = null; + if (!isUseSeperator) { + selectedView = mContainer.getChildAt(position); + } else { + selectedView = mContainer.getChildAt(position * 2); + } + + int tabWidth = selectedView.getMeasuredWidth(); + int tabLeft = selectedView.getLeft(); + + //距离左边屏幕的位置加上该Tab宽度的一半正好是该Tab中心点的位置。我们需要让该Tab的中心点移动到屏幕的中心点。 + int distance = (tabLeft + tabWidth / 2) - mWindowWidth / 2; + //移动 + smoothScrollTo(distance, this.getScrollY()); + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/Selector\344\275\277\347\224\250.md" "b/AndroidBasicPart/Selector\344\275\277\347\224\250.md" similarity index 98% rename from "Android\345\237\272\347\241\200/Selector\344\275\277\347\224\250.md" rename to "AndroidBasicPart/Selector\344\275\277\347\224\250.md" index d0f016b6..d088d70f 100644 --- "a/Android\345\237\272\347\241\200/Selector\344\275\277\347\224\250.md" +++ "b/AndroidBasicPart/Selector\344\275\277\347\224\250.md" @@ -1,106 +1,106 @@ -Selector使用 -=== - -**Selector**使其能够在不同的状态下更换某个View的背景图片。 -```xml - - - - - - - - - - - - - -``` -`Selector`最终会被`Android`框架解析成`StateListDrawable`类对象。 - -1. StateListDrawable类介绍 - 该类定义了不同状态值下与之对应的图片资源,即我们可以利用该类保存多种状态值,多种图片资源。 - 方法: - - `public void addState (int[] stateSet, Drawable drawable)` - 功能: 给特定的状态集合设置drawable图片资源 - ```java - //初始化一个空对象 - StateListDrawable stalistDrawable = new StateListDrawable(); - //获取对应的属性值 Android框架自带的属性 attr - int pressed = android.R.attr.state_pressed; - int window_focused = android.R.attr.state_window_focused; - int focused = android.R.attr.state_focused; - int selected = android.R.attr.state_selected; - - stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1)); - stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2); - stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3); - stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4); - //没有任何状态时显示的图片,我们给它设置我空集合 - stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5); - 上面的“-”负号表示对应的属性值为 false - 当我们为某个View使用其作为背景色时,会根据状态进行背景图的转换。 - ``` - - public boolean isStateful () - 功能: 表明该状态改变了,对应的drawable图片是否会改变。 - 注:在StateListDrawable类中,该方法返回为true,显然状态改变后,我们的图片会跟着改变。 - -2. GridView之Selector使用: - GridView在点击每一个条目的时候黄色的背景,很难看,那么怎么才能让其不显示这个颜色呢?就是在GridView中将listSelector这个属性指定为透明的, - 这样再点击的时候就不显示黄色了,但是这样用户不知道自己点击了没有,所以要让它在点击的时候显示一个我们自定义的颜色 - ```xml - - - ``` - 1. drawable目录新建xml文件 - ```xml - - - - - - - - ``` - *这里android:drawable="@color/gray"必须通过将颜色放到res下的color.xml中然后通过@color/gray这种方式指定而不能通过#000000这样直接写颜色,如果直接写颜色会报错* - - 2. 在控件中通过背景使用这个状态选择器 - 对每个GridView的子条目设置相应的背景为改状态选择器 - ```xml - - - - - - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +Selector使用 +=== + +**Selector**使其能够在不同的状态下更换某个View的背景图片。 +```xml + + + + + + + + + + + + + +``` +`Selector`最终会被`Android`框架解析成`StateListDrawable`类对象。 + +1. StateListDrawable类介绍 + 该类定义了不同状态值下与之对应的图片资源,即我们可以利用该类保存多种状态值,多种图片资源。 + 方法: + - `public void addState (int[] stateSet, Drawable drawable)` + 功能: 给特定的状态集合设置drawable图片资源 + ```java + //初始化一个空对象 + StateListDrawable stalistDrawable = new StateListDrawable(); + //获取对应的属性值 Android框架自带的属性 attr + int pressed = android.R.attr.state_pressed; + int window_focused = android.R.attr.state_window_focused; + int focused = android.R.attr.state_focused; + int selected = android.R.attr.state_selected; + + stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1)); + stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2); + stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3); + stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4); + //没有任何状态时显示的图片,我们给它设置我空集合 + stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5); + 上面的“-”负号表示对应的属性值为 false + 当我们为某个View使用其作为背景色时,会根据状态进行背景图的转换。 + ``` + - public boolean isStateful () + 功能: 表明该状态改变了,对应的drawable图片是否会改变。 + 注:在StateListDrawable类中,该方法返回为true,显然状态改变后,我们的图片会跟着改变。 + +2. GridView之Selector使用: + GridView在点击每一个条目的时候黄色的背景,很难看,那么怎么才能让其不显示这个颜色呢?就是在GridView中将listSelector这个属性指定为透明的, + 这样再点击的时候就不显示黄色了,但是这样用户不知道自己点击了没有,所以要让它在点击的时候显示一个我们自定义的颜色 + ```xml + + + ``` + 1. drawable目录新建xml文件 + ```xml + + + + + + + + ``` + *这里android:drawable="@color/gray"必须通过将颜色放到res下的color.xml中然后通过@color/gray这种方式指定而不能通过#000000这样直接写颜色,如果直接写颜色会报错* + + 2. 在控件中通过背景使用这个状态选择器 + 对每个GridView的子条目设置相应的背景为改状态选择器 + ```xml + + + + + + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/SlidingMenu.md" b/AndroidBasicPart/SlidingMenu.md similarity index 96% rename from "Android\345\237\272\347\241\200/SlidingMenu.md" rename to AndroidBasicPart/SlidingMenu.md index 3824f596..2bdf47f8 100644 --- "a/Android\345\237\272\347\241\200/SlidingMenu.md" +++ b/AndroidBasicPart/SlidingMenu.md @@ -1,216 +1,216 @@ -SlidingMenu -=== - -先看一下图片 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/slidingmenu_1.png?raw=true) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/slidingmenu_2.png?raw=true) -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/slidingmenu_3.png?raw=true) - - -原理 ---- - -`SlidingMenu`无非就是一个包含三个`View`的控件,**左边View**、**中间View(默认时全屏)**、**右边View**,默认的情况下中间`View`会把两边的`View`覆盖住, -在手指滑动的时候,会根据手指的滑动方向以及滑动距离去移动中间的那个`View`,从而能让两边`View`完全可见。 -在定义该View的时候,首先会想到继承`RelativeLayout`,能简单的实现这种左、中、右三个View的布局。 - -1. 继承RelativeLayout - ```java - public class SlidingMenu extends RelativeLayout { - - public SlidingMenu(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - public SlidingMenu(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public SlidingMenu(Context context) { - super(context); - init(context); - } - - private void init(Context context) { - mContext = context; - mScroller = new Scroller(context); - mWindowWidth = getWindowWidth(context); - } - } - ``` - -2. 具体的三个View需要暴露给外界调用,所以我们要提供一个setView()的方法。 - ```java - public void setView(View leftView, View rightView, View centerView, - int leftViewWidth, int rightViewWidth) { - //添加左边View - RelativeLayout.LayoutParams leftParams = new LayoutParams( - (int) convertDpToPixel(leftViewWidth, mContext), - LayoutParams.MATCH_PARENT); - leftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); - addView(leftView, leftParams); - - //右边的View - RelativeLayout.LayoutParams rightParams = new LayoutParams( - (int) convertDpToPixel(rightViewWidth, mContext), - LayoutParams.MATCH_PARENT); - rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); - addView(rightView, rightParams); - - //添加中间的View - RelativeLayout.LayoutParams centerParams = new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - addView(centerView, centerParams); - - mLeftView = leftView; - mRightView = rightView; - mCenterView = centerView; - } - ``` - 外界使用`SlidingMenu`类的时候需要首先调用该方法去设置相应的View,一旦调用该方法后,我们就将布局设置完了,下一步就是对`touch`事件进行处理,然后去移动中间的View。 - -3. 处理Touch事件 - 在手指按下的时候,我们去控制两边View的显示与隐藏 - ```java - public boolean onInterceptTouchEvent(MotionEvent ev) { - int x = (int) ev.getRawX(); - int y = (int) ev.getRawY(); - - int action = ev.getAction(); - - switch (action) { - case MotionEvent.ACTION_DOWN: - mLastPostionX = x; - mLastPostionY = y; - //通过变量记录当前可以显示左边的View还是可以显示右边的View - if (mCanLeftViewShow) { - //如果当前,中间的View往右滑,那么这时候左边的View就要能显示了 - mLeftView.setVisibility(View.VISIBLE); - mRightView.setVisibility(View.GONE); - } else if (mCanRightViewShow) { - mLeftView.setVisibility(View.GONE); - mRightView.setVisibility(View.VISIBLE); - } - - break; - case MotionEvent.ACTION_MOVE: - - break; - case MotionEvent.ACTION_UP: - - break; - - default: - break; - } - - return false; - } - ``` - 在`onTouch()`中,我们去获取手指移动的距离 - ```java - public boolean onTouchEvent(MotionEvent event) { - int x = (int) event.getRawX(); - int y = (int) event.getRawY(); - - int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mLastPostionX = x; - mLastPostionY = y; - - if (!mScroller.isFinished()) { - mScroller.abortAnimation(); - } - - break; - case MotionEvent.ACTION_MOVE: - int distance = x - mLastPostionX; - int targetPositon = mCenterView.getScrollX() - distance; - mLastPostionX = x; - - if (mCanLeftViewShow) { - if (targetPositon > 0) { - targetPositon = 0; - } - - if (targetPositon < -mLeftViewWidth) { - targetPositon = -mLeftViewWidth; - } - } - - if (mCanRightViewShow) { - if (targetPositon < 0) { - targetPositon = 0; - } - - if (targetPositon > mRightViewWidth) { - targetPositon = mRightViewWidth; - } - } - - mClicked = false; - //让中间的View随着手指的移动而移动 - mCenterView.scrollTo(targetPositon, 0); - - break; - case MotionEvent.ACTION_UP: - //你手指移动后抬起的时候需要注意,如果现在左边的View已经超过一半可见了,这时候就算你抬起手指了,SlidingMenu也要滑动到右边让左边View完全可见。当然还有就是你滑动的飞快,然后突然抬起了手指,这时候就要进行速率的计算了,我们先不说速率 - int dx = 0; - if (mCanLeftViewShow) { - if (mCenterView.getScrollX() <= -mLeftViewWidth / 2) { - //已经超过左边View的一般了,应该让中间View继续移动,移动到左边View完全可见 - dx = -mLeftViewWidth - mCenterView.getScrollX(); - } else { - // 滚回原来的位置 - dx = -mCenterView.getScrollX(); - resumeLeftViewClickState(); - } - - } else if (mCanRightViewShow) { - if (mCenterView.getScrollX() >= mRightViewWidth / 2) { - dx = mRightViewWidth - mCenterView.getScrollX(); - } else { - dx = -mCenterView.getScrollX(); - resumeRightViewClickState(); - } - } - //手指抬起后,要让中间View有过程的滑过去,所以要用到Scroller类 - smoothScrollTo(dx); - break; - - default: - break; - } - - return true; - } - ``` - - `Scroller`的实现 - ```java - private void smoothScrollTo(int distance) { - mScroller.startScroll(mCenterView.getScrollX(), 0, distance, 0, - sDuration); - invalidate(); - } - - @Override - public void computeScroll() { - if (mScroller.computeScrollOffset()) { - mCenterView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - postInvalidate(); - } - } - ``` - -4. 到这里SlidingMenu的大体实现已经完成了 - 剩下的就是对速率的计算,已经添加显示左边与显示右边的View的按钮。当左边View完全显示的时候,点击中间View可见部分时需要让中间View全屏。 - 至于这些细节的东西就不再仔细说了,大家自己看源码吧。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +SlidingMenu +=== + +先看一下图片 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/slidingmenu_1.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/slidingmenu_2.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/slidingmenu_3.png?raw=true) + + +原理 +--- + +`SlidingMenu`无非就是一个包含三个`View`的控件,**左边View**、**中间View(默认时全屏)**、**右边View**,默认的情况下中间`View`会把两边的`View`覆盖住, +在手指滑动的时候,会根据手指的滑动方向以及滑动距离去移动中间的那个`View`,从而能让两边`View`完全可见。 +在定义该View的时候,首先会想到继承`RelativeLayout`,能简单的实现这种左、中、右三个View的布局。 + +1. 继承RelativeLayout + ```java + public class SlidingMenu extends RelativeLayout { + + public SlidingMenu(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public SlidingMenu(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public SlidingMenu(Context context) { + super(context); + init(context); + } + + private void init(Context context) { + mContext = context; + mScroller = new Scroller(context); + mWindowWidth = getWindowWidth(context); + } + } + ``` + +2. 具体的三个View需要暴露给外界调用,所以我们要提供一个setView()的方法。 + ```java + public void setView(View leftView, View rightView, View centerView, + int leftViewWidth, int rightViewWidth) { + //添加左边View + RelativeLayout.LayoutParams leftParams = new LayoutParams( + (int) convertDpToPixel(leftViewWidth, mContext), + LayoutParams.MATCH_PARENT); + leftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + addView(leftView, leftParams); + + //右边的View + RelativeLayout.LayoutParams rightParams = new LayoutParams( + (int) convertDpToPixel(rightViewWidth, mContext), + LayoutParams.MATCH_PARENT); + rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + addView(rightView, rightParams); + + //添加中间的View + RelativeLayout.LayoutParams centerParams = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + addView(centerView, centerParams); + + mLeftView = leftView; + mRightView = rightView; + mCenterView = centerView; + } + ``` + 外界使用`SlidingMenu`类的时候需要首先调用该方法去设置相应的View,一旦调用该方法后,我们就将布局设置完了,下一步就是对`touch`事件进行处理,然后去移动中间的View。 + +3. 处理Touch事件 + 在手指按下的时候,我们去控制两边View的显示与隐藏 + ```java + public boolean onInterceptTouchEvent(MotionEvent ev) { + int x = (int) ev.getRawX(); + int y = (int) ev.getRawY(); + + int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mLastPostionX = x; + mLastPostionY = y; + //通过变量记录当前可以显示左边的View还是可以显示右边的View + if (mCanLeftViewShow) { + //如果当前,中间的View往右滑,那么这时候左边的View就要能显示了 + mLeftView.setVisibility(View.VISIBLE); + mRightView.setVisibility(View.GONE); + } else if (mCanRightViewShow) { + mLeftView.setVisibility(View.GONE); + mRightView.setVisibility(View.VISIBLE); + } + + break; + case MotionEvent.ACTION_MOVE: + + break; + case MotionEvent.ACTION_UP: + + break; + + default: + break; + } + + return false; + } + ``` + 在`onTouch()`中,我们去获取手指移动的距离 + ```java + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getRawX(); + int y = (int) event.getRawY(); + + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mLastPostionX = x; + mLastPostionY = y; + + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + break; + case MotionEvent.ACTION_MOVE: + int distance = x - mLastPostionX; + int targetPositon = mCenterView.getScrollX() - distance; + mLastPostionX = x; + + if (mCanLeftViewShow) { + if (targetPositon > 0) { + targetPositon = 0; + } + + if (targetPositon < -mLeftViewWidth) { + targetPositon = -mLeftViewWidth; + } + } + + if (mCanRightViewShow) { + if (targetPositon < 0) { + targetPositon = 0; + } + + if (targetPositon > mRightViewWidth) { + targetPositon = mRightViewWidth; + } + } + + mClicked = false; + //让中间的View随着手指的移动而移动 + mCenterView.scrollTo(targetPositon, 0); + + break; + case MotionEvent.ACTION_UP: + //你手指移动后抬起的时候需要注意,如果现在左边的View已经超过一半可见了,这时候就算你抬起手指了,SlidingMenu也要滑动到右边让左边View完全可见。当然还有就是你滑动的飞快,然后突然抬起了手指,这时候就要进行速率的计算了,我们先不说速率 + int dx = 0; + if (mCanLeftViewShow) { + if (mCenterView.getScrollX() <= -mLeftViewWidth / 2) { + //已经超过左边View的一般了,应该让中间View继续移动,移动到左边View完全可见 + dx = -mLeftViewWidth - mCenterView.getScrollX(); + } else { + // 滚回原来的位置 + dx = -mCenterView.getScrollX(); + resumeLeftViewClickState(); + } + + } else if (mCanRightViewShow) { + if (mCenterView.getScrollX() >= mRightViewWidth / 2) { + dx = mRightViewWidth - mCenterView.getScrollX(); + } else { + dx = -mCenterView.getScrollX(); + resumeRightViewClickState(); + } + } + //手指抬起后,要让中间View有过程的滑过去,所以要用到Scroller类 + smoothScrollTo(dx); + break; + + default: + break; + } + + return true; + } + ``` + + `Scroller`的实现 + ```java + private void smoothScrollTo(int distance) { + mScroller.startScroll(mCenterView.getScrollX(), 0, distance, 0, + sDuration); + invalidate(); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + mCenterView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + postInvalidate(); + } + } + ``` + +4. 到这里SlidingMenu的大体实现已经完成了 + 剩下的就是对速率的计算,已经添加显示左边与显示右边的View的按钮。当左边View完全显示的时候,点击中间View可见部分时需要让中间View全屏。 + 至于这些细节的东西就不再仔细说了,大家自己看源码吧。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/String\346\240\274\345\274\217\345\214\226.md" "b/AndroidBasicPart/String\346\240\274\345\274\217\345\214\226.md" similarity index 100% rename from "Android\345\237\272\347\241\200/String\346\240\274\345\274\217\345\214\226.md" rename to "AndroidBasicPart/String\346\240\274\345\274\217\345\214\226.md" diff --git "a/Android\345\237\272\347\241\200/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" "b/AndroidBasicPart/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" similarity index 96% rename from "Android\345\237\272\347\241\200/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" rename to "AndroidBasicPart/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" index ab5de10d..046e211e 100644 --- "a/Android\345\237\272\347\241\200/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" +++ "b/AndroidBasicPart/TextView\350\267\221\351\251\254\347\201\257\346\225\210\346\236\234.md" @@ -1,71 +1,71 @@ -TextView跑马灯效果 -=== - -TextView跑马灯效果实现方式一: ---- - -当`TextView`内容过多时默认会采用截取的方式以`...`来截取。如何能够实现内容过多时的跑马灯效果。 - -自定义视图步骤: ----- - -1. 自定义一个类继承`TextView`,重写它的`isFocused()`方法 -2. 在布局的文件中使用自定义的`TextView` - - -示例代码: ----- - -1. 继承TextView - ```java - //继承TextView并且实现抽象方法 - public class FocusedTextView extends TextView { - - public FocusedTextView(Context context, AttributeSet attrs, int defStyle){ - super(context, attrs, defStyle); - } - - public FocusedTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FocusedTextView(Context context) { - super(context); - } - - //重写isFocused方法,让其一直返回true - public boolean isFocused() { - return true; - } - } - ``` - -2. 在清单文件中使用该类 -```xml - - ``` - -TextView跑马灯效果实现方式二: ---- - -TextView实现跑马灯的效果,不用自定义View -```xml - -``` - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! +TextView跑马灯效果 +=== + +TextView跑马灯效果实现方式一: +--- + +当`TextView`内容过多时默认会采用截取的方式以`...`来截取。如何能够实现内容过多时的跑马灯效果。 + +自定义视图步骤: +---- + +1. 自定义一个类继承`TextView`,重写它的`isFocused()`方法 +2. 在布局的文件中使用自定义的`TextView` + + +示例代码: +---- + +1. 继承TextView + ```java + //继承TextView并且实现抽象方法 + public class FocusedTextView extends TextView { + + public FocusedTextView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + public FocusedTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FocusedTextView(Context context) { + super(context); + } + + //重写isFocused方法,让其一直返回true + public boolean isFocused() { + return true; + } + } + ``` + +2. 在清单文件中使用该类 +```xml + + ``` + +TextView跑马灯效果实现方式二: +--- + +TextView实现跑马灯的效果,不用自定义View +```xml + +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" "b/AndroidBasicPart/WebView\346\200\273\347\273\223.md" similarity index 100% rename from "Android\345\237\272\347\241\200/WebView\346\200\273\347\273\223.md" rename to "AndroidBasicPart/WebView\346\200\273\347\273\223.md" diff --git "a/Android\345\237\272\347\241\200/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" "b/AndroidBasicPart/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" similarity index 97% rename from "Android\345\237\272\347\241\200/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" rename to "AndroidBasicPart/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" index cd7df519..be840dcf 100644 --- "a/Android\345\237\272\347\241\200/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" +++ "b/AndroidBasicPart/Widget(\347\252\227\345\217\243\345\260\217\351\203\250\344\273\266).md" @@ -1,160 +1,160 @@ -Widget简介 -=== - -可以使用`AppWidgetManager`更新`Widget`中的数据,但这样最短也要半个小时才能更新一次,一般不用他更新,而是自己定义一个服务去更新`Widget`中的数据。 - -## Widget的创建步骤 - -1. 写一个类继承AppWidgetProvider,这个是一个广播接收者,所以要在清单文件中进行配置 - ```java - public class MyWidget extends AppWidgetProvider { - @Override - public void onEnabled(Context context) { - //开启服务定期的更新界面. - Intent intent = new Intent(context,UpdateWidgetService.class); - context.startService(intent); - super.onEnabled(context); - } - - @Override - public void onDisabled(Context context) { - //关闭掉服务 - Intent intent = new Intent(context,UpdateWidgetService.class); - context.stopService(intent); - super.onDisabled(context); - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, - int[] appWidgetIds) { - - //Widget布局中定义的更新时间到了。检查下 服务是否还活着. - if(!ServiceStatusUtil.isServiceRunning(context, "com.itheima.mobilesafe.service.UpdateWidgetService")){ - Intent intent = new Intent(context,UpdateWidgetService.class); - context.startService(intent); - } - super.onUpdate(context, appWidgetManager, appWidgetIds); - } - } - ``` - -2. 在清单文件中进行配置,内容如下: - ```xml - - - - - //这里使用到了一个xml文件,所以要创建这个文件 - - ``` - -3. 在res下面新建一个名为xml的文件件,然后新建example_appwidget_info.xml内容如下 - ```xml - - - //这个是Android3.0的一个新特性,是可以让widget改变大小,在2.3时候创建出来的Widget多大就是多大,不能改变,可以把这个去掉 - - ``` - -4. 更新Widget数据的服务 - ```java - public class UpdateWidgetService extends Service { - private Timer timer; - private TimerTask task; - @Override - public IBinder onBind(Intent intent) { - return null; - } - @Override - public void onCreate() { - super.onCreate(); - // 开启定期的任务更新widget. - timer = new Timer(); - task = new TimerTask() { - @Override - public void run() { - AppWidgetManager awm = AppWidgetManager - .getInstance(getApplicationContext()); - ComponentName component = new ComponentName( - getApplicationContext(), MyWidget.class); - RemoteViews views = new RemoteViews(getPackageName(), - R.layout.process_widget); - views.setTextViewText( - R.id.process_count, - "正在运行:" - + ProcessStatusUtils.getProcessCount(getApplicationContext()) - + "个"); - views.setTextViewText( - R.id.process_memory, - "可用内存:" - + Formatter - .formatFileSize( - getApplicationContext(), ProcessStatusUtils.getAvailRAM(getApplicationContext()))); - Intent intent = new Intent(); - intent.setAction("com.itheima.killall"); - //设置一个自定义的广播事件 动作 com.itheima.killall - PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, 0); - views.setOnClickPendingIntent(R.id.btn_clear, pendingIntent); - awm.updateAppWidget(component, views); - } - }; - timer.schedule(task, 1000, 2000); - } - @Override - public void onDestroy() { - timer.cancel(); - task.cancel(); - timer = null; - task = null; - super.onDestroy(); - } - } - ``` - -## Widget的声明周期 - - `Widget`就是一个特殊的广播接收者 - 1. 当界面上第一个`widget`被创建的时候 - 01-14 02:17:14.348: INFO/System.out(1853): onEnabled 当`widget`第一次被创建的时候调用. 非常适合做应用程序的初始化. - 01-14 02:17:14.348: INFO/System.out(1853): onReceive - 01-14 02:17:14.357: INFO/System.out(1853): onUpdate 当有新的`widget`被创建的时候 更新界面的操作. 当时间片到的时候`onupdate()`调用. - 01-14 02:17:14.357: INFO/System.out(1853): onReceive - - 2. 当界面上第二个`widget`被创建的时候 - 01-14 02:18:10.148: INFO/System.out(1853): onUpdate - 01-14 02:18:10.148: INFO/System.out(1853): onReceive - - 3. 再创建新的`widget` - 01-14 02:18:10.148: INFO/System.out(1853): onUpdate - 01-14 02:18:10.148: INFO/System.out(1853): onReceive - - 4. 从界面上移除一个`widget` - 01-14 02:19:11.709: INFO/System.out(1853): onDeleted - 01-14 02:19:11.709: INFO/System.out(1853): onReceive - - 5. 最后一个`widget`被移除 - 01-14 02:19:37.509: INFO/System.out(1853): onDeleted - 01-14 02:19:37.509: INFO/System.out(1853): onReceive - 01-14 02:19:37.509: INFO/System.out(1853): onDisabled 当`widget`从界面上全部移除的时候调用的方法. 非常适合删除临时文件停止后台服务. - 01-14 02:19:37.509: INFO/System.out(1853): onReceive - - 6. `widget`就是一个特殊的广播接受者 当有新的事件产生的是 肯定会调用 `onReceive()`; - -** -注意: 在不同的手机上 widget的生命周期调用方法 可能有细微的不同. -360桌面 go桌面 awt桌面 腾讯桌面 小米桌面 -** - ---- - -- 邮箱 :charon.chui@gmail.com +Widget简介 +=== + +可以使用`AppWidgetManager`更新`Widget`中的数据,但这样最短也要半个小时才能更新一次,一般不用他更新,而是自己定义一个服务去更新`Widget`中的数据。 + +## Widget的创建步骤 + +1. 写一个类继承AppWidgetProvider,这个是一个广播接收者,所以要在清单文件中进行配置 + ```java + public class MyWidget extends AppWidgetProvider { + @Override + public void onEnabled(Context context) { + //开启服务定期的更新界面. + Intent intent = new Intent(context,UpdateWidgetService.class); + context.startService(intent); + super.onEnabled(context); + } + + @Override + public void onDisabled(Context context) { + //关闭掉服务 + Intent intent = new Intent(context,UpdateWidgetService.class); + context.stopService(intent); + super.onDisabled(context); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + + //Widget布局中定义的更新时间到了。检查下 服务是否还活着. + if(!ServiceStatusUtil.isServiceRunning(context, "com.itheima.mobilesafe.service.UpdateWidgetService")){ + Intent intent = new Intent(context,UpdateWidgetService.class); + context.startService(intent); + } + super.onUpdate(context, appWidgetManager, appWidgetIds); + } + } + ``` + +2. 在清单文件中进行配置,内容如下: + ```xml + + + + + //这里使用到了一个xml文件,所以要创建这个文件 + + ``` + +3. 在res下面新建一个名为xml的文件件,然后新建example_appwidget_info.xml内容如下 + ```xml + + + //这个是Android3.0的一个新特性,是可以让widget改变大小,在2.3时候创建出来的Widget多大就是多大,不能改变,可以把这个去掉 + + ``` + +4. 更新Widget数据的服务 + ```java + public class UpdateWidgetService extends Service { + private Timer timer; + private TimerTask task; + @Override + public IBinder onBind(Intent intent) { + return null; + } + @Override + public void onCreate() { + super.onCreate(); + // 开启定期的任务更新widget. + timer = new Timer(); + task = new TimerTask() { + @Override + public void run() { + AppWidgetManager awm = AppWidgetManager + .getInstance(getApplicationContext()); + ComponentName component = new ComponentName( + getApplicationContext(), MyWidget.class); + RemoteViews views = new RemoteViews(getPackageName(), + R.layout.process_widget); + views.setTextViewText( + R.id.process_count, + "正在运行:" + + ProcessStatusUtils.getProcessCount(getApplicationContext()) + + "个"); + views.setTextViewText( + R.id.process_memory, + "可用内存:" + + Formatter + .formatFileSize( + getApplicationContext(), ProcessStatusUtils.getAvailRAM(getApplicationContext()))); + Intent intent = new Intent(); + intent.setAction("com.itheima.killall"); + //设置一个自定义的广播事件 动作 com.itheima.killall + PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, 0); + views.setOnClickPendingIntent(R.id.btn_clear, pendingIntent); + awm.updateAppWidget(component, views); + } + }; + timer.schedule(task, 1000, 2000); + } + @Override + public void onDestroy() { + timer.cancel(); + task.cancel(); + timer = null; + task = null; + super.onDestroy(); + } + } + ``` + +## Widget的声明周期 + + `Widget`就是一个特殊的广播接收者 + 1. 当界面上第一个`widget`被创建的时候 + 01-14 02:17:14.348: INFO/System.out(1853): onEnabled 当`widget`第一次被创建的时候调用. 非常适合做应用程序的初始化. + 01-14 02:17:14.348: INFO/System.out(1853): onReceive + 01-14 02:17:14.357: INFO/System.out(1853): onUpdate 当有新的`widget`被创建的时候 更新界面的操作. 当时间片到的时候`onupdate()`调用. + 01-14 02:17:14.357: INFO/System.out(1853): onReceive + + 2. 当界面上第二个`widget`被创建的时候 + 01-14 02:18:10.148: INFO/System.out(1853): onUpdate + 01-14 02:18:10.148: INFO/System.out(1853): onReceive + + 3. 再创建新的`widget` + 01-14 02:18:10.148: INFO/System.out(1853): onUpdate + 01-14 02:18:10.148: INFO/System.out(1853): onReceive + + 4. 从界面上移除一个`widget` + 01-14 02:19:11.709: INFO/System.out(1853): onDeleted + 01-14 02:19:11.709: INFO/System.out(1853): onReceive + + 5. 最后一个`widget`被移除 + 01-14 02:19:37.509: INFO/System.out(1853): onDeleted + 01-14 02:19:37.509: INFO/System.out(1853): onReceive + 01-14 02:19:37.509: INFO/System.out(1853): onDisabled 当`widget`从界面上全部移除的时候调用的方法. 非常适合删除临时文件停止后台服务. + 01-14 02:19:37.509: INFO/System.out(1853): onReceive + + 6. `widget`就是一个特殊的广播接受者 当有新的事件产生的是 肯定会调用 `onReceive()`; + +** +注意: 在不同的手机上 widget的生命周期调用方法 可能有细微的不同. +360桌面 go桌面 awt桌面 腾讯桌面 小米桌面 +** + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/Wifi\347\212\266\346\200\201\347\233\221\345\220\254.md" "b/AndroidBasicPart/Wifi\347\212\266\346\200\201\347\233\221\345\220\254.md" similarity index 100% rename from "Android\345\237\272\347\241\200/Wifi\347\212\266\346\200\201\347\233\221\345\220\254.md" rename to "AndroidBasicPart/Wifi\347\212\266\346\200\201\347\233\221\345\220\254.md" diff --git "a/Android\345\237\272\347\241\200/XmlPullParser.md" b/AndroidBasicPart/XmlPullParser.md similarity index 97% rename from "Android\345\237\272\347\241\200/XmlPullParser.md" rename to AndroidBasicPart/XmlPullParser.md index 8936bb3b..69c10ee1 100644 --- "a/Android\345\237\272\347\241\200/XmlPullParser.md" +++ b/AndroidBasicPart/XmlPullParser.md @@ -1,80 +1,80 @@ -XmlPullParser -=== - -```java -public class PersonService { - /** - * 接收一个包含XML文件的输入流, 解析出XML中的Person对象, 装入一个List返回 - * @param in 包含XML数据的输入流 - * @return 包含Person对象的List集合 - */ - public List getPersons(InputStream in) throws Exception { - //1.获取xml文件 - InputStream is = PersonService.class.getClassLoader().getResourceAsStream(); - //2.获取解析器(Android中提供了方便的方法就是使用Android中的工具类Xml) - XmlPullParser parser = Xml.newPullParser(); - //3.解析器解析xml文件 - parser.setInput(in, "UTF-8"); - - List persons = new ArrayList(); - Person p = null; - //4.循环解析 - for (int type = parser.getEventType(); type != XmlPullParser.END_DOCUMENT; type = parser.next()) { // 循环解析 - if (type == XmlPullParser.START_TAG) { // 判断如果遇到开始标签事件 - if ("person".equals(parser.getName())) { // 标签名为person - p = new Person(); // 创建Person对象 - String id = parser.getAttributeValue(0); // 获取属性 - p.setId(Integer.parseInt(id)); // 设置ID - persons.add(p); // 把Person对象装入集合 - } else if ("name".equals(parser.getName())) { // 标签名为name - String name = parser.nextText(); // 获取下一个文本 - p.setName(name); // 设置name - } else if ("age".equals(parser.getName())) { // 标签名为age - String age = parser.nextText(); // 获取下一个文本 - p.setAge(Integer.parseInt(age)); // 设置age - } - } - } - return persons; - } - - /** - *将数据写入到Xml文件中. - *@param out 输出到要被写入数据的Xml文件的输出流//就相当于 OutputStream os = new FileOutputStream("a.xml"); - */ - public void writePersons(List Books, OutputStream out) throws Exception { - - //1.获得XmlSerializer(Xml序列化工具)(通过Android中的工具类Xml得到) - XmlSerializer serializer = Xml.newSerializer(); - //2.设置输出流(明确要将数据写入那个xml文件中) - serializer.setOutput(out, "UTF-8"); - //3.写入开始文档 - serializer.startDocument("UTF-8", true); - //4.开始标签 - serializer.startTag(null, "bookstore"); - //5.循环遍历 - for (Book p : books) { - //6.开始标签 - serializer.startTag(null, "book"); - //7.给这个标签设置属性 - serializer.attribute(null, "id", book.getId().toString()); - //8.子标签 - serializer.startTag(null, "name"); - //9.设置子标签的内容 - serializer.text(book.getName()); - //10.子标签结束 - serializer.endTag(null, "name"); - //11.标签结束 - serializer.endTag(null, "person"); - } - //12.根标签结束 - serializer.endTag(null, "persons"); - //13.文档结束 - serializer.endDocument(); - } -} -``` - ---- -- 邮箱 :charon.chui@gmail.com +XmlPullParser +=== + +```java +public class PersonService { + /** + * 接收一个包含XML文件的输入流, 解析出XML中的Person对象, 装入一个List返回 + * @param in 包含XML数据的输入流 + * @return 包含Person对象的List集合 + */ + public List getPersons(InputStream in) throws Exception { + //1.获取xml文件 + InputStream is = PersonService.class.getClassLoader().getResourceAsStream(); + //2.获取解析器(Android中提供了方便的方法就是使用Android中的工具类Xml) + XmlPullParser parser = Xml.newPullParser(); + //3.解析器解析xml文件 + parser.setInput(in, "UTF-8"); + + List persons = new ArrayList(); + Person p = null; + //4.循环解析 + for (int type = parser.getEventType(); type != XmlPullParser.END_DOCUMENT; type = parser.next()) { // 循环解析 + if (type == XmlPullParser.START_TAG) { // 判断如果遇到开始标签事件 + if ("person".equals(parser.getName())) { // 标签名为person + p = new Person(); // 创建Person对象 + String id = parser.getAttributeValue(0); // 获取属性 + p.setId(Integer.parseInt(id)); // 设置ID + persons.add(p); // 把Person对象装入集合 + } else if ("name".equals(parser.getName())) { // 标签名为name + String name = parser.nextText(); // 获取下一个文本 + p.setName(name); // 设置name + } else if ("age".equals(parser.getName())) { // 标签名为age + String age = parser.nextText(); // 获取下一个文本 + p.setAge(Integer.parseInt(age)); // 设置age + } + } + } + return persons; + } + + /** + *将数据写入到Xml文件中. + *@param out 输出到要被写入数据的Xml文件的输出流//就相当于 OutputStream os = new FileOutputStream("a.xml"); + */ + public void writePersons(List Books, OutputStream out) throws Exception { + + //1.获得XmlSerializer(Xml序列化工具)(通过Android中的工具类Xml得到) + XmlSerializer serializer = Xml.newSerializer(); + //2.设置输出流(明确要将数据写入那个xml文件中) + serializer.setOutput(out, "UTF-8"); + //3.写入开始文档 + serializer.startDocument("UTF-8", true); + //4.开始标签 + serializer.startTag(null, "bookstore"); + //5.循环遍历 + for (Book p : books) { + //6.开始标签 + serializer.startTag(null, "book"); + //7.给这个标签设置属性 + serializer.attribute(null, "id", book.getId().toString()); + //8.子标签 + serializer.startTag(null, "name"); + //9.设置子标签的内容 + serializer.text(book.getName()); + //10.子标签结束 + serializer.endTag(null, "name"); + //11.标签结束 + serializer.endTag(null, "person"); + } + //12.根标签结束 + serializer.endTag(null, "persons"); + //13.文档结束 + serializer.endDocument(); + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" "b/AndroidBasicPart/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" similarity index 100% rename from "Android\345\237\272\347\241\200/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" rename to "AndroidBasicPart/adb logcat\344\275\277\347\224\250\347\256\200\344\273\213.md" diff --git "a/Android\345\237\272\347\241\200/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" "b/AndroidBasicPart/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" rename to "AndroidBasicPart/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" index 87096aa9..1ab65299 100644 --- "a/Android\345\237\272\347\241\200/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" +++ "b/AndroidBasicPart/\344\270\213\346\213\211\345\210\267\346\226\260ListView.md" @@ -1,302 +1,302 @@ -下拉刷新ListView -=== - -PullToRefreshListView ---- - -**原理:** -拉刷新`ListView`无非就是对普通的`List View`添加一个`HeaderView`,然后通过对`ListView onTouchEvent`来获取当前下拉刷新的状态。然后去改变`HeaderView`的状态。 - -1. 自定义`ListView`,在构造方法中去添加`HeaderView` - 通过`ListView.addHeaderView()`去添加`HeaderView`的时候,`HeaderView`会显示在屏幕的最初位置,我们需要它默认的时候是在屏幕的上方,这样默认时是不可见的, - 但是我们下拉`ListView`的时候,它就能够显示出来。这就要通过设置`HeaderView`的`padding`来实现它的隐藏。注意:View的显示最初要经过`Measure`测量宽高, - 我们在构造方法去添加的时候,该View可能并没有被测量,所以在获取`HeaderView`高度的时候会为0,这时候我们要手动的去测量一下`HeaderView`。 - ```java - /** - * 当前状态 - */ - private State mState = State.ORIGNAL; - - public PullToRefreshListView(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - initView(context); - } - - public PullToRefreshListView(Context context, AttributeSet attrs) { - super(context, attrs); - initView(context); - } - - public PullToRefreshListView(Context context) { - super(context); - initView(context); - } - - private void initView(Context context) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - mHeader = inflater.inflate(R.layout.pull_to_refresh_header, null); - iv_arrow = (ImageView) mHeader.findViewById(R.id.iv_arrow); - pb_refresh = (ProgressBar) mHeader.findViewById(R.id.pb_refresh); - tv_title = (TextView) mHeader.findViewById(R.id.tv_title); - tv_time = (TextView) mHeader.findViewById(R.id.tv_time); - - measureHeaderView(mHeader); - - mHeaderHeight = mHeader.getMeasuredHeight(); - // To make header view above the window, so use -mHeaderHeight. - mHeader.setPadding(0, -mHeaderHeight, 0, 0); - - mHeader.invalidate(); - - addHeaderView(mHeader); - } - - /** - * 下拉刷新所有的状态 - */ - public enum State { - ORIGNAL, PULL_TO_REFRESH, REFRESHING, RELEASE_TO_REFRESH; - } - ``` - -2. 重写onTouchEvent,来监听手指的下拉 - ```java - public boolean onTouchEvent(MotionEvent ev) { - int y = (int) ev.getRawY(); - int action = ev.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - //记录手指点下的位置 - downPositionY = y; - break; - case MotionEvent.ACTION_MOVE: - //手指当前滑动的位置 - currentPositionY = y; - //手指移动的距离,由于HeaderView高度固定,但是手指下拉的高度可以最大为屏幕的高度,如手指下拉屏幕高度时,HeaderView会很难看, - // 所以我们让下拉的距离进行一个缩放。 - pullDistance = (currentPositionY - downPositionY) / RATIO; - - if (mState == State.REFRESHING) { - break; - } else if (mState == State.ORIGNAL && pullDistance > 0) { - //如果现在处理起始的状态,并且距离大于0,就说明是下拉了,这时候状态需要变为下拉刷新的状态 - mState = State.PULL_TO_REFRESH; - changeState(); - } else if (mState == State.PULL_TO_REFRESH - && pullDistance > mHeaderHeight) { - //当时为下拉刷新的状态,但是下拉的距离大于HeaderView的高度。这时状态要变为松手即可刷新 - mState = State.RELEASE_TO_REFRESH; - changeState(); - } else if (mState == State.RELEASE_TO_REFRESH) { - //释放刷新时有三种情况,一是我继续下啦,这时候不用管,因为继续下拉还是释放刷新。二是我手指往上移动,此时HeaderView不完全可见, - // 这时候状态要改变为下拉刷新了。三是我手指上移的很厉害,导致HeaderView完全不可见了,这是状态要改变为起始状态。 - if (pullDistance < 0) { - // 如果当时状态为松手刷新,但是这时候我并没有松手,而是直接将手指往上移动,移动回手指最先的位置,这时候状态要变为起始状态。 - mState = State.ORIGNAL; - changeState(); - } else if (pullDistance < mHeaderHeight) { - //手指上移,但是并没有移动到HeaderView完全不可见,这时候要将状态改变为下拉刷新 - mState = State.PULL_TO_REFRESH; - isBack = true; - changeState(); - } - - } - - //在移动的过程中不断的去改版Padding的值,控制其显示的大小 - if (mState != State.REFRESHING) { - mHeader.setPadding(0, (int) (pullDistance - mHeaderHeight), 0, - 0); - } - - break; - case MotionEvent.ACTION_UP: - if (mState == State.REFRESHING) { - //如果当前已经是正在刷新中了,再去下拉就不要处理了 - break; - } else if (mState == State.PULL_TO_REFRESH) { - //显现下拉刷新时,松手了,这时候要将其改为起始状态 - mState = State.ORIGNAL; - } else if (mState == State.RELEASE_TO_REFRESH) { - //松手刷新时松手了,这时候状态要变为正在刷新中。 - mState = State.REFRESHING; - } else { - break; - } - changeState(); - break; - - default: - break; - } - - return super.onTouchEvent(ev); - } - ``` - -3. 上面的写法仍有些问题。就是我们在`onTouchEvent`中`Move`里面对移动的距离进行了判断,但是`ListView`本身就是一个可以上下滑动的组件, - 如果我们直接这样判断,那`ListView`本上上下滑动的功能就被我们给抹去了。 - ```java - @Override - public boolean onTouchEvent(MotionEvent ev) { - int y = (int) ev.getRawY(); - int action = ev.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - downPositionY = y; - break; - case MotionEvent.ACTION_MOVE: - //一定要加上这句话,来判断当前是否可以下拉刷新 - if (!isCanPullToRefresh) { - break; - } - ..... - break; - ``` - 而对于`isCanPullToRefresh`的判断是通过`ListView.setOnScrollListener`去进行判断当前第一个可见条目是不是`ListView`的第一个条目, - 只有第一个条目在最顶端位置的时候才可以进行下拉刷新。 - ```java - super.setOnScrollListener(new OnScrollListener() { - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - - if (firstVisibleItem == 0) { - isCanPullToRefresh = true; - } else { - isCanPullToRefresh = false; - } - } - }); - ``` -4. 提供刷新接口 - - ```java - public interface OnRefreshListener { - abstract void onRefresh(); - } - ``` - -5. ChangeState方法 - - ```java - /** - * Change the state of header view when ListView in different state. - */ - private void changeState() { - //在该方法中对mState进行判断,根据不同状态作出处理,并且去调用刷新方法 - } - - /** - *数据刷新完成后需要调用此方法去恢复装填 - */ - @SuppressWarnings("deprecation") - public void onRefreshComplete() { - mState = State.ORIGNAL; - changeState(); - tv_time.setText(getResources().getString(R.string.update_time) - + new Date().toLocaleString()); - } - ``` - -LoadMoreListView ---- -**原理:** -滑动到底部自动加载更多的`ListView`,无非就是通过对其滑动过程进行监听,一旦滑动到底部的时候我们就去加载新的数据。通过对`ListView`添加`FooterView`, -然后在不同的状态控制它的显示与隐藏。 - -1. 自定义ListView的子类,在构造方法中去添加FooterView. - ```java - public class LoadMoreListView extends ListView { - - public LoadMoreListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - public LoadMoreListView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public LoadMoreListView(Context context) { - super(context); - init(context); - } - - private void init(Context context) { - mFooterView = View.inflate(context, R.layout.load_more_footer, null); - addFooterView(mFooterView); - hideFooterView(); - } - ``` - -2. 监听滑动状态,通过`setOnScrollListener`即可实现对状态的监听。 - ```java - private void init(Context context) { - mFooterView = View.inflate(context, R.layout.load_more_footer, null); - addFooterView(mFooterView); - hideFooterView(); - //为了防止在使用时调用setOnScrollListener会覆盖此时设置的Listener,我们在此使用super.setOnScrollListenr() - super.setOnScrollListener(superOnScrollListener); - } - ``` - -3. 在`ScrollListener`中去判断当前滑动的状态。从而依据不同的状态去控制`FooterView`的显示与隐藏。 - ```java - private OnScrollListener superOnScrollListener = new OnScrollListener() { - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - mCurrentScrollState = scrollState; - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - if (visibleItemCount == totalItemCount) { - //此时说明当前ListView所有的条目比较少,不足一屏 - hideFooterView(); - } else if (!mIsLoading - && (firstVisibleItem + visibleItemCount >= totalItemCount) - && mCurrentScrollState != SCROLL_STATE_IDLE) { - //当第一个可见的条目位置加上当前也所有可见的条目数 等于 ListView当前总的条目数时,就说明已经滑动到了底部,这时候就要去显示FooterView。 - showFooterView(); - mIsLoading = true; - if (mOnLoadMoreListener != null) { - mOnLoadMoreListener.onLoadMore(); - } - } - } - }; - ``` - -4. 提供自动加载更多的接口。 - ```java - public interface OnLoadMoreListener { - /** - * Load more data. - */ - void onLoadMore(); - } - ``` - -在使用的时候,与ListView使用方法相同,只需调用`setOnLoadMoreListener`对其设置`OnLoadMoreListener`即可,然后在数据加载完成后调用`onLoadComplete`方法去恢复状态。 - - -PullAndLoadMoreListView ---- -将下拉刷新和自动加载更多进行整合。就不多说了 - ---- - -- 邮箱 :charon.chui@gmail.com +下拉刷新ListView +=== + +PullToRefreshListView +--- + +**原理:** +拉刷新`ListView`无非就是对普通的`List View`添加一个`HeaderView`,然后通过对`ListView onTouchEvent`来获取当前下拉刷新的状态。然后去改变`HeaderView`的状态。 + +1. 自定义`ListView`,在构造方法中去添加`HeaderView` + 通过`ListView.addHeaderView()`去添加`HeaderView`的时候,`HeaderView`会显示在屏幕的最初位置,我们需要它默认的时候是在屏幕的上方,这样默认时是不可见的, + 但是我们下拉`ListView`的时候,它就能够显示出来。这就要通过设置`HeaderView`的`padding`来实现它的隐藏。注意:View的显示最初要经过`Measure`测量宽高, + 我们在构造方法去添加的时候,该View可能并没有被测量,所以在获取`HeaderView`高度的时候会为0,这时候我们要手动的去测量一下`HeaderView`。 + ```java + /** + * 当前状态 + */ + private State mState = State.ORIGNAL; + + public PullToRefreshListView(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initView(context); + } + + public PullToRefreshListView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + public PullToRefreshListView(Context context) { + super(context); + initView(context); + } + + private void initView(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + mHeader = inflater.inflate(R.layout.pull_to_refresh_header, null); + iv_arrow = (ImageView) mHeader.findViewById(R.id.iv_arrow); + pb_refresh = (ProgressBar) mHeader.findViewById(R.id.pb_refresh); + tv_title = (TextView) mHeader.findViewById(R.id.tv_title); + tv_time = (TextView) mHeader.findViewById(R.id.tv_time); + + measureHeaderView(mHeader); + + mHeaderHeight = mHeader.getMeasuredHeight(); + // To make header view above the window, so use -mHeaderHeight. + mHeader.setPadding(0, -mHeaderHeight, 0, 0); + + mHeader.invalidate(); + + addHeaderView(mHeader); + } + + /** + * 下拉刷新所有的状态 + */ + public enum State { + ORIGNAL, PULL_TO_REFRESH, REFRESHING, RELEASE_TO_REFRESH; + } + ``` + +2. 重写onTouchEvent,来监听手指的下拉 + ```java + public boolean onTouchEvent(MotionEvent ev) { + int y = (int) ev.getRawY(); + int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + //记录手指点下的位置 + downPositionY = y; + break; + case MotionEvent.ACTION_MOVE: + //手指当前滑动的位置 + currentPositionY = y; + //手指移动的距离,由于HeaderView高度固定,但是手指下拉的高度可以最大为屏幕的高度,如手指下拉屏幕高度时,HeaderView会很难看, + // 所以我们让下拉的距离进行一个缩放。 + pullDistance = (currentPositionY - downPositionY) / RATIO; + + if (mState == State.REFRESHING) { + break; + } else if (mState == State.ORIGNAL && pullDistance > 0) { + //如果现在处理起始的状态,并且距离大于0,就说明是下拉了,这时候状态需要变为下拉刷新的状态 + mState = State.PULL_TO_REFRESH; + changeState(); + } else if (mState == State.PULL_TO_REFRESH + && pullDistance > mHeaderHeight) { + //当时为下拉刷新的状态,但是下拉的距离大于HeaderView的高度。这时状态要变为松手即可刷新 + mState = State.RELEASE_TO_REFRESH; + changeState(); + } else if (mState == State.RELEASE_TO_REFRESH) { + //释放刷新时有三种情况,一是我继续下啦,这时候不用管,因为继续下拉还是释放刷新。二是我手指往上移动,此时HeaderView不完全可见, + // 这时候状态要改变为下拉刷新了。三是我手指上移的很厉害,导致HeaderView完全不可见了,这是状态要改变为起始状态。 + if (pullDistance < 0) { + // 如果当时状态为松手刷新,但是这时候我并没有松手,而是直接将手指往上移动,移动回手指最先的位置,这时候状态要变为起始状态。 + mState = State.ORIGNAL; + changeState(); + } else if (pullDistance < mHeaderHeight) { + //手指上移,但是并没有移动到HeaderView完全不可见,这时候要将状态改变为下拉刷新 + mState = State.PULL_TO_REFRESH; + isBack = true; + changeState(); + } + + } + + //在移动的过程中不断的去改版Padding的值,控制其显示的大小 + if (mState != State.REFRESHING) { + mHeader.setPadding(0, (int) (pullDistance - mHeaderHeight), 0, + 0); + } + + break; + case MotionEvent.ACTION_UP: + if (mState == State.REFRESHING) { + //如果当前已经是正在刷新中了,再去下拉就不要处理了 + break; + } else if (mState == State.PULL_TO_REFRESH) { + //显现下拉刷新时,松手了,这时候要将其改为起始状态 + mState = State.ORIGNAL; + } else if (mState == State.RELEASE_TO_REFRESH) { + //松手刷新时松手了,这时候状态要变为正在刷新中。 + mState = State.REFRESHING; + } else { + break; + } + changeState(); + break; + + default: + break; + } + + return super.onTouchEvent(ev); + } + ``` + +3. 上面的写法仍有些问题。就是我们在`onTouchEvent`中`Move`里面对移动的距离进行了判断,但是`ListView`本身就是一个可以上下滑动的组件, + 如果我们直接这样判断,那`ListView`本上上下滑动的功能就被我们给抹去了。 + ```java + @Override + public boolean onTouchEvent(MotionEvent ev) { + int y = (int) ev.getRawY(); + int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + downPositionY = y; + break; + case MotionEvent.ACTION_MOVE: + //一定要加上这句话,来判断当前是否可以下拉刷新 + if (!isCanPullToRefresh) { + break; + } + ..... + break; + ``` + 而对于`isCanPullToRefresh`的判断是通过`ListView.setOnScrollListener`去进行判断当前第一个可见条目是不是`ListView`的第一个条目, + 只有第一个条目在最顶端位置的时候才可以进行下拉刷新。 + ```java + super.setOnScrollListener(new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + + if (firstVisibleItem == 0) { + isCanPullToRefresh = true; + } else { + isCanPullToRefresh = false; + } + } + }); + ``` +4. 提供刷新接口 + + ```java + public interface OnRefreshListener { + abstract void onRefresh(); + } + ``` + +5. ChangeState方法 + + ```java + /** + * Change the state of header view when ListView in different state. + */ + private void changeState() { + //在该方法中对mState进行判断,根据不同状态作出处理,并且去调用刷新方法 + } + + /** + *数据刷新完成后需要调用此方法去恢复装填 + */ + @SuppressWarnings("deprecation") + public void onRefreshComplete() { + mState = State.ORIGNAL; + changeState(); + tv_time.setText(getResources().getString(R.string.update_time) + + new Date().toLocaleString()); + } + ``` + +LoadMoreListView +--- +**原理:** +滑动到底部自动加载更多的`ListView`,无非就是通过对其滑动过程进行监听,一旦滑动到底部的时候我们就去加载新的数据。通过对`ListView`添加`FooterView`, +然后在不同的状态控制它的显示与隐藏。 + +1. 自定义ListView的子类,在构造方法中去添加FooterView. + ```java + public class LoadMoreListView extends ListView { + + public LoadMoreListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public LoadMoreListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public LoadMoreListView(Context context) { + super(context); + init(context); + } + + private void init(Context context) { + mFooterView = View.inflate(context, R.layout.load_more_footer, null); + addFooterView(mFooterView); + hideFooterView(); + } + ``` + +2. 监听滑动状态,通过`setOnScrollListener`即可实现对状态的监听。 + ```java + private void init(Context context) { + mFooterView = View.inflate(context, R.layout.load_more_footer, null); + addFooterView(mFooterView); + hideFooterView(); + //为了防止在使用时调用setOnScrollListener会覆盖此时设置的Listener,我们在此使用super.setOnScrollListenr() + super.setOnScrollListener(superOnScrollListener); + } + ``` + +3. 在`ScrollListener`中去判断当前滑动的状态。从而依据不同的状态去控制`FooterView`的显示与隐藏。 + ```java + private OnScrollListener superOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mCurrentScrollState = scrollState; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (visibleItemCount == totalItemCount) { + //此时说明当前ListView所有的条目比较少,不足一屏 + hideFooterView(); + } else if (!mIsLoading + && (firstVisibleItem + visibleItemCount >= totalItemCount) + && mCurrentScrollState != SCROLL_STATE_IDLE) { + //当第一个可见的条目位置加上当前也所有可见的条目数 等于 ListView当前总的条目数时,就说明已经滑动到了底部,这时候就要去显示FooterView。 + showFooterView(); + mIsLoading = true; + if (mOnLoadMoreListener != null) { + mOnLoadMoreListener.onLoadMore(); + } + } + } + }; + ``` + +4. 提供自动加载更多的接口。 + ```java + public interface OnLoadMoreListener { + /** + * Load more data. + */ + void onLoadMore(); + } + ``` + +在使用的时候,与ListView使用方法相同,只需调用`setOnLoadMoreListener`对其设置`OnLoadMoreListener`即可,然后在数据加载完成后调用`onLoadComplete`方法去恢复状态。 + + +PullAndLoadMoreListView +--- +将下拉刷新和自动加载更多进行整合。就不多说了 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\344\273\243\347\240\201\346\267\267\346\267\206.md" "b/AndroidBasicPart/\344\273\243\347\240\201\346\267\267\346\267\206.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\344\273\243\347\240\201\346\267\267\346\267\206.md" rename to "AndroidBasicPart/\344\273\243\347\240\201\346\267\267\346\267\206.md" index 194ba7f9..a8771465 100644 --- "a/Android\345\237\272\347\241\200/\344\273\243\347\240\201\346\267\267\346\267\206.md" +++ "b/AndroidBasicPart/\344\273\243\347\240\201\346\267\267\346\267\206.md" @@ -1,113 +1,113 @@ -代码混淆 -=== - -混淆器(ProGuard) ---- - -混淆器通过删除从未用过的代码和使用晦涩名字重命名类、字段和方法,对代码进行压缩,优化和混淆。 - -1. 修改project.properties - ```xml - # This file is automatically generated by Android Tools. - # Do not modify this file -- YOUR CHANGES WILL BE ERASED! - # - # This file must be checked in Version Control Systems. - # - # To customize properties used by the Ant build system edit - # "ant.properties", and override values to adapt the script to your - # project structure. - # - # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): - #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - - # Project target. - target=android-19 - ``` - 将proguard.config前面的注释去掉 - -2. 修改proguard-project.txt - ```xml - # To enable ProGuard in your project, edit project.properties - # to define the proguard.config property as described in that file. - # - # Add project specific ProGuard rules here. - # By default, the flags in this file are appended to flags specified - # in ${sdk.dir}/tools/proguard/proguard-android.txt - # You can edit the include path and order by changing the ProGuard - # include property in project.properties. - # - # For more details, see - # http://developer.android.com/guide/developing/tools/proguard.html - - # Add any project specific keep options here: - - # If your project uses WebView with JS, uncomment the following - # and specify the fully qualified class name to the JavaScript interface - # class: - #-keepclassmembers class fqcn.of.javascript.interface.for.webview { - # public *; - #} - ``` - 如果在程序中使用了第三方的`jar`包,在混淆后导致出错,这时我们需要在proguard-project.txt中去进行相应的配置, - 来让其在混淆时不要混淆相应的jar包。对改配置文件中的相关配置解释如下: - ```java - -keep public class * extends android.app.Activity  【不进行混淆类名的类,保持其原类名和包名】 - - -keep public abstract interface com.asqw.android.Listener{ - public protected ; 【所有public protected的方法名不进行混淆】 - } - -keep public class com.asqw.android{ - public void Start(java.lang.String); 【对该方法不进行混淆】 - } - -keepclasseswithmembernames class * { 【对所有类的native方法名不进行混淆】 - native ; - } - -keepclasseswithmembers class * { 【对所有类的指定方法的方法名不进行混淆】 - public (android.content.Context, android.util.AttributeSet); - } - -keepclassmembers class * extends android.app.Activity {【对所有类的指定方法的方法名不进行混淆】 - public void *(android.view.View); - } - -keepclassmembers enum * {【对枚举类型enum的所有类的以下指定方法的方法名不进行混淆】 - public static **[] values(); - public static ** valueOf(java.lang.String); - } - -keep class * implements android.os.Parcelable {【对实现了Parcelable接口的所有类的类名不进行混淆,对其成员变量为Parcelable$Creator类型的成员变量的变量名不进行混淆】 - public static final android.os.Parcelable$Creator *; - } - -keepclasseswithmembers class org.jboss.netty.util.internal.LinkedTransferQueue {【对指定类的指定变量的变量名不进行混淆】 - volatile transient org.jboss.netty.util.internal.LinkedTransferQueue$Node head; - volatile transient org.jboss.netty.util.internal.LinkedTransferQueue$Node tail; - volatile transient int sweepVotes; - - } - -keep public class com.unionpay.** {*; }【对com.unionpay包下所有的类都不进行混淆,即不混淆类名,也不混淆方法名和变量名】 - ``` - - 经过上面这两部之后反编译后就能混淆了,但是四大组件还在,为什么四大组件还在呢,因为四大组件是在清单文件中进行配置的, - 如果混淆后就不能根据清单文件的配置去寻找了。 - 如果对于一些自己的代码中要想提供出来让别人通过反射调用的方法时,我们不想让部分代码被混淆,或者是我们使用别人提供的第三方jar包, - 因为第三方的jar包一般都是已经混淆过的,我们要是再混淆就会报错了,所以我们要保证这些内容不用混淆,这里我们只需修改这个文件,然后加上后面的一句话, - 他就不会混淆我们给出的内容 - ```xml - -keepattributes *Annotation* - -keep public class * extends android.app.Activity - -keep public class * extends android.app.Application - -keep public class * extends android.app.Service - -keep public class * extends android.content.BroadcastReceiver - -keep public class * extends android.content.ContentProvider - -keep public class * extends android.app.backup.BackupAgent - -keep public class * extends android.preference.Preference - -keep public class * extends android.support.v4.app.Fragment - -keep public class * extends android.app.Fragment - -keep public class com.android.vending.licensing.ILicensingService - -keep class net.youmi.android.** { - *; - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - +代码混淆 +=== + +混淆器(ProGuard) +--- + +混淆器通过删除从未用过的代码和使用晦涩名字重命名类、字段和方法,对代码进行压缩,优化和混淆。 + +1. 修改project.properties + ```xml + # This file is automatically generated by Android Tools. + # Do not modify this file -- YOUR CHANGES WILL BE ERASED! + # + # This file must be checked in Version Control Systems. + # + # To customize properties used by the Ant build system edit + # "ant.properties", and override values to adapt the script to your + # project structure. + # + # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): + #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + + # Project target. + target=android-19 + ``` + 将proguard.config前面的注释去掉 + +2. 修改proguard-project.txt + ```xml + # To enable ProGuard in your project, edit project.properties + # to define the proguard.config property as described in that file. + # + # Add project specific ProGuard rules here. + # By default, the flags in this file are appended to flags specified + # in ${sdk.dir}/tools/proguard/proguard-android.txt + # You can edit the include path and order by changing the ProGuard + # include property in project.properties. + # + # For more details, see + # http://developer.android.com/guide/developing/tools/proguard.html + + # Add any project specific keep options here: + + # If your project uses WebView with JS, uncomment the following + # and specify the fully qualified class name to the JavaScript interface + # class: + #-keepclassmembers class fqcn.of.javascript.interface.for.webview { + # public *; + #} + ``` + 如果在程序中使用了第三方的`jar`包,在混淆后导致出错,这时我们需要在proguard-project.txt中去进行相应的配置, + 来让其在混淆时不要混淆相应的jar包。对改配置文件中的相关配置解释如下: + ```java + -keep public class * extends android.app.Activity  【不进行混淆类名的类,保持其原类名和包名】 + + -keep public abstract interface com.asqw.android.Listener{ + public protected ; 【所有public protected的方法名不进行混淆】 + } + -keep public class com.asqw.android{ + public void Start(java.lang.String); 【对该方法不进行混淆】 + } + -keepclasseswithmembernames class * { 【对所有类的native方法名不进行混淆】 + native ; + } + -keepclasseswithmembers class * { 【对所有类的指定方法的方法名不进行混淆】 + public (android.content.Context, android.util.AttributeSet); + } + -keepclassmembers class * extends android.app.Activity {【对所有类的指定方法的方法名不进行混淆】 + public void *(android.view.View); + } + -keepclassmembers enum * {【对枚举类型enum的所有类的以下指定方法的方法名不进行混淆】 + public static **[] values(); + public static ** valueOf(java.lang.String); + } + -keep class * implements android.os.Parcelable {【对实现了Parcelable接口的所有类的类名不进行混淆,对其成员变量为Parcelable$Creator类型的成员变量的变量名不进行混淆】 + public static final android.os.Parcelable$Creator *; + } + -keepclasseswithmembers class org.jboss.netty.util.internal.LinkedTransferQueue {【对指定类的指定变量的变量名不进行混淆】 + volatile transient org.jboss.netty.util.internal.LinkedTransferQueue$Node head; + volatile transient org.jboss.netty.util.internal.LinkedTransferQueue$Node tail; + volatile transient int sweepVotes; + + } + -keep public class com.unionpay.** {*; }【对com.unionpay包下所有的类都不进行混淆,即不混淆类名,也不混淆方法名和变量名】 + ``` + + 经过上面这两部之后反编译后就能混淆了,但是四大组件还在,为什么四大组件还在呢,因为四大组件是在清单文件中进行配置的, + 如果混淆后就不能根据清单文件的配置去寻找了。 + 如果对于一些自己的代码中要想提供出来让别人通过反射调用的方法时,我们不想让部分代码被混淆,或者是我们使用别人提供的第三方jar包, + 因为第三方的jar包一般都是已经混淆过的,我们要是再混淆就会报错了,所以我们要保证这些内容不用混淆,这里我们只需修改这个文件,然后加上后面的一句话, + 他就不会混淆我们给出的内容 + ```xml + -keepattributes *Annotation* + -keep public class * extends android.app.Activity + -keep public class * extends android.app.Application + -keep public class * extends android.app.Service + -keep public class * extends android.content.BroadcastReceiver + -keep public class * extends android.content.ContentProvider + -keep public class * extends android.app.backup.BackupAgent + -keep public class * extends android.preference.Preference + -keep public class * extends android.support.v4.app.Fragment + -keep public class * extends android.app.Fragment + -keep public class com.android.vending.licensing.ILicensingService + -keep class net.youmi.android.** { + *; + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/Android\345\237\272\347\241\200/\344\273\273\345\212\241\347\256\241\347\220\206\345\231\250(ActivityManager).md" "b/AndroidBasicPart/\344\273\273\345\212\241\347\256\241\347\220\206\345\231\250(ActivityManager).md" similarity index 100% rename from "Android\345\237\272\347\241\200/\344\273\273\345\212\241\347\256\241\347\220\206\345\231\250(ActivityManager).md" rename to "AndroidBasicPart/\344\273\273\345\212\241\347\256\241\347\220\206\345\231\250(ActivityManager).md" diff --git "a/Android\345\237\272\347\241\200/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" "b/AndroidBasicPart/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" rename to "AndroidBasicPart/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" index 794f234b..1751bc08 100644 --- "a/Android\345\237\272\347\241\200/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" +++ "b/AndroidBasicPart/\344\277\256\346\224\271\347\263\273\347\273\237\347\273\204\344\273\266\346\240\267\345\274\217.md" @@ -1,155 +1,155 @@ -修改系统组件样式 -=== - -系统所有组件的样式声明都在`data-res-values-styles.xml`中,如果我们想要修改某个系统组件的样式只需要拷贝它的样式到本地后修改一下就行了。 - -- 自定义ProgressBar样式 - 1. 去系统的styles.xml中搜寻ProgressBar的样式 - ```xml - - ``` - 2. 看到有一个属性引用了@android:drawable/progress_medium_white,内容如下 - ```xml - - //这样报错了,是因为framesCount和framesDuration这两个属性在高版本才有,在2.2没有所以把这两个属性给去了就可以了 - 这个文件定义了一个旋转动画的背景,它里面引用了drawable中的spinner_white_48这个图片 - ``` - - 3. 自定义一个样式继承Widget.ProgeessBar,然后重写android:indeterminateDrawable让它使用我们自己的资源 - ```xml - - //那么这个drawable下的progress_medium_white.xml就是将系统的拷贝到我们自己的程序中 - - 4. 拷贝Android中的progress_medium_white.xml到自己的系统中 - 5. 将里面的 android:drawable="@drawable/spinner_white_48"给修改成自己的图片 - 6. 在xml文件中使用我们自定义的ProgressBar - ```xml - - ``` - -- 自定义进度条 - 1. 系统所有的组件都在D:\android-sdk-windows\platforms\android-8\data\res\values\styles.xml - ```xml - - ``` - 2. 引用了一个drawable资源@android:drawable/progress_horizontal,打开后内容如下 - ```xml - Layer-list代表的是一个图层的列表 - - - //背景,这里它是通过图形资源shape的方式定义的背景 - - - - - - - - - - - - - - - - //进度 - - - - - - - - - - ``` - - 3. 自定义一个样式继承系统的Widget.ProgressBar.Horizontal,然后重写android:progressDrawable属性,让其指向我们的样式 - ```xml - - ``` - - 4. scrubber_progress_horizontal_holo_dark.xml变成我们自己的图片 - ```xml - - - //好看的图片 - - - - - //好看的图片 - - - - ``` - - 5. SeekBar使用自定义样式 - ```xml - - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +修改系统组件样式 +=== + +系统所有组件的样式声明都在`data-res-values-styles.xml`中,如果我们想要修改某个系统组件的样式只需要拷贝它的样式到本地后修改一下就行了。 + +- 自定义ProgressBar样式 + 1. 去系统的styles.xml中搜寻ProgressBar的样式 + ```xml + + ``` + 2. 看到有一个属性引用了@android:drawable/progress_medium_white,内容如下 + ```xml + + //这样报错了,是因为framesCount和framesDuration这两个属性在高版本才有,在2.2没有所以把这两个属性给去了就可以了 + 这个文件定义了一个旋转动画的背景,它里面引用了drawable中的spinner_white_48这个图片 + ``` + + 3. 自定义一个样式继承Widget.ProgeessBar,然后重写android:indeterminateDrawable让它使用我们自己的资源 + ```xml + + //那么这个drawable下的progress_medium_white.xml就是将系统的拷贝到我们自己的程序中 + + 4. 拷贝Android中的progress_medium_white.xml到自己的系统中 + 5. 将里面的 android:drawable="@drawable/spinner_white_48"给修改成自己的图片 + 6. 在xml文件中使用我们自定义的ProgressBar + ```xml + + ``` + +- 自定义进度条 + 1. 系统所有的组件都在D:\android-sdk-windows\platforms\android-8\data\res\values\styles.xml + ```xml + + ``` + 2. 引用了一个drawable资源@android:drawable/progress_horizontal,打开后内容如下 + ```xml + Layer-list代表的是一个图层的列表 + + + //背景,这里它是通过图形资源shape的方式定义的背景 + + + + + + + + + + + + + + + + //进度 + + + + + + + + + + ``` + + 3. 自定义一个样式继承系统的Widget.ProgressBar.Horizontal,然后重写android:progressDrawable属性,让其指向我们的样式 + ```xml + + ``` + + 4. scrubber_progress_horizontal_holo_dark.xml变成我们自己的图片 + ```xml + + + //好看的图片 + + + + + //好看的图片 + + + + ``` + + 5. SeekBar使用自定义样式 + ```xml + + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" "b/AndroidBasicPart/\345\206\205\345\255\230\346\263\204\346\274\217.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" rename to "AndroidBasicPart/\345\206\205\345\255\230\346\263\204\346\274\217.md" index 01063c87..d02c339f 100644 --- "a/Android\345\237\272\347\241\200/\345\206\205\345\255\230\346\263\204\346\274\217.md" +++ "b/AndroidBasicPart/\345\206\205\345\255\230\346\263\204\346\274\217.md" @@ -1,259 +1,259 @@ -内存泄露 -=== - - -##定义 -内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。 - -##原因 -长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收, -这就是`java`中内存泄露的发生场景。 - -##危害 -只有一个,那就是虚拟机占用内存过高,导致`OOM`(内存溢出)。 -对于`Android`应用来说,就是你的用户打开一个`Activity`,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制就会出现`FC`。 - - -先来说一下`Java`程序运行时的内存分配策略: - -- 静态内存:存放静态数据,这块内存是在编译时就已经分配好的,在程序整个运行期间都存在。 -- 栈内存:程序执行时,局部变量的创建存储区,执行结束后将自动释放。(当时学习java时花的内存分配图,现在都生疏了- -!) -- 堆内存:存储一些new出来的内存,也叫动态内存分配,这部分内存在不使用时将会有`Java`垃圾回收器来负责回收。 - -举一个典型的内存泄漏的例子: -```java -Vector v = new Vector(100); -for (int i = 1; i < 100,; i++) { - Object o = new Object(); - v.add(o); - o = null; -} -``` -在这个例子中,我们循环申请了`Object`对象,并将所申请的对象放入一个集合中,如果我们仅仅释放引用本身,那么`Vector`仍然引用 -该对象,所以这个对象对`GC`来说是不可回收的。因此,如果对象假如`Vector`后,还必须从`Vector`中删除,最简单的方法就是将 -`Vector`对象设置为`null`. - -`Java`和`C++`一个很大的区别就是`Java`有垃圾回收`GC(Garbage Collection)`自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。 -`Java`中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。 -`GC`会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么`GC`就认为这块内存不再使用了。 - -`Android`手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成`OutOfMemoryError` -`OutOfMemoryError`主要由以下几种情况造成: - -1. 数据库`Cursor`没关。 - 当我们操作完数据库后,一定要调用`close()`释放资源。 - -2. 构造`Adapter`没有使用缓存`ContentView`。 - ```java - @Override - public View getView(int position, View convertView, ViewGroup parent) { - ViewHolder vHolder = null; - //如果convertView对象为空则创建新对象,不为空则复用 - if (convertView == null) { - convertView = inflater.inflate(..., null); - // 创建 ViewHodler 对象 - vHolder = new ViewHolder(); - vHolder.img= (ImageView) convertView.findViewById(...); - vHolder.tv= (TextView) convertView - .findViewById(...); - // 将ViewHodler保存到Tag中 - convertView.setTag(vHolder); - } else { - //当convertView不为空时,通过getTag()得到View - vHolder = (ViewHolder) convertView.getTag(); - } - // 给对象赋值,修改显示的值 - vHolder.img.setImageBitmap(...); - vHolder.tv.setText(...); - return convertView; - } - - static class ViewHolder { - TextView tv; - ImageView img; - } - ``` - -3. 未取消注册广播接收者 - `registerReceiver()`和`unregisterReceiver()`要成对出现,通常需要在`Activity`的`onDestory()`方法去取消注册广播接收者。 - -4. `IO`流未关闭 - 注意用完后及时关闭 - -5. `Bitmap`使用后未调用`recycle()`。 - -6. `Context`泄漏 - 这是一个很隐晦的`OutOfMemoryError`的情况。先看一个Android官网提供的例子: - - ```java - private static Drawable sBackground; - @Override - protected void onCreate(Bundle state) { - super.onCreate(state); - TextView label = new TextView(this); - label.setText("Leaks are bad"); - if (sBackground == null) { - sBackground = getDrawable(R.drawable.large_bitmap); - } - label.setBackgroundDrawable(sBackground); - setContentView(label); - } - ``` - 这段代码效率很快,但同时又是极其错误的: - 我们看一下`setBackgroundDrawable(Drawable background)`的源码: - ```java - public void setBackgroundDrawable(Drawable background) { - ... - background.setCallback(this); - } - ``` - 有`background.setCallback(this);`方法,也就是说`Drawable`拥有`TextView`的引用,而`TextView`又拥有`Activity`*(Context类型)*的引用, - 因为`sBackground`为`static`的,即使`Activity`被销毁,但是`sBackground`的生命周期还没走完,所以内存仍然不会被释放。这样就会有内存泄露了。 - 对,这样想是对的,但是我们看一下`setCallback`的源码: - ```java - public final void setCallback(Callback cb) { - mCallback = new WeakReference(cb); - } - ``` - 我们会发现里面使用了`WeakReference`,所以不会存在内存泄露了,但是官网当时提供的例子明明说有泄露,这是因为在3.0之后, - 修复了这个内存泄露的问题,在3.0之前`setCallback`,方法是没有使用`WeakReference`的,所以这种泄露的情况在3.0之前会发生,3.0之后已经被修复。 - -7. 线程 - 线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。 - ```java - public class MyActivity extends Activity { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main); - new MyThread().start(); - } - private class MyThread extends Thread{ - @Override - public void run() { - super.run(); - //耗时的操作 - } - } - } - ``` - 假设`MyThread`的`run`函数是一个很费时的操作,当调用`finish`的时候`Activity`会销毁掉吗? - 事实上由于我们的线程是`Activity`的内部类,所以`MyThread`中保存了`Activity`的一个引用,当`MyThread`的`run`函数没有结束时,`MyThread`是不会被销毁的, - 因此它所引用的`Activity`也不会被销毁,因此就出现了内存泄露的问题。 - -11. 单例造成的内存泄漏 - 由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏,比如: - ```java - public class AppManager { - private static AppManager instance; - private Context context; - private AppManager(Context context) { - this.context = context; - } - public static AppManager getInstance(Context context) { - if (instance != null) { - instance = new AppManager(context); - } - return instance; - } - } - ``` - 这里如果传入的是`Activity`的`Context`,当该`Context`的`Activity`退出后,由于其被单例对象引用,所以会导致`Activity`无法被回收,就造成内存泄漏。 - -8. 尽量使用`ApplicationContext` - **`Context`引用的生命周期超过它本身的生命周期,也会导致`Context`泄漏**。 - 所以如果打算保存一个长时间的对象时尽量使用`Application`这种`Context`类型。 - 例如: - ```java - mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE); - 改成: - mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE); - ``` - 按道理来说这种系统服务是不会有问题,但是有些厂商在修改的时候,可能会导致`Context`无法被及时释放。 - -9. Handler的使用,在Activity退出的时候注意移除(尤其是循环的时候) - ```java - public class ThreadDemo extends Activity { - private static final String TAG = "ThreadDemo"; - private int count = 0; - private Handler mHandler = new Handler(); - - private Runnable mRunnable = new Runnable() { - - public void run() { - //为了方便 查看,我们用Log打印出来 - Log.e(TAG, Thread.currentThread().getName() + " " +count); - //每2秒执行一次 - mHandler.postDelayed(mRunnable, 2000); - } - }; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main); - //通过Handler启动线程 - mHandler.post(mRunnable); - } - } - ``` - 这样在也会引发内存泄露。我们应该在`onDestory`方法中移除`Handler`,代码如下: - ```java - @Override - protected void onDestroy() { - super.onDestroy(); - mHandler.removeCallbacks(mRunnable); - } - ``` -10. 由上面的`Handler`可以引伸出来的匿名内部类、非静态内部类和异步现成导致的内存泄漏。 - 下面看一个非静态内部类创建静态实例导致的内存泄漏 - ```java - public class MainActivity extends AppCompatActivity { - private static TestResource mResource = null; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - if(mManager == null){ - mManager = new TestResource(); - } - //... - } - class TestResource { - //... - } - } - ``` - 因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态实例,该实例的声明周期与应用的一样长, - 这就导致了静态实例一直会持有`Activity`的引用而造成内存泄漏。 - 下面再看一个匿名内部类和异步现成的现象: - ```java - public class MainActivity extends Activity { - ... - Runnable ref1 = new MyRunable(); - Runnable ref2 = new Runnable() { - @Override - public void run() { - - } - }; - ... - } - ``` - 上面的离职中`ref1`对象是没问题的,但是`ref2`这个匿名类的实现对象中有外部类的引用,如果此时线程的生命周期与`Activity`的不一致时就会造成了泄漏。 - -10. 集合类泄漏 - 集合类中如果只有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性),那么没有 - 响应的删除机制,很可能导致集合所占用的内存只增不减。 - - -总结一下避免`Contex`t泄漏应该注意的问题: - -- 使用`getApplicationContext()`类型。 -- 注意对`Context`的引用不要超过它本身的生命周期。 -- 慎重的使用`static`关键字。 -- `Activity`里如果有线程或`Handler`时,一定要在`onDestroy()`里及时停掉。 - ---- - -- 邮箱 :charon.chui@gmail.com +内存泄露 +=== + + +##定义 +内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。 + +##原因 +长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收, +这就是`java`中内存泄露的发生场景。 + +##危害 +只有一个,那就是虚拟机占用内存过高,导致`OOM`(内存溢出)。 +对于`Android`应用来说,就是你的用户打开一个`Activity`,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制就会出现`FC`。 + + +先来说一下`Java`程序运行时的内存分配策略: + +- 静态内存:存放静态数据,这块内存是在编译时就已经分配好的,在程序整个运行期间都存在。 +- 栈内存:程序执行时,局部变量的创建存储区,执行结束后将自动释放。(当时学习java时花的内存分配图,现在都生疏了- -!) +- 堆内存:存储一些new出来的内存,也叫动态内存分配,这部分内存在不使用时将会有`Java`垃圾回收器来负责回收。 + +举一个典型的内存泄漏的例子: +```java +Vector v = new Vector(100); +for (int i = 1; i < 100,; i++) { + Object o = new Object(); + v.add(o); + o = null; +} +``` +在这个例子中,我们循环申请了`Object`对象,并将所申请的对象放入一个集合中,如果我们仅仅释放引用本身,那么`Vector`仍然引用 +该对象,所以这个对象对`GC`来说是不可回收的。因此,如果对象假如`Vector`后,还必须从`Vector`中删除,最简单的方法就是将 +`Vector`对象设置为`null`. + +`Java`和`C++`一个很大的区别就是`Java`有垃圾回收`GC(Garbage Collection)`自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。 +`Java`中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。 +`GC`会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么`GC`就认为这块内存不再使用了。 + +`Android`手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成`OutOfMemoryError` +`OutOfMemoryError`主要由以下几种情况造成: + +1. 数据库`Cursor`没关。 + 当我们操作完数据库后,一定要调用`close()`释放资源。 + +2. 构造`Adapter`没有使用缓存`ContentView`。 + ```java + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder vHolder = null; + //如果convertView对象为空则创建新对象,不为空则复用 + if (convertView == null) { + convertView = inflater.inflate(..., null); + // 创建 ViewHodler 对象 + vHolder = new ViewHolder(); + vHolder.img= (ImageView) convertView.findViewById(...); + vHolder.tv= (TextView) convertView + .findViewById(...); + // 将ViewHodler保存到Tag中 + convertView.setTag(vHolder); + } else { + //当convertView不为空时,通过getTag()得到View + vHolder = (ViewHolder) convertView.getTag(); + } + // 给对象赋值,修改显示的值 + vHolder.img.setImageBitmap(...); + vHolder.tv.setText(...); + return convertView; + } + + static class ViewHolder { + TextView tv; + ImageView img; + } + ``` + +3. 未取消注册广播接收者 + `registerReceiver()`和`unregisterReceiver()`要成对出现,通常需要在`Activity`的`onDestory()`方法去取消注册广播接收者。 + +4. `IO`流未关闭 + 注意用完后及时关闭 + +5. `Bitmap`使用后未调用`recycle()`。 + +6. `Context`泄漏 + 这是一个很隐晦的`OutOfMemoryError`的情况。先看一个Android官网提供的例子: + + ```java + private static Drawable sBackground; + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + TextView label = new TextView(this); + label.setText("Leaks are bad"); + if (sBackground == null) { + sBackground = getDrawable(R.drawable.large_bitmap); + } + label.setBackgroundDrawable(sBackground); + setContentView(label); + } + ``` + 这段代码效率很快,但同时又是极其错误的: + 我们看一下`setBackgroundDrawable(Drawable background)`的源码: + ```java + public void setBackgroundDrawable(Drawable background) { + ... + background.setCallback(this); + } + ``` + 有`background.setCallback(this);`方法,也就是说`Drawable`拥有`TextView`的引用,而`TextView`又拥有`Activity`*(Context类型)*的引用, + 因为`sBackground`为`static`的,即使`Activity`被销毁,但是`sBackground`的生命周期还没走完,所以内存仍然不会被释放。这样就会有内存泄露了。 + 对,这样想是对的,但是我们看一下`setCallback`的源码: + ```java + public final void setCallback(Callback cb) { + mCallback = new WeakReference(cb); + } + ``` + 我们会发现里面使用了`WeakReference`,所以不会存在内存泄露了,但是官网当时提供的例子明明说有泄露,这是因为在3.0之后, + 修复了这个内存泄露的问题,在3.0之前`setCallback`,方法是没有使用`WeakReference`的,所以这种泄露的情况在3.0之前会发生,3.0之后已经被修复。 + +7. 线程 + 线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。 + ```java + public class MyActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + new MyThread().start(); + } + private class MyThread extends Thread{ + @Override + public void run() { + super.run(); + //耗时的操作 + } + } + } + ``` + 假设`MyThread`的`run`函数是一个很费时的操作,当调用`finish`的时候`Activity`会销毁掉吗? + 事实上由于我们的线程是`Activity`的内部类,所以`MyThread`中保存了`Activity`的一个引用,当`MyThread`的`run`函数没有结束时,`MyThread`是不会被销毁的, + 因此它所引用的`Activity`也不会被销毁,因此就出现了内存泄露的问题。 + +11. 单例造成的内存泄漏 + 由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏,比如: + ```java + public class AppManager { + private static AppManager instance; + private Context context; + private AppManager(Context context) { + this.context = context; + } + public static AppManager getInstance(Context context) { + if (instance != null) { + instance = new AppManager(context); + } + return instance; + } + } + ``` + 这里如果传入的是`Activity`的`Context`,当该`Context`的`Activity`退出后,由于其被单例对象引用,所以会导致`Activity`无法被回收,就造成内存泄漏。 + +8. 尽量使用`ApplicationContext` + **`Context`引用的生命周期超过它本身的生命周期,也会导致`Context`泄漏**。 + 所以如果打算保存一个长时间的对象时尽量使用`Application`这种`Context`类型。 + 例如: + ```java + mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE); + 改成: + mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE); + ``` + 按道理来说这种系统服务是不会有问题,但是有些厂商在修改的时候,可能会导致`Context`无法被及时释放。 + +9. Handler的使用,在Activity退出的时候注意移除(尤其是循环的时候) + ```java + public class ThreadDemo extends Activity { + private static final String TAG = "ThreadDemo"; + private int count = 0; + private Handler mHandler = new Handler(); + + private Runnable mRunnable = new Runnable() { + + public void run() { + //为了方便 查看,我们用Log打印出来 + Log.e(TAG, Thread.currentThread().getName() + " " +count); + //每2秒执行一次 + mHandler.postDelayed(mRunnable, 2000); + } + }; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + //通过Handler启动线程 + mHandler.post(mRunnable); + } + } + ``` + 这样在也会引发内存泄露。我们应该在`onDestory`方法中移除`Handler`,代码如下: + ```java + @Override + protected void onDestroy() { + super.onDestroy(); + mHandler.removeCallbacks(mRunnable); + } + ``` +10. 由上面的`Handler`可以引伸出来的匿名内部类、非静态内部类和异步现成导致的内存泄漏。 + 下面看一个非静态内部类创建静态实例导致的内存泄漏 + ```java + public class MainActivity extends AppCompatActivity { + private static TestResource mResource = null; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if(mManager == null){ + mManager = new TestResource(); + } + //... + } + class TestResource { + //... + } + } + ``` + 因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态实例,该实例的声明周期与应用的一样长, + 这就导致了静态实例一直会持有`Activity`的引用而造成内存泄漏。 + 下面再看一个匿名内部类和异步现成的现象: + ```java + public class MainActivity extends Activity { + ... + Runnable ref1 = new MyRunable(); + Runnable ref2 = new Runnable() { + @Override + public void run() { + + } + }; + ... + } + ``` + 上面的离职中`ref1`对象是没问题的,但是`ref2`这个匿名类的实现对象中有外部类的引用,如果此时线程的生命周期与`Activity`的不一致时就会造成了泄漏。 + +10. 集合类泄漏 + 集合类中如果只有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性),那么没有 + 响应的删除机制,很可能导致集合所占用的内存只增不减。 + + +总结一下避免`Contex`t泄漏应该注意的问题: + +- 使用`getApplicationContext()`类型。 +- 注意对`Context`的引用不要超过它本身的生命周期。 +- 慎重的使用`static`关键字。 +- `Activity`里如果有线程或`Handler`时,一定要在`onDestroy()`里及时停掉。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213\346\226\255\347\202\271\344\270\213\350\275\275.md" "b/AndroidBasicPart/\345\244\232\347\272\277\347\250\213\346\226\255\347\202\271\344\270\213\350\275\275.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213\346\226\255\347\202\271\344\270\213\350\275\275.md" rename to "AndroidBasicPart/\345\244\232\347\272\277\347\250\213\346\226\255\347\202\271\344\270\213\350\275\275.md" diff --git "a/Android\345\237\272\347\241\200/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" "b/AndroidBasicPart/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" similarity index 96% rename from "Android\345\237\272\347\241\200/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" rename to "AndroidBasicPart/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" index 8a231008..10f821c1 100644 --- "a/Android\345\237\272\347\241\200/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" +++ "b/AndroidBasicPart/\345\256\211\345\205\250\351\200\200\345\207\272\345\272\224\347\224\250\347\250\213\345\272\217.md" @@ -1,106 +1,106 @@ -安全退出应用程序 -=== - -1. 杀死进程。 - 这种方法是没有效果的只能杀死当前的`Activity`无法关闭程序,在1.5的时候有用,谷歌设计的时候规定程序不能自杀 - `android.os.Process.killProcess(android.os.Process.myPid())`. -2. 终止当前正在运行的Java虚拟机,导致程序终止. - 这种方法也是没有效果的,因为`Android`用的是`dalvik`虚拟机 - `System.exit(0);` -3. 强制关闭与该包有关联的一切执行 - 这种方法只能杀死别人,无法杀死自己 - ```java - ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - manager.restartPackage(getPackageName()); - 同时需要申请权限 - - ``` - -既然上面介绍的三种方法都没有效果,那么怎么才能退出应用程序呢? -就是自定义一个`Application`,在该`Application`中去定义一个`List`的集合来记录中每一个开启的`Activity`,在退出的时候去遍历这个`List`集合, -然后挨个的进行`mActivity.finish()`方法,这要求在每开启一个`Activity`的时候都加入到`List`集合中,并且在`Activity`退出的时候从`List`集合中将其移除。 -```java -public class Activity01 extends Activity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main); - MyApp myApp = (MyApp) getApplication(); - myApp.activies.add(this); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - MyApp myApp = (MyApp) getApplication(); - myApp.activies.remove(this); - } - - public void click1(View view){ - Intent intent = new Intent(this,Activity01.class); - startActivity(intent); - } - public void click2(View view){ - Intent intent = new Intent(this,Activity02.class); - startActivity(intent); - } - - public void exit(View view){ - MyApp myApp = (MyApp) getApplication(); - for(Activity ac : myApp.activies){ - ac.finish(); - } - } -} - -public class Activity02 extends Activity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main2); - MyApp myApp = (MyApp) getApplication(); - myApp.activies.add(this); - } - - public void click1(View view) { - Intent intent = new Intent(this, Activity01.class); - startActivity(intent); - } - - public void click2(View view) { - Intent intent = new Intent(this, Activity02.class); - startActivity(intent); - } - - public void exit(View view) { - MyApp myApp = (MyApp) getApplication(); - for (Activity ac : myApp.activies) { - ac.finish(); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - MyApp myApp = (MyApp) getApplication(); - myApp.activies.remove(this); - } -} - -public class MyApp extends Application { - //存放当前应用程序里面打开的所有的activity - public List activies; - @Override - public void onCreate() { - activies = new ArrayList(); - super.onCreate(); - } -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +安全退出应用程序 +=== + +1. 杀死进程。 + 这种方法是没有效果的只能杀死当前的`Activity`无法关闭程序,在1.5的时候有用,谷歌设计的时候规定程序不能自杀 + `android.os.Process.killProcess(android.os.Process.myPid())`. +2. 终止当前正在运行的Java虚拟机,导致程序终止. + 这种方法也是没有效果的,因为`Android`用的是`dalvik`虚拟机 + `System.exit(0);` +3. 强制关闭与该包有关联的一切执行 + 这种方法只能杀死别人,无法杀死自己 + ```java + ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + manager.restartPackage(getPackageName()); + 同时需要申请权限 + + ``` + +既然上面介绍的三种方法都没有效果,那么怎么才能退出应用程序呢? +就是自定义一个`Application`,在该`Application`中去定义一个`List`的集合来记录中每一个开启的`Activity`,在退出的时候去遍历这个`List`集合, +然后挨个的进行`mActivity.finish()`方法,这要求在每开启一个`Activity`的时候都加入到`List`集合中,并且在`Activity`退出的时候从`List`集合中将其移除。 +```java +public class Activity01 extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + MyApp myApp = (MyApp) getApplication(); + myApp.activies.add(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + MyApp myApp = (MyApp) getApplication(); + myApp.activies.remove(this); + } + + public void click1(View view){ + Intent intent = new Intent(this,Activity01.class); + startActivity(intent); + } + public void click2(View view){ + Intent intent = new Intent(this,Activity02.class); + startActivity(intent); + } + + public void exit(View view){ + MyApp myApp = (MyApp) getApplication(); + for(Activity ac : myApp.activies){ + ac.finish(); + } + } +} + +public class Activity02 extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main2); + MyApp myApp = (MyApp) getApplication(); + myApp.activies.add(this); + } + + public void click1(View view) { + Intent intent = new Intent(this, Activity01.class); + startActivity(intent); + } + + public void click2(View view) { + Intent intent = new Intent(this, Activity02.class); + startActivity(intent); + } + + public void exit(View view) { + MyApp myApp = (MyApp) getApplication(); + for (Activity ac : myApp.activies) { + ac.finish(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + MyApp myApp = (MyApp) getApplication(); + myApp.activies.remove(this); + } +} + +public class MyApp extends Application { + //存放当前应用程序里面打开的所有的activity + public List activies; + @Override + public void onCreate() { + activies = new ArrayList(); + super.onCreate(); + } +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" "b/AndroidBasicPart/\345\261\217\345\271\225\351\200\202\351\205\215.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\345\261\217\345\271\225\351\200\202\351\205\215.md" rename to "AndroidBasicPart/\345\261\217\345\271\225\351\200\202\351\205\215.md" diff --git "a/Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" "b/AndroidBasicPart/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" rename to "AndroidBasicPart/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" index faf07508..7e552c79 100644 --- "a/Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" +++ "b/AndroidBasicPart/\345\272\224\347\224\250\345\220\216\345\217\260\345\224\244\351\206\222\345\220\216\346\225\260\346\215\256\347\232\204\345\210\267\346\226\260.md" @@ -1,66 +1,66 @@ -应用后台唤醒后数据的刷新 -=== - -1. 如何判断程序是否是在后台运行了 - ```java - /** - * 判断当前的应用程序是否在后台运行,使用该程序需要声明权限android.permission.GET_TASKS - * @param context Context - * @return true表示当前应用程序在后台运行。false为在前台运行 - */ - public static boolean isApplicationBroughtToBackground(Context context) { - ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - List tasks = am.getRunningTasks(1); - if (tasks != null && !tasks.isEmpty()) { - ComponentName topActivity = tasks.get(0).topActivity; - if (!topActivity.getPackageName().equals(context.getPackageName())) { - return true; - } - } - return false; - } - ``` - -2. 在Activity中的onStop方法中去判断当前的应用程序是否在后台运行,同时用一个成员变量去记录该Activity是否为后台, - 在onResume方法中去判断记录程序后台的变量是否为true,如为true就说明现在是程序从后台切换到前台了,这时候就要去刷新数据了 - ```java - /** - * 用于记录当前应用程序是否在后台运行,这样只作用于一个Activity,如果想让所有的 - * Activity都知道程序从后台到前台了,这时候要弄一个基类BaseActivity了,在 - * BaseActivity中去执行这些代码,让其他的Activity都继承该BaseActivity。并且要将 - * isApplicationBroughtToBackground变成static的。 - * 然后在onResume方法中不要执行isApplicationBroughtToBackground = false;这样其他 - * Activity在onResume方法中判断时就知道应用是从后台切换到前台的了,不用担心这样会导 - * 致isApplicationBroughtToBackground无法恢复为false,因为在onStop方法中,我们 - * 判断了如果现在程序不是后台,就将isApplicationBroughtToBackground 变为false了 - */ - private boolean isApplicationBroughtToBackground; - - @Override - protected void onStop() { - super.onStop(); - if(isApplicationBroughtToBackground(this)) { - //程序后台了 - LogUtil.i(TAG, "后台了..."); - isApplicationBroughtToBackground = true; - }else { - LogUtil.i(TAG, "木有后台"); - isApplicationBroughtToBackground = false; - } - - } - - protected void onResume() { - super.onResume(); - if(isApplicationBroughtToBackground) { - //从后台切换到前台了 - LogUtil.i(TAG, "从后台切换到前台了,刷新数据"); - loadFocusInfoData(); - } - isApplicationBroughtToBackground = false; - }; - ``` - ---- -- 邮箱 :charon.chui@gmail.com +应用后台唤醒后数据的刷新 +=== + +1. 如何判断程序是否是在后台运行了 + ```java + /** + * 判断当前的应用程序是否在后台运行,使用该程序需要声明权限android.permission.GET_TASKS + * @param context Context + * @return true表示当前应用程序在后台运行。false为在前台运行 + */ + public static boolean isApplicationBroughtToBackground(Context context) { + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List tasks = am.getRunningTasks(1); + if (tasks != null && !tasks.isEmpty()) { + ComponentName topActivity = tasks.get(0).topActivity; + if (!topActivity.getPackageName().equals(context.getPackageName())) { + return true; + } + } + return false; + } + ``` + +2. 在Activity中的onStop方法中去判断当前的应用程序是否在后台运行,同时用一个成员变量去记录该Activity是否为后台, + 在onResume方法中去判断记录程序后台的变量是否为true,如为true就说明现在是程序从后台切换到前台了,这时候就要去刷新数据了 + ```java + /** + * 用于记录当前应用程序是否在后台运行,这样只作用于一个Activity,如果想让所有的 + * Activity都知道程序从后台到前台了,这时候要弄一个基类BaseActivity了,在 + * BaseActivity中去执行这些代码,让其他的Activity都继承该BaseActivity。并且要将 + * isApplicationBroughtToBackground变成static的。 + * 然后在onResume方法中不要执行isApplicationBroughtToBackground = false;这样其他 + * Activity在onResume方法中判断时就知道应用是从后台切换到前台的了,不用担心这样会导 + * 致isApplicationBroughtToBackground无法恢复为false,因为在onStop方法中,我们 + * 判断了如果现在程序不是后台,就将isApplicationBroughtToBackground 变为false了 + */ + private boolean isApplicationBroughtToBackground; + + @Override + protected void onStop() { + super.onStop(); + if(isApplicationBroughtToBackground(this)) { + //程序后台了 + LogUtil.i(TAG, "后台了..."); + isApplicationBroughtToBackground = true; + }else { + LogUtil.i(TAG, "木有后台"); + isApplicationBroughtToBackground = false; + } + + } + + protected void onResume() { + super.onResume(); + if(isApplicationBroughtToBackground) { + //从后台切换到前台了 + LogUtil.i(TAG, "从后台切换到前台了,刷新数据"); + loadFocusInfoData(); + } + isApplicationBroughtToBackground = false; + }; + ``` + +--- +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\256\211\350\243\205.md" "b/AndroidBasicPart/\345\272\224\347\224\250\345\256\211\350\243\205.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\256\211\350\243\205.md" rename to "AndroidBasicPart/\345\272\224\347\224\250\345\256\211\350\243\205.md" index a723a0a5..824f2d5a 100644 --- "a/Android\345\237\272\347\241\200/\345\272\224\347\224\250\345\256\211\350\243\205.md" +++ "b/AndroidBasicPart/\345\272\224\347\224\250\345\256\211\350\243\205.md" @@ -1,54 +1,54 @@ -应用安装 -=== - -1. 在应用程序中安装程序需要权限 - ` ` - -2. 示例代码 - 安卓中提供了安装程序的功能,我们只要启动安装程序的Activity,并把我们的数据传入即可。 - ```java - //获取到要安装的apk文件的File对象 - File file = new File(Environment.getExternalStorageDirectory(), "test.apk"); - //创建一个意图 - Intent intent = new Intent(); - //设置意图动作 - intent.setAction(Intent.ACTION_VIEW); //android.intent.action.VIEW - //设置意图数据和类型 - intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); - //启动安装程序的Activity - startActivity(intent); - ``` - -**Tip:** -- `Uri.fromFile(File file)`方法能从一个`File`对象得到它的`Uri` -- `Intent`有`setData(Uri uri)`和`setType(String type)`方法,但是这里如果我们分开写就会报错, - 原因是`setData()`方法在执行的时候会自动清空所有在此之前调用的`setType`方法所设置过的type, - 同样`setType`方法在执行的时候也会自动清空所有在此之前调用`setData`设置的`Data`,所以这里必须使用`setDataAndType`方法而不能分开使用`setData`和`setType`. -- `Android`中提供了安装应用程序的功能,在`Android`系统源码中`apps/PackageInstaller`中。我们找到这个`PackageInstaller`的清单文件, - 然后找到`PackageInstallerActivity`来查找该`Activity`的意图:如下 - `android_source/packages/apps/PackageInstaller/AndroidManifest.xml` - ```xml - - - - - - - - - - - - - - - - - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +应用安装 +=== + +1. 在应用程序中安装程序需要权限 + ` ` + +2. 示例代码 + 安卓中提供了安装程序的功能,我们只要启动安装程序的Activity,并把我们的数据传入即可。 + ```java + //获取到要安装的apk文件的File对象 + File file = new File(Environment.getExternalStorageDirectory(), "test.apk"); + //创建一个意图 + Intent intent = new Intent(); + //设置意图动作 + intent.setAction(Intent.ACTION_VIEW); //android.intent.action.VIEW + //设置意图数据和类型 + intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); + //启动安装程序的Activity + startActivity(intent); + ``` + +**Tip:** +- `Uri.fromFile(File file)`方法能从一个`File`对象得到它的`Uri` +- `Intent`有`setData(Uri uri)`和`setType(String type)`方法,但是这里如果我们分开写就会报错, + 原因是`setData()`方法在执行的时候会自动清空所有在此之前调用的`setType`方法所设置过的type, + 同样`setType`方法在执行的时候也会自动清空所有在此之前调用`setData`设置的`Data`,所以这里必须使用`setDataAndType`方法而不能分开使用`setData`和`setType`. +- `Android`中提供了安装应用程序的功能,在`Android`系统源码中`apps/PackageInstaller`中。我们找到这个`PackageInstaller`的清单文件, + 然后找到`PackageInstallerActivity`来查找该`Activity`的意图:如下 + `android_source/packages/apps/PackageInstaller/AndroidManifest.xml` + ```xml + + + + + + + + + + + + + + + + + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" "b/AndroidBasicPart/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" similarity index 95% rename from "Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" rename to "AndroidBasicPart/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" index bcf0acf6..2bc6ff49 100644 --- "a/Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" +++ "b/AndroidBasicPart/\345\274\200\345\217\221\344\270\255Log\347\232\204\347\256\241\347\220\206.md" @@ -1,51 +1,51 @@ -开发中Log的管理 -=== - -**LogUtil**是一个管理`Log`打印的工具类。在开发的不同阶段中通过对该类的控制来实现不同级别`Log`的打印。 -```java -public class LogUtil { - public static final int VERBOSE = 5; - public static final int DEBUG = 4; - public static final int INFO = 3; - public static final int WARN = 2; - public static final int ERROR = 1; - - /** 通过改变该值来进行不同级别的Log打印,上线的时将该值改为0来关闭所有log打印 */ - public final static int LOG_LEVEL = 6; - - public static void v(String tag, String msg) { - if (LOG_LEVEL > VERBOSE) { - Log.v(tag, msg); - } - } - - public static void d(String tag, String msg) { - if (LOG_LEVEL > DEBUG) { - Log.d(tag, msg); - } - } - - public static void i(String tag, String msg) { - if (LOG_LEVEL > INFO) { - Log.i(tag, msg); - } - } - - public static void w(String tag, String msg) { - if (LOG_LEVEL > WARN) { - Log.w(tag, msg); - } - } - - public static void e(String tag, String msg) { - if (LOG_LEVEL > ERROR) { - Log.e(tag, msg); - } - } -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +开发中Log的管理 +=== + +**LogUtil**是一个管理`Log`打印的工具类。在开发的不同阶段中通过对该类的控制来实现不同级别`Log`的打印。 +```java +public class LogUtil { + public static final int VERBOSE = 5; + public static final int DEBUG = 4; + public static final int INFO = 3; + public static final int WARN = 2; + public static final int ERROR = 1; + + /** 通过改变该值来进行不同级别的Log打印,上线的时将该值改为0来关闭所有log打印 */ + public final static int LOG_LEVEL = 6; + + public static void v(String tag, String msg) { + if (LOG_LEVEL > VERBOSE) { + Log.v(tag, msg); + } + } + + public static void d(String tag, String msg) { + if (LOG_LEVEL > DEBUG) { + Log.d(tag, msg); + } + } + + public static void i(String tag, String msg) { + if (LOG_LEVEL > INFO) { + Log.i(tag, msg); + } + } + + public static void w(String tag, String msg) { + if (LOG_LEVEL > WARN) { + Log.w(tag, msg); + } + } + + public static void e(String tag, String msg) { + if (LOG_LEVEL > ERROR) { + Log.e(tag, msg); + } + } +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" "b/AndroidBasicPart/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" rename to "AndroidBasicPart/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" index 19fd63b2..e0fd2969 100644 --- "a/Android\345\237\272\347\241\200/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" +++ "b/AndroidBasicPart/\345\274\200\345\217\221\344\270\255\345\274\202\345\270\270\347\232\204\345\244\204\347\220\206.md" @@ -1,57 +1,57 @@ -开发中异常的处理 -=== - -1. 实现未捕捉异常处理器 - ```java - public class MyExceptionHandler implements UncaughtExceptionHandler { - private static final String TAG = "MyExceptionHandler"; - @Override - public void uncaughtException(Thread arg0, Throwable arg1) { - Logger.i(TAG, "发生了异常,但是被哥捕获了..."); - try { - Field[] fields = Build.class.getDeclaredFields();//可以通过Build的属性来获取到手机的硬件信息,由于不同手机的硬件信息部一定有,所以要用反射得到 - StringBuffer sb = new StringBuffer(); - for(Field field: fields){ - String info = field.getName()+ ":"+field.get(null)+"\n"; - sb.append(info); - } - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - arg1.printStackTrace(pw);//通过这个来得到异常信息 - String errorlog = sw.toString(); - - File file = new File(Environment.getExternalStorageDirectory(), - "error.log"); - FileOutputStream fos = new FileOutputStream(file); - sb.append(errorlog); - fos.write(sb.toString().getBytes()); - fos.close(); - } catch (Exception e) { - e.printStackTrace(); - } - android.os.Process.killProcess(android.os.Process.myPid());//这个是只能杀死自己不能杀死别人,这时候系统发现程序在自己的范围之内死了, - 系统就会重启程序到出现错误之前的那个Activity。 - } - } - ``` - -2. 让这个处理器生效 - ```java - /** - * 代表的是当前应用程序的进程. - */ - public class MobliesafeApplication extends Application { - public BlackNumberInfo info; - - @Override - public void onCreate() { - super.onCreate(); - Thread.currentThread().setUncaughtExceptionHandler(new MyExceptionHandler());//这样就能够让异常的处理器设置到我们的程序中 - } - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +开发中异常的处理 +=== + +1. 实现未捕捉异常处理器 + ```java + public class MyExceptionHandler implements UncaughtExceptionHandler { + private static final String TAG = "MyExceptionHandler"; + @Override + public void uncaughtException(Thread arg0, Throwable arg1) { + Logger.i(TAG, "发生了异常,但是被哥捕获了..."); + try { + Field[] fields = Build.class.getDeclaredFields();//可以通过Build的属性来获取到手机的硬件信息,由于不同手机的硬件信息部一定有,所以要用反射得到 + StringBuffer sb = new StringBuffer(); + for(Field field: fields){ + String info = field.getName()+ ":"+field.get(null)+"\n"; + sb.append(info); + } + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + arg1.printStackTrace(pw);//通过这个来得到异常信息 + String errorlog = sw.toString(); + + File file = new File(Environment.getExternalStorageDirectory(), + "error.log"); + FileOutputStream fos = new FileOutputStream(file); + sb.append(errorlog); + fos.write(sb.toString().getBytes()); + fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + android.os.Process.killProcess(android.os.Process.myPid());//这个是只能杀死自己不能杀死别人,这时候系统发现程序在自己的范围之内死了, + 系统就会重启程序到出现错误之前的那个Activity。 + } + } + ``` + +2. 让这个处理器生效 + ```java + /** + * 代表的是当前应用程序的进程. + */ + public class MobliesafeApplication extends Application { + public BlackNumberInfo info; + + @Override + public void onCreate() { + super.onCreate(); + Thread.currentThread().setUncaughtExceptionHandler(new MyExceptionHandler());//这样就能够让异常的处理器设置到我们的程序中 + } + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" "b/AndroidBasicPart/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" rename to "AndroidBasicPart/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" index 97142327..28049f25 100644 --- "a/Android\345\237\272\347\241\200/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" +++ "b/AndroidBasicPart/\345\277\253\346\215\267\346\226\271\345\274\217\345\267\245\345\205\267\347\261\273.md" @@ -1,104 +1,104 @@ -快捷方式工具类 -== - -```java -/** - * 快捷方式工具类 - */ -public class ShortCutUtils { - /** - * 添加当前应用的桌面快捷方式 - * - * @param cx - */ - public static void addShortcut(Context cx) { - Intent shortcut = new Intent( - "com.android.launcher.action.INSTALL_SHORTCUT"); - Intent shortcutIntent = cx.getPackageManager() - .getLaunchIntentForPackage(cx.getPackageName()); - shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - // 获取当前应用名称 - String title = null; - try { - final PackageManager pm = cx.getPackageManager(); - title = pm.getApplicationLabel( - pm.getApplicationInfo(cx.getPackageName(), - PackageManager.GET_META_DATA)).toString(); - } catch (Exception e) { - } - // 快捷方式名称 - shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); - // 不允许重复创建(不一定有效) - shortcut.putExtra("duplicate", false); - // 快捷方式的图标 - Parcelable iconResource = Intent.ShortcutIconResource.fromContext(cx, - R.drawable.ic_launcher); - shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); - cx.sendBroadcast(shortcut); - } - /** - * 删除当前应用的桌面快捷方式 - * - * @param cx - */ - public static void delShortcut(Context cx) { - Intent shortcut = new Intent( - "com.android.launcher.action.UNINSTALL_SHORTCUT"); - // 获取当前应用名称 - String title = null; - try { - final PackageManager pm = cx.getPackageManager(); - title = pm.getApplicationLabel( - pm.getApplicationInfo(cx.getPackageName(), - PackageManager.GET_META_DATA)).toString(); - } catch (Exception e) { - } - // 快捷方式名称 - shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); - Intent shortcutIntent = cx.getPackageManager() - .getLaunchIntentForPackage(cx.getPackageName()); - shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - cx.sendBroadcast(shortcut); - } - /** - * 判断当前应用在桌面是否有桌面快捷方式 - * - * @param cx - */ - public static boolean hasShortcut(Context cx) { - boolean result = false; - String title = null; - try { - final PackageManager pm = cx.getPackageManager(); - title = pm.getApplicationLabel( - pm.getApplicationInfo(cx.getPackageName(), - PackageManager.GET_META_DATA)).toString(); - } catch (Exception e) { - } - final String uriStr; - if (android.os.Build.VERSION.SDK_INT < 8) { - uriStr = "content://com.android.launcher.settings/favorites?notify=true"; - } else { - uriStr = "content://com.android.launcher2.settings/favorites?notify=true"; - } - final Uri CONTENT_URI = Uri.parse(uriStr); - final Cursor c = cx.getContentResolver().query(CONTENT_URI, null, - "title=?", new String[] { title }, null); - if (c != null && c.getCount() > 0) { - result = true; - } - return result; - } -} -``` -其中涉及的3个权限: -```xml - - - -``` - ---- - -- 邮箱 :charon.chui@gmail.com +快捷方式工具类 +== + +```java +/** + * 快捷方式工具类 + */ +public class ShortCutUtils { + /** + * 添加当前应用的桌面快捷方式 + * + * @param cx + */ + public static void addShortcut(Context cx) { + Intent shortcut = new Intent( + "com.android.launcher.action.INSTALL_SHORTCUT"); + Intent shortcutIntent = cx.getPackageManager() + .getLaunchIntentForPackage(cx.getPackageName()); + shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + // 获取当前应用名称 + String title = null; + try { + final PackageManager pm = cx.getPackageManager(); + title = pm.getApplicationLabel( + pm.getApplicationInfo(cx.getPackageName(), + PackageManager.GET_META_DATA)).toString(); + } catch (Exception e) { + } + // 快捷方式名称 + shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); + // 不允许重复创建(不一定有效) + shortcut.putExtra("duplicate", false); + // 快捷方式的图标 + Parcelable iconResource = Intent.ShortcutIconResource.fromContext(cx, + R.drawable.ic_launcher); + shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); + cx.sendBroadcast(shortcut); + } + /** + * 删除当前应用的桌面快捷方式 + * + * @param cx + */ + public static void delShortcut(Context cx) { + Intent shortcut = new Intent( + "com.android.launcher.action.UNINSTALL_SHORTCUT"); + // 获取当前应用名称 + String title = null; + try { + final PackageManager pm = cx.getPackageManager(); + title = pm.getApplicationLabel( + pm.getApplicationInfo(cx.getPackageName(), + PackageManager.GET_META_DATA)).toString(); + } catch (Exception e) { + } + // 快捷方式名称 + shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); + Intent shortcutIntent = cx.getPackageManager() + .getLaunchIntentForPackage(cx.getPackageName()); + shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + cx.sendBroadcast(shortcut); + } + /** + * 判断当前应用在桌面是否有桌面快捷方式 + * + * @param cx + */ + public static boolean hasShortcut(Context cx) { + boolean result = false; + String title = null; + try { + final PackageManager pm = cx.getPackageManager(); + title = pm.getApplicationLabel( + pm.getApplicationInfo(cx.getPackageName(), + PackageManager.GET_META_DATA)).toString(); + } catch (Exception e) { + } + final String uriStr; + if (android.os.Build.VERSION.SDK_INT < 8) { + uriStr = "content://com.android.launcher.settings/favorites?notify=true"; + } else { + uriStr = "content://com.android.launcher2.settings/favorites?notify=true"; + } + final Uri CONTENT_URI = Uri.parse(uriStr); + final Cursor c = cx.getContentResolver().query(CONTENT_URI, null, + "title=?", new String[] { title }, null); + if (c != null && c.getCount() > 0) { + result = true; + } + return result; + } +} +``` +其中涉及的3个权限: +```xml + + + +``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\346\211\213\346\234\272\346\221\207\346\231\203.md" "b/AndroidBasicPart/\346\211\213\346\234\272\346\221\207\346\231\203.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\346\211\213\346\234\272\346\221\207\346\231\203.md" rename to "AndroidBasicPart/\346\211\213\346\234\272\346\221\207\346\231\203.md" diff --git "a/Android\345\237\272\347\241\200/\346\220\234\347\264\242\346\241\206.md" "b/AndroidBasicPart/\346\220\234\347\264\242\346\241\206.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\346\220\234\347\264\242\346\241\206.md" rename to "AndroidBasicPart/\346\220\234\347\264\242\346\241\206.md" index 298e2041..3fb9d17b 100644 --- "a/Android\345\237\272\347\241\200/\346\220\234\347\264\242\346\241\206.md" +++ "b/AndroidBasicPart/\346\220\234\347\264\242\346\241\206.md" @@ -1,115 +1,115 @@ -搜索框 -=== - -1. 在res-xml中新建一个searchable.xml - ```xml - - - - ``` - -2. 创建一个搜索的Activity,并且在清单文件中进行配置,这个Activity就是点击搜索结果后的页面(显示搜索结果的Activity) - ```xml - - - - - - - ``` - **在这个Activity中的内容要这样** - ```java - Intent intent = getIntent(); - //如果是从搜索开启的这个页面就获取到搜索框中输入的内容 - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - //获取到在搜索框中输入的内容 - String query = intent.getStringExtra(SearchManager.QUERY); - //执行搜索的方法 - doMySearch(query); - Log.i("i", " query " + query); - } - ``` - -3. 如何实现在输入框中输入内容后下面就立马提示相应的搜索结果呢?要通过Provider来操作 - ```java - public class MyProvider extends SearchRecentSuggestionsProvider { - //指定查询的authority - public final static String AUTHORITY = "com.charon.MyProvider"; - //指定数据库的操作是查询的方式 - public final static int MODE = DATABASE_MODE_QUERIES; - public MySuggestionProvider() { - setupSuggestions(AUTHORITY, MODE); - } - - //然后重写SearchRecentSuggestionsProvider中的方法,有增删改查四个方法 - private final static String[] sms_projection = new String[]{Sms._ID,Sms.ADDRESS,Sms.BODY}; - - private final static String[] columnNames = new String[]{BaseColumns._ID, - SearchManager.SUGGEST_COLUMN_TEXT_1, //指定搜索自动提示的框的样式 - SearchManager.SUGGEST_COLUMN_TEXT_2, - SearchManager.SUGGEST_COLUMN_QUERY}; //这个参数能够让点击某个搜索提示的时候自动让搜索内容变成点击的条目的内容 - - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - if(selectionArgs != null){ - String query = selectionArgs[0]; - if(TextUtils.isEmpty(query)){ - return null; - } - - Uri uri1 = Sms.CONTENT_URI; - String where = Sms.BODY + " like '%" + query + "%'"; - Cursor cursor = getContext().getContentResolver().query(uri1, sms_projection, where, null, Sms.DATE + " desc "); - return changeCursor(cursor); - } - return null; - } - - private Cursor changeCursor(Cursor cursor){ - MatrixCursor result = new MatrixCursor(columnNames); - if(cursor != null){ - while(cursor.moveToNext()){ - Object[] columnValues = new Object[]{cursor.getString(cursor.getColumnIndex(Sms._ID)), - cursor.getString(cursor.getColumnIndex(Sms.ADDRESS)), - cursor.getString(cursor.getColumnIndex(Sms.BODY)), - cursor.getString(cursor.getColumnIndex(Sms.BODY))}; //点击时候让内容变为短信内容 - result.addRow(columnValues); - } - } - return result; - } - } - ``` - -4. 为了让不管在哪个界面只要点击手机的搜索键都能够弹出我们的搜索(全局搜索),需要在Application节点中去配置下面的meta-data - ```xml - - - ``` - -5. 在Activtity中有一个onSearchRequested方法,执行此方法时能够激活搜索框,所以在点击搜索按钮时执行此方法即可 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - - - - - - - +搜索框 +=== + +1. 在res-xml中新建一个searchable.xml + ```xml + + + + ``` + +2. 创建一个搜索的Activity,并且在清单文件中进行配置,这个Activity就是点击搜索结果后的页面(显示搜索结果的Activity) + ```xml + + + + + + + ``` + **在这个Activity中的内容要这样** + ```java + Intent intent = getIntent(); + //如果是从搜索开启的这个页面就获取到搜索框中输入的内容 + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + //获取到在搜索框中输入的内容 + String query = intent.getStringExtra(SearchManager.QUERY); + //执行搜索的方法 + doMySearch(query); + Log.i("i", " query " + query); + } + ``` + +3. 如何实现在输入框中输入内容后下面就立马提示相应的搜索结果呢?要通过Provider来操作 + ```java + public class MyProvider extends SearchRecentSuggestionsProvider { + //指定查询的authority + public final static String AUTHORITY = "com.charon.MyProvider"; + //指定数据库的操作是查询的方式 + public final static int MODE = DATABASE_MODE_QUERIES; + public MySuggestionProvider() { + setupSuggestions(AUTHORITY, MODE); + } + + //然后重写SearchRecentSuggestionsProvider中的方法,有增删改查四个方法 + private final static String[] sms_projection = new String[]{Sms._ID,Sms.ADDRESS,Sms.BODY}; + + private final static String[] columnNames = new String[]{BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_TEXT_1, //指定搜索自动提示的框的样式 + SearchManager.SUGGEST_COLUMN_TEXT_2, + SearchManager.SUGGEST_COLUMN_QUERY}; //这个参数能够让点击某个搜索提示的时候自动让搜索内容变成点击的条目的内容 + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if(selectionArgs != null){ + String query = selectionArgs[0]; + if(TextUtils.isEmpty(query)){ + return null; + } + + Uri uri1 = Sms.CONTENT_URI; + String where = Sms.BODY + " like '%" + query + "%'"; + Cursor cursor = getContext().getContentResolver().query(uri1, sms_projection, where, null, Sms.DATE + " desc "); + return changeCursor(cursor); + } + return null; + } + + private Cursor changeCursor(Cursor cursor){ + MatrixCursor result = new MatrixCursor(columnNames); + if(cursor != null){ + while(cursor.moveToNext()){ + Object[] columnValues = new Object[]{cursor.getString(cursor.getColumnIndex(Sms._ID)), + cursor.getString(cursor.getColumnIndex(Sms.ADDRESS)), + cursor.getString(cursor.getColumnIndex(Sms.BODY)), + cursor.getString(cursor.getColumnIndex(Sms.BODY))}; //点击时候让内容变为短信内容 + result.addRow(columnValues); + } + } + return result; + } + } + ``` + +4. 为了让不管在哪个界面只要点击手机的搜索键都能够弹出我们的搜索(全局搜索),需要在Application节点中去配置下面的meta-data + ```xml + + + ``` + +5. 在Activtity中有一个onSearchRequested方法,执行此方法时能够激活搜索框,所以在点击搜索按钮时执行此方法即可 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\346\225\260\346\215\256\345\255\230\345\202\250.md" "b/AndroidBasicPart/\346\225\260\346\215\256\345\255\230\345\202\250.md" similarity index 96% rename from "Android\345\237\272\347\241\200/\346\225\260\346\215\256\345\255\230\345\202\250.md" rename to "AndroidBasicPart/\346\225\260\346\215\256\345\255\230\345\202\250.md" index bf749d93..33ddcc48 100644 --- "a/Android\345\237\272\347\241\200/\346\225\260\346\215\256\345\255\230\345\202\250.md" +++ "b/AndroidBasicPart/\346\225\260\346\215\256\345\255\230\345\202\250.md" @@ -1,325 +1,325 @@ -数据存储 -=== - -## `Android`数据存储的几种形式 - -1. `Internal Storage` - `Store private data on the device memory.` - 通过`mContext.getFilesDir()`来得到`data/data/包名/File`目录 - -2. `External Storage` - `Store public data on the shared external storage.` - ```java - TextView tv = (TextView) findViewById(R.id.tv_sdsize); - File path = Environment.getExternalStorageDirectory(); - StatFs stat = new StatFs(path.getPath()); - long blockSize = stat.getBlockSize(); - long availableBlocks = stat.getAvailableBlocks(); - long sizeAvailSize = blockSize * availableBlocks; - String str = Formatter.formatFileSize(this, sizeAvailSize); - tv.setText(str); - ``` - -3. `SharedPreferences` - `Store private primitive data in key-value pairs.` - 会在`data/data/包名/shared_prefes`里面去创建相应的`xml`文件,根节点是`Map`,其实内部就是将数据保存到`Map`集合中, - 然后将该集合中的数据写到`xml`文件中进行保存。 - ```xml - - 123 - - - ``` - - ```java - //获取系统的一个sharedpreference文件 名字叫sp - SharedPreferences sp = context.getSharedPreferences("sp", Context.MODE_WORLD_READABLE+Context.MODE_WORLD_WRITEABLE); - //创建一个编辑器 可以编辑 sp - Editor editor = sp.edit(); - editor.putString("name", name); - editor.putString("password", password); - editor.putBoolean("boolean", false); - editor.putInt("int", 8888); - editor.putFloat("float",3.14159f); - //注意:调用 commit 提交 数据到文件. - editor.commit(); - //editor.clear(); - ``` - -4. `SQLiteDatabase` - `Store structured data in a private database.` - `Android`平台中嵌入了一个关系型数据库`SQLite`,和其他数据库不同的是`SQLite`存储数据时不区分类型,例如一个字段声明为`Integer`类型, - 我们也可以将一个字符串存入, - 一个字段声明为布尔型,我们也可以存入浮点数。除非是主键被定义为`Integer`,这时只能存储64位整数创建数据库的表时可以不指定数据类型,例如: - ```java - CREATE TABLE person(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(20)) - CREATE TABLE person(id INTEGER PRIMARY KEY AUTOINCREMENT, name) - ``` - - `SQLite`支持大部分标准`SQL`语句,增删改查语句都是通用的,分页查询语句和`MySQL`相同 - ```java - SELECT * FROM person LIMIT 20 OFFSET 10 - SELECT * FROM person LIMIT 10,20 - SELECT * FROM reading_history ORDER BY _id DESC LIMIT 3, 4 - DELETE FROM test WHERE _id IN (SELECT _id FROM test ORDER BY _id DESC LIMIT 3, 20) - ``` - - `SQLite`数据库的使用 - - - 继承`SQLiteOpenHelper` - ```java - public class NoteSQLiteOpenHelper extends SQLiteOpenHelper { - - /** - * name 数据库的名称 cursorfactory 游标工厂 一般设置null 默认游标工厂 version 数据库的版本 - * 版本号从1开始的 - * - * @param context - */ - public NoteSQLiteOpenHelper(Context context) { - super(context, "note.db", null, 1); - } - - /** - * oncreate 方法 会在数据库第一创建的时候的是被调用 适合做数据库表结构的初始化 - */ - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("create table account (id integer primary key autoincrement , name varchar(20), money varchar(20) )"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) { - db.execSQL("DROP TABLE IF EXISTS " + account); - this.onCreate(db); - } - } - ``` - - - 获取 - ```java - public class NoteDao { - private NoteSQLiteOpenHelper helper; - - public NoteDao(Context context) { - helper = new NoteSQLiteOpenHelper(context); - } - - /** - * 添加一条账目信息 到数据库 - * - * @param name - * 花销的名称 - * @param money - * 金额 - */ - public void add(String name, float money) { - SQLiteDatabase db = helper.getWritableDatabase(); - db.execSQL("insert into account (name,money) values (?,?)", - new Object[] { name, money }); - // 记住 关闭. - db.close(); - } - - public void delete(int id) { - SQLiteDatabase db = helper.getWritableDatabase(); - db.execSQL("delete from account where id=?", new Object[] { id }); - db.close(); - } - - public void update(int id, float newmoney) { - SQLiteDatabase db = helper.getWritableDatabase(); - db.execSQL("update account set money =? where id=?", new Object[] { - newmoney, id }); - db.close(); - } - - /** - * 返回数据库所有的条目 - * - * @return - */ - public List findAll() { - // 得到可读的数据库 - SQLiteDatabase db = helper.getReadableDatabase(); - List noteBeans = new ArrayList(); - // 获取到数据库查询的结果游标 - Cursor cursor = db.rawQuery("select id,money,name from account ", null); - while (cursor.moveToNext()) { - int id = cursor.getInt(cursor.getColumnIndex("id")); - String name = cursor.getString(cursor.getColumnIndex("name")); - float money = cursor.getFloat(cursor.getColumnIndex("money")); - NoteBean bean = new NoteBean(id, money, name); - noteBeans.add(bean); - bean = null; - } - - db.close(); - return noteBeans; - } - - /** - * 模拟一个转账的操作. 使用数据库的事务 - * - * @throws Exception - */ - public void testTransaction() throws Exception { - // 得到可写的数据库 - SQLiteDatabase db = helper.getWritableDatabase(); - db.beginTransaction(); // 开始事务 - try { - db.execSQL("update account set money = money - 5 where id=? ", - new String[] { "2" }); - db.execSQL("update account set money = money + 5 where id=? ", - new String[] { "3" }); - db.setTransactionSuccessful(); - } catch (Exception e) { - // TODO: handle exception - } finally { - db.endTransaction();//关闭事务. - db.close(); - } - } - } - ``` - - ```java - public class NoteDao2 { - private NoteSQLiteOpenHelper helper; - - public NoteDao2(Context context) { - helper = new NoteSQLiteOpenHelper(context); - } - - /** - * 添加一条账目信息 到数据库 - * - * @param name - * 花销的名称 - * @param money - * 金额 - * - * @return true 插入成功 false 失败 - */ - public boolean add(String name, float money) { - SQLiteDatabase db = helper.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put("name", name); - values.put("money", money); - long rawid = db.insert("account", null, values); - db.close(); - if (rawid > 0) { - return true; - } else { - return false; - } - } - - public boolean delete(int id) { - SQLiteDatabase db = helper.getWritableDatabase(); - int result = db.delete("account", "id=?", new String[] { id + "" }); - db.close(); - if (result > 0) { - return true; - } else { - return false; - } - } - - public boolean update(int id, float newmoney) { - SQLiteDatabase db = helper.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put("id", id); - values.put("money", newmoney); - int result = db.update("account", values, "id=?", new String[] { id - + "" }); - db.close(); - if (result > 0) { - return true; - } else { - return false; - } - } - - /** - * 返回数据库所有的条目 - * - * @return - */ - public List findAll() { - // 得到可读的数据库 - SQLiteDatabase db = helper.getReadableDatabase(); - List noteBeans = new ArrayList(); - Cursor cursor = db.query("account", new String[] { "id", "name", - "money" }, null, null, null, null, null); - while (cursor.moveToNext()) { - int id = cursor.getInt(cursor.getColumnIndex("id")); - String name = cursor.getString(cursor.getColumnIndex("name")); - float money = cursor.getFloat(cursor.getColumnIndex("money")); - NoteBean bean = new NoteBean(id, money, name); - noteBeans.add(bean); - bean = null; - } - db.close(); - return noteBeans; - } - } - ``` - -5. `Network` - `Store data on the web with your own network server.` - -6. `/data/data/包名`下的apk在安装时提示解析失败。 - 我们在更新或安装`apk`时一般将其放到外部存储设备中来进行安装,但是如果一个手机没有外部存储设备该怎么办呢?总不能就不给更新或者安装了。 - 可能你会觉得很简单啊,我用`mContext.getCacheDir()`或者`mContext.getFilesDir()`等获取内部路径,把`apk`放到这里面进行安装,但是你会发现安装 - 不了,提示解析失败。 - 这是为什么呢?其实是权限的问题。安装应用的`app`是没有权限获取你应用的内部存储文件的,所以才会安装不上,那该怎么解决呢? 答案就是修改权限。 - - ```java - try { - InputStream is = getAssets().open(fileName); - File file; - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - file = new File(Environment.getExternalStorageDirectory() - + File.separator + fileName); - } else { - file = new File(mContext.getFilesDir() - + File.separator + fileName); - String cmd = "chmod 777 " + file.getPath(); - try { - Runtime.getRuntime().exec(cmd); - } catch (IOException e) { - e.printStackTrace(); - return; - } - } - - file.createNewFile(); - FileOutputStream fos = new FileOutputStream(file); - byte[] temp = new byte[1024]; - int i = 0; - while ((i = is.read(temp)) > 0) { - fos.write(temp, 0, i); - } - fos.close(); - is.close(); - - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(Uri.parse("file://" + file), - "application/vnd.android.package-archive"); - startActivity(intent); - } catch (Exception e) { - LogUtil.e("@@@", e.toString()); - e.printStackTrace(); - } - ``` - -7. 清除缓存&清除数据 - 清除数据会清除`/data/data/`包名中的所有文件 - 清楚缓存会清楚`getCacheDir()`目录下的内容,也就是`/data/data/<当前应用程序包名>/cache/` - ---- - -- 邮箱 :charon.chui@gmail.com +数据存储 +=== + +## `Android`数据存储的几种形式 + +1. `Internal Storage` + `Store private data on the device memory.` + 通过`mContext.getFilesDir()`来得到`data/data/包名/File`目录 + +2. `External Storage` + `Store public data on the shared external storage.` + ```java + TextView tv = (TextView) findViewById(R.id.tv_sdsize); + File path = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + long sizeAvailSize = blockSize * availableBlocks; + String str = Formatter.formatFileSize(this, sizeAvailSize); + tv.setText(str); + ``` + +3. `SharedPreferences` + `Store private primitive data in key-value pairs.` + 会在`data/data/包名/shared_prefes`里面去创建相应的`xml`文件,根节点是`Map`,其实内部就是将数据保存到`Map`集合中, + 然后将该集合中的数据写到`xml`文件中进行保存。 + ```xml + + 123 + + + ``` + + ```java + //获取系统的一个sharedpreference文件 名字叫sp + SharedPreferences sp = context.getSharedPreferences("sp", Context.MODE_WORLD_READABLE+Context.MODE_WORLD_WRITEABLE); + //创建一个编辑器 可以编辑 sp + Editor editor = sp.edit(); + editor.putString("name", name); + editor.putString("password", password); + editor.putBoolean("boolean", false); + editor.putInt("int", 8888); + editor.putFloat("float",3.14159f); + //注意:调用 commit 提交 数据到文件. + editor.commit(); + //editor.clear(); + ``` + +4. `SQLiteDatabase` + `Store structured data in a private database.` + `Android`平台中嵌入了一个关系型数据库`SQLite`,和其他数据库不同的是`SQLite`存储数据时不区分类型,例如一个字段声明为`Integer`类型, + 我们也可以将一个字符串存入, + 一个字段声明为布尔型,我们也可以存入浮点数。除非是主键被定义为`Integer`,这时只能存储64位整数创建数据库的表时可以不指定数据类型,例如: + ```java + CREATE TABLE person(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(20)) + CREATE TABLE person(id INTEGER PRIMARY KEY AUTOINCREMENT, name) + ``` + + `SQLite`支持大部分标准`SQL`语句,增删改查语句都是通用的,分页查询语句和`MySQL`相同 + ```java + SELECT * FROM person LIMIT 20 OFFSET 10 + SELECT * FROM person LIMIT 10,20 + SELECT * FROM reading_history ORDER BY _id DESC LIMIT 3, 4 + DELETE FROM test WHERE _id IN (SELECT _id FROM test ORDER BY _id DESC LIMIT 3, 20) + ``` + + `SQLite`数据库的使用 + + - 继承`SQLiteOpenHelper` + ```java + public class NoteSQLiteOpenHelper extends SQLiteOpenHelper { + + /** + * name 数据库的名称 cursorfactory 游标工厂 一般设置null 默认游标工厂 version 数据库的版本 + * 版本号从1开始的 + * + * @param context + */ + public NoteSQLiteOpenHelper(Context context) { + super(context, "note.db", null, 1); + } + + /** + * oncreate 方法 会在数据库第一创建的时候的是被调用 适合做数据库表结构的初始化 + */ + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("create table account (id integer primary key autoincrement , name varchar(20), money varchar(20) )"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) { + db.execSQL("DROP TABLE IF EXISTS " + account); + this.onCreate(db); + } + } + ``` + + - 获取 + ```java + public class NoteDao { + private NoteSQLiteOpenHelper helper; + + public NoteDao(Context context) { + helper = new NoteSQLiteOpenHelper(context); + } + + /** + * 添加一条账目信息 到数据库 + * + * @param name + * 花销的名称 + * @param money + * 金额 + */ + public void add(String name, float money) { + SQLiteDatabase db = helper.getWritableDatabase(); + db.execSQL("insert into account (name,money) values (?,?)", + new Object[] { name, money }); + // 记住 关闭. + db.close(); + } + + public void delete(int id) { + SQLiteDatabase db = helper.getWritableDatabase(); + db.execSQL("delete from account where id=?", new Object[] { id }); + db.close(); + } + + public void update(int id, float newmoney) { + SQLiteDatabase db = helper.getWritableDatabase(); + db.execSQL("update account set money =? where id=?", new Object[] { + newmoney, id }); + db.close(); + } + + /** + * 返回数据库所有的条目 + * + * @return + */ + public List findAll() { + // 得到可读的数据库 + SQLiteDatabase db = helper.getReadableDatabase(); + List noteBeans = new ArrayList(); + // 获取到数据库查询的结果游标 + Cursor cursor = db.rawQuery("select id,money,name from account ", null); + while (cursor.moveToNext()) { + int id = cursor.getInt(cursor.getColumnIndex("id")); + String name = cursor.getString(cursor.getColumnIndex("name")); + float money = cursor.getFloat(cursor.getColumnIndex("money")); + NoteBean bean = new NoteBean(id, money, name); + noteBeans.add(bean); + bean = null; + } + + db.close(); + return noteBeans; + } + + /** + * 模拟一个转账的操作. 使用数据库的事务 + * + * @throws Exception + */ + public void testTransaction() throws Exception { + // 得到可写的数据库 + SQLiteDatabase db = helper.getWritableDatabase(); + db.beginTransaction(); // 开始事务 + try { + db.execSQL("update account set money = money - 5 where id=? ", + new String[] { "2" }); + db.execSQL("update account set money = money + 5 where id=? ", + new String[] { "3" }); + db.setTransactionSuccessful(); + } catch (Exception e) { + // TODO: handle exception + } finally { + db.endTransaction();//关闭事务. + db.close(); + } + } + } + ``` + + ```java + public class NoteDao2 { + private NoteSQLiteOpenHelper helper; + + public NoteDao2(Context context) { + helper = new NoteSQLiteOpenHelper(context); + } + + /** + * 添加一条账目信息 到数据库 + * + * @param name + * 花销的名称 + * @param money + * 金额 + * + * @return true 插入成功 false 失败 + */ + public boolean add(String name, float money) { + SQLiteDatabase db = helper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("name", name); + values.put("money", money); + long rawid = db.insert("account", null, values); + db.close(); + if (rawid > 0) { + return true; + } else { + return false; + } + } + + public boolean delete(int id) { + SQLiteDatabase db = helper.getWritableDatabase(); + int result = db.delete("account", "id=?", new String[] { id + "" }); + db.close(); + if (result > 0) { + return true; + } else { + return false; + } + } + + public boolean update(int id, float newmoney) { + SQLiteDatabase db = helper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("id", id); + values.put("money", newmoney); + int result = db.update("account", values, "id=?", new String[] { id + + "" }); + db.close(); + if (result > 0) { + return true; + } else { + return false; + } + } + + /** + * 返回数据库所有的条目 + * + * @return + */ + public List findAll() { + // 得到可读的数据库 + SQLiteDatabase db = helper.getReadableDatabase(); + List noteBeans = new ArrayList(); + Cursor cursor = db.query("account", new String[] { "id", "name", + "money" }, null, null, null, null, null); + while (cursor.moveToNext()) { + int id = cursor.getInt(cursor.getColumnIndex("id")); + String name = cursor.getString(cursor.getColumnIndex("name")); + float money = cursor.getFloat(cursor.getColumnIndex("money")); + NoteBean bean = new NoteBean(id, money, name); + noteBeans.add(bean); + bean = null; + } + db.close(); + return noteBeans; + } + } + ``` + +5. `Network` + `Store data on the web with your own network server.` + +6. `/data/data/包名`下的apk在安装时提示解析失败。 + 我们在更新或安装`apk`时一般将其放到外部存储设备中来进行安装,但是如果一个手机没有外部存储设备该怎么办呢?总不能就不给更新或者安装了。 + 可能你会觉得很简单啊,我用`mContext.getCacheDir()`或者`mContext.getFilesDir()`等获取内部路径,把`apk`放到这里面进行安装,但是你会发现安装 + 不了,提示解析失败。 + 这是为什么呢?其实是权限的问题。安装应用的`app`是没有权限获取你应用的内部存储文件的,所以才会安装不上,那该怎么解决呢? 答案就是修改权限。 + + ```java + try { + InputStream is = getAssets().open(fileName); + File file; + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + file = new File(Environment.getExternalStorageDirectory() + + File.separator + fileName); + } else { + file = new File(mContext.getFilesDir() + + File.separator + fileName); + String cmd = "chmod 777 " + file.getPath(); + try { + Runtime.getRuntime().exec(cmd); + } catch (IOException e) { + e.printStackTrace(); + return; + } + } + + file.createNewFile(); + FileOutputStream fos = new FileOutputStream(file); + byte[] temp = new byte[1024]; + int i = 0; + while ((i = is.read(temp)) > 0) { + fos.write(temp, 0, i); + } + fos.close(); + is.close(); + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setDataAndType(Uri.parse("file://" + file), + "application/vnd.android.package-archive"); + startActivity(intent); + } catch (Exception e) { + LogUtil.e("@@@", e.toString()); + e.printStackTrace(); + } + ``` + +7. 清除缓存&清除数据 + 清除数据会清除`/data/data/`包名中的所有文件 + 清楚缓存会清楚`getCacheDir()`目录下的内容,也就是`/data/data/<当前应用程序包名>/cache/` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\346\226\207\344\273\266\344\270\212\344\274\240.md" "b/AndroidBasicPart/\346\226\207\344\273\266\344\270\212\344\274\240.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\346\226\207\344\273\266\344\270\212\344\274\240.md" rename to "AndroidBasicPart/\346\226\207\344\273\266\344\270\212\344\274\240.md" diff --git "a/Android\345\237\272\347\241\200/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" "b/AndroidBasicPart/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" rename to "AndroidBasicPart/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" index c35820cb..867dfa3f 100644 --- "a/Android\345\237\272\347\241\200/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" +++ "b/AndroidBasicPart/\346\235\245\347\224\265\345\217\267\347\240\201\345\275\222\345\261\236\345\234\260\346\217\220\347\244\272\346\241\206.md" @@ -1,318 +1,318 @@ -来电号码归属地提示框 -=== - -模仿Toast实现提示框 ---- - -Toast提示只要提示的时间够长,就可以浮动到其他任何界面之上,所以我们可以模仿Toast来实现来电号码归属地的提示框 - -1. WindowManager - The interface that apps use to talk to the window manager. Use Context.getSystemService(Context.WINDOW_SERVICE) to get one of these. - Each window manager instance is bound to a particular Display. - - 1. void addView(View view,ViewGroup.LayoutParams params) - 将一个View视图显示到当前窗口,LayoutParams are used by views to tell their parents how they want to be laid out. - - 2. void removeView(View view); - 将一个View视图从当前窗口中移除。 - - -2. 自定义窗体提示框(参考Toast源码) - ```java - WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); - View view = View.inflate(getApplicationContext(), R.layout.toast_location, - null); - TextView tv = (TextView) view.findViewById(R.id.tv_toast_address); - tv.setText(address); - LayoutParams params = new LayoutParams(); - params.height = WindowManager.LayoutParams.WRAP_CONTENT; - params.width = WindowManager.LayoutParams.WRAP_CONTENT; - params.gravity = Gravity.LEFT | Gravity.TOP; - params.x = sp.getInt("lastx", 0); - params.y = sp.getInt("lasty", 0); - //本来还有一个FLAG_NOTUCHALBE为了让下面能触摸把这个给去掉了 - params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - params.format = PixelFormat.TRANSLUCENT; //源码中这里是TYPE_TAOST但是这里为了下面要进行点击拖动事件,而Toast不能拖动, - 所以这里改成了TYPE_PRIORITY_PHONE,这是一个系统类型的提示框,使用这个提示框必须要申请权限,android.permission.SYSTEM_ALERT_WINDOW - params.type = WindowManager.LayoutParams.TYPE_PRIORITY_PHONE; - wm.addView(view, params); - ``` - -3. WindowManager添加的显示框的简单拖动 - 该这个View注册一个onTouchListener - ```java - public void showLocation(String address) { - view = View.inflate(getApplicationContext(), R.layout.toast_location, - null); - // 得到sp - int which = sp.getInt("which", 0); - view.setBackgroundResource(bgs[which]); - view.setOnTouchListener(new OnTouchListener() { - int startX ,startY; - - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - - case MotionEvent.ACTION_DOWN: - Log.i(TAG,"摸到"); - startX = (int) event.getRawX(); - startY = (int) event.getRawY(); - break; - case MotionEvent.ACTION_MOVE: - Log.i(TAG,"移动"); - int newX = (int) event.getRawX(); - int newY = (int) event.getRawY(); - int dx = newX - startX; - int dy = newY - startY; - params.x+=dx; - params.y+=dy; //这里在WindowManager中不能够使用layout方法了,无效,只能使用layoutparams来更新位置,这里的params就是上面的那个params - wm.updateViewLayout(view, params); - //重新初始化 手指的位置 - startX = (int) event.getRawX(); - startY = (int) event.getRawY(); - break; - } - return true; - } - }); - } - ``` - -4. 普通ImageView随手指拖动改变位置 - ```java - iv_drag_view.setOnTouchListener(new OnTouchListener() { - //记录住最初手指按下时的位置 - int startX , startY; - //onTouch方法的返回值如果是true监听器会把这个事件给消费掉, false则监听器不会消费掉这个事件 - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - Log.i(TAG,"摸到这个控件了"); - startX = (int) event.getRawX();//记录手指第一次点击到屏幕时候距离x和y轴的距离 - startY = (int) event.getRawY(); - break; - case MotionEvent.ACTION_MOVE:// 手指在屏幕上移动的事件 - Log.i(TAG,"移动"); - int newX = (int) event.getRawX(); //在移动的过程中不断的获取到手指当前移动到的位置 - int newY = (int) event.getRawY(); - int dx = newX - startX; //计算出手指移动了多少 - int dy = newY - startY; - int l = iv_drag_view.getLeft(); //获取图片上下左右的长度 - int r = iv_drag_view.getRight(); - int b = iv_drag_view.getBottom(); - int t = iv_drag_view.getTop(); - - int newl = l+dx; //计算图片应该移动的距离 - int newr = r+dx; - int newt = t+dy;//imageview 在窗体中新的位置 - int newb = b+dy; - - //判断如果图片准备移动到的位置超出了屏幕就不让它移动,这里减去30是减去窗体上面的状态栏的高度 - if(newl<0||newt < 0 ||newb>display.getHeight()-30||newr>display.getWidth()){ - break; - } - //将图片移动到新的位置。直接调用ImageView的layout方法 - iv_drag_view.layout(newl, newt, newr, newb); - //一旦图片移动到新的位置就重新计算手指当前的位置,这样循环下去就能实现随着手指的拖动 - startX = (int) event.getRawX(); - startY = (int) event.getRawY(); - break; - case MotionEvent.ACTION_UP: // 手指在离开屏幕的一瞬间对应的事件. - Log.i(TAG,"放手"); - int lasty = iv_drag_view.getTop();//得到最后在离屏幕上方的距离 - int lastx = iv_drag_view.getLeft();//得到最后离屏幕左边的距离 - Editor editor = sp.edit(); - editor.putInt("lastx", lastx); - editor.putInt("lasty", lasty); - editor.commit(); - break; - } - return true; //这地方一定要返回true告诉系统这个事件做完了 - } - }); - ``` - - **注意:在onCreate方法中使用layout方法是没有效果的,因为在进入一个Activity中系统首先会执行一个计算的操作,计算各个控件的布局,然后调用setContentView方法显示出来这个控件,第二步才会执行这个layout方法,但是在onCreate方法中设置了layout,在执行layout这段代码的时候,窗体有可能还没有计算完控件的布局,所以先执行了这个layout,然后又执行了计算控件布局来显示,这样layout就没效了,这里要怎么弄呢只能是通过设置这个控件的layout布局,这样在计算位置的时候就能计算了,这样设置布局能让它在计算的时候就计算了。如下,在onCreate方法中去这样设置。** - - ```java - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sp = getSharedPreferences("config", MODE_PRIVATE); - // Have the system blur any windows behind this one. - getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, - WindowManager.LayoutParams.FLAG_BLUR_BEHIND); - wm = (WindowManager) getSystemService(WINDOW_SERVICE);//窗体管理者 - display = wm.getDefaultDisplay(); - - setContentView(R.layout.activity_drag_view); - tv_drag_view = (TextView) findViewById(R.id.tv_drag_view); - iv_drag_view = (ImageView) findViewById(R.id.iv_drag_view); - - int lastx = sp.getInt("lastx", 0); - int lasty = sp.getInt("lasty", 0); - - RelativeLayout.LayoutParams params = (LayoutParams) iv_drag_view.getLayoutParams(); - params.leftMargin = lastx; - params.topMargin = lasty; - iv_drag_view.setLayoutParams(params); - } - ``` - - **注意:在WindowManager中要想更新控件的距离就不能用layout方法了,只能用mWindowManager.updateViewLayout(view, params);** - -5. 实现双击事件 - - 1. 双击的定义 - Android中没有提供双击的点击事件,双击就是单位时间内的两次点击 - - 2. 触摸和点击事件的区别 - 点击事件: 一组动作的集合 点击 - 停留 - 离开. - 触摸事件: 手指按下屏幕 手指在屏幕上移动 手指离开屏幕的一瞬间 - - ```java - public class DragViewActivity extends Activity { - protected static final String TAG = "DragViewActivity"; - private ImageView iv_drag_view; - private TextView tv_drag_view; - private SharedPreferences sp; - - private WindowManager wm; - private Display display; //窗体的显示的分辨率 - - private long firstClickTime;//第一次点击时候的事件 - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sp = getSharedPreferences("config", MODE_PRIVATE); - // Have the system blur any windows behind this one. - getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, - WindowManager.LayoutParams.FLAG_BLUR_BEHIND); - wm = (WindowManager) getSystemService(WINDOW_SERVICE);//窗体管理者 - display = wm.getDefaultDisplay(); - - setContentView(R.layout.activity_drag_view); - tv_drag_view = (TextView) findViewById(R.id.tv_drag_view); - iv_drag_view = (ImageView) findViewById(R.id.iv_drag_view); - - int lastx = sp.getInt("lastx", 0); - int lasty = sp.getInt("lasty", 0); - - RelativeLayout.LayoutParams params = (LayoutParams) iv_drag_view.getLayoutParams(); - params.leftMargin = lastx; - params.topMargin = lasty; - iv_drag_view.setLayoutParams(params); - - iv_drag_view.setOnClickListener(new OnClickListener() { - - public void onClick(View v) { - Log.i(TAG,"被点击了."); - if(firstClickTime>0){//说明这是第二次点击. - long secondTime = System.currentTimeMillis(); - long dtime = secondTime - firstClickTime; - if(dtime<500){ - //双击事件. - Log.i(TAG,"双击居中"); - int iv_width = iv_drag_view.getRight() - iv_drag_view.getLeft(); - iv_drag_view.layout(display.getWidth()/2-iv_width/2, iv_drag_view.getTop(), display.getWidth()/2+iv_width/2, iv_drag_view.getBottom()); - int lasty = iv_drag_view.getTop();//得到最后在离屏幕上方的距离 - int lastx = iv_drag_view.getLeft();//得到最后离屏幕左边的距离 - Editor editor = sp.edit(); - editor.putInt("lastx", lastx); - editor.putInt("lasty", lasty); - editor.commit(); - } - firstClickTime = 0;//将第一次点击的时间还原成0。 - return; - } else { - //第一次点击 - firstClickTime = System.currentTimeMillis();// 记录第一次点击的时间 - //新开一个线程,在这个子线程中如果是500毫秒内没有再点击就将第一次点击的时间设置为0 - new Thread(){ - public void run() { - try { - Thread.sleep(500); - firstClickTime = 0; - } catch (InterruptedException e) { - e.printStackTrace(); - } - }; - }.start(); - } - } - }); - } - } - ``` - -6. 触摸和双击同时发生时候的返回值 - ```java - //onTouch方法的返回值,True if the listener has consumed the event, false otherwise,true 监听器会把这个事件给消费掉, false 不会消费掉这个事件 - iv_drag_view.setOnTouchListener(new OnTouchListener() { - - int startX , startY; - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN:// 手指触摸到屏幕的事件 - Log.i(TAG,"摸到这个控件了"); - startX = (int) event.getRawX(); - startY = (int) event.getRawY(); - break; - case MotionEvent.ACTION_MOVE:// 手指在屏幕上移动的事件 - Log.i(TAG,"移动"); - int newX = (int) event.getRawX(); - int newY = (int) event.getRawY(); - int dx = newX - startX; - int dy = newY - startY; - int l = iv_drag_view.getLeft(); - int r = iv_drag_view.getRight(); - int b = iv_drag_view.getBottom(); - int t = iv_drag_view.getTop(); - - int newl = l+dx; - int newr = r+dx; - int newt = t+dy;//imageview 在窗体中新的位置 - int newb = b+dy; - - if(newl<0||newt < 0 ||newb>display.getHeight()-30||newr>display.getWidth()){ - break; - } - - int tv_height = tv_drag_view.getBottom() - tv_drag_view.getTop(); - - if(newt>display.getHeight()/2){//imageview在窗体的下方 - //textview在窗体的上方 - tv_drag_view.layout(tv_drag_view.getLeft(), 0, tv_drag_view.getRight(), tv_height); - }else{ - tv_drag_view.layout(tv_drag_view.getLeft(), display.getHeight()-tv_height-30, tv_drag_view.getRight(), display.getHeight()-30); - //textview在窗体的下方 - } - - iv_drag_view.layout(newl, newt, newr, newb); - - //更新手指开始的位置. - startX = (int) event.getRawX(); - startY = (int) event.getRawY(); - break; - case MotionEvent.ACTION_UP: // 手指在离开屏幕的一瞬间对应的事件. - Log.i(TAG,"放手"); - int lasty = iv_drag_view.getTop();//得到最后在离屏幕上方的距离 - int lastx = iv_drag_view.getLeft();//得到最后离屏幕左边的距离 - Editor editor = sp.edit(); - editor.putInt("lastx", lastx); - editor.putInt("lasty", lasty); - editor.commit(); - break; - } - // 这里对于触摸事件应该是返回true为什么这里返回false呢,因为这里这一个控件同时实现了点击和触摸这两个事件,如果返回true, - // 那么就不可能发生点击事件了,所以对于同时实现点击和触摸的控件返回值要为false - return false; - } - }); - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! +来电号码归属地提示框 +=== + +模仿Toast实现提示框 +--- + +Toast提示只要提示的时间够长,就可以浮动到其他任何界面之上,所以我们可以模仿Toast来实现来电号码归属地的提示框 + +1. WindowManager + The interface that apps use to talk to the window manager. Use Context.getSystemService(Context.WINDOW_SERVICE) to get one of these. + Each window manager instance is bound to a particular Display. + + 1. void addView(View view,ViewGroup.LayoutParams params) + 将一个View视图显示到当前窗口,LayoutParams are used by views to tell their parents how they want to be laid out. + + 2. void removeView(View view); + 将一个View视图从当前窗口中移除。 + + +2. 自定义窗体提示框(参考Toast源码) + ```java + WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); + View view = View.inflate(getApplicationContext(), R.layout.toast_location, + null); + TextView tv = (TextView) view.findViewById(R.id.tv_toast_address); + tv.setText(address); + LayoutParams params = new LayoutParams(); + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + params.width = WindowManager.LayoutParams.WRAP_CONTENT; + params.gravity = Gravity.LEFT | Gravity.TOP; + params.x = sp.getInt("lastx", 0); + params.y = sp.getInt("lasty", 0); + //本来还有一个FLAG_NOTUCHALBE为了让下面能触摸把这个给去掉了 + params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + params.format = PixelFormat.TRANSLUCENT; //源码中这里是TYPE_TAOST但是这里为了下面要进行点击拖动事件,而Toast不能拖动, + 所以这里改成了TYPE_PRIORITY_PHONE,这是一个系统类型的提示框,使用这个提示框必须要申请权限,android.permission.SYSTEM_ALERT_WINDOW + params.type = WindowManager.LayoutParams.TYPE_PRIORITY_PHONE; + wm.addView(view, params); + ``` + +3. WindowManager添加的显示框的简单拖动 + 该这个View注册一个onTouchListener + ```java + public void showLocation(String address) { + view = View.inflate(getApplicationContext(), R.layout.toast_location, + null); + // 得到sp + int which = sp.getInt("which", 0); + view.setBackgroundResource(bgs[which]); + view.setOnTouchListener(new OnTouchListener() { + int startX ,startY; + + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + Log.i(TAG,"摸到"); + startX = (int) event.getRawX(); + startY = (int) event.getRawY(); + break; + case MotionEvent.ACTION_MOVE: + Log.i(TAG,"移动"); + int newX = (int) event.getRawX(); + int newY = (int) event.getRawY(); + int dx = newX - startX; + int dy = newY - startY; + params.x+=dx; + params.y+=dy; //这里在WindowManager中不能够使用layout方法了,无效,只能使用layoutparams来更新位置,这里的params就是上面的那个params + wm.updateViewLayout(view, params); + //重新初始化 手指的位置 + startX = (int) event.getRawX(); + startY = (int) event.getRawY(); + break; + } + return true; + } + }); + } + ``` + +4. 普通ImageView随手指拖动改变位置 + ```java + iv_drag_view.setOnTouchListener(new OnTouchListener() { + //记录住最初手指按下时的位置 + int startX , startY; + //onTouch方法的返回值如果是true监听器会把这个事件给消费掉, false则监听器不会消费掉这个事件 + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + Log.i(TAG,"摸到这个控件了"); + startX = (int) event.getRawX();//记录手指第一次点击到屏幕时候距离x和y轴的距离 + startY = (int) event.getRawY(); + break; + case MotionEvent.ACTION_MOVE:// 手指在屏幕上移动的事件 + Log.i(TAG,"移动"); + int newX = (int) event.getRawX(); //在移动的过程中不断的获取到手指当前移动到的位置 + int newY = (int) event.getRawY(); + int dx = newX - startX; //计算出手指移动了多少 + int dy = newY - startY; + int l = iv_drag_view.getLeft(); //获取图片上下左右的长度 + int r = iv_drag_view.getRight(); + int b = iv_drag_view.getBottom(); + int t = iv_drag_view.getTop(); + + int newl = l+dx; //计算图片应该移动的距离 + int newr = r+dx; + int newt = t+dy;//imageview 在窗体中新的位置 + int newb = b+dy; + + //判断如果图片准备移动到的位置超出了屏幕就不让它移动,这里减去30是减去窗体上面的状态栏的高度 + if(newl<0||newt < 0 ||newb>display.getHeight()-30||newr>display.getWidth()){ + break; + } + //将图片移动到新的位置。直接调用ImageView的layout方法 + iv_drag_view.layout(newl, newt, newr, newb); + //一旦图片移动到新的位置就重新计算手指当前的位置,这样循环下去就能实现随着手指的拖动 + startX = (int) event.getRawX(); + startY = (int) event.getRawY(); + break; + case MotionEvent.ACTION_UP: // 手指在离开屏幕的一瞬间对应的事件. + Log.i(TAG,"放手"); + int lasty = iv_drag_view.getTop();//得到最后在离屏幕上方的距离 + int lastx = iv_drag_view.getLeft();//得到最后离屏幕左边的距离 + Editor editor = sp.edit(); + editor.putInt("lastx", lastx); + editor.putInt("lasty", lasty); + editor.commit(); + break; + } + return true; //这地方一定要返回true告诉系统这个事件做完了 + } + }); + ``` + + **注意:在onCreate方法中使用layout方法是没有效果的,因为在进入一个Activity中系统首先会执行一个计算的操作,计算各个控件的布局,然后调用setContentView方法显示出来这个控件,第二步才会执行这个layout方法,但是在onCreate方法中设置了layout,在执行layout这段代码的时候,窗体有可能还没有计算完控件的布局,所以先执行了这个layout,然后又执行了计算控件布局来显示,这样layout就没效了,这里要怎么弄呢只能是通过设置这个控件的layout布局,这样在计算位置的时候就能计算了,这样设置布局能让它在计算的时候就计算了。如下,在onCreate方法中去这样设置。** + + ```java + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sp = getSharedPreferences("config", MODE_PRIVATE); + // Have the system blur any windows behind this one. + getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + wm = (WindowManager) getSystemService(WINDOW_SERVICE);//窗体管理者 + display = wm.getDefaultDisplay(); + + setContentView(R.layout.activity_drag_view); + tv_drag_view = (TextView) findViewById(R.id.tv_drag_view); + iv_drag_view = (ImageView) findViewById(R.id.iv_drag_view); + + int lastx = sp.getInt("lastx", 0); + int lasty = sp.getInt("lasty", 0); + + RelativeLayout.LayoutParams params = (LayoutParams) iv_drag_view.getLayoutParams(); + params.leftMargin = lastx; + params.topMargin = lasty; + iv_drag_view.setLayoutParams(params); + } + ``` + + **注意:在WindowManager中要想更新控件的距离就不能用layout方法了,只能用mWindowManager.updateViewLayout(view, params);** + +5. 实现双击事件 + + 1. 双击的定义 + Android中没有提供双击的点击事件,双击就是单位时间内的两次点击 + + 2. 触摸和点击事件的区别 + 点击事件: 一组动作的集合 点击 - 停留 - 离开. + 触摸事件: 手指按下屏幕 手指在屏幕上移动 手指离开屏幕的一瞬间 + + ```java + public class DragViewActivity extends Activity { + protected static final String TAG = "DragViewActivity"; + private ImageView iv_drag_view; + private TextView tv_drag_view; + private SharedPreferences sp; + + private WindowManager wm; + private Display display; //窗体的显示的分辨率 + + private long firstClickTime;//第一次点击时候的事件 + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sp = getSharedPreferences("config", MODE_PRIVATE); + // Have the system blur any windows behind this one. + getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + wm = (WindowManager) getSystemService(WINDOW_SERVICE);//窗体管理者 + display = wm.getDefaultDisplay(); + + setContentView(R.layout.activity_drag_view); + tv_drag_view = (TextView) findViewById(R.id.tv_drag_view); + iv_drag_view = (ImageView) findViewById(R.id.iv_drag_view); + + int lastx = sp.getInt("lastx", 0); + int lasty = sp.getInt("lasty", 0); + + RelativeLayout.LayoutParams params = (LayoutParams) iv_drag_view.getLayoutParams(); + params.leftMargin = lastx; + params.topMargin = lasty; + iv_drag_view.setLayoutParams(params); + + iv_drag_view.setOnClickListener(new OnClickListener() { + + public void onClick(View v) { + Log.i(TAG,"被点击了."); + if(firstClickTime>0){//说明这是第二次点击. + long secondTime = System.currentTimeMillis(); + long dtime = secondTime - firstClickTime; + if(dtime<500){ + //双击事件. + Log.i(TAG,"双击居中"); + int iv_width = iv_drag_view.getRight() - iv_drag_view.getLeft(); + iv_drag_view.layout(display.getWidth()/2-iv_width/2, iv_drag_view.getTop(), display.getWidth()/2+iv_width/2, iv_drag_view.getBottom()); + int lasty = iv_drag_view.getTop();//得到最后在离屏幕上方的距离 + int lastx = iv_drag_view.getLeft();//得到最后离屏幕左边的距离 + Editor editor = sp.edit(); + editor.putInt("lastx", lastx); + editor.putInt("lasty", lasty); + editor.commit(); + } + firstClickTime = 0;//将第一次点击的时间还原成0。 + return; + } else { + //第一次点击 + firstClickTime = System.currentTimeMillis();// 记录第一次点击的时间 + //新开一个线程,在这个子线程中如果是500毫秒内没有再点击就将第一次点击的时间设置为0 + new Thread(){ + public void run() { + try { + Thread.sleep(500); + firstClickTime = 0; + } catch (InterruptedException e) { + e.printStackTrace(); + } + }; + }.start(); + } + } + }); + } + } + ``` + +6. 触摸和双击同时发生时候的返回值 + ```java + //onTouch方法的返回值,True if the listener has consumed the event, false otherwise,true 监听器会把这个事件给消费掉, false 不会消费掉这个事件 + iv_drag_view.setOnTouchListener(new OnTouchListener() { + + int startX , startY; + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN:// 手指触摸到屏幕的事件 + Log.i(TAG,"摸到这个控件了"); + startX = (int) event.getRawX(); + startY = (int) event.getRawY(); + break; + case MotionEvent.ACTION_MOVE:// 手指在屏幕上移动的事件 + Log.i(TAG,"移动"); + int newX = (int) event.getRawX(); + int newY = (int) event.getRawY(); + int dx = newX - startX; + int dy = newY - startY; + int l = iv_drag_view.getLeft(); + int r = iv_drag_view.getRight(); + int b = iv_drag_view.getBottom(); + int t = iv_drag_view.getTop(); + + int newl = l+dx; + int newr = r+dx; + int newt = t+dy;//imageview 在窗体中新的位置 + int newb = b+dy; + + if(newl<0||newt < 0 ||newb>display.getHeight()-30||newr>display.getWidth()){ + break; + } + + int tv_height = tv_drag_view.getBottom() - tv_drag_view.getTop(); + + if(newt>display.getHeight()/2){//imageview在窗体的下方 + //textview在窗体的上方 + tv_drag_view.layout(tv_drag_view.getLeft(), 0, tv_drag_view.getRight(), tv_height); + }else{ + tv_drag_view.layout(tv_drag_view.getLeft(), display.getHeight()-tv_height-30, tv_drag_view.getRight(), display.getHeight()-30); + //textview在窗体的下方 + } + + iv_drag_view.layout(newl, newt, newr, newb); + + //更新手指开始的位置. + startX = (int) event.getRawX(); + startY = (int) event.getRawY(); + break; + case MotionEvent.ACTION_UP: // 手指在离开屏幕的一瞬间对应的事件. + Log.i(TAG,"放手"); + int lasty = iv_drag_view.getTop();//得到最后在离屏幕上方的距离 + int lastx = iv_drag_view.getLeft();//得到最后离屏幕左边的距离 + Editor editor = sp.edit(); + editor.putInt("lastx", lastx); + editor.putInt("lasty", lasty); + editor.commit(); + break; + } + // 这里对于触摸事件应该是返回true为什么这里返回false呢,因为这里这一个控件同时实现了点击和触摸这两个事件,如果返回true, + // 那么就不可能发生点击事件了,所以对于同时实现点击和触摸的控件返回值要为false + return false; + } + }); + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\237\272\347\241\200/\346\235\245\347\224\265\347\233\221\345\220\254\345\217\212\345\275\225\351\237\263.md" "b/AndroidBasicPart/\346\235\245\347\224\265\347\233\221\345\220\254\345\217\212\345\275\225\351\237\263.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\346\235\245\347\224\265\347\233\221\345\220\254\345\217\212\345\275\225\351\237\263.md" rename to "AndroidBasicPart/\346\235\245\347\224\265\347\233\221\345\220\254\345\217\212\345\275\225\351\237\263.md" diff --git "a/Android\345\237\272\347\241\200/\346\250\252\345\220\221ListView.md" "b/AndroidBasicPart/\346\250\252\345\220\221ListView.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\346\250\252\345\220\221ListView.md" rename to "AndroidBasicPart/\346\250\252\345\220\221ListView.md" diff --git "a/Android\345\237\272\347\241\200/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" "b/AndroidBasicPart/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" similarity index 97% rename from "Android\345\237\272\347\241\200/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" rename to "AndroidBasicPart/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" index 6fb635dd..4455bb47 100644 --- "a/Android\345\237\272\347\241\200/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" +++ "b/AndroidBasicPart/\346\273\221\345\212\250\345\210\207\346\215\242Activity(GestureDetector).md" @@ -1,138 +1,138 @@ -滑动切换Activity(GestureDetector) -=== - -1. 实现手势滑动切换`Activity` - - 创建一个手势识别器(`GestureDetector`) - - 在`Activity`的`onTouchEvent`中去使用该手势识别器 - ```java - public abstract class SetupBaseActivity extends Activity { - protected SharedPreferences sp; - protected GestureDetector mGestureDetector; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sp =getSharedPreferences("config", MODE_PRIVATE); - initView(); - setupView(); - //1.创建一个手势识别器 new 对象,并给这个手势识别器设置监听器 - mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener(){ - //当手指在屏幕上滑动的时候 调用的方法. - @Override - //e1代表的是手指刚开始滑动的事件,e2代表手指滑动完了的事件 - public boolean onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY) { - if(e1.getRawX() - e2.getRawX() > 200){ - showNext();//向右滑动,显示下一个界面 - return true; - } - - if(e2.getRawX() - e1.getRawX() > 200){ - showPre();//向左滑动,显示上一个界面 - return true; - } - return super.onFling(e1, e2, velocityX, velocityY); - } - }); - } - - //2.让手势识别器生效,重写Activity的触摸事件,并且将Activity的触摸事件传入到手势识别器中 - @Override - public boolean onTouchEvent(MotionEvent event) { - mGestureDetector.onTouchEvent(event); - return super.onTouchEvent(event); - } - } - ``` - -2. 实现切换效果 - 经过上一步已经实现了滑动界面的切换,但是切换界面时的效果不好看,我们需要自定义切换的效果 - - - 在res目录下面新建一个anim文件夹在这个文件夹中新建动画效果 - tran_next_in.xml//下一个界面进入的样式 - tran_next_out.xml//下一个界面进入时当前页面出去的样式 - tran_pre_in.xml//上一个界面进入的样式 - tran_pre_out.xml//上一个界面进入时当前页面出去的样式 - - - tran_next_in.xml里面的内容 - ```xml - - - - ``` - - - tran_next_out.xml里面的内容 - ```xml - - - - ``` - - - tran_pre_in.xml里面的内容 - ```xml - - - - ``` - - - tran_pre_out.xml里面的内容 - ```xml - - - - ``` - - - 让`Activity`在创建和销毁时使用上面自定义的动画 - - `public void overridePendingTransition(int enterAnim, int exitAnim);` - `Call immediately after one of the flavors of startActivity(Intent) or finish() to specify an explicit transition animation to perform next.` - `Parameters:` - `enterAnim - A resource ID of the animation resource to use for the incoming activity. Use 0 for no animation.` - `exitAnim - A resource ID of the animation resource to use for the outgoing activity. Use 0 for no animation.` - - ```java - public void showNext() { - Intent intent = new Intent(this, Setup3Activity.class); - startActivity(intent); - finish(); - //调用此方法让动画效果生效 - overridePendingTransition(R.anim.tran_next_in, R.anim.tran_next_out); - } - - public void showPre() { - Intent intent = new Intent(this, Setup1Activity.class); - startActivity(intent); - finish(); - overridePendingTransition(R.anim.tran_pre_in, R.anim.tran_pre_out); - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - +滑动切换Activity(GestureDetector) +=== + +1. 实现手势滑动切换`Activity` + - 创建一个手势识别器(`GestureDetector`) + - 在`Activity`的`onTouchEvent`中去使用该手势识别器 + ```java + public abstract class SetupBaseActivity extends Activity { + protected SharedPreferences sp; + protected GestureDetector mGestureDetector; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sp =getSharedPreferences("config", MODE_PRIVATE); + initView(); + setupView(); + //1.创建一个手势识别器 new 对象,并给这个手势识别器设置监听器 + mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener(){ + //当手指在屏幕上滑动的时候 调用的方法. + @Override + //e1代表的是手指刚开始滑动的事件,e2代表手指滑动完了的事件 + public boolean onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY) { + if(e1.getRawX() - e2.getRawX() > 200){ + showNext();//向右滑动,显示下一个界面 + return true; + } + + if(e2.getRawX() - e1.getRawX() > 200){ + showPre();//向左滑动,显示上一个界面 + return true; + } + return super.onFling(e1, e2, velocityX, velocityY); + } + }); + } + + //2.让手势识别器生效,重写Activity的触摸事件,并且将Activity的触摸事件传入到手势识别器中 + @Override + public boolean onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + return super.onTouchEvent(event); + } + } + ``` + +2. 实现切换效果 + 经过上一步已经实现了滑动界面的切换,但是切换界面时的效果不好看,我们需要自定义切换的效果 + + - 在res目录下面新建一个anim文件夹在这个文件夹中新建动画效果 + tran_next_in.xml//下一个界面进入的样式 + tran_next_out.xml//下一个界面进入时当前页面出去的样式 + tran_pre_in.xml//上一个界面进入的样式 + tran_pre_out.xml//上一个界面进入时当前页面出去的样式 + + - tran_next_in.xml里面的内容 + ```xml + + + + ``` + + - tran_next_out.xml里面的内容 + ```xml + + + + ``` + + - tran_pre_in.xml里面的内容 + ```xml + + + + ``` + + - tran_pre_out.xml里面的内容 + ```xml + + + + ``` + + - 让`Activity`在创建和销毁时使用上面自定义的动画 + + `public void overridePendingTransition(int enterAnim, int exitAnim);` + `Call immediately after one of the flavors of startActivity(Intent) or finish() to specify an explicit transition animation to perform next.` + `Parameters:` + `enterAnim - A resource ID of the animation resource to use for the incoming activity. Use 0 for no animation.` + `exitAnim - A resource ID of the animation resource to use for the outgoing activity. Use 0 for no animation.` + + ```java + public void showNext() { + Intent intent = new Intent(this, Setup3Activity.class); + startActivity(intent); + finish(); + //调用此方法让动画效果生效 + overridePendingTransition(R.anim.tran_next_in, R.anim.tran_next_out); + } + + public void showPre() { + Intent intent = new Intent(this, Setup1Activity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.tran_pre_in, R.anim.tran_pre_out); + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\347\227\205\346\257\222.md" "b/AndroidBasicPart/\347\227\205\346\257\222.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\347\227\205\346\257\222.md" rename to "AndroidBasicPart/\347\227\205\346\257\222.md" index 97c23a1a..eb560925 100644 --- "a/Android\345\237\272\347\241\200/\347\227\205\346\257\222.md" +++ "b/AndroidBasicPart/\347\227\205\346\257\222.md" @@ -1,146 +1,146 @@ -病毒 -=== - -病毒:一个特殊计算机程序. -对于病毒的查杀都是基于特征码的识别.杀毒软件都需要有一个病毒信息的数据库.常用病毒数据库2000万条 -杀毒引擎: 一套复杂高效的数据库查询算法. -1. 提取文件特征码 1秒 -2. 查询特征码是否在数据库里面 8秒 -就是根据文件的特征码去病毒数据库中查询,如果能找到就说明这个文件是一个病毒 - -病毒的查杀 -下面以一个文件的MD5编码后的签名来判断是否是病毒 - -```java -public class AntiVirusActivity extends Activity { - private ImageView iv_scan; - private PackageManager pm; - private TextView scan_status; - private ProgressBar pb; - private LinearLayout ll_container; - private List virusInfos; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_anti_virus); - iv_scan = (ImageView) findViewById(R.id.iv_scan); - RotateAnimation ra = new RotateAnimation(0, 360, - Animation.RELATIVE_TO_SELF, 1.0f, Animation.RELATIVE_TO_SELF, - 1.0f); - ra.setDuration(800); - ra.setRepeatCount(Animation.INFINITE); - ra.setRepeatMode(Animation.RESTART); - iv_scan.startAnimation(ra); - pb = (ProgressBar) findViewById(R.id.progressBar1); - scan_status = (TextView) findViewById(R.id.scan_status); - ll_container = (LinearLayout) findViewById(R.id.ll_container); - pm = getPackageManager(); - virusInfos = new ArrayList(); - new AsyncTask() { - - @Override - protected Void doInBackground(Void... params) { - // 检查应用程序的签名 是否在病毒数据库里面. - - try { - Thread.sleep(500); - List infos = pm - .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES - | PackageManager.GET_SIGNATURES); - pb.setMax(infos.size()); - int total = 0; - for (PackageInfo info : infos) { - Signature[] signatures = info.signatures; - // 得到应用程序的签名信息, 用签名来判断 - String sign = signatures[0].toCharsString(); - String md5 = MD5Utils.encode(sign); - String result = VirusDao.findVirsu(md5); - if (result != null) { - publishProgress(info, true); - virusInfos.add(info); - } else { - publishProgress(info, false); - } - total++; - pb.setProgress(total); - Thread.sleep(40); - } - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return null; - } - - @Override - protected void onPreExecute() { - scan_status.setText("正在初始化双核杀毒引擎..."); - super.onPreExecute(); - } - - @Override - protected void onPostExecute(Void result) { - scan_status.setText("扫描完毕!"); - iv_scan.clearAnimation(); - - if (virusInfos.size() > 0) { - AlertDialog.Builder builder = new Builder( - AntiVirusActivity.this); - builder.setTitle("发现病毒"); - builder.setMessage("是否立刻清理?"); - builder.setPositiveButton("确定", new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - for (PackageInfo info : virusInfos) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_DELETE); - intent.setData(Uri.parse("package:" - + info.packageName)); - startActivity(intent); - } - } - }); - builder.setNegativeButton("取消", new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - // TODO Auto-generated method stub - - } - }); - builder.show(); - } - - super.onPostExecute(result); - } - - @Override - protected void onProgressUpdate(Object... values) { - PackageInfo packinfo = (PackageInfo) values[0]; - Boolean result = (Boolean) values[1]; - scan_status.setText("正在扫描:" - + packinfo.applicationInfo.loadLabel(pm)); - TextView tv = new TextView(getApplicationContext()); - tv.setTextSize(16); - if (result) {// 发现病毒 - tv.setTextColor(Color.RED); - tv.setText("发现病毒程序:" - + packinfo.applicationInfo.loadLabel(pm)); - } else { - tv.setTextColor(Color.BLACK); - tv.setText("扫描安全:" + packinfo.applicationInfo.loadLabel(pm)); - } - ll_container.addView(tv, 0); - super.onProgressUpdate(values); - } - - }.execute(); - } -} -``` - ----- -- 邮箱 :charon.chui@gmail.com +病毒 +=== + +病毒:一个特殊计算机程序. +对于病毒的查杀都是基于特征码的识别.杀毒软件都需要有一个病毒信息的数据库.常用病毒数据库2000万条 +杀毒引擎: 一套复杂高效的数据库查询算法. +1. 提取文件特征码 1秒 +2. 查询特征码是否在数据库里面 8秒 +就是根据文件的特征码去病毒数据库中查询,如果能找到就说明这个文件是一个病毒 + +病毒的查杀 +下面以一个文件的MD5编码后的签名来判断是否是病毒 + +```java +public class AntiVirusActivity extends Activity { + private ImageView iv_scan; + private PackageManager pm; + private TextView scan_status; + private ProgressBar pb; + private LinearLayout ll_container; + private List virusInfos; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_anti_virus); + iv_scan = (ImageView) findViewById(R.id.iv_scan); + RotateAnimation ra = new RotateAnimation(0, 360, + Animation.RELATIVE_TO_SELF, 1.0f, Animation.RELATIVE_TO_SELF, + 1.0f); + ra.setDuration(800); + ra.setRepeatCount(Animation.INFINITE); + ra.setRepeatMode(Animation.RESTART); + iv_scan.startAnimation(ra); + pb = (ProgressBar) findViewById(R.id.progressBar1); + scan_status = (TextView) findViewById(R.id.scan_status); + ll_container = (LinearLayout) findViewById(R.id.ll_container); + pm = getPackageManager(); + virusInfos = new ArrayList(); + new AsyncTask() { + + @Override + protected Void doInBackground(Void... params) { + // 检查应用程序的签名 是否在病毒数据库里面. + + try { + Thread.sleep(500); + List infos = pm + .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES + | PackageManager.GET_SIGNATURES); + pb.setMax(infos.size()); + int total = 0; + for (PackageInfo info : infos) { + Signature[] signatures = info.signatures; + // 得到应用程序的签名信息, 用签名来判断 + String sign = signatures[0].toCharsString(); + String md5 = MD5Utils.encode(sign); + String result = VirusDao.findVirsu(md5); + if (result != null) { + publishProgress(info, true); + virusInfos.add(info); + } else { + publishProgress(info, false); + } + total++; + pb.setProgress(total); + Thread.sleep(40); + } + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + @Override + protected void onPreExecute() { + scan_status.setText("正在初始化双核杀毒引擎..."); + super.onPreExecute(); + } + + @Override + protected void onPostExecute(Void result) { + scan_status.setText("扫描完毕!"); + iv_scan.clearAnimation(); + + if (virusInfos.size() > 0) { + AlertDialog.Builder builder = new Builder( + AntiVirusActivity.this); + builder.setTitle("发现病毒"); + builder.setMessage("是否立刻清理?"); + builder.setPositiveButton("确定", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + for (PackageInfo info : virusInfos) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_DELETE); + intent.setData(Uri.parse("package:" + + info.packageName)); + startActivity(intent); + } + } + }); + builder.setNegativeButton("取消", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + // TODO Auto-generated method stub + + } + }); + builder.show(); + } + + super.onPostExecute(result); + } + + @Override + protected void onProgressUpdate(Object... values) { + PackageInfo packinfo = (PackageInfo) values[0]; + Boolean result = (Boolean) values[1]; + scan_status.setText("正在扫描:" + + packinfo.applicationInfo.loadLabel(pm)); + TextView tv = new TextView(getApplicationContext()); + tv.setTextSize(16); + if (result) {// 发现病毒 + tv.setTextColor(Color.RED); + tv.setText("发现病毒程序:" + + packinfo.applicationInfo.loadLabel(pm)); + } else { + tv.setTextColor(Color.BLACK); + tv.setText("扫描安全:" + packinfo.applicationInfo.loadLabel(pm)); + } + ll_container.addView(tv, 0); + super.onProgressUpdate(values); + } + + }.execute(); + } +} +``` + +---- +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" "b/AndroidBasicPart/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" rename to "AndroidBasicPart/\347\237\245\350\257\206\345\244\247\346\235\202\347\203\251.md" diff --git "a/Android\345\237\272\347\241\200/\347\237\255\344\277\241\345\271\277\346\222\255\346\216\245\346\224\266\350\200\205.md" "b/AndroidBasicPart/\347\237\255\344\277\241\345\271\277\346\222\255\346\216\245\346\224\266\350\200\205.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\347\237\255\344\277\241\345\271\277\346\222\255\346\216\245\346\224\266\350\200\205.md" rename to "AndroidBasicPart/\347\237\255\344\277\241\345\271\277\346\222\255\346\216\245\346\224\266\350\200\205.md" diff --git "a/Android\345\237\272\347\241\200/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" "b/AndroidBasicPart/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" rename to "AndroidBasicPart/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" index f6b1d823..02428d28 100644 --- "a/Android\345\237\272\347\241\200/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" +++ "b/AndroidBasicPart/\347\250\213\345\272\217\347\232\204\345\220\257\345\212\250\343\200\201\345\215\270\350\275\275\345\222\214\345\210\206\344\272\253.md" @@ -1,76 +1,76 @@ -程序的启动、卸载和分享 -=== - -1. 启动 - - ```java - /** - * 开启一个应用程序 - */ - private void startApk() { - PackageManager pm = getPackageManager(); - try { - // 原来的时候我们在得到PakageInfo的时候第二个参数都是设置为0.这个PackageInfo代表的就是某个程序的清单文件, - // 默认情况下在解析这个清单文件的时候得到的只是清单文件中的一些版本信息的等这些常用的内容,因为要获取更多的内容需要解析更多的内容, - // 就会消耗时间消耗资源,所以默认的时候都是只解析一些常用的,当我们要获取Activity等这些的时候就要给它一个标记,让它知道多解析这些你想要得到的内容, - // 如果我们想得到里面的activity或者service等这些啊就必须将第二个参数设置为相应的PackageManager.GET_ACTIVITYS等 - PackageInfo info = pm.getPackageInfo(selectedAppInfo.getPackname(),PackageManager.GET_ACTIVITIES); - ActivityInfo[] activityInfos = info.activities;//获取清单中所有Activity信息的数据 - if (activityInfos != null && activityInfos.length > 0) {//由于一些服务或者接收者等没有Activity所以这里必须进行判断 - ActivityInfo activitInfo = activityInfos[0];//清单文件中配置的第一个Activity就是程序的启动Activity - Intent intent = new Intent(); - intent.setClassName(selectedAppInfo.getPackname(),activitInfo.name);//这个activityInfo就是清单中activity节点的name,这样就能得到Activity的全类名 - startActivity(intent); - } else { - Toast.makeText(this, "无法启动应用程序", 0).show(); - } - } - } - ``` - -2. 卸载 - - ```java - 安卓系统提供了程序的卸载Activity,我们只要调用它的卸载就可以了,也是系统的PackageInstaller中的 - Intent intent = new Intent(); - intent.setAction("android.intent.action.VIEW"); - intent.setAction("android.intent.action.DELETE"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.setData(Uri.parse("package:" + selectedAppInfo.getPackname()));//意图的数据必须是package://和包名 - startActivity(intent); - ``` - -3. 分享 - - 就是启动出来信息的发送页面,将内容给填充进去所以这里要启动系统发送短信的Activity,要用到系统发送短信的Activity - - ```java - /** - * 分享一个应用程序 - */ - private void shareApk() { - Intent intent = new Intent(); - // - // - // - // - // - intent.setAction(Intent.ACTION_SEND); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setType("text/plain"); - intent.putExtra( - Intent.EXTRA_TEXT, - "推荐你使用一款软件,名称为" + selectedAppInfo.getAppName() - + "下载地址:google play xxx,版本:" - + selectedAppInfo.getVersion()); - startActivity(intent); - } - ``` - - 谷歌工程师在设计这个程序的时候,任何应用程序如果想使用分享的功能都可以通过实现它的Intent来实现,点击的时候可以选择不同的程序,同样也能分享到微博, - 邮件等程序 - ---- - -- 邮箱 :charon.chui@gmail.com +程序的启动、卸载和分享 +=== + +1. 启动 + + ```java + /** + * 开启一个应用程序 + */ + private void startApk() { + PackageManager pm = getPackageManager(); + try { + // 原来的时候我们在得到PakageInfo的时候第二个参数都是设置为0.这个PackageInfo代表的就是某个程序的清单文件, + // 默认情况下在解析这个清单文件的时候得到的只是清单文件中的一些版本信息的等这些常用的内容,因为要获取更多的内容需要解析更多的内容, + // 就会消耗时间消耗资源,所以默认的时候都是只解析一些常用的,当我们要获取Activity等这些的时候就要给它一个标记,让它知道多解析这些你想要得到的内容, + // 如果我们想得到里面的activity或者service等这些啊就必须将第二个参数设置为相应的PackageManager.GET_ACTIVITYS等 + PackageInfo info = pm.getPackageInfo(selectedAppInfo.getPackname(),PackageManager.GET_ACTIVITIES); + ActivityInfo[] activityInfos = info.activities;//获取清单中所有Activity信息的数据 + if (activityInfos != null && activityInfos.length > 0) {//由于一些服务或者接收者等没有Activity所以这里必须进行判断 + ActivityInfo activitInfo = activityInfos[0];//清单文件中配置的第一个Activity就是程序的启动Activity + Intent intent = new Intent(); + intent.setClassName(selectedAppInfo.getPackname(),activitInfo.name);//这个activityInfo就是清单中activity节点的name,这样就能得到Activity的全类名 + startActivity(intent); + } else { + Toast.makeText(this, "无法启动应用程序", 0).show(); + } + } + } + ``` + +2. 卸载 + + ```java + 安卓系统提供了程序的卸载Activity,我们只要调用它的卸载就可以了,也是系统的PackageInstaller中的 + Intent intent = new Intent(); + intent.setAction("android.intent.action.VIEW"); + intent.setAction("android.intent.action.DELETE"); + intent.addCategory("android.intent.category.DEFAULT"); + intent.setData(Uri.parse("package:" + selectedAppInfo.getPackname()));//意图的数据必须是package://和包名 + startActivity(intent); + ``` + +3. 分享 + + 就是启动出来信息的发送页面,将内容给填充进去所以这里要启动系统发送短信的Activity,要用到系统发送短信的Activity + + ```java + /** + * 分享一个应用程序 + */ + private void shareApk() { + Intent intent = new Intent(); + // + // + // + // + // + intent.setAction(Intent.ACTION_SEND); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setType("text/plain"); + intent.putExtra( + Intent.EXTRA_TEXT, + "推荐你使用一款软件,名称为" + selectedAppInfo.getAppName() + + "下载地址:google play xxx,版本:" + + selectedAppInfo.getVersion()); + startActivity(intent); + } + ``` + + 谷歌工程师在设计这个程序的时候,任何应用程序如果想使用分享的功能都可以通过实现它的Intent来实现,点击的时候可以选择不同的程序,同样也能分享到微博, + 邮件等程序 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\347\253\226\347\235\200\347\232\204Seekbar.md" "b/AndroidBasicPart/\347\253\226\347\235\200\347\232\204Seekbar.md" similarity index 96% rename from "Android\345\237\272\347\241\200/\347\253\226\347\235\200\347\232\204Seekbar.md" rename to "AndroidBasicPart/\347\253\226\347\235\200\347\232\204Seekbar.md" index 350cdcae..58a19761 100644 --- "a/Android\345\237\272\347\241\200/\347\253\226\347\235\200\347\232\204Seekbar.md" +++ "b/AndroidBasicPart/\347\253\226\347\235\200\347\232\204Seekbar.md" @@ -1,80 +1,80 @@ -竖着的Seekbar -=== - -视频播放器页面音量控制`Seekbar`实现竖直的效果。竖直只是将`Seekbar`转了90度或-90度,我们可以把画布转一个角度,然后交给系统去画, -具体的做法就是重写`ondraw()`调整画布,然后调用`super.onDraw()`。 - -- 向上的Seekbar -```java - protected void onDraw(Canvas c) { - c.rotate(-90); - c.translate(-height,0);//height是你的verticalseekbar的高 - super.onDraw(c); - } -``` - -- 向下的seekbar则应该是: -```java - protected void onDraw(Canvas c) { - c.rotate(90); - c.translate(0,-width);//width是你的verticalseekbar的宽 - super.onDraw(c); - } -``` -- 示例代码 -```java - public class VerticalSeekBar extends SeekBar { - public VerticalSeekBar(Context context) { - super(context); - } - public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - public VerticalSeekBar(Context context, AttributeSet attrs) { - super(context, attrs); - } - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(h, w, oldh, oldw); - } - - @Override - protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(heightMeasureSpec, widthMeasureSpec); - setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); - } - - protected void onDraw(Canvas c) { - c.rotate(-90); - c.translate(-getHeight(), 0); - super.onDraw(c); - } - - @Override - public synchronized void setProgress(int progress) { - super.setProgress(progress); - onSizeChanged(getWidth(), getHeight(), 0, 0); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!isEnabled()) { - return false; - } - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_MOVE: - case MotionEvent.ACTION_UP: - setProgress((int) ((int) getMax()- (getMax() * event.getY() / getHeight()))); - break; - default: - return super.onTouchEvent(event); - } - return true; - } - } -``` - ---- - -- 邮箱 :charon.chui@gmail.com +竖着的Seekbar +=== + +视频播放器页面音量控制`Seekbar`实现竖直的效果。竖直只是将`Seekbar`转了90度或-90度,我们可以把画布转一个角度,然后交给系统去画, +具体的做法就是重写`ondraw()`调整画布,然后调用`super.onDraw()`。 + +- 向上的Seekbar +```java + protected void onDraw(Canvas c) { + c.rotate(-90); + c.translate(-height,0);//height是你的verticalseekbar的高 + super.onDraw(c); + } +``` + +- 向下的seekbar则应该是: +```java + protected void onDraw(Canvas c) { + c.rotate(90); + c.translate(0,-width);//width是你的verticalseekbar的宽 + super.onDraw(c); + } +``` +- 示例代码 +```java + public class VerticalSeekBar extends SeekBar { + public VerticalSeekBar(Context context) { + super(context); + } + public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + public VerticalSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(h, w, oldh, oldw); + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(heightMeasureSpec, widthMeasureSpec); + setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); + } + + protected void onDraw(Canvas c) { + c.rotate(-90); + c.translate(-getHeight(), 0); + super.onDraw(c); + } + + @Override + public synchronized void setProgress(int progress) { + super.setProgress(progress); + onSizeChanged(getWidth(), getHeight(), 0, 0); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) { + return false; + } + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_UP: + setProgress((int) ((int) getMax()- (getMax() * event.getY() / getHeight()))); + break; + default: + return super.onTouchEvent(event); + } + return true; + } + } +``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211Toast.md" "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211Toast.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211Toast.md" rename to "AndroidBasicPart/\350\207\252\345\256\232\344\271\211Toast.md" index d1843b02..6ac89c34 100644 --- "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211Toast.md" +++ "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211Toast.md" @@ -1,186 +1,186 @@ -自定义Toast -=== - -系统`Toast`提示时不能够进行取消,如果有多个`Toast`时会很长时间才消失。自定义`Toast`通过`WindowManager`来进行手动的控制`Toast`的显示与隐藏。能有效的解决该问题。 - -`Toast`提示的布局 -```xml - - - - - -``` - -```java -/** - * 吐司提示的工具类,能够控制吐司的显示和隐藏 - */ -public class ToastUtil { - public static final int LENGTH_SHORT = 0; - public static final int LENGTH_LONG = 1; - private static View toastView; - private WindowManager mWindowManager; - private static int mDuration; - private final int WHAT = 100; - private static View oldView; - private static Toast toast; - private static CharSequence oldText; - private static CharSequence currentText; - private static ToastUtil instance = null; - private static TextView textView; - - private ToastUtil(Context context) { - mWindowManager = (WindowManager) context.getApplicationContext() - .getSystemService(Context.WINDOW_SERVICE); - toastView = LayoutInflater.from(context).inflate(R.layout.toast_view, - null); - textView = (TextView) toastView.findViewById(R.id.toast_text); - toast = Toast.makeText(context, "", Toast.LENGTH_SHORT); - } - private static ToastUtil getInstance(Context context) { - if (instance == null) { - synchronized (ToastUtil.class) { - if (instance == null) - instance = new ToastUtil(context); - } - } - return instance; - } - public static ToastUtil makeText(Context context, CharSequence text, - int duration) { - ToastUtil util = getInstance(context); - mDuration = duration; - toast.setText(text); - currentText = text; - textView.setText(text); - return util; - } - public static ToastUtil makeText(Context context, int resId, int duration{ - ToastUtil util = getInstance(context); - mDuration = duration; - toast.setText(resId); - currentText = context.getResources().getString(resId); - textView.setText(context.getResources().getString(resId)); - return util; - } - /** - * 进行Toast显示,在显示之前会取消当前已经存在的Toast - */ - public void show() { - long time = 0; - switch (mDuration) { - case LENGTH_SHORT: - time = 2000; - break; - case LENGTH_LONG: - time = 3500; - break; - default: - time = 2000; - break; - } - if (currentText.equals(oldText) && oldView.getParent() != null) { - toastHandler.removeMessages(WHAT); - toastView = oldView; - oldText = currentText; - toastHandler.sendEmptyMessageDelayed(WHAT, time); - return; - } - cancelOldAlert(); - toastHandler.removeMessages(WHAT); - WindowManager.LayoutParams params = new WindowManager.LayoutParams(); - params.height = WindowManager.LayoutParams.WRAP_CONTENT; - params.width = WindowManager.LayoutParams.WRAP_CONTENT; - params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - params.format = PixelFormat.TRANSLUCENT; - params.windowAnimations = android.R.style.Animation_Toast; - params.type = WindowManager.LayoutParams.TYPE_TOAST; - params.setTitle("Toast"); - params.gravity = toast.getGravity(); - params.y = toast.getYOffset(); - if (toastView.getParent() == null) { - mWindowManager.addView(toastView, params); - } - oldView = toastView; - oldText = currentText; - toastHandler.sendEmptyMessageDelayed(WHAT, time); - } - private Handler toastHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - super.handleMessage(msg); - cancelOldAlert(); - int id = msg.what; - if (WHAT == id) { - cancelCurrentAlert(); - } - } - }; - private void cancelOldAlert() { - if (oldView != null && oldView.getParent() != null) { - mWindowManager.removeView(oldView); - } - } - public void cancelCurrentAlert() { - if (toastView != null && toastView.getParent() != null) { - mWindowManager.removeView(toastView); - } - } -} -``` - - -在某些Pad上面Toast显示出来后就不会自动消失,在这些Pad上`toastView.getParent()会为nul`这样就导致无法移除。可以将`cancelOldAlert()`以及 -`cancelCurrentAlert()`进行如下修改。 - -```java -private void cancelOldAlert() { - if (oldView != null) { // 去掉 oldView.getParent() != null 这个参数,然后加上try catch代码块,解决在部分Pad上oldView.getParent()不准确的问题 - try { - mWindowManager.removeView(oldView); - } catch (Exception e) { - e.printStackTrace(); - } - } -} - -public void cancelCurrentAlert() { - if (toastView != null) { - try { - // 去掉 oldView.getParent() != null 这个参数,然后加上try catch代码块,解决在部分Pad上oldView.getParent()不准确的问题 - mWindowManager.removeView(toastView); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (oldView != null) { - try { - mWindowManager.removeView(oldView); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - ----- -- 邮箱 :charon.chui@gmail.com +自定义Toast +=== + +系统`Toast`提示时不能够进行取消,如果有多个`Toast`时会很长时间才消失。自定义`Toast`通过`WindowManager`来进行手动的控制`Toast`的显示与隐藏。能有效的解决该问题。 + +`Toast`提示的布局 +```xml + + + + + +``` + +```java +/** + * 吐司提示的工具类,能够控制吐司的显示和隐藏 + */ +public class ToastUtil { + public static final int LENGTH_SHORT = 0; + public static final int LENGTH_LONG = 1; + private static View toastView; + private WindowManager mWindowManager; + private static int mDuration; + private final int WHAT = 100; + private static View oldView; + private static Toast toast; + private static CharSequence oldText; + private static CharSequence currentText; + private static ToastUtil instance = null; + private static TextView textView; + + private ToastUtil(Context context) { + mWindowManager = (WindowManager) context.getApplicationContext() + .getSystemService(Context.WINDOW_SERVICE); + toastView = LayoutInflater.from(context).inflate(R.layout.toast_view, + null); + textView = (TextView) toastView.findViewById(R.id.toast_text); + toast = Toast.makeText(context, "", Toast.LENGTH_SHORT); + } + private static ToastUtil getInstance(Context context) { + if (instance == null) { + synchronized (ToastUtil.class) { + if (instance == null) + instance = new ToastUtil(context); + } + } + return instance; + } + public static ToastUtil makeText(Context context, CharSequence text, + int duration) { + ToastUtil util = getInstance(context); + mDuration = duration; + toast.setText(text); + currentText = text; + textView.setText(text); + return util; + } + public static ToastUtil makeText(Context context, int resId, int duration{ + ToastUtil util = getInstance(context); + mDuration = duration; + toast.setText(resId); + currentText = context.getResources().getString(resId); + textView.setText(context.getResources().getString(resId)); + return util; + } + /** + * 进行Toast显示,在显示之前会取消当前已经存在的Toast + */ + public void show() { + long time = 0; + switch (mDuration) { + case LENGTH_SHORT: + time = 2000; + break; + case LENGTH_LONG: + time = 3500; + break; + default: + time = 2000; + break; + } + if (currentText.equals(oldText) && oldView.getParent() != null) { + toastHandler.removeMessages(WHAT); + toastView = oldView; + oldText = currentText; + toastHandler.sendEmptyMessageDelayed(WHAT, time); + return; + } + cancelOldAlert(); + toastHandler.removeMessages(WHAT); + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + params.width = WindowManager.LayoutParams.WRAP_CONTENT; + params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + params.format = PixelFormat.TRANSLUCENT; + params.windowAnimations = android.R.style.Animation_Toast; + params.type = WindowManager.LayoutParams.TYPE_TOAST; + params.setTitle("Toast"); + params.gravity = toast.getGravity(); + params.y = toast.getYOffset(); + if (toastView.getParent() == null) { + mWindowManager.addView(toastView, params); + } + oldView = toastView; + oldText = currentText; + toastHandler.sendEmptyMessageDelayed(WHAT, time); + } + private Handler toastHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + cancelOldAlert(); + int id = msg.what; + if (WHAT == id) { + cancelCurrentAlert(); + } + } + }; + private void cancelOldAlert() { + if (oldView != null && oldView.getParent() != null) { + mWindowManager.removeView(oldView); + } + } + public void cancelCurrentAlert() { + if (toastView != null && toastView.getParent() != null) { + mWindowManager.removeView(toastView); + } + } +} +``` + + +在某些Pad上面Toast显示出来后就不会自动消失,在这些Pad上`toastView.getParent()会为nul`这样就导致无法移除。可以将`cancelOldAlert()`以及 +`cancelCurrentAlert()`进行如下修改。 + +```java +private void cancelOldAlert() { + if (oldView != null) { // 去掉 oldView.getParent() != null 这个参数,然后加上try catch代码块,解决在部分Pad上oldView.getParent()不准确的问题 + try { + mWindowManager.removeView(oldView); + } catch (Exception e) { + e.printStackTrace(); + } + } +} + +public void cancelCurrentAlert() { + if (toastView != null) { + try { + // 去掉 oldView.getParent() != null 这个参数,然后加上try catch代码块,解决在部分Pad上oldView.getParent()不准确的问题 + mWindowManager.removeView(toastView); + } catch (Exception e) { + e.printStackTrace(); + } + } else if (oldView != null) { + try { + mWindowManager.removeView(oldView); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +---- +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" rename to "AndroidBasicPart/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" index 301c8c00..ddc35a13 100644 --- "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" +++ "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211\346\216\247\344\273\266.md" @@ -1,166 +1,166 @@ -自定义控件 -=== - -自定义控件的步骤 ---- - -- 自定义一个View继承ViewGroup等相似效果的View; -- 重写构造方法 -可以在构造方法中附加要显示的内容如下: -`View.inflate(context, R.layout.ui_setting_view, this);` -这里就是让这个填充出来的`View`显示到当前我们自定义的这个布局中`View`的构造方法共有三个,其中一个参数的构造方法,是通过代码`new`对象的时候调用, -两个参数的构造方法是通过在`xml`布局文件中声明的构造 -- 实现通过`Xml`文件配置属性 - - `values`目录下新建`attrs.xml` - - 内容如下 - ```xml - - - - ``` -配置完成后会自动在`R`文件中生产对应的`R.styleable`内部类 -- 代码设置 - - 在构造函数中将`Xml`配置与属性值建立映射关系 - - 使用`typedArray.getString(R.styleable.SettingView_title)`得到`xml`中的属性值,并且设置给相应控件的属性。 - - 调用`typedArray.recycle()`; 回收掉资源. - ```java - public SettingView(Context context, AttributeSet attrs) { - super(context, attrs); - TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.SettingView); - //R.styleable.SettingView_title为R文件中自动生成的相应属性名 - String title = typedArray.getString(R.styleable.SettingView_title); - setTitle(title); - typedArray.recycle(); - } - ``` -- 布局使用 - `Android`的命名空间为`xmlns:android=http://schemas.android.com/apk/res/android` -定义自己的命名空间时只需要把最后面的`android`改为我们应用程序的包名 -`xmlns:itheima="http://schemas.android.com/apk/res/com.charon.test"` - -以系统设置页面的选中为例 - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/custom_widget.jpg) - -1. 样式 - 示例代码:`settingview.xml` - ```xml - - - - - - - ``` - -2. 在`res-values`下面新建一个`attrs.xml`文件 - ```xml - - - //这个name就是我们自定义的组合控件的名字 - //这个attr的name就是我们要在xml文件中直接使用的属性format是指这个属性的值是什么类型的 - - - - - ``` - -3. 自定义一个类继承`ViewGroup` - ```java - public class SettingView extends RelativeLayout { - private TextView tv_settingview_title; - private TextView tv_settingview_status; - private CheckBox cb_settingview_status; - private String check_text; // 选中文本 - private String uncheck_text; // 未选中文本 - - public SettingView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - /** - * 布局文件创建view对象 会使用 有两个参数的构造方法. - */ - public SettingView(Context context, AttributeSet attrs) { - super(context, attrs); - View view = View.inflate(context, R.layout.ui_setting_view, this);//inflate之后直接指定了父元素就是this,所以这句代码一执行就会在Relativelayout中显示出来这个样式 - cb_settingview_status = (CheckBox) view - .findViewById(R.id.cb_settingview_status); - tv_settingview_status = (TextView) view - .findViewById(R.id.tv_settingview_status); - tv_settingview_title = (TextView) view - .findViewById(R.id.tv_settingview_title); - // 把自定义的属性 和 属性集attrs 建立一个对应关系.在用attrs.xml声明了之后会在R文件中生成一个R.styleable.SettingView这是一个int型的数组,数组中是我们在attrs中声明的三个attr属性 - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SettingView); - String title = a.getString(R.styleable.SettingView_title);//这个R.styleable.SettingView_title就是我们在attrs中定义的title - check_text = a.getString(R.styleable.SettingView_checked_text); - uncheck_text = a.getString(R.styleable.SettingView_unchecked_text); - a.recycle(); - } - public SettingView(Context context) { - super(context); - initView(context); - } - } - ``` - -4. 使用自定义控件 - ```xml - - - - - - - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +自定义控件 +=== + +自定义控件的步骤 +--- + +- 自定义一个View继承ViewGroup等相似效果的View; +- 重写构造方法 +可以在构造方法中附加要显示的内容如下: +`View.inflate(context, R.layout.ui_setting_view, this);` +这里就是让这个填充出来的`View`显示到当前我们自定义的这个布局中`View`的构造方法共有三个,其中一个参数的构造方法,是通过代码`new`对象的时候调用, +两个参数的构造方法是通过在`xml`布局文件中声明的构造 +- 实现通过`Xml`文件配置属性 + - `values`目录下新建`attrs.xml` + - 内容如下 + ```xml + + + + ``` +配置完成后会自动在`R`文件中生产对应的`R.styleable`内部类 +- 代码设置 + - 在构造函数中将`Xml`配置与属性值建立映射关系 + - 使用`typedArray.getString(R.styleable.SettingView_title)`得到`xml`中的属性值,并且设置给相应控件的属性。 + - 调用`typedArray.recycle()`; 回收掉资源. + ```java + public SettingView(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.SettingView); + //R.styleable.SettingView_title为R文件中自动生成的相应属性名 + String title = typedArray.getString(R.styleable.SettingView_title); + setTitle(title); + typedArray.recycle(); + } + ``` +- 布局使用 + `Android`的命名空间为`xmlns:android=http://schemas.android.com/apk/res/android` +定义自己的命名空间时只需要把最后面的`android`改为我们应用程序的包名 +`xmlns:itheima="http://schemas.android.com/apk/res/com.charon.test"` + +以系统设置页面的选中为例 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/custom_widget.jpg) + +1. 样式 + 示例代码:`settingview.xml` + ```xml + + + + + + + ``` + +2. 在`res-values`下面新建一个`attrs.xml`文件 + ```xml + + + //这个name就是我们自定义的组合控件的名字 + //这个attr的name就是我们要在xml文件中直接使用的属性format是指这个属性的值是什么类型的 + + + + + ``` + +3. 自定义一个类继承`ViewGroup` + ```java + public class SettingView extends RelativeLayout { + private TextView tv_settingview_title; + private TextView tv_settingview_status; + private CheckBox cb_settingview_status; + private String check_text; // 选中文本 + private String uncheck_text; // 未选中文本 + + public SettingView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * 布局文件创建view对象 会使用 有两个参数的构造方法. + */ + public SettingView(Context context, AttributeSet attrs) { + super(context, attrs); + View view = View.inflate(context, R.layout.ui_setting_view, this);//inflate之后直接指定了父元素就是this,所以这句代码一执行就会在Relativelayout中显示出来这个样式 + cb_settingview_status = (CheckBox) view + .findViewById(R.id.cb_settingview_status); + tv_settingview_status = (TextView) view + .findViewById(R.id.tv_settingview_status); + tv_settingview_title = (TextView) view + .findViewById(R.id.tv_settingview_title); + // 把自定义的属性 和 属性集attrs 建立一个对应关系.在用attrs.xml声明了之后会在R文件中生成一个R.styleable.SettingView这是一个int型的数组,数组中是我们在attrs中声明的三个attr属性 + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SettingView); + String title = a.getString(R.styleable.SettingView_title);//这个R.styleable.SettingView_title就是我们在attrs中定义的title + check_text = a.getString(R.styleable.SettingView_checked_text); + uncheck_text = a.getString(R.styleable.SettingView_unchecked_text); + a.recycle(); + } + public SettingView(Context context) { + super(context); + initView(context); + } + } + ``` + +4. 使用自定义控件 + ```xml + + + + + + + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" similarity index 98% rename from "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" rename to "AndroidBasicPart/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" index 17e82d18..6e93737e 100644 --- "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" +++ "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211\347\212\266\346\200\201\346\240\217\351\200\232\347\237\245.md" @@ -1,129 +1,129 @@ -自定义状态栏通知 -=== - -状态栏通知布局 -`custom_notification.xml` -```xml - - - - - - - - - - ``` - -这里面的style都是使用的继承系统的文字样式 -```xml - - - -``` - -```java -/** - * 自定义通知 - */ -private void createCustomNotification(Context ctxt, String tickerText, - int drawable, String title, String content, int id, - PendingIntent pendingIntent) { - int icon = R.drawable.ic_launcher; - long when = System.currentTimeMillis(); - Notification notification = new Notification(icon, tickerText, when);//必须要有这三个参数,不然出来的状态栏显示不全 - RemoteViews contentView = new RemoteViews(mContext.getPackageName(), - R.layout.custom_notification); - contentView.setImageViewResource(R.id.image, drawable); - contentView.setTextViewText(R.id.title, title); - contentView.setTextViewText(R.id.text, content); - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式 - String time = df.format(new Date()); - contentView.setTextViewText(R.id.time, - time.substring((time.length() - 8), (time.length() - 3))); - notification.contentView = contentView; - notification.contentIntent = pendingIntent; - notification.flags |= Notification.FLAG_AUTO_CANCEL; - notification.defaults = Notification.DEFAULT_SOUND; - String ns = Context.NOTIFICATION_SERVICE; - NotificationManager mNotificationManager = (NotificationManager) ctxt - .getSystemService(ns); - mNotificationManager.notify(id, notification); -} - -/** - *状态栏通知 - */ -public void stateBar(View view) { - // 1.获取通知管理器 - NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - // 2.创建通知对象 - int icon = R.drawable.icon; // 图标 - CharSequence tickerText = "下载完成"; // 提示文本 - long when = System.currentTimeMillis(); // 时间 - Notification notification = new Notification(icon, tickerText, when); - // 3.设置通知 - Context context = getApplicationContext(); // 上下文环境 - String contentTitle = "FeiQ下载完成"; // 消息标题 - String contentText = "FeiQ.exe下载完成, 耗时1分35秒"; // 消息内容 - Intent intent = new Intent(); // 用来开启Activity的意图 - intent.setClassName("com.itheima.downloader", "com.itheima.downloader.MainActivity"); // 意图指定Activity - PendingIntent pedningIntent = PendingIntent.getActivity(this, 100, intent, PendingIntent.FLAG_ONE_SHOT); // 定义待定意图,第三个参数就是指定如果多个状态栏通知时,后面的通知怎么处理前面的通知 - notification.setLatestEventInfo(context, contentTitle, contentText, pedningIntent); // 设置通知的具体信息 - notification.flags = Notification.FLAG_AUTO_CANCEL; // 设置自动清除,如果设置为FLAG_NO_CLEAR就是点击不会删除,像安全卫士程序的状态栏图标点击就不会删除 - // 设置通知的声音 - notification.sound = Uri.parse("file:///mnt/sdcard/jiaodizhu.mp3"); - // 4.发送通知 - manager.notify(id++, notification); -} - -/*这个PendingIntent是指定点击这个状态栏通知的时候的点击事件,注意点击的时候这个状态栏其实是运行在系统的程序中,并不是运行在我们自己的程序中, -而这个pendingIntent就是指定在点击这个通知的时候怎么让别的程序来执行的意图,就是包装出来一个动作让另外一个程序使用、 -这个自动清除就是点击一次之后这个通知就在状态栏中自动清除了, -发送通知时的id++就是每次的id值,不同则你发一次通知状态栏上就会显示一个,点多次就发送多个,如果设置为1,则不管点多少次后面的都把前面的给覆盖了*/ -``` - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! +自定义状态栏通知 +=== + +状态栏通知布局 +`custom_notification.xml` +```xml + + + + + + + + + + ``` + +这里面的style都是使用的继承系统的文字样式 +```xml + + + +``` + +```java +/** + * 自定义通知 + */ +private void createCustomNotification(Context ctxt, String tickerText, + int drawable, String title, String content, int id, + PendingIntent pendingIntent) { + int icon = R.drawable.ic_launcher; + long when = System.currentTimeMillis(); + Notification notification = new Notification(icon, tickerText, when);//必须要有这三个参数,不然出来的状态栏显示不全 + RemoteViews contentView = new RemoteViews(mContext.getPackageName(), + R.layout.custom_notification); + contentView.setImageViewResource(R.id.image, drawable); + contentView.setTextViewText(R.id.title, title); + contentView.setTextViewText(R.id.text, content); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式 + String time = df.format(new Date()); + contentView.setTextViewText(R.id.time, + time.substring((time.length() - 8), (time.length() - 3))); + notification.contentView = contentView; + notification.contentIntent = pendingIntent; + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.defaults = Notification.DEFAULT_SOUND; + String ns = Context.NOTIFICATION_SERVICE; + NotificationManager mNotificationManager = (NotificationManager) ctxt + .getSystemService(ns); + mNotificationManager.notify(id, notification); +} + +/** + *状态栏通知 + */ +public void stateBar(View view) { + // 1.获取通知管理器 + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + // 2.创建通知对象 + int icon = R.drawable.icon; // 图标 + CharSequence tickerText = "下载完成"; // 提示文本 + long when = System.currentTimeMillis(); // 时间 + Notification notification = new Notification(icon, tickerText, when); + // 3.设置通知 + Context context = getApplicationContext(); // 上下文环境 + String contentTitle = "FeiQ下载完成"; // 消息标题 + String contentText = "FeiQ.exe下载完成, 耗时1分35秒"; // 消息内容 + Intent intent = new Intent(); // 用来开启Activity的意图 + intent.setClassName("com.itheima.downloader", "com.itheima.downloader.MainActivity"); // 意图指定Activity + PendingIntent pedningIntent = PendingIntent.getActivity(this, 100, intent, PendingIntent.FLAG_ONE_SHOT); // 定义待定意图,第三个参数就是指定如果多个状态栏通知时,后面的通知怎么处理前面的通知 + notification.setLatestEventInfo(context, contentTitle, contentText, pedningIntent); // 设置通知的具体信息 + notification.flags = Notification.FLAG_AUTO_CANCEL; // 设置自动清除,如果设置为FLAG_NO_CLEAR就是点击不会删除,像安全卫士程序的状态栏图标点击就不会删除 + // 设置通知的声音 + notification.sound = Uri.parse("file:///mnt/sdcard/jiaodizhu.mp3"); + // 4.发送通知 + manager.notify(id++, notification); +} + +/*这个PendingIntent是指定点击这个状态栏通知的时候的点击事件,注意点击的时候这个状态栏其实是运行在系统的程序中,并不是运行在我们自己的程序中, +而这个pendingIntent就是指定在点击这个通知的时候怎么让别的程序来执行的意图,就是包装出来一个动作让另外一个程序使用、 +这个自动清除就是点击一次之后这个通知就在状态栏中自动清除了, +发送通知时的id++就是每次的id值,不同则你发一次通知状态栏上就会显示一个,点多次就发送多个,如果设置为1,则不管点多少次后面的都把前面的给覆盖了*/ +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\350\203\214\346\231\257.md" "b/AndroidBasicPart/\350\207\252\345\256\232\344\271\211\350\203\214\346\231\257.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\350\207\252\345\256\232\344\271\211\350\203\214\346\231\257.md" rename to "AndroidBasicPart/\350\207\252\345\256\232\344\271\211\350\203\214\346\231\257.md" diff --git "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\344\275\215\347\275\256(LocationManager).md" "b/AndroidBasicPart/\350\216\267\345\217\226\344\275\215\347\275\256(LocationManager).md" similarity index 100% rename from "Android\345\237\272\347\241\200/\350\216\267\345\217\226\344\275\215\347\275\256(LocationManager).md" rename to "AndroidBasicPart/\350\216\267\345\217\226\344\275\215\347\275\256(LocationManager).md" diff --git "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" "b/AndroidBasicPart/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" rename to "AndroidBasicPart/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" index 7f836937..c73f9d22 100644 --- "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" +++ "b/AndroidBasicPart/\350\216\267\345\217\226\345\272\224\347\224\250\347\250\213\345\272\217\347\274\223\345\255\230\345\217\212\344\270\200\351\224\256\346\270\205\347\220\206.md" @@ -1,92 +1,92 @@ -获取应用程序缓存及一键清理 -=== - -1. 什么是缓存呢? - 在手机ROM里面的缓存就是每个程序的cache文件夹 - -2. 获取缓存思路(参考手机设置页面) - 通过`PakcageManager.getPakcageSizeInfo()`能得到程序的缓存,但是这个方法被隐藏了,而系统的`Setting`页面之所以能使用是因为它们的权限高, - 我们要想使用就必须通过反射来得到,这里`getPackageSizeInfo()`方法的第二个参数是一个远程的`aidl`文件。 - - 所以必须要在本地的工程中新建一个包,名字为`android.content.pm` - - 拷贝`IPakcageStatsObserver.aidl`到该包中,导入后发现报错,是因为还要导入另外一个`aidl`文件`PackageStats.aidl` - -3. 获取缓存大小 - ```java - protected Void doInBackground(Void... params) { - try { - List infos = pm.getInstalledPackages(0); - //pm.getInstalledApplications(flags); - pb.setMax(infos.size()); - int total = 0; - - for (PackageInfo info : infos) { - String packname = info.packageName; - Method method = PackageManager.class.getDeclaredMethod( - "getPackageSizeInfo", new Class[] { - String.class, - IPackageStatsObserver.class }); - method.invoke(pm, new Object[] { packname, - new MyObserver(packname) }); - publishProgress("正在扫描:" + packname); - total++; - pb.setProgress(total); - Thread.sleep(80); - } - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - private class MyObserver extends IPackageStatsObserver.Stub { - private String packname; - public MyObserver(String packname) { - this.packname = packname; - } - //回调方法,到得到状态之后就会调用该方法,我们可以通过PackageStats中的属性来得到缓存的大小 - public void onGetStatsCompleted(PackageStats pStats, boolean succeeded) - throws RemoteException { - long cache = pStats.cacheSize; - long code = pStats.codeSize; - long data = pStats.dataSize; - if (cache > 0) { - cacheInfo.put(packname, cache); - } - } - } - ``` - -4. 缓存清理 - 得到每个程序的缓存大小后,该怎么去清理程序的缓存呢?调用`PackageManager.deleteApplicationCacheFiles`,这个方法是隐藏的,我们通过反射来执行但是发现需要权限, - 设置权限后还是提示需要权限,这是因为没有系统权限我们不能清理,设置页面之所以能够使用这个方法,因为是系统的`API`, - 所以我们只能是点击条目之后跳转到系统的设置页面,让通过设置页面来删除缓存. - -5. 一键清理 - 一键自动清理使用`freeStorageAndNotify`方法,该方法能够向系统申请释放多大的内存,系统会根据你申请的大小,尽可能的去是释放可以释放的大小。 - ```java - public void cleanAll(View view) { - try { - Method[] ms = PackageManager.class.getDeclaredMethods(); - for (Method m : ms) { - if ("freeStorageAndNotify".equals(m.getName())) { - m.invoke(pm, new Object[] { Long.MAX_VALUE, - new MyDataObersver() }); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - //这是一个aidl - private class MyDataObersver extends IPackageDataObserver.Stub { - public void onRemoveCompleted(String packageName, boolean succeeded) - throws RemoteException { - } - } - ``` - ---- - -- 邮箱 :charon.chui@gmail.com +获取应用程序缓存及一键清理 +=== + +1. 什么是缓存呢? + 在手机ROM里面的缓存就是每个程序的cache文件夹 + +2. 获取缓存思路(参考手机设置页面) + 通过`PakcageManager.getPakcageSizeInfo()`能得到程序的缓存,但是这个方法被隐藏了,而系统的`Setting`页面之所以能使用是因为它们的权限高, + 我们要想使用就必须通过反射来得到,这里`getPackageSizeInfo()`方法的第二个参数是一个远程的`aidl`文件。 + - 所以必须要在本地的工程中新建一个包,名字为`android.content.pm` + - 拷贝`IPakcageStatsObserver.aidl`到该包中,导入后发现报错,是因为还要导入另外一个`aidl`文件`PackageStats.aidl` + +3. 获取缓存大小 + ```java + protected Void doInBackground(Void... params) { + try { + List infos = pm.getInstalledPackages(0); + //pm.getInstalledApplications(flags); + pb.setMax(infos.size()); + int total = 0; + + for (PackageInfo info : infos) { + String packname = info.packageName; + Method method = PackageManager.class.getDeclaredMethod( + "getPackageSizeInfo", new Class[] { + String.class, + IPackageStatsObserver.class }); + method.invoke(pm, new Object[] { packname, + new MyObserver(packname) }); + publishProgress("正在扫描:" + packname); + total++; + pb.setProgress(total); + Thread.sleep(80); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private class MyObserver extends IPackageStatsObserver.Stub { + private String packname; + public MyObserver(String packname) { + this.packname = packname; + } + //回调方法,到得到状态之后就会调用该方法,我们可以通过PackageStats中的属性来得到缓存的大小 + public void onGetStatsCompleted(PackageStats pStats, boolean succeeded) + throws RemoteException { + long cache = pStats.cacheSize; + long code = pStats.codeSize; + long data = pStats.dataSize; + if (cache > 0) { + cacheInfo.put(packname, cache); + } + } + } + ``` + +4. 缓存清理 + 得到每个程序的缓存大小后,该怎么去清理程序的缓存呢?调用`PackageManager.deleteApplicationCacheFiles`,这个方法是隐藏的,我们通过反射来执行但是发现需要权限, + 设置权限后还是提示需要权限,这是因为没有系统权限我们不能清理,设置页面之所以能够使用这个方法,因为是系统的`API`, + 所以我们只能是点击条目之后跳转到系统的设置页面,让通过设置页面来删除缓存. + +5. 一键清理 + 一键自动清理使用`freeStorageAndNotify`方法,该方法能够向系统申请释放多大的内存,系统会根据你申请的大小,尽可能的去是释放可以释放的大小。 + ```java + public void cleanAll(View view) { + try { + Method[] ms = PackageManager.class.getDeclaredMethods(); + for (Method m : ms) { + if ("freeStorageAndNotify".equals(m.getName())) { + m.invoke(pm, new Object[] { Long.MAX_VALUE, + new MyDataObersver() }); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + //这是一个aidl + private class MyDataObersver extends IPackageDataObserver.Stub { + public void onRemoveCompleted(String packageName, boolean succeeded) + throws RemoteException { + } + } + ``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\346\211\213\346\234\272\344\270\255\346\211\200\346\234\211\345\256\211\350\243\205\347\232\204\347\250\213\345\272\217.md" "b/AndroidBasicPart/\350\216\267\345\217\226\346\211\213\346\234\272\344\270\255\346\211\200\346\234\211\345\256\211\350\243\205\347\232\204\347\250\213\345\272\217.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\350\216\267\345\217\226\346\211\213\346\234\272\344\270\255\346\211\200\346\234\211\345\256\211\350\243\205\347\232\204\347\250\213\345\272\217.md" rename to "AndroidBasicPart/\350\216\267\345\217\226\346\211\213\346\234\272\344\270\255\346\211\200\346\234\211\345\256\211\350\243\205\347\232\204\347\250\213\345\272\217.md" diff --git "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\346\211\213\346\234\272\345\217\212SD\345\215\241\345\217\257\347\224\250\345\255\230\345\202\250\347\251\272\351\227\264.md" "b/AndroidBasicPart/\350\216\267\345\217\226\346\211\213\346\234\272\345\217\212SD\345\215\241\345\217\257\347\224\250\345\255\230\345\202\250\347\251\272\351\227\264.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\350\216\267\345\217\226\346\211\213\346\234\272\345\217\212SD\345\215\241\345\217\257\347\224\250\345\255\230\345\202\250\347\251\272\351\227\264.md" rename to "AndroidBasicPart/\350\216\267\345\217\226\346\211\213\346\234\272\345\217\212SD\345\215\241\345\217\257\347\224\250\345\255\230\345\202\250\347\251\272\351\227\264.md" diff --git "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" "b/AndroidBasicPart/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" similarity index 97% rename from "Android\345\237\272\347\241\200/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" rename to "AndroidBasicPart/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" index 67e5ee9d..e136b1d9 100644 --- "a/Android\345\237\272\347\241\200/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" +++ "b/AndroidBasicPart/\350\216\267\345\217\226\350\201\224\347\263\273\344\272\272.md" @@ -1,56 +1,56 @@ -获取联系人 -=== - -`Android`系统中的联系人也是通过`ContentProvider`来对外提供数据的 -数据库路径为:`/data/data/com.android.providers.contacts/database/contacts2.db` -我们需要关注的有3张表 -- raw_contacts:其中保存了联系人id -- data:和raw_contacts是多对一的关系,保存了联系人的各项数据 -- mimetypes:为数据类型 - -先查询`raw_contacts`得到每个联系人的`id`,在使用`id`从`data`表中查询对应数据,根据`mimetype`分类数据这地方在数据库中的是一个`mimetype_id` -是一个外键引用到了`mimetype`这个表中,它内部做了一个关联查询,这地方不能写`mimetype_id`,只能写`mimetype`不然会报错 - -```java -/** - * 获取联系人 - * @return - */ -public static List getContactInfos(Context context) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = Uri.parse("content://com.android.contacts/raw_contacts"); - Uri dataUri = Uri.parse("content://com.android.contacts/data"); - List contactInfos = new ArrayList(); - Cursor cursor = resolver.query(uri, new String[] { "contact_id" }, - null, null, null); - while (cursor.moveToNext()) { - String id = cursor.getString(0); - if (id != null) { - ContactInfo info = new ContactInfo(); - Cursor dataCursor = resolver.query(dataUri, new String[] { - "mimetype", "data1" }, "raw_contact_id=?", - new String[] { id }, null); - while (dataCursor.moveToNext()) { - if("vnd.android.cursor.item/name".equals( dataCursor.getString(0))){ - info.setName(dataCursor.getString(1)); - }else if("vnd.android.cursor.item/phone_v2".equals( dataCursor.getString(0))){ - info.setPhone(dataCursor.getString(1)); - } - } - contactInfos.add(info); - dataCursor.close(); - } - } - cursor.close(); - return contactInfos; -} -``` -要使用到这三个表,但是谷歌内部在查询这个`data`表的时候内部使用的并不是查询`data`表而是查询了`data`表的视图, -这个视图中直接将`mime_Type`类型给关联好了,所以这里直接查询的就是`mimetype`这个类型就可以了, -还有就是如果自己的手机中如果删除了一个联系人在查询的时候就会报错,这里因为是在手机中删除一个联系人的时候并*不是清空数据库中的数据*, -而是将这个**raw_contacks中的id置为null**,所以会报错,这里就要进行判断一下查出来的id是否为null。 - ---- - -- 邮箱 :charon.chui@gmail.com +获取联系人 +=== + +`Android`系统中的联系人也是通过`ContentProvider`来对外提供数据的 +数据库路径为:`/data/data/com.android.providers.contacts/database/contacts2.db` +我们需要关注的有3张表 +- raw_contacts:其中保存了联系人id +- data:和raw_contacts是多对一的关系,保存了联系人的各项数据 +- mimetypes:为数据类型 + +先查询`raw_contacts`得到每个联系人的`id`,在使用`id`从`data`表中查询对应数据,根据`mimetype`分类数据这地方在数据库中的是一个`mimetype_id` +是一个外键引用到了`mimetype`这个表中,它内部做了一个关联查询,这地方不能写`mimetype_id`,只能写`mimetype`不然会报错 + +```java +/** + * 获取联系人 + * @return + */ +public static List getContactInfos(Context context) { + ContentResolver resolver = context.getContentResolver(); + Uri uri = Uri.parse("content://com.android.contacts/raw_contacts"); + Uri dataUri = Uri.parse("content://com.android.contacts/data"); + List contactInfos = new ArrayList(); + Cursor cursor = resolver.query(uri, new String[] { "contact_id" }, + null, null, null); + while (cursor.moveToNext()) { + String id = cursor.getString(0); + if (id != null) { + ContactInfo info = new ContactInfo(); + Cursor dataCursor = resolver.query(dataUri, new String[] { + "mimetype", "data1" }, "raw_contact_id=?", + new String[] { id }, null); + while (dataCursor.moveToNext()) { + if("vnd.android.cursor.item/name".equals( dataCursor.getString(0))){ + info.setName(dataCursor.getString(1)); + }else if("vnd.android.cursor.item/phone_v2".equals( dataCursor.getString(0))){ + info.setPhone(dataCursor.getString(1)); + } + } + contactInfos.add(info); + dataCursor.close(); + } + } + cursor.close(); + return contactInfos; +} +``` +要使用到这三个表,但是谷歌内部在查询这个`data`表的时候内部使用的并不是查询`data`表而是查询了`data`表的视图, +这个视图中直接将`mime_Type`类型给关联好了,所以这里直接查询的就是`mimetype`这个类型就可以了, +还有就是如果自己的手机中如果删除了一个联系人在查询的时候就会报错,这里因为是在手机中删除一个联系人的时候并*不是清空数据库中的数据*, +而是将这个**raw_contacks中的id置为null**,所以会报错,这里就要进行判断一下查出来的id是否为null。 + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/Android\345\237\272\347\241\200/\350\257\273\345\217\226\347\224\250\346\210\267logcat\346\227\245\345\277\227.md" "b/AndroidBasicPart/\350\257\273\345\217\226\347\224\250\346\210\267logcat\346\227\245\345\277\227.md" similarity index 100% rename from "Android\345\237\272\347\241\200/\350\257\273\345\217\226\347\224\250\346\210\267logcat\346\227\245\345\277\227.md" rename to "AndroidBasicPart/\350\257\273\345\217\226\347\224\250\346\210\267logcat\346\227\245\345\277\227.md" diff --git "a/Android\345\237\272\347\241\200/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" "b/AndroidBasicPart/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" similarity index 98% rename from "Android\345\237\272\347\241\200/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" rename to "AndroidBasicPart/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" index 291ad661..2fa17936 100644 --- "a/Android\345\237\272\347\241\200/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" +++ "b/AndroidBasicPart/\350\265\204\346\272\220\346\226\207\344\273\266\346\213\267\350\264\235\347\232\204\344\270\211\347\247\215\346\226\271\345\274\217.md" @@ -1,29 +1,29 @@ -资源文件拷贝的三种方式 -=== - -- 类加载器(类路径) - - 用`Classloader.getResourceAsStream()`来读取类路径中的资源,然后用`FileOutputStream`写入到自己的应用中(*sdk开发的时候经常用这种方式*)。 - - 这种方式**必须**要将数据库`address.db`放到**src**目录下,这样编译后就会直接将`address.db`生成到`bin/classes`目录中,会在类路径下, - 所以可以使用`Classloader`进行加载. - - ```java - InputStream is = getClassLoader().getResourceAsStream("address.db"); - File file = new File(/data/data/包名/files/address.db); - FileOutputStream fos = new FileOutputStream(file); - ``` - -- Raw目录 - 将资源文件放到`res-raw`下, 然后用`getResources.openRawResource(R.raw.addresss);`(要求资源最好不超过1M,因为系统会编译`res`目录) - -- Assets目录 - 将资源文件放到`Assets`目录中。然后用`mContext.getAssets().open("address.db");`来读取该资源(`Assets`目录中的文件不会被编译,会原封不动的打包到apk中, - 所以一般用来存放比较大的资源文件) - 注意:上面这是在`Eclipse`中的使用方式,因为在`Eclipse`中`assets`目录是在`res`下,但是在`Android Studio`中如果这样使用会报`FileNotFoundException`,为什么呢? - 文件名字没错,而且明明是在`assets`目录中的,这是因为在`Studio`中`assets`文件夹的目录层级发生了变化,直接在`module`下,与`src`目录平级了?该如何修改呢? - 答案就是把`assets`目录移`src/main`下面就可以了?为什么呢?因为我们打开`app.impl`可以看到`