概述

对于刚入门的同学来说,往往都会对Handler比较迷茫,到底Handler是个什么样的东西。当然,可能对于一些有工作经验的工程师来说,他们也不一定能很准确地描述,我们来看下API的介绍。

Handler是用来结合线程的消息队列来发送、处理“Message对象”和“Runnable对象”的工具。每一个Handler实例之后会关联一个线程和该线程的消息队列。当你创建一个Handler的时候,从这时开始,它就会自动关联到所在的线程/消息队列,然后它就会陆续把Message/Runnalbe分发到消息队列,并在它们出队的时候处理掉。

从官方文档中,我们不难找出其中的关键词,就是“线程”。我们都知道,一个涉及到网络操作,耗时操作等的Android应用,都离不开多线程操作,然而,如果这时我们允许并发更新UI,那么最终导致控件的状态都是不可确定的。所以,我们可以通过对控件进行加锁,在不需要用时解锁,这是一个解决方案之一,但最后很容易造成线程阻塞,效率会非常差。所以,谷歌采用了只允许在主线程更新UI,所以作为线程通信桥梁的Handler也就应运而生了。

Looper、MessageQueue、Message、Handler的关系

讲到Handler,肯定离不开Looper、MessageQueue、Message这三者和Handler之间的关系,下面简略地带过,详细自己可以查阅相关资料,或者查看源码,这样更方便大家深入学习。

Looper

每一个线程只有一个Looper,每个线程在初始化Looper之后,然后Looper会维护好该线程的消息队列,用来存放Handler发送的Message,并处理消息队列出队的Message。它的特点是它跟它的线程是绑定的,处理消息也是在Looper所在的线程去处理,所以当我们在主线程创建Handler时,它就会跟主线程唯一的Looper绑定,从而我们使用Handler在子线程发消息时,最终也是在主线程处理,达到了异步的效果。

那么就会有人问,为什么我们使用Handler的时候从来都不需要创建Looper呢?这是因为在主线程中,ActivityThread默认会把Looper初始化好,prepare以后,当前线程就会变成一个Looper线程。

MessageQueue

MessageQueue是一个消息队列,用来存放Handler发送的消息。每个线程最多只有一个MessageQueue。MessageQueue通常都是由Looper来管理,而主线程创建时,会创建一个默认的Looper对象,而Looper对象的创建,将自动创建一个MessageQueue。其他非主线程,不会自动创建Looper。

Message

消息对象,就是MessageQueue里面存放的对象,一个MessageQueu可以包括多个Message。当我们需要发送一个Message时,我们一般不建议使用new Message()的形式来创建,更推荐使用Message.obtain()来获取Message实例,因为在Message类里面定义了一个消息池,当消息池里存在未使用的消息时,便返回,如果没有未使用的消息,则通过new的方式创建返回,所以使用Message.obtain()的方式来获取实例可以大大减少当有大量Message对象而产生的垃圾回收问题。

四者关系总体如下(如有不对的地方,谢谢指出)

Handler的主要用途

  1. 推送未来某个时间点将要执行的Message或者Runnable到消息队列。
  2. 在子线程把需要在另一个线程执行的操作加入到消息队列中去。

废话不多说,通过举例来说明Handler的两个主要用途。

1. 推送未来某个时间点将要执行的Message或者Runnable到消息队列

实例:通过Handler配合Message或者Runnable实现倒计时

  • 首先看一下效果图

  • 方法一,通过Handler + Message的方式实现倒计时。代码如下:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mBinding; private Handler mHandler ; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); //设置监听事件
mBinding.clickBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//通过Handler + Message的方式实现倒计时
for (int i = 1; i <= 10; i++) {
Message message = Message.obtain(mHandler);
message.what = 10 - i;
mHandler.sendMessageDelayed(message, 1000 * i); //通过延迟发送消息,每隔一秒发送一条消息
}
}
}); mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mBinding.time.setText(msg.what + ""); //在handleMessage中处理消息队列中的消息
}
};
}
}

其实代码不用怎么解释,都比较通俗易懂,但是这里用到了DataBiding,可能没用过的同学看起来有点奇怪,但其实反而简略了代码,有一定基础的同学看起来都不会有太大压力,所以不做太多解释。通过这个小程序,作者希望大家可以了解到Handler的一个作用就是,在主线程中,可以通过Handler来处理一些有顺序的操作,让它们在固定的时间点被执行。

  • 方法二,通过Handler + Runnable的方式实现倒计时。代码如下:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mBinding;
private Handler mHandler = new Handler(); @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); //设置监听事件
mBinding.clickBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (int i = 1; i <= 10; i++) {
final int fadedSecond = i;
//每延迟一秒,发送一个Runnable对象
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mBinding.time.setText((10 - fadedSecond) + "");
}
}, 1000 * i);
}
}
});
}
}

方法二也是通过代码让大家加深Handler处理有序事件的用途,之所以分开Runnable和Message两种方法来实现,是因为很多人都搞不清楚为什么Handler可以推送Runnable和Message两种对象。其实,无论Handler将Runnable还是Message加入MessageQueue,最终都只是将Message加入到MessageQueue。只要大家看一下源码就可以知道,Handler的post Runnable对象这个方法只是对post Message进行了一层封装,所以最终我们都是通过Handler推送了一个Message罢了,至于为什么会分开两种方法,下文会给大家详说究竟。下面再来看看Handler的第二个主要用途。

2. 在子线程把需要在另一个线程执行的操作加入到消息队列中去

实例:通过Handler + Message来实现子线程加载图片,在UI线程显示图片

  • 效果图如下

  • 代码如下(布局代码也不放出来了)

public class ThreadActivity extends AppCompatActivity implements View.OnClickListener {
private ActivityThreadBinding mBinding = null; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_thread);
// 设置点击事件
mBinding.clickBtn.setOnClickListener(this);
mBinding.resetBtn.setOnClickListener(this);
} @Override
public void onClick(View v) {
switch (v.getId()) {
// 响应load按钮
case R.id.clickBtn:
// 开启一个线程
new Thread(new Runnable() {
@Override
public void run() {
// 在Runnable中进行网络读取操作,返回bitmap
final Bitmap bitmap = loadPicFromInternet();
// 在子线程中实例化Handler同样是可以的,只要在构造函数的参数中传入主线程的Looper即可
Handler handler = new Handler(Looper.getMainLooper());
// 通过Handler的post Runnable到UI线程的MessageQueue中去即可
handler.post(new Runnable() {
@Override
public void run() {
// 在MessageQueue出队该Runnable时进行的操作
mBinding.photo.setImageBitmap(bitmap);
}
});
}
}).start();
break;
case R.id.resetBtn:
mBinding.photo.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.default_pic));
break;
}
} /***
* HttpUrlConnection加载图片,不多说
* @return
*/
public Bitmap loadPicFromInternet() {
Bitmap bitmap = null;
int respondCode = 0;
InputStream is = null;
try {
URL url = new URL("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1421494343,3838991329&fm=23&gp=0.jpg");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10 * 1000);
connection.setReadTimeout(5 * 1000);
connection.connect();
respondCode = connection.getResponseCode();
if (respondCode == 200) {
is = connection.getInputStream();
bitmap = BitmapFactory.decodeStream(is);
}
} catch (MalformedURLException e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "访问失败", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}
}

Handler推送Message和Runnable的区别

在上文我们通过用Handler推送Message和Runnable实现相同的倒计时效果,这里我们就说一下Post(Runnable)和SendMessage(Message)的区别。

首先我们看看post方法和sendMessage方法的源码:

    public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
    public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}

可见,两个方法都是通过调用sendMessageDelayed方法实现的,所以可以知道它们的底层逻辑是一致的。

但是,post方法的底层调用sendMessageDelayed的时候,却是通过getPostMessage(r)来将Runnable对象来转为Message,我们点进方getPostMessage()法可以看到:

    private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

其实,最终runnable最终也是转化为一个Message,而这个Message只有一个被赋值的成员变量,就是Runnable的回调函数,也就是说,这个Message在进入MessageQueue之后,它只是一个“动作”,即我们Runnbale的run方法里面的操作。

要知道,我们的Message类可是有很多参数的,所以你可以理解为它是一个非常丰富的JavaBean,可以看看它的成员变量:

  • public int what;
  • public int arg1;
  • public int arg2;
  • public Object obj;
  • ...

那么讲到这里,大家也应该有所理解为什么Google工程师为什么会封装这两种方法,我总结如为:为了更方便开发者根据不同需要进行调用。当我们需要传输很多数据时,我们可以使用sendMessage来实现,因为通过给Message的不同成员变量赋值可以封装成数据非常丰富的对象,从而进行传输;当我们只需要进行一个动作时,直接使用Runnable,在run方法中实现动作内容即可。当然我们也可以通过Message.obtain(Handler h, Runnable callback)来传入callback接口,但这样看起来就没有post(Ruannable callback)那么直观。

API

API是我们学习最好的文档,所以我也简要跟大家学习一下,其实大家认真看我上面的介绍加上自己亲手实践,Handler的API大家都可以随便翻阅了。

构造函数

  • Handler()
  • Handler(Handler.Callback callback):传入一个实现的Handler.Callback接口,接口只需要实现handleMessage方法。
  • Handler(Looper looper):将Handler关联到任意一个线程的Looper,在实现子线程之间通信可以用到。
  • Handler(Looper looper, Handler.Callback callback)

主要方法

  • void dispatchMessage (Message msg)

一般情况下不会使用,因为它的底层实现其实是作为处理系统消息的一个方法,如果真要用,效果和sendMessage(Message m)效果一样。

    public void dispatchMessage(Message msg) {
if (msg.callback != null) {
// 如果有Runnbale,则直接执行它的run方法
handleCallback(msg);
} else {
//如果有实现自己的callback接口
if (mCallback != null) {
//执行callback的handleMessage方法
if (mCallback.handleMessage(msg)) {
return;
}
}
//否则执行自身的handleMessage方法
handleMessage(msg);
}
} private static void handleCallback(Message message) {
message.callback.run();
}
  • void dump (Printer pw, String prefix)

主要Debug时使用的一个方法,dump函数只是使用了Printer对象进行了打印,打印出Handler以及Looper和Queue中的一些信息,源码如下:

    public final void dump(Printer pw, String prefix) {
pw.println(prefix + this + " @ " + SystemClock.uptimeMillis());
// 如果Looper为空,输出Looper没有初始化
if (mLooper == null) {
pw.println(prefix + "looper uninitialized");
} else {
// 否则调用Looper的dump方法,Looper的dump方法也是
mLooper.dump(pw, prefix + " ");
}
}

通过测试用例大家会了解得更清晰:

        //测试代码
Printer pw = new LogPrinter(Log.ERROR, "MyTag");
mHandler.dump(pw, "prefix");

结果:

  • Looper getLooper ()

拿到Handler相关联的Looper

  • String getMessageName (Message message)

获取Message的名字,默认名字为message.what的值。

  • void handleMessage (Message msg)

处理消息。

  • boolean hasMessages (int what)

判断是否有Message的what值为参数what。

  • boolean hasMessages (int what, Object object)

判断是否有Message的what值为参数what,obj值为参数object。

  • Message obtainMessage (int what, Object obj)

从消息池中拿到一个消息并赋值what和obj,其他重载函数同理。

  • boolean post (Runnable r)

将Runnable对象加入MessageQueue。

  • boolean post (Runnable r)

将Runnbale加入到消息队列的队首。但是官方不推荐这么做,因为很容易打乱队列顺序。

  • boolean postAtTime (Runnable r, Object token, long uptimeMillis)

在某个时间点执行Runnable r。

  • boolean postDelayed (Runnable r, long delayMillis)

当前时间延迟delayMillis个毫秒后执行Runnable r。

  • void removeCallbacks (Runnable r, Object token)

移除MessageQueue中的所有Runnable对象。

  • void removeCallbacksAndMessages (Object token)

移除MessageQueue中的所有Runnable和Message对象。

  • void removeMessages (int what)

移除所有what值得Message对象。

  • boolean sendEmptyMessage (int what)

直接拿到一个空的消息,并赋值what,然后发送到MessageQueue。

  • boolean sendMessageDelayed (Message msg, long delayMillis)

在延迟delayMillis毫秒之后发送一个Message到MessageQueue。

Handler引发的内存泄漏

在上面的例子中,为了展示方便,我都没有考虑内存泄漏的情况,但是在实际开发中,如果不考虑代码的安全性的话,尤其当一个项目到达了一定的规模之后,那么对于代码的维护和系统的调试都是非常困难的。而Handler的内存泄漏在Android中也是一个非常经典的案例。

详细可以参考:How to Leak a Context: Handlers & Inner Classes

或参考翻译文:Android中Handler引起的内存泄露

通常我们都会在一个Activity内部定义一个Handler的内部类:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
...
}
}
}; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); mHandler.postDelayed(new Runnable() {
@Override
public void run() {
...
}
}, 1000000); }
}

(1)外部类Activity中定义了一个非静态内部类Handler,非静态内部类默认持有对外部类的引用。如果外部Activity突然关闭了,但是MessageQueue中的消息还没处理完,那么Handler就会一直持有对外部Activty的引用,垃圾回收器无法回收Activity,从而导致内存泄漏。

(2) 如上代码,在postDelayed中,我们在参数中传入一个非静态内部类Runnable,这同样会造成内存泄漏,假如此时关闭了Activity,那么垃圾回收器在接下来的1000000ms内都无法回收Activity,造成内存泄漏。

解决方案:

(1) 将非静态内部类Handler和Runnable转为静态内部类,因为非静态内部类(匿名内部类)都会默认持有对外部类的强引用。

(2) 改成静态内部类后,对外部类的引用设为弱引用,因为在垃圾回收时,会自动将弱引用的对象回收。

避免内存泄漏的例子:

public class HandlerActivity extends AppCompatActivity {

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable mRunnable = new Runnable() {
@Override
public void run() {
// 操作
}
}; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fourth); mHandler.postDelayed(mRunnable, 1000*10); finish();
} private static class MyHandler extends Handler {
WeakReference<HandlerActivity> mWeakActivity; public MyHandler(HandlerActivity activity) {
this.mWeakActivity = new WeakReference<HandlerActivity>(activity);
} @Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
final HandlerActivity mActivity = mWeakActivity.get();
if (mActivity != null) {
// 处理消息
}
}
} }

HandlerThread

思考一下,假如我们需要同时下载A和B,下载A需要6s,下载B需要5s,在它们下载完成后Toast信息出来即可,此时HandlerThread便是一种解决方式之一。那么HandlerThread到底是什么?

  • HandlerThread就是一种线程。
  • HandlerThread和普通的Thread之间的区别就是HandlerThread在创建的时候会提供自己该线程的Looper对象。

所以,如果大家了解清楚了我前面所讲的Looper、Message、Handler、MessageQueue的关系的话,这里就很清楚HandlerThread是什么东西了。大家都知道,我们在Actvity创建时系统会自动帮我们初始化好主线程的Looper,然后这个Looper就会管理主线程的消息队列。但是在我们创建子线程时,系统并不会帮我们创建子线程的Looper,需要我们自己手动创建,如下:

    new Thread(){
@Override
public void run() {
super.run();
Looper.prepare();
Handler mHandler = new Handler(Looper.myLooper());
Looper.loop();
}
}.start();

所以HandlerThread就在内部帮我们封装了Looper的创建过程,从源码可以看到,HandlerThread集成于Thread,然后覆写run方法,进行Looper的创建,从而通过getLooper方法暴露出该线程的Looper对象

public class HandlerThread extends Thread {
int mPriority;
int mTid = -1;
Looper mLooper; ... @Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
} public Looper getLooper() {
if (!isAlive()) {
return null;
} // If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
} ...
}

所以通过HandlerThread,我们可以轻松创建一个包含了Looper的子线程:

final HandlerThread mHandlerThread = new HandlerThread("HandlerThread");

mHandlerThread.start();

Handler mHandler = new Handler(mHandlerThread.getLooper());

使用HandlerThread同时下载A和B的Demo:

代码:

public class HandlerThreadActivity extends AppCompatActivity {
private TextView tv_A, tv_B; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_thread); tv_A = (TextView) findViewById(R.id.txt_dlA);
tv_B = (TextView) findViewById(R.id.txt_dlB); final Handler mainHandler = new Handler(); final HandlerThread downloadAThread = new HandlerThread("downloadAThread");
downloadAThread.start();
Handler downloadAHandler = new Handler(downloadAThread.getLooper()); // 通过postDelayed模拟耗时操作
downloadAHandler.postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "下载A完成", Toast.LENGTH_SHORT).show();
mainHandler.post(new Runnable() {
@Override
public void run() {
tv_A.setText("A任务已经下载完成");
}
});
}
}, 1000 * 5); final HandlerThread downloadBThread = new HandlerThread("downloadBThread");
downloadBThread.start();
Handler downloadBHandler = new Handler(downloadBThread.getLooper()); // 通过postDelayed模拟耗时操作
downloadBHandler.postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "下载B完成", Toast.LENGTH_SHORT).show();
mainHandler.post(new Runnable() {
@Override
public void run() {
tv_B.setText("B任务已经下载完成");
}
}); }
}, 1000 * 7);
}
}

总结

由于Android的UI更新只能在主线程,所以Handler是Android中一套非常重要的更新UI线程机制,虽然在很多框架的帮助下我们可以减少了很多Handler的代码编写,但实际上很多框架的底层实现都是通过Handler来更新UI的,所以可见掌握好Handler对我们来说是多么重要,所以这也是很多面试官在面试中的高频考点之一。虽然Handler对开发者来说是一个非常方便的存在,但是我们也不能否认它也是存在缺点的,如处理不当,Handler所造成的的内存泄漏对开发者来说也是一个非常头疼的难题。

Android基础夯实--你了解Handler有多少?的更多相关文章

  1. Android基础夯实--重温动画(二)之Frame Animation

    心灵鸡汤:天下事有难易乎,为之,则难者亦易矣:不为,则易者亦难矣. 摘要 当你已经掌握了Tween Animation之后,再来看Frame Animation,你就会顿悟,喔,原来Frame Ani ...

  2. Android基础夯实--重温动画(五)之属性动画 ObjectAnimator详解

    只有一种真正的英雄主义 一.摘要 ObjectAnimator是ValueAnimator的子类,它和ValueAnimator一样,同样具有计算属性值的功能,但对比ValueAnimator,它会更 ...

  3. Android基础夯实--重温动画(四)之属性动画 ValueAnimator详解

    宝剑锋从磨砺出,梅花香自苦寒来:千淘万漉虽辛苦,吹尽狂沙始到金: 长风破浪会有时,直挂云帆济沧海 一.摘要 Animator类作为属性动画的基类,它是一个抽象类,它提供了实现动画的基本架构,但是我们不 ...

  4. Android基础夯实--重温动画(三)之初识Property Animation

    每个人都有一定的理想,这种理想决定着他的努力和判断的方向.就在这个意义上,我从来不把安逸和快乐看作生活目的的本身--这种伦理基础,我叫它猪栏的理想.--爱因斯坦 一.摘要 Property Anima ...

  5. Android基础夯实--重温动画(一)之Tween Animation

    心灵鸡汤:真正成功的人生,不在于成就的大小,而在于你是否努力地去实现自我,喊出自己的声音,走出属于自己的道路. 摘要 不积跬步,无以至千里:不积小流,无以成江海.学习任何东西我们都离不开扎实的基础知识 ...

  6. android基础---->JSON数据的解析

    上篇博客,我们谈到了XML两种常用的解析技术,详细可以参见我的博客(android基础---->XMl数据的解析).网络传输另外一种数据格式JSON就是我们今天要讲的,它是比XML体积更小的数据 ...

  7. 基础4 Android基础

    基础4 Android基础 1. Activity与Fragment的生命周期. Activity生命周期 打开应用 onCreate()->onStart()->onResume 按BA ...

  8. Android基础总结(8)——服务

    服务(Service)是Android中实现程序后台运行的解决方案,它非常适合用于去执行哪些不需要和用户交互而且还要长期运行的任务.服务的运行不依赖任何用户界面,即使当程序被切换到后台,或者用户打开了 ...

  9. 【Xamarin开发 Android 系列 4】 Android 基础知识

    原文:[Xamarin开发 Android 系列 4] Android 基础知识 什么是Android? Android一词的本义指“机器人”,同时也是Google于2007年11月5日宣布的基于Li ...

随机推荐

  1. Swift Runtime ?

    你肯定也想过 在OC中相信每一个iOS开发都知道Runtime, 现在Swift也更新到4.0版本了,要是你也学习过Swift的话你可能也会想过这样一个问题,OC大家都是到是有动态性的,你能通过run ...

  2. 【17-06-16】Java入门测试题,测测你基础知识掌握程度(附答案及个人解析)

    描述 前几天在知乎里看到一份这样的题,当时只是随便做了一下,对了一下答案.昨天又有了一份进阶的题,里面有些还是需要记录一下,于是就从这个入门的题开始. 题目和答案来自阿里云大学 - 知乎专栏 题目 现 ...

  3. noip考前模板大整理

    //归并排序求逆序对 #include<bits/stdc++.h> #define ll long long using namespace std; ]; ll ans; ]; voi ...

  4. [行业关键词] review code review

    意思是   代码评审  或是 代码回顾 代码评审是指在软件开发过程中,通过对源代码进行系统性检查的过程.通常的目的是查找系统缺陷,保证软件总体质量和提高开发者自身水平. Code Review是轻量级 ...

  5. Elasticsearch的基友Logstash

    Logstash 是一款强大的数据处理工具,它可以实现数据传输,格式处理,格式化输出,还有强大的插件功能,常用于日志处理. 一.原理 Input 可以从文件中.存储中.数据库中抽取数据,Input有两 ...

  6. 关于PHP新手学习的一些指导与建议,新手快到我碗里来!

    新手小白想要系统性学好PHP开发,首先需要了解需要学些什么,然后给自己定下来一个学习路线,然后就朝着这个路线奋斗吧! 关于学习路线:(1) 熟悉HTML/CSS/JS等网页基本元素,完成阶段可自行制作 ...

  7. Java中的比较总结

    Java中的比较问题是一个很基础又很容易混淆的问题.今天就几个容易出错的点作一个比较详细的归纳与整理,希望对大家的学习与面试有帮助. 一.==与equals()的区别 首先,我们需要知道==与equa ...

  8. Sagit.Framework For IOS 开发框架入门开发教程1:框架下载与环境配置

    背景: 前天开源了框架:开源:Sagit.Framework For IOS 开发框架 所以注定要追补一套开发教程了,所以尽量抽空了!!! 步骤 1:下载框架源码 GitHub:https://git ...

  9. Spring Cloud Zuul 添加 ZuulFilter

    紧接着上篇随笔Spring Cloud Zuul写,添加过滤器,进行权限验证 1.添加过滤器 package com.dzpykj.filter; import java.io.IOException ...

  10. webpack入门之打包html,css,js,img(一)

    webpack到底是什么,网上一大堆介绍的东西,越看越不知道说的什么,所以今天打算自己来记录一下这段时间学习webpack的成果, webpack就是打包文件用的,html,css,js,img,为什 ...