Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android 开发 Tip 1

$
0
0

转载请注明出处:http://blog.csdn.net/crazy1235/article/details/70036554


使用RadioButton时,如果需要自定义样式,则要注意使用 android:state_checked 这个属性。

举个例子:

一个选择性别的RadioGroup:

<RadioGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="20dp"
        android:orientation="horizontal">
        <RadioButton
            style="@style/custom_rbtn"
            android:layout_width="70dp"
            android:text="男士" />
        <RadioButton
            style="@style/custom_rbtn"
            android:layout_width="70dp"
            android:layout_marginLeft="15dp"
            android:text="女士" />
    </RadioGroup>

style :

<style name="custom_rbtn">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:paddingLeft">10dp</item>
        <item name="android:paddingRight">10dp</item>
        <item name="android:layout_height">30dp</item>
        <item name="android:background">@drawable/selector_rbtn_bg</item>
        <item name="android:button">@null</item>
        <item name="android:gravity">center</item>
        <item name="android:textColor">@color/selector_rbtn_text</item>
    </style>

android:background & android:textColor 分别定义了selector

selector_rbtn_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/shape_bg_rbtn" android:state_checked="false" />
    <item android:drawable="@drawable/shape_bg_rbtn_down" android:state_checked="true" />
</selector>

selector_rbtn_text.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/gray" android:state_checked="false" />
    <item android:color="@android:color/white" android:state_checked="true" />
</selector>

如果该文件放在 res/drawable/ 路径下,则AS界面预览时会出现下面的error

这里写图片描述

但是不影响正常编译运行。

实际上主需要把这个文件放到 res/color/ 路径下即可。


再来说一下GridView

自定义GridView的item样式的时候,注意使用 android:state_activated 属性

item_tag_grid_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/item_tag_tv"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/selector_gv_item_bg"
        android:gravity="center"
        android:textColor="@color/selector_gv_tag_text" />
</LinearLayout>

selector_gv_item_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/shape_bg_rbtn" android:state_activated="false" />
    <item android:drawable="@drawable/shape_bg_rbtn_down" android:state_activated="true" />
</selector>

selector_gv_tag_text.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#a09ca4" android:state_activated="false" />
    <item android:color="@android:color/white" android:state_activated="true" />
</selector>

各种selector的属性定义

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

参考:

https://developer.android.com/guide/topics/resources/color-list-resource.html

作者:crazy1235 发表于2017/4/14 16:20:32 原文链接
阅读:285 评论:0 查看评论

Android 开发 Tip 2

$
0
0

转载请注明出处:http://blog.csdn.net/crazy1235/article/details/70173560


多主题下引用attr的问题。

在5.0以下手机,如果drawable xml文件中引用了attr ,则会出现类似下面的错误

04-14 14:43:06.413: W/System.err(13850): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.jacksen.demo.view/com.jacksen.demo.view.selector.TestSelectorActivity}: android.view.InflateException: Binary XML file line #17: Error inflating class RadioButton
04-14 14:43:06.413: W/System.err(13850):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2459)
04-14 14:43:06.413: W/System.err(13850):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2521)
04-14 14:43:06.413: W/System.err(13850):    at android.app.ActivityThread.access$800(ActivityThread.java:172)
04-14 14:43:06.413: W/System.err(13850):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1393)
04-14 14:43:06.413: W/System.err(13850):    at android.os.Handler.dispatchMessage(Handler.java:110)
04-14 14:43:06.413: W/System.err(13850):    at android.os.Looper.loop(Looper.java:193)

但是在5.0及以上手机上并没有问题。


经过排除法确定是 ?attr 的问题。

在stackoverflow上有关于该问题的讨论

http://stackoverflow.com/questions/30756729/android-selector-drawable-doesnt-work-with-attributes

这里写图片描述

这里写图片描述

这是一个5.0之前的系统bug。

在Google Issue Tracker网站上也有开发者提交此问题:

https://issuetracker.google.com/issues/36941443


解决方案:

其实也简单,不是在xml中,定义drawable时,引用?attr有问题嘛!那就不用?attr,直接通过@drawable的形式引用资源

然后需要做的就是对每个主题定义一个drawable.xml

http://stackoverflow.com/questions/8041537/how-to-reference-style-attributes-from-a-drawable

作者:crazy1235 发表于2017/4/14 16:31:28 原文链接
阅读:316 评论:0 查看评论

Android进阶——自定义View之组合系统控件实现水珠形状的ItemView

$
0
0

引言

相信大家在项目开发的过程中一定会有不少需要在上方显示一张图片,而在其下方显示提示标题的效果,作为一个界面的功能按钮或者单纯作为一个列表的item项,尤其是当这个item还需要显示一些动画效果时候,此时更应该当成一个整体,否则动画效果就会需要额外的调整,否则就会不协调。

一、水珠形状ItemView功能概述

这里写图片描述
所上图所示,所谓ItemView本质就是一个组合控件————上方显示一个图片作为ItenView的图标下方显示文字作为补充说明,功能和普通的ImageViewButton大同小异,优势在于扩展性更好些,客制化也更强些,可以根据不同的素材灵活组合显示各种形状的的ItenView,当然这样的需求只使用系统控件也可以实现,只不过布局就麻烦些和复杂些,而且处理还需要做额外的动画效果处理,统一为一个整体优势显然更明显。至于其中的成本就各自衡量,个人建议多复用。

二、水珠形状ItemView设计思想

无论是继承系统自有控件还是组合系统控件,核心都是重写构造方法,在构造方法里扩展自己的功能

1、定义基本布局

实现控件布局有两种方式,一种是通过代码生成,第二种是通过xml定义。

2、提供交互反馈的效果

这里的反馈效果指的是按下的时候显示不同的图片和改变文字的颜色等,所以需要提供一个接口,而一切的交互源自Touch,前面的文章也说了通过重写onTouch可以实现自定义的交互。

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                listener.onItemDown(v);
                textView.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
                Log.e("TOUCHING", "onTouch: "+"fuckckck  onItemDown" );
                break;
            case MotionEvent.ACTION_MOVE:
                listener.onItemMove(v);
                break;
            case MotionEvent.ACTION_UP:
                listener.onItemUp(v);
                Log.e("TOUCHING", "onTouch: "+"fuckckck  ACTION_UP" );
                textView.setTextColor(getResources().getColor(android.R.color.white));
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.e("TOUCHING", "onTouch: "+"fuckckck  ACTION_CANCEL" );
                break;
        }
        return true;//return false,会不能触发UP、MOVE操作
    }
    public interface OnItemTouchListener {
        void onItemDown(View view);//当Item按下的时候

        void onItemUp(View view);//按下之后,ACTION_UP的时候触发

        void onItemMove(View view);
    }

三、实现水珠形状ItemView

1、定义ItemView的基本布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@mipmap/bcg_btn_press">
    <ImageView
        android:id="@+id/item_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/twenty_four_len"
        android:layout_gravity="center_horizontal"/>
    <TextView
        android:id="@+id/item_txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textColor="@android:color/white"
        android:textSize="@dimen/eighteen_font"
        />
</LinearLayout>

2、实现ItemView

package com.xiaoi.app.robot.view.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.xiaoi.app.robot.R;

/**
 * auther: MO
 * Date: 2017/1/10
 * Time:15:42
 * Des:
 */

public class ItemView extends RelativeLayout implements View.OnTouchListener {

    private Context context;
    private TextView textView;
    private ImageView imageView;
    private OnItemTouchListener listener;
    private int imgId;
    private String textId;

    public ItemView(Context context) {
        super(context);
        this.context = context;
    }

    public ItemView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init(attrs);
    }

    public ItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init(attrs);
    }

    private void initAttr(AttributeSet attrs) {
        TypedArray types = context.obtainStyledAttributes(attrs,
                R.styleable.item_view);
        try {
            imgId = types.getResourceId(R.styleable.item_view_img_src, R.mipmap.ic_user_btn);
            textId = types.getString(R.styleable.item_view_text);
        } finally {
            types.recycle(); // TypeArray用完需要recycle
        }
    }

    private void init(AttributeSet attrs) {
        View contentView = LayoutInflater.from(this.context).inflate(R.layout.item_view, this, true);
        imageView = (ImageView) contentView.findViewById(R.id.item_img);
        textView = (TextView) contentView.findViewById(R.id.item_txt);
        initAttr(attrs);
        setImageView(imgId);
        textView.setText(textId);
        setOnTouchListener(this);
    }

    public ImageView getImageView(){
        return this.imageView;
    }
    public void setImageView(int resId) {
        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resId);
        imageView.setImageBitmap(bitmap);
    }

    public void setTextView(String txt){
        textView.setText(txt);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                listener.onItemDown(v);
                textView.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
                Log.e("TOUCHING", "onTouch: "+"fuckckck  onItemDown" );
                break;
            case MotionEvent.ACTION_MOVE:
                listener.onItemMove(v);
                break;
            case MotionEvent.ACTION_UP:
                listener.onItemUp(v);
                Log.e("TOUCHING", "onTouch: "+"fuckckck  ACTION_UP" );
                textView.setTextColor(getResources().getColor(android.R.color.white));
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.e("TOUCHING", "onTouch: "+"fuckckck  ACTION_CANCEL" );
                break;
        }
        return true;//return false,会不能触发UP、MOVE操作
    }

    public void setItemTouchListener(OnItemTouchListener listener) {
        this.listener = listener;
    }

    public interface OnItemTouchListener {
        void onItemDown(View view);//当Item按下的时候

        void onItemUp(View view);//按下之后,ACTION_UP的时候触发

        void onItemMove(View view);
    }
}

四、应用水珠形状ItemView

使用起来很简单,我就贴部分代码

布局代码activity_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:crazymo="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bcg_activity">
    <ImageView
        android:id="@+id/bcg_light"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:scaleType="centerInside"
        android:src="@mipmap/bcg_main" />
    <ImageView
        android:id="@+id/btn_speak_bgn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:background="@mipmap/voice_01"
        android:clickable="true"
        android:layout_marginLeft="15dp"
        android:visibility="invisible"
        android:scaleType="centerInside" />

    <ImageView
        android:id="@+id/slide_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        android:clickable="true"
        android:src="@drawable/selctor_slide_btn" />
    <ImageView
        android:id="@+id/btn_speak"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@mipmap/voice_main_default"
        android:clickable="true"
        android:scaleType="centerInside" />


    <com.xiaoi.app.robot.view.widget.ItemView
        android:id="@+id/main_user_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="@dimen/one_hundred_len"
        crazymo:text="@string/auth" />
    <com.xiaoi.app.robot.view.widget.ItemView
        android:id="@+id/main_biz_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="@dimen/one_hundred_len"
        crazymo:img_src="@mipmap/ic_biz"
        crazymo:text="@string/biz" />

    <com.xiaoi.app.robot.view.widget.ItemView
        android:id="@+id/main_dance_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="@dimen/one_hundred_len"
        crazymo:img_src="@mipmap/ic_dance"
        crazymo:text="@string/dance"
        />
    <Button
        android:id="@+id/main_logout_btn"
        android:layout_width="@dimen/senventy_len"
        android:layout_height="@dimen/fifty_five_len"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginLeft="@dimen/one_hundred_len"
        android:layout_marginBottom="@dimen/thirty_len"
        android:background="@mipmap/bcg_btn_press"
        android:text="@string/logout"
        android:textColor="@color/white"
        android:textSize="@dimen/eighteen_font"
        android:visibility="visible"
        />
    <com.xiaoi.app.robot.view.widget.ItemView
        android:id="@+id/main_game_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_marginRight="@dimen/one_hundred_len"
        crazymo:img_src="@mipmap/ic_game"
        crazymo:text="@string/game"
        />
</RelativeLayout>

动画xml代码res/animator/animator_menu_in.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:propertyName="scaleX"
        android:valueFrom="0"
        android:valueTo="1"
        android:duration="1000"
        android:interpolator="@android:anim/accelerate_interpolator"
        />
    <objectAnimator
        android:propertyName="scaleY"
        android:valueFrom="0"
        android:valueTo="1"
        android:duration="1000"
        android:interpolator="@android:anim/accelerate_interpolator"
        />
    <objectAnimator
        android:propertyName="alpha"
        android:valueFrom="0"
        android:valueTo="1"
        android:duration="1000"
        android:interpolator="@android:anim/accelerate_interpolator"
        />
</set>

MainActivity.java


public class MainActivity extends BaseActivity implements ItemView.OnItemTouchListener, MainView {

    @Bind(R.id.bcg_light)
    ImageView bcgLight;
    @Bind(R.id.slide_btn)
    ImageView slideBtn;
    @Bind(R.id.btn_speak)
    ImageView btnSpeak;
    @Bind(R.id.main_user_content)
    ItemView mainUserContent;
    @Bind(R.id.main_biz_content)
    ItemView mainBizContent;
    @Bind(R.id.main_dance_content)
    ItemView mainDanceContent;
    @Bind(R.id.main_game_content)
    ItemView mainGameContent;
    private static final String TAG = "MainActivity";
    @Bind(R.id.btn_speak_bgn)
    ImageView btnSpeakBgn;
    @Bind(R.id.main_logout_btn)
    Button mainLogoutBtn;
    private boolean isShow = false, isVerify = false, isSpeaking = false, needRun = true;
    private int startX;
    private SlidrConfig config;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        int primary = getResources().getColor(R.color.colorPrimaryDark);
        int secondary = getResources().getColor(R.color.colorPrimary);
        config = new SlidrConfig.Builder()
                .primaryColor(primary)
                .secondaryColor(secondary)
                .position(SlidrPosition.LEFT)
                .velocityThreshold(2400)
                .distanceThreshold(.25f)
                .scrimStartAlpha(0.8f)
                .scrimEndAlpha(0f)
                .touchSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, getResources().getDisplayMetrics()))
                .build();

        // Attach the Slidr Mechanism to this activity
        Slidr.attach(this, config);
        ButterKnife.bind(this);
        init();
    }

    private void init() {
        setItemViewListen();
        showAnmail();
    }


    @Override
    protected void onResume() {
        super.onResume();
        mainLogoutBtn.setVisibility(View.GONE);
    }

    /**
     * @param v
     * @param animator
     */
    public void runXmlAnimSet(final View v, final int animator) {
        Animator anim = AnimatorInflater.loadAnimator(this, animator);
        /*Point center = ScreenUtil.getScreenCenterNoTitle(this);
        if (v.getId() == R.id.main_user_content) {
            v.setPivotX(center.x);
            v.setPivotY(center.y);
        } else if (v.getId() == R.id.main_biz_content) {
            v.setPivotX(-center.x);
            v.setPivotY(center.y);
        } else if (v.getId() == R.id.main_dance_content) {
            v.setPivotX(center.x);
            v.setPivotY(-center.y);
        } else if (v.getId() == R.id.main_game_content) {
            v.setPivotX(-center.x);
            v.setPivotY(-center.y);
        }*/
        anim.setTarget(v);
        anim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                if (animator == R.animator.animator_menu_in) {
                    v.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (animator == R.animator.animator_menu_out) {
                    v.setVisibility(View.GONE);
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                return;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                return;
            }
        });
        anim.start();
    }

    private void setItemViewListen() {
        mainUserContent.setItemTouchListener(this);
        mainBizContent.setItemTouchListener(this);
        mainDanceContent.setItemTouchListener(this);
        mainGameContent.setItemTouchListener(this);
    }

    private void setTouchListen() {
        bcgLight.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int endX = (int) event.getX();
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        startX = (int) event.getX();
                        Log.e("TOUCHING", "onTouch: startX" + startX + "startX" + startX);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        //向右滑动超过50px
                        if (endX - startX > 50) {
                            startVoiceActivity();
                            Toast.makeText(MainActivity.this, "**Moving to RIght** ", Toast.LENGTH_SHORT).show();
                        } else if (startX - endX > 50) {
                            Log.e("TOUCHING", "onTouch:ACTION_MOVE endX" + endX + "startX:" + startX);
                            Toast.makeText(MainActivity.this, "**Moving to Left** ", Toast.LENGTH_SHORT).show();
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        break;
                    default:
                        break;
                }
                return false;
            }
        });

        mainLogoutBtn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        logout();
                        mainLogoutBtn.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
                        break;
                    case MotionEvent.ACTION_UP:
                        mainLogoutBtn.setTextColor(getResources().getColor(android.R.color.white));
                        mainLogoutBtn.setVisibility(View.GONE);
                        break;
                    default:
                        break;
                }
                return false;
            }
        });
    }

    private void setPressBackground(View view) {
        switch (view.getId()) {
            case R.id.main_user_content:
                if (isVerify) {
                    mainUserContent.setImageView(R.mipmap.ic_user_btn_press);
                } else {
                    mainUserContent.getImageView().setImageBitmap(bitmap);
                }
                break;
            case R.id.main_biz_content:
                mainBizContent.setImageView(R.mipmap.ic_biz_press);
                break;
            case R.id.main_dance_content:
                mainDanceContent.setImageView(R.mipmap.ic_dance_press);
                break;
            case R.id.main_game_content:
                mainGameContent.setImageView(R.mipmap.ic_game_press);
                break;
            default:
                break;
        }
    }

    private void setNormalBackground(View view) {
        switch (view.getId()) {
            case R.id.main_user_content:
                if (isVerify) {
                    mainUserContent.setImageView(R.mipmap.ic_user_btn);
                } else {
                    mainUserContent.getImageView().setImageBitmap(bitmap);
                }
                break;
            case R.id.main_biz_content:
                mainBizContent.setImageView(R.mipmap.ic_biz);
                break;
            case R.id.main_dance_content:
                mainDanceContent.setImageView(R.mipmap.ic_dance);
                break;
            case R.id.main_game_content:
                mainGameContent.setImageView(R.mipmap.ic_game);
                break;
            default:
                break;
        }
    }


    private void verifyFace() {

    }

    private void openBiz() {

    }

    private void showDance() {
        startActivity(new Intent(this,DanceActivity.class));
        overridePendingTransition(R.anim.enter_left_to_right, R.anim.back_right_to_left);
    }

    private void palyGame() {

    }

    @OnClick({R.id.slide_btn, R.id.btn_speak, R.id.bcg_light})
    void onClick(View view) {
        switch (view.getId()) {
            case R.id.slide_btn:
                openSlide();
                break;
            case R.id.bcg_light:
                showMenu();
                break;
            case R.id.btn_speak:
                startSpeak();
                break;
            default:
                break;
        }

    }

    private void openSlide() {
        startVoiceActivity();
    }

    private void startSpeak() {
        requestionPermission();
    }

    private void showMenu() {
        if (isShow) {
            hideAnmail();
        } else {
            showAnmail();
        }
    }

    //隐藏动画
    private void hideAnmail() {
        runXmlAnimSet(mainUserContent, R.animator.animator_menu_out);
        runXmlAnimSet(mainBizContent, R.animator.animator_menu_out);
        runXmlAnimSet(mainDanceContent, R.animator.animator_menu_out);
        runXmlAnimSet(mainGameContent, R.animator.animator_menu_out);
        isShow = false;
    }

    //显示动画
    private void showAnmail() {
        runXmlAnimSet(mainUserContent, R.animator.animator_menu_in);
        runXmlAnimSet(mainBizContent, R.animator.animator_menu_in);
        runXmlAnimSet(mainDanceContent, R.animator.animator_menu_in);
        runXmlAnimSet(mainGameContent, R.animator.animator_menu_in);
        isShow = true;
    }

    public void logout() {
        Toast.makeText(this, "**注销** ", Toast.LENGTH_SHORT).show();
        mainUserContent.setImageView(R.mipmap.ic_user_btn);
        mainUserContent.setTextView("身份注册");
        isVerify = false;
    }

    @Override
    public void onItemDown(View view) {
        setPressBackground(view);
        switch (view.getId()) {
            case R.id.main_user_content:
                verifyFace();
                break;
            case R.id.main_biz_content:
                openBiz();
                break;
            case R.id.main_dance_content:
                showDance();
                break;
            case R.id.main_game_content:
                palyGame();
                break;
            default:
                break;
        }
    }

    @Override
    public void onItemUp(View view) {
        setNormalBackground(view);
    }

    @Override
    public void onItemMove(View view) {
        return;
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        voiceUtil.onDestroy(false);
    }
}

这里写图片描述

作者:CrazyMo_ 发表于2017/4/14 17:38:32 原文链接
阅读:126 评论:0 查看评论

Java设计模式(适配器模式)

$
0
0

想了解一下Java设计模式的适配器模式,然后在网上翻了翻。唉~痛心!就看到一篇让我满意的,然后就发给李达康书记看了看。达康书记说:“确实不错!”;我认真研究了一下,根据自己的理解做了一下记录!

什么是适配器模式(adapter)??将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

优点:将目标类和适配者类解耦,通过引入一个适配器类(adapter)来重用现有的适配者类(adaptee),而无须修改原有代码,而且提高了适配者的复用性。

怎么理解“适配器这个词儿”呢?举个简单的栗子:就说我们使用的手机,苹果手机和安卓手机。他们为各自进行电池充电的时候,使用的充电接口类型是不一样的。苹果手机的是apple lighting,安卓手机的是Type-C。如果你分别有一个这样的手机,平时没电的时候使用各自的充电数据线插一下就行了。但是,如果突然发现苹果的数据线没了怎么办?简单,买一个唄!请不要这样,人家第一次遇到这种突发事件。接着说,如果你有一个转换头,这样所有事情不久暂时解决了吗!

而这个“转换头”就是这样的:好博文地址

在我们的代码中我们可以把它当作“适配器Adapter”。

对于普通的充电方式,比如安卓手机(代码实现):

//接口:安卓充电接口
public interface IBatteryChargingForAndroid {
    //type-c 充电接口
    void batteryChargingWithTypeC();
}

//安卓充电器
public class AndroidBatteryCharger implements IBatteryChargingForAndroid{
    @Override
    public void batteryChargingWithTypeC() {
        System.out.println("安卓手机正在充电中... \n请稍后骚扰我~");
    }
}

//安卓手机(独立完成各自的电充)
public class AndroidPhone {
    private IBatteryChargingForAndroid iAndroid;
    public AndroidPhone(IBatteryChargingForAndroid IAndroid) {
        this.iAndroid = iAndroid;
    }

    //给安卓手机充电噻~
    public void batteryCharging(){
        iAndroid.batteryChargingWithTypeC();
    }
}

对于普通的充电方式,比如苹果手机(代码实现):

//接口:苹果充电接口
public interface IBatteryChargingForIOS {
    //苹果专用充电接口
    void batteryChargingWithIOS();
}
//苹果充电器
public class IOSBatteryCharger implements IBatteryChargingForIOS {
    @Override
    public void batteryChargingWithIOS() {
        System.out.println("IPhone7 正在充电中... \n请稍后骚扰我~");
    }
}
//苹果手机(独立完成各自的电充)
public class IOSPhone {
    private IBatteryChargingForIOS iosPhone;

    public IOSPhone(IBatteryChargingForIOS iosPhone) {
        this.iosPhone = iosPhone;
    }

    //给IPhone7 手机充电噻~(普通充电)
    public void batteryCharging(){
        iosPhone.batteryChargingWithIOS();
    }

}


用户拿起手机进行充电:
public class MainCharger {
    public static void main(String[] args){
        //安卓手机的普通充电
        IBatteryChargingForAndroid iAndroid = new AndroidBatteryCharger();
        AndroidPhone androidPhone = new AndroidPhone(iAndroid);
        androidPhone.batteryCharging();

        //苹果手机的普通充电
        IBatteryChargingForIOS iIOS = new IOSBatteryCharger();
        IOSPhone iosPhone = new IOSPhone(iIOS);
        iosPhone.batteryCharging();

    }
}

然后再看一下突发情况处理:如果没有苹果的数据线怎么弄?这个时候需要使用一个适配器来完成苹果手机的充电功能。比如;
//充电器 适配器(通过充电器的转换插头,能实现对IPhone的充电功能)
public class BatteryChargerAdapter implements IBatteryChargingForIOS{

    /**
     * 从当前类完成适配器的功能;
     * 目标(target):IBatteryChargingForIOS
     * 适配者(adaptee):IBatteryChargingForAndroid
     * 适配器(adapter):BatteryChargerAdapter
     */
    private IBatteryChargingForAndroid iAndroid;
    public BatteryChargerAdapter(IBatteryChargingForAndroid iAndroid){
        this.iAndroid = iAndroid;
    }


    @Override
    public void batteryChargingWithIOS() {
        //通过转换插头,将安卓的type-c接入,完成苹果充电功能
        iAndroid.batteryChargingWithTypeC();
    }
}
苹果的充电也要做出简单的调整:
//苹果手机(独立完成各自的电充)
public class IOSPhone {
    private IBatteryChargingForIOS iosPhone;

    public IOSPhone(){}

    public IOSPhone(IBatteryChargingForIOS iosPhone) {
        this.iosPhone = iosPhone;
    }

    //给IPhone7 手机充电噻~(普通充电)
    public void batteryCharging(){
        iosPhone.batteryChargingWithIOS();
    }

    //使用适配器进行转换-为转换器充电做准备
    public void setAdapter(IBatteryChargingForIOS iosPhone){
        this.iosPhone = iosPhone;
    }
}

最后再次拿起手机充电:
public class MainCharger {
    public static void main(String[] args){
        //安卓手机的普通充电
        IBatteryChargingForAndroid iAndroid = new AndroidBatteryCharger();
        AndroidPhone androidPhone = new AndroidPhone(iAndroid);
        androidPhone.batteryCharging();

        //苹果手机的普通充电
        IBatteryChargingForIOS iIOS = new IOSBatteryCharger();
        IOSPhone iosPhone = new IOSPhone(iIOS);
        iosPhone.batteryCharging();

        //使用转化器 之后对苹果手机进行充电
        //其实使用转化器充电的核心就是:可以在 BatteryChargerAdapter$batteryChargingWithIOS 方法
        //中看到,其实是变相的重写了苹果充电器接口 进行充电的 方法。
        BatteryChargerAdapter adapter = new BatteryChargerAdapter(iAndroid);
        iosPhone.setAdapter(adapter);
        iosPhone.batteryCharging();


    }
}






作者:u012827205 发表于2017/4/14 10:58:18 原文链接
阅读:47 评论:0 查看评论

Java设计模式(命令模式)

$
0
0

今天介绍一下命令模式!昨天晚上给老板怼了一架,这么虐待我们“改变世界的人”,对我可以但是怼我的兄弟就不行!当场提出离职,所以为了庆祝一下。今天就介绍介绍这种模式。

最近我想我们都在看一部电视剧《人民的名义》,没看过的赶紧去脑补一下!这么经典的电视剧,就是抛下女朋友不管也得看一看。特别式里面的人物,我的偶像,李达康书记还有小鲜肉侯亮平反贪局长!来看一张图,就这眼神这演技,满满的一百分~


书归正传,谈咱们的java设计模式(命令模式):

什么式命令模式麽?将一个命令请求封装成一个对象,从而使我们可用不同的命令请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

命令模式的优点是啥?命令模式的优点是实现了命令的发起对象和命令的具体实现对象———命令接收者的完全解耦,这样很方便扩展新的命令,只要实现新的命令对象,然后在装配的时候,把具体实现对象设置到命令中,就可以使用这个命令对象了,不用改变已有的实现。

假如有这样一个情景:李达康书记发现高晓琴贪污犯罪,这时候就命令侯亮平局长立刻前往山水庄园进行逮捕!好,我们使用代码实现一下:

//李达康 京州市市长 (命令的发起人)
public class LiDakang {

    private ICommand iCommand;
    public LiDakang(ICommand iCommand){
        this.iCommand = iCommand;
    }

    //李达康书记,发起命令:赶紧的~逮捕那个贪官!
    public void sendCommand(){
        System.out.println("命令:立刻去她家里进行逮捕~");
        iCommand.executeCommand();
    }
}

//命令接口
public interface ICommand {
    //执行命令方法(并非实际执行操作)
    void executeCommand();
}


//命令的实现类(真正收到达康书记命令的类,并要求亮平执行任务)
//命令实现类是没有真正实现命令要求的功能的;又被称做"命令触发器",功能是持有命令的对象
public class CommandExecute implements ICommand {
    private HouLingping lingping;
    public CommandExecute(HouLingping lingping){
        this.lingping = lingping;
    }
    @Override
    public void executeCommand() {
        //执行逮捕
        lingping.arrestedSomeBody();
    }
}


//侯亮平 反贪局局长 (命令的执行者 Receiver)
//真正执行命令的功能是接收者
public class HouLingping {
    /**
     * 功能:实施命令:逮捕 某人
     */
    public void arrestedSomeBody(){
        System.out.println("我立刻开着我的法拉利去逮捕她~");
    }
}



代码实现:

//功能实现类
public class ClientExecute {
    public static void main(String[] args){
        HouLingping lingping = new HouLingping();
        ICommand iCommand = new CommandExecute(lingping);
        LiDakang dakang = new LiDakang(iCommand);

        //达康书记的命令一发起,侯局长就会立刻收到命令,并开着跑车去执行命令,进行逮捕任务工作
        dakang.sendCommand();
    }
}


作者:u012827205 发表于2017/4/15 11:41:48 原文链接
阅读:89 评论:0 查看评论

Flutter基础—你好,Flutter!

$
0
0

什么是Flutter

Flutter是一个移动应用程序的软件开发工具包(SDK),用一个代码库构建高性能、高保真的iOS和Android应用程序。目标是使开发人员能够为Android和iOS提供自然的高质量的应用,在滚动行为、排版、图标等方面实现零差异。

学习Flutter不需要Android或iOS的开发经验,因为Flutter程序使用Dart语言编写,如果您使用过Java或JavaScript等语言,轻易就能上手,只要有面向对象语言的经验,即使不是程序员也能开发Flutter应用程序!

这里写图片描述

这是一个画廊的演示应用程序,里面有高质量的滚动图像、互动卡、按钮、下拉列表和购物车页面,可以看到Flutter应用程序在Android和iOS的显示几乎一样。

Flutter的优势

  • 高效率
    • 用一套代码库就能开发iOS和Android应用
    • 使用新型的、表现力强的语言和声明式的方法,用更少的代码来做更多的事情
    • 开发调试更容易方便
      • 可以在应用程序运行时更改代码并重新加载查看效果,也就是热重新加载
      • 修复崩溃时可以从应用程序停止的位置继续调试
  • 创建美观、高度定制的用户体验
    • Flutter框架内置了一组丰富的质感设计控件
    • 实现定制、美观、品牌驱动的设计,而不受OEM控件集的限制

Flutter的核心内容

Flutter包括功能反应框架、2D渲染引擎、现成的控件和开发工具,这些组件协同工作,帮助您设计、构建、测试和调试应用程序。

一切都是控件

控件是每个Flutter应用程序的基本构建块,与分离视图、控制器、布局和其他属性的框架不同,Flutter具有一致的统一对象模型:控件。

一个控件可以定义:

  • 结构元素(比如按钮或菜单)
  • 风格元素(比如字体或颜色方案)
  • 布局的方面(比如填充)
  • 一些业务逻辑
  • 等等…

控件是基于构图形成层次结构,每个控件嵌套在其中,并从其父代继承属性,没有单独的“应用程序”对象,只有根控件。

您可以通过告知框架用另一个控件替换层次结构中的控件来响应事件,比如用户交互,然后框架会对比新的控件和旧的控件,并有效的更新用户界面,即更新有变化的控件。

组合大于继承

控件本身通常由许多小型、单用途的控件组成,结合起来产生强大的效果,例如,Container是一种常用的控件,由负责布局、绘画、定位和大小调整的几个控件组成,具体来说,Container是由LimitedBox、ConstrainedBox、 Align、Padding、DecoratedBox和Transform控件组成,而不是将Container子类化来产生自定义效果,您可以用这种新颖的方式组合这些以及其他简单的控件。

类的层次结构是扁平的,以最大化可能的组合数量。

这里写图片描述

您还可以通过与其他控件组合来控制控件的布局,例如,要居中一个控件,您可以将其包装到Center控件中,有一些填充、对齐、行、列和网格的控件,这些布局控件没有自己的视觉表示,相反,它们的唯一目的是控制另一个控件布局的某些方面,当你想了解为什么一个控件以某种方式呈现,检查邻近的控件通常就可以知道原因。

层次结构

Flutter框架被组织成一系列的层次,每一层都建立在底层之上。

图片位置

上图显示的框架上层,比下层使用的更频繁,这个设计的目标是帮助你用更少的代码来做更多的事情。例如,通过从Widgets层(控件层)组成基本控件来构建Material层(质感设计层),并且Widgets层(控件层)本身是通过从Rendering层(渲染层)编排下层对象来构建的。

这些层级提供了许多构建应用程序的选项,选择一种自定义的方法来发挥框架的完整表现力,或者从Widgets层(控件层)使用构建块,或混合搭配。您可以使用Flutter提供的现成控件,或使用Flutter团队用于构建框架的相同工具和技术来创建自己的自定义控件。

这就是Flutter,您可以获得高层次、统一的控件概念的生产力优势,同时还能深入到较低层次自由探索。

构建控件

您可以通过实现返回控件的树(或层次结构)的构建函数来定义控件的独特特征,这个树更具体地表示控件的用户界面的一部分。例如,工具栏控件可能具有返回一些文本和各种按钮的水平布局的构建函数,框架会递归地询问每个这些控件的构建,直到该过程在完全具体的控件中出现,然后框架再将其拼接成树。

控件的构建功能应​​该没有副本的,无论何时被要求构建,控件应该返回一个新控件,而不管控件先前返回的是什么。但是框架会将先前版本与当前版本进行比较,并确定需要对用户界面进行哪些修改。

这种自动比较是非常有效的,可以实现高性能、互动的应用程序。构建函数的设计通过专注于声明控件是什么来简化代码,而不是将用户界面从一个状态更新到另一个状态的复杂化。

处理用户交互

如果控件的独特特征需要根据用户交互或其他因素进行更改,则该控件是有状态的。例如,如果控件具有每当用户点击按钮时递增的计数器,则计数器的值是该控件的状态,当该值更改时,需要重新构建控件以更新UI。

这些控件子类StatefulWidget(而不是StatelessWidget),并将其可变状态存储在State的子类中。

插入图片

每当你突然变化一个状态对象(例如增加计数器)时,你必须调用setState()来通知调用状态再次创建框架来更新用户界面。

具有单独的状态和控件对象可以让其他控件以相同的方式处理无状态和有状态控件,而不用担心丢失状态。父类可以自由地创建一个新的子类实例,也不会失去子类的持续状态,更不需要坚持一个子类来保持自己的状态。该框架在适当时执行查找和重用现有状态对象的所有工作。

作者:hekaiyou 发表于2017/4/15 15:12:33 原文链接
阅读:103 评论:0 查看评论

SpriteKit中Node数量只增不降的原因和解决

$
0
0

上篇 SpriteKit代码动态调整sks文件中粒子的颜色 博客中的App,运行中发现场景中的Node数量不断在增多,并没有减少,只见内存占用不断变大,虽然增长幅度很小,但强迫症伤不起…

只有一个可能,Node没有被释放!

检查代码发现,动态生成的Node只有小球和粒子效果,将粒子产生的代码注释掉后发现Node数量恢复正常了!!!那么就是粒子没有从场景中删掉.

虽然粒子正常播放完后会hide,但并没有从删除掉…

所以我们写个Action吧:

if let fireParticles = SKEmitterNode(fileNamed: "FireParticles") {
            fireParticles.position = ball.position
            addChild(fireParticles)
            let actWait = SKAction.wait(forDuration: 2.5)
            let actBlk = SKAction.run {
                fireParticles.removeFromParent()
            }
            let seq = SKAction.sequence([actWait,actBlk])
            fireParticles.run(seq)
        }

因为粒子播放时间是2.5秒,所以我们等待2.5后将其删除,其实SKAction还提供了一种更简单的方法,就是removeFromParent方法,上面的代码可以修改如下:

if let fireParticles = SKEmitterNode(fileNamed: "FireParticles") {
            fireParticles.position = ball.position
            addChild(fireParticles)
            let actWait = SKAction.wait(forDuration: 2.5)
            let actRemove = SKAction.removeFromParent()

            let seq = SKAction.sequence([actWait,actRemove])

            fireParticles.run(seq)
        }

再次运行App,发现场景节点的数量始终保持不变,我们也就解决了这个问题.

作者:mydo 发表于2017/4/15 16:26:36 原文链接
阅读:96 评论:0 查看评论

SpriteKit中节点存储个性数据的办法

$
0
0

我们知道大多数情况下,我们可以在SKNode实例的name属性中添加一些个性数据,不过一来只能使用一次,如果还有其他数据就无解了,二来只能存放String类型,如果还要存放其他类型呢?还是无解!

不过不知各位是否注意,其实SKNode还提供一个非常有用的属性,可以用它存放任意数据类型,它就是userData!

它是一个字典类型,我们可以任意组织数据存入其中供后来使用:

let imageName = rndBallName()
let ball = SKSpriteNode(imageNamed: imageName)
ball.userData = ["imageName":imageName]
//......

在需要的地方我们可以解包,然后使用:

let imageName = ball.userData!["imageName"] as! String
//......

很方便吧! :)

作者:mydo 发表于2017/4/15 16:35:06 原文链接
阅读:192 评论:0 查看评论

iOS中的浅复制与深复制

$
0
0

当谈到对象复制时都绕不开浅复制与深复制的区分,它们是什么意思呢?

  • 浅复制:只复制对象的指针,两个对象指向的还是同一个地址的内容,操作一个时会影响另一个的值。
  • 深复制:复制对象的内容,两个对象指向两个不同地址的内容,操作一个时不会影响另一个的值。

在OC中,因为采用内存计数的方式管理内存,所以浅复制时会对同一个内容计数加一,深复制则不会。

在OC中,复制操作有copy和mutableCopy两种方法,那哪种是浅复制哪种是深复制呢?

非集合对象

先把对象大致分为两类:非集合对象与集合对象,至于为什么要这么分,待会讲集合对象的时候再说。

非集合对象就是指NSString、NSNumber等本身就是具体内容的对象。像NSString这种对象,还有一个相关的叫NSMutableString。所以在非集合对象中又可以分为可变对象和不可变对象。

对他们进行copy与mutableCopy的含义是:

  • 对于不可变非集合对象(如NSString),copy操作是浅复制,只会复制指针,mutableCopy操作是深复制,
  • 对于可变非集合对象(如NSMutableString),copy和mutableCopy都是深复制,都会创建一个新的同样的内容来返回,但是要注意,copy返回的是不可变对象,也就是说即使你对一个NSMutableString做copy操作,返回给另一个NSMutableString,然后去对这个NSMutableString做变化操作,会报错。

用代码来看如下:

NSString *string = @"origin";
NSString *stringCopy = [string copy];// 浅复制
NSMutableString *stringMCopy = [string mutableCopy];// 深复制

NSMutableString *string = [NSMutableString stringWithString: @"origin"];
NSString *stringCopy = [string copy];// 深复制
NSMutableString *mStringCopy = [string copy];// 深复制
NSMutableString *stringMCopy = [string mutableCopy];// 深复制
[mStringCopy appendString:@"mm"]; // crash

集合对象

上面说了非集合对象,那什么是集合对象呢?集合对象就是指NSArray、NSDictionary、NSSet这些包含其他对象的对象。

为什么要做这个区分呢?因为浅复制、深复制这两个概念其实并不完全,更准确的应该分为三种:浅复制、深复制、完全深复制。

在OC中,当你对一个集合对象做深复制时,这个深复制只是单层的,集合内的元素对象其实还只是引用,并不是每一层都是深复制,这一情况,苹果定义为单层深复制(one-level-deep copy)。只有对集合内的每一层都去做深复制,才能够称为完全深复制。

先说说简单的浅复制与深复制,其实与非集合对象差不多:

  • 对于不可变集合对象(如NSArray),copy操作是浅复制,只会复制指针,mutableCopy操作是深复制,
  • 对于可变集合对象(如NSMutableArray),copy和mutableCopy都是深复制,都会创建一个新的同样的内容来返回,但是要注意两点,一是copy返回的还是不可变对象,二就是上面说的,这个深复制只是单层深复制,里面包含的元素还是指针浅复制。

代码来说就是:

NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"];
NSArray *copyArray = [array copy];// 浅复制
NSMutableArray *mCopyArray = [array mutableCopy];// 单层深复制

NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];// 单层深复制,返回不可变对象
NSMutableArray *mCopyArray = [array mutableCopy];// 单层深复制

那如果要做完全深复制该怎么做呢?

有两种方法:

  • 一种是用如下所示的方法:
NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];

copyItems设为YES会对集合中的每一个对象尝试做深复制,但是要求集合中的元素对象遵循NSCopying 协议,否则就会报错。但这其实还是对元素这一层的单层深复制,再下去也没有完全深复制。

NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

以上就是OC中浅复制与深复制的各种应用了,要自己测试到底操作是浅复制还是深复制,可以通过看对象的引用计数是否增加:

NSLog(@"mArray retain count: %d", [mArray retainCount]);// ARC下不可用

或者直接看两个对象的地址是否一致:

NSLog(@"string自身指针的地址:%x", &str);
NSLog(@"string指针指向的对象的地址:%p", str);

版权所有:http://blog.csdn.net/cloudox_

作者:Cloudox_ 发表于2017/4/15 20:39:27 原文链接
阅读:156 评论:0 查看评论

谈谈程序员的焦虑感

$
0
0

谈谈程序员的焦虑感

@(随笔)

这篇文章继续不谈技术,我们来谈谈关于程序员的焦虑感,写这篇文章源自有位同学问了我一个问题:

我接触Android 应用层也快3,4年了,说实话,公司一直让做app 层开发,工作之余,学完ReactNative 和Weex,简单的做了Demo,实际开发中不让上,学完一年忘完了,之前大学专业还专门学了一年Linux 内核,开发中很少去用,慢慢就废了。有公司前辈建议我学java后端 ,有前辈建议我去学混合开发,说实话,好迷茫,尤其是偶尔学的新东西,如果成为不了一种知识体系,很碎片化的在学习,跟我这样,学了一个月的node.js,学完没处上,慢慢就发现,不知道学啥了。

我一看,这不是我曾经迷茫过的类似的问题么。我相信这也是工作几年的程序员会产生的迷茫,因为工作的几年已经不是那时刚步入社会的新人(菜鸟),已经可以做一些曾经做不了的事情了,也算是个熟练工了,能够自己解决日常工作的一些问题,能够比较轻松实现一些功能需求了。但是现在问题来了,技术到了瓶颈,怎么办?我曾经在知乎回答过一个问题:

感觉安卓应用开发就是一些接口的调用和ui界面的逻辑处理,还有其他可以深入学习的吗? - 来自知乎

当时我的回答是这样的:

独立开发过几个产品,说一下自己的见解,基本上有完整经历过一个产品的开发过程都应该清楚UI层是应用开发中最常变的一部分,app开发者绝大部分的时间都花在UI微调上,而业务逻辑基本上需求定下来不会变动太多,除非是产品迭代一些功能性的追加,不然就是不停的堆UI、堆UI,干过两年以上的开发者基本的开发能力已经有了,业务能力也差不多,这时候会遇到瓶颈,基本上就是找不到更加深入的方向,这个时候就可以考虑一下自己参与过的产品所使用到的技术是自己深究过的,打个比方,开发app的时候,一般会用到Http框架,这样的框架你是否有真正去实践过,或者有研究过它的实现思路和设计思想,我们一般都会拿过来用,但为什么要这样设计就是我们需要去了解的,这可以提高你的架构的设计能力和编码的水平,除了框架,更多的是深入framework层相关的,一些特殊的需求,比如动态热更新、插件化的实现,对应用开发者来说都是新的挑战,还有对app的性能调优,产品上的优化,这些东西都是可以根据其中某一个点进行发散的。只做UI层的尝试是没有什么前途的,不管干多少年,眼界也是极其有限,做技术的只能不停的深挖,构建完整的知识体系,才能打通自己的任督二脉。我个人的一些观点,仅供参考。

这个只是针对App开发的一些见解罢了。想一想,现在技术日新月异,层出不穷, 就拿Android领域来说:

RxJava、Kotlin、React native、MVP、MVVM、插件化、热更新、Data Binding…

这些技术的出现都是为了去解决以往解决不了的问题,那么我们的问题来了,我们是不是每出一个新技术都要去学一下,当然我们作为程序员也应该与时俱进,去接受一些先进的思想。但这里有个问题就是,我们太过于强调新,而忘记了这些新技术的本质,任何一门新的技术都改不了一个事实:底层技术是不会变的。至于学不学,可以问自己一个问题:

学这门技术能够解决什么问题?我如何在实际工作中运用上?

我们学习一门新的技术不是为了学习而学,最终还是要回归实际应用上去,不然过一段时间你发现自己又给忘记了,不然怎么叫学以致用呢。

前面的问题还有一个就是,要往哪个方向学更有前景?这个真不好说,很多人经常犯的一个毛病就是想得太多,做得太少,每天都在纠结学什么,就是不动手。我们可以看到是前几年移动开发很火,大家一股脑的就往这个领域钻,再看看现在,移动开发似乎没有这么火了,想找工作的同学和想换工作的同学是不是发现难很多了,你能想到会是这样的局面吗,真不好说,现在也许是市场冷静了,任何一门领域的火热都跟市场的需求相挂钩,一旦市场需求量少了,或者不再需要,我们都有可能面临失业。

我们来看看现在什么最火热,列举一些:

VR、AR、机器学习、深度学习、人工智能

想必大家每天都会看到这些相关的新闻和资讯,也许这些是未来比较有前景的领域了,但我们不能因为这些概念的火热就一股脑得投入到里面,我的看法就是找一个自己感兴趣的方向,持续发力,让自己成为这领域的专家,但前提是你得打好基础。

好了,说到这里你的焦虑感是否少了些许呢?如果还没有,我提个问题就是你想成为哪个领域的工程师:

  • 终端
    • Android/iOS
    • Cocos2d
    • Unity 3D
  • Web前端
  • 后台
  • 运维
  • 测试

其实每个领域都有相应的技术栈,你可以从中选一个自己感兴趣的方向去学习,但并不局限于说你只关注于某一个方向,我的建议是:

专注于一个领域,横向扩展其他领域的技术。

总之一句话,不要自我设限,没有人说你一定要成为什么样的人,而是你想成为什么样的人。谁知道哪天你不想做程序员了,想回家卖咸鸭蛋了,那你就不需要纠结学什么的问题了,及时行乐,开心就好。

作者:wwj_748 发表于2017/4/15 21:10:24 原文链接
阅读:132 评论:0 查看评论

Android 开发 Tip 4 -- You must call removeView() on the child's parent first

$
0
0

转载请注明出处:http://blog.csdn.net/crazy1235/article/details/70188640


问题描述:

Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

at android.view.ViewGroup.addViewInner(ViewGroup.java:4417)
at android.view.ViewGroup.addView(ViewGroup.java:4258)
at android.view.ViewGroup.addView(ViewGroup.java:4198)
at android.view.ViewGroup.addView(ViewGroup.java:4171)

通常发生在动态添加view的时候,要添加的view有parent view。所以报这个错误。


还原现场

LayoutInflater inflater = getLayoutInflater();
LinearLayout tempLayout = (LinearLayout) inflater.inflate(R.layout.temp_layout, null);
TextView textView = (TextView) tempLayout.findViewById(R.id.text_view);
rootLayout.addView(textView);

temp_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/child" />

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hope is a good thing, maybe the best of things" />

</LinearLayout>

解决方案

先将要添加的view从其原先parent viewgroup中移除。

TextView textView = (TextView) tempLayout.findViewById(R.id.text_view);
tempLayout.removeView(textView);
rootLayout.addView(textView);
作者:crazy1235 发表于2017/4/15 23:43:50 原文链接
阅读:15 评论:0 查看评论

Android开发笔记(一百四十一)读取PPT和PDF文件

$
0
0

读取ppt文件

读取纯文本

上一篇博文讲到在Android上如何读取word文件内容,那么office三剑客中还剩ppt文件的读取。前面解析word文件和excel文件时,都用到了poi库读取文件内容,对于ppt一样也可以通过poi读取幻灯片中的文本。HSLFSlideShow类就是poi中专门用于解析幻灯片的工具类,每张幻灯片又分别由单独的HSLFSlide类处理,幻灯片中的具体图文内容则由HSLFTextParagraph和HSLFTextRun进行分辨。

下面是使用poi解析ppt文件(2003格式)的效果图:


不同版本的poi库在解析ppt的代码略有区别,下面是使用poi15读取ppt的代码:
	public static ArrayList<String> readPPT(String path) {
		ArrayList<String> contentArray = new ArrayList<String>();
		try {
			FileInputStream fis = new FileInputStream(new File(path));
			HSLFSlideShow hslf = new HSLFSlideShow(fis);
			List<HSLFSlide> slides = hslf.getSlides();
			for (int i = 0; i < slides.size(); i++) {
				String content = "";
				HSLFSlide item = slides.get(i);
				// 读取一张幻灯片的内容(包括标题)
				List<List<HSLFTextParagraph>> tps = item.getTextParagraphs();
				for (int j = 0; j < tps.size(); j++) {
					List<HSLFTextParagraph> tps_row = tps.get(j);
					for (int k = 0; k < tps_row.size(); k++) {
						HSLFTextParagraph tps_item = tps_row.get(k);
						List<HSLFTextRun> trs = tps_item.getTextRuns();
						for (int l = 0; l < trs.size(); l++) {
							HSLFTextRun trs_item = trs.get(l);
							content = String.format("%s%s\n", content, trs_item.getRawText());
						}
					}
				}
				contentArray.add(content);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return contentArray;
	}


读取图文样式

poi方式只能有效读取ppt内部的文字信息,对于ppt内带的图片以及文字样式,便力有不逮了。在博文《Android开发笔记(一百四十)Word文件的读取与显示》中,提到可以解析docx内部的document.xml文件,从xml标记中获取图片信息与样式信息,然后把图文格式构造成html文件,最后由WebView网页视图加载显示html。对于pptx文件,也可以解析pptx内部的slide*.xml幻灯片文件,采用跟解析docx类似的做法,把解析得到的图片与样式数据写入到html文件,从而曲线实现了pptx文件的读取功能。

下面是以HTML格式显示pptx文件的效果图:


下面是解析pptx并生成htmml文件的主要代码:
	private void readPPTX(String pptPath) {
		try {
			ZipFile pptxFile = new ZipFile(new File(pptPath));
			int pic_index = 1; // pptx中的图片名从image1开始,所以索引从1开始
			for (int i = 1; i < 100; i++) { // 最多支持100张幻灯片
				String filePath = String.format("%s%d.html", FileUtil.getFileName(pptPath), i);
				String htmlPath = FileUtil.createFile("html", filePath);
				Log.d(TAG, "i="+i+", htmlPath=" + htmlPath);
				output = new FileOutputStream(new File(htmlPath));
				presentPicture = 0;
				output.write(htmlBegin.getBytes());
				
				ZipEntry sharedStringXML = pptxFile.getEntry("ppt/slides/slide" + i + ".xml"); // 获取每张幻灯片
				InputStream inputStream = pptxFile.getInputStream(sharedStringXML);
				XmlPullParser xmlParser = Xml.newPullParser();
				xmlParser.setInput(inputStream, "utf-8");

				boolean isTitle = false; // 标题
				boolean isTable = false; // 表格
				boolean isSize = false; // 文字大小
				boolean isColor = false; // 文字颜色
				boolean isCenter = false; // 居中对齐
				boolean isRight = false; // 靠右对齐
				boolean isItalic = false; // 斜体
				boolean isUnderline = false; // 下划线
				boolean isBold = false; // 加粗
				int event_type = xmlParser.getEventType();// 得到标签类型的状态
				while (event_type != XmlPullParser.END_DOCUMENT) {// 循环读取流
					switch (event_type) {
					case XmlPullParser.START_TAG: // 开始标签
						String tagBegin = xmlParser.getName();
						if (tagBegin.equalsIgnoreCase("ph")) { // 判断是否标题
							String titleType = getAttrValue(xmlParser, "type", "text");
							if (titleType.equals("text")) {
								isTitle = false;
							} else {
								isTitle = true;
								isSize = true;
								if (titleType.equals("ctrTitle")) {
									output.write(centerBegin.getBytes());
									isCenter = true;
									output.write(String.format(fontSizeTag, getSize(60)).getBytes());
								} else if (titleType.equals("subTitle")) {
									output.write(centerBegin.getBytes());
									isCenter = true;
									output.write(String.format(fontSizeTag, getSize(24)).getBytes());
								} else if (titleType.equals("title")) {
									output.write(String.format(fontSizeTag, getSize(44)).getBytes());
								}
							}
						}
						if (tagBegin.equalsIgnoreCase("pPr") && !isTitle) { // 判断对齐方式
							String align = getAttrValue(xmlParser, "algn", "l");
									xmlParser.getAttributeValue(0);
							if (align.equals("ctr")) {
								output.write(centerBegin.getBytes());
								isCenter = true;
							}
							if (align.equals("r")) {
								output.write(divRight.getBytes());
								isRight = true;
							}
						}
						if (tagBegin.equalsIgnoreCase("srgbClr")) { // 判断文字颜色
							String color = xmlParser.getAttributeValue(0);
							output.write(String.format(spanColor, color).getBytes());
							isColor = true;
						}
						if (tagBegin.equalsIgnoreCase("rPr")) {
							if (!isTitle) {
								// 判断文字大小
								String sizeStr = getAttrValue(xmlParser, "sz", "2800");
								int size = getSize(Integer.valueOf(sizeStr)/100);
								output.write(String.format(fontSizeTag, size).getBytes());
								isSize = true;
							}
							// 检测到加粗
							String bStr = getAttrValue(xmlParser, "b", "");
							if (bStr.equals("1")) {
								isBold = true;
							}
							// 检测到斜体
							String iStr = getAttrValue(xmlParser, "i", "");
							if (iStr.equals("1")) {
								isItalic = true;
							}
							// 检测到下划线
							String uStr = getAttrValue(xmlParser, "u", "");
							if (uStr.equals("sng")) {
								isUnderline = true;
							}
						}
						if (tagBegin.equalsIgnoreCase("tbl")) { // 检测到表格
							output.write(tableBegin.getBytes());
							isTable = true;
						} else if (tagBegin.equalsIgnoreCase("tr")) { // 表格行
							output.write(rowBegin.getBytes());
						} else if (tagBegin.equalsIgnoreCase("tc")) { // 表格列
							output.write(columnBegin.getBytes());
						}
						if (tagBegin.equalsIgnoreCase("pic")) { // 检测到图片
							ZipEntry pic_entry = FileUtil.getPicEntry(pptxFile, "ppt", pic_index);
							if (pic_entry != null) {
								byte[] pictureBytes = FileUtil.getPictureBytes(pptxFile, pic_entry);
								writeDocumentPicture(i, pictureBytes);
							}
							pic_index++; // 转换一张后,索引+1
						}
						if (tagBegin.equalsIgnoreCase("p") && !isTable) {// 检测到段落,如果在表格中就无视
							output.write(lineBegin.getBytes());
						}
						// 检测到文本
						if (tagBegin.equalsIgnoreCase("t")) {
							if (isBold == true) { // 加粗
								output.write(boldBegin.getBytes());
							}
							if (isUnderline == true) { // 检测到下划线,输入<u>
								output.write(underlineBegin.getBytes());
							}
							if (isItalic == true) { // 检测到斜体,输入<i>
								output.write(italicBegin.getBytes());
							}
							String text = xmlParser.nextText();
							output.write(text.getBytes()); // 写入文本
							if (isItalic == true) { // 输入斜体结束标签</i>
								output.write(italicEnd.getBytes());
								isItalic = false;
							}
							if (isUnderline == true) { // 输入下划线结束标签</u>
								output.write(underlineEnd.getBytes());
								isUnderline = false;
							}
							if (isBold == true) { // 输入加粗结束标签</b>
								output.write(boldEnd.getBytes());
								isBold = false;
							}
							if (isSize == true) { // 输入字体结束标签</font>
								output.write(fontEnd.getBytes());
								isSize = false;
							}
							if (isColor == true) { // 输入跨度结束标签</span>
								output.write(spanEnd.getBytes());
								isColor = false;
							}
//							if (isCenter == true) { // 输入居中结束标签</center>。要在段落结束之前再输入该标签,因为该标签会强制换行
//								output.write(centerEnd.getBytes());
//								isCenter = false;
//							}
							if (isRight == true) { // 输入区块结束标签</div>
								output.write(divEnd.getBytes());
								isRight = false;
							}
						}
						break;
					// 结束标签
					case XmlPullParser.END_TAG:
						String tagEnd = xmlParser.getName();
						if (tagEnd.equalsIgnoreCase("tbl")) { // 输入表格结束标签</table>
							output.write(tableEnd.getBytes());
							isTable = false;
						}
						if (tagEnd.equalsIgnoreCase("tr")) { // 输入表格行结束标签</tr>
							output.write(rowEnd.getBytes());
						}
						if (tagEnd.equalsIgnoreCase("tc")) { // 输入表格列结束标签</td>
							output.write(columnEnd.getBytes());
						}
						if (tagEnd.equalsIgnoreCase("p")) { // 输入段落结束标签</p>,如果在表格中就无视
							if (isTable == false) {
								if (isCenter == true) { // 输入居中结束标签</center>
									output.write(centerEnd.getBytes());
									isCenter = false;
								}
								output.write(lineEnd.getBytes());
							}
						}
						break;
					default:
						break;
					}
					event_type = xmlParser.next();// 读取下一个标签
				}
				output.write(htmlEnd.getBytes());
				output.close();
				htmlArray.add(htmlPath);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}


读取pdf文件

Vudroid方式读取

上面以html方式显示pptx文件,虽然能够读取图片与文字样式,但是与原始的幻灯片内容相差还是比较大的,主要问题包括:
1、ppt中的图文不像word那样一般是上下排列,而是既有上下排列又有左右排列,还有根据相对位置的排列。可是简单的html格式只能上下排列,难以适应其它方向的图文排版。
2、ppt通常自带幻灯片背景,也就是每个幻灯片都有的背景图片,可是slide*.xml文件中解析不到背景图片;况且由于背景图的存在,使得图片序号与幻灯片插图对应不上,造成幻灯片页面上的插图产生混乱。
3、每张ppt的尺寸规格是固定的,及长度和高度的比例是不变的;但是一旦转为html格式,页面的长宽比例就乱套了,完全不是ppt原来的排版布局。

如果在java服务端,可以调用HSLFSlide类的draw方法,直接把每张幻灯片原样画到临时的图像文件。然而在手机端,无法调用draw方法,因为该方法用到了java的awt图像库,而Android并不提供该图像库,所以poi不能直接绘制ppt的原始页面。

既然直接显示原样的幻灯片难以实现,那么就得考虑其它的办法,一种思路是先在服务端把ppt文件转换为pdf文件,然后手机端再来读取pdf文件。正好Android平台上拥有多种pdf的解析方案,其中之一是开源框架Vudroid,该框架允许读取pdf文件,并把pdf文件内容以列表形式打印在屏幕上。下面是使用Vudroid框架解析pdf文件的效果图:


若要在Android项目中集成Vudroid框架,可按照以下步骤处理:
1、在AndroidManifest.xml中添加SD卡的操作权限;
2、在libs目录下导入Vudroid的so库libvudroid.so;(使用ADT开发时)
3、在工程源码中导入org.vudroid.pdfdroid包下的所有源码;

下面是使用Vudroid框架解析pdf文件的代码:
public class VudroidActivity extends Activity implements 
		OnClickListener, FileSelectCallbacks {
	private final static String TAG = "VudroidActivity";
	private FrameLayout fr_content;
	private DecodeService decodeService;
	
	@Override
	protected void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_pdf_vudroid);

		decodeService = new DecodeServiceBase(new PdfContext());
		findViewById(R.id.btn_open).setOnClickListener(this);
		fr_content = (FrameLayout) findViewById(R.id.fr_content);
	}
	
	@Override
	protected void onDestroy() {
        decodeService.recycle();
        decodeService = null;
		super.onDestroy();
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.btn_open) {
			FileSelectFragment.show(this, new String[] {"pdf"}, null);
		}
	}

	@Override
	public void onConfirmSelect(String absolutePath, String fileName, Map<String, Object> map_param) {
		String path = String.format("%s/%s", absolutePath, fileName);
		Log.d(TAG, "path="+path);
		DocumentView documentView = new DocumentView(this);
		documentView.setLayoutParams(new ViewGroup.LayoutParams(
				ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
		decodeService.setContentResolver(getContentResolver());
		decodeService.setContainerView(documentView);
		documentView.setDecodeService(decodeService);
		decodeService.open(Uri.fromFile(new File(path)));
		fr_content.addView(documentView);
		documentView.showDocument();
	}

	@Override
	public boolean isFileValid(String absolutePath, String fileName, Map<String, Object> map_param) {
		return true;
	}

}


MuPDF方式读取

虽然Vudroid框架能够正常解析并显示pdf文件内容,但美中不足的是:
1、Vudroid框架解析速度偏慢;
2、显示pdf页面时采用马赛克逐格展示,不够友好;
3、整个pdf文件内容都调用draw方法绘制,难以改造为翻页浏览的形式;

基于以上情况,博主又尝试了其它的pdf解析框架,发现MuPDF这个解决方案较为理想。MuPDF的实现代码相对较少,调用起来也比较方便,而且支持只浏览指定页面,这意味着我们可以使用翻页形式来逐页浏览pdf文件,更加符合普通用户的使用习惯。下面是采用MuPDF框架解析pdf文件的效果图(列表形式):


下面是采用MuPDF框架解析pdf文件的效果图(翻页形式):


若要在Android项目中集成MuPDF框架,可按照以下步骤处理:
1、在AndroidManifest.xml中添加SD卡的操作权限;
2、在libs目录下导入MuPDF的so库libmupdf.so;(使用ADT开发时)
3、在工程源码中导入com.artifex.mupdf包下的所有源码;

下面是使用MuPDF框架解析pdf文件的代码:
public class PdfFragment extends Fragment implements OnPDFListener {
	private static final String TAG = "PdfFragment";
	protected View mView;
	protected Context mContext;
	private int position;

	public static PdfFragment newInstance(int position) {
		PdfFragment fragment = new PdfFragment();
		Bundle bundle = new Bundle();
		bundle.putInt("position", position);
		fragment.setArguments(bundle);
		return fragment;
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		Log.d(TAG, "width="+container.getMeasuredWidth()+", height="+container.getMeasuredHeight());
		mContext = getActivity();
		if (getArguments() != null) {
			position = getArguments().getInt("position");
		}
		MuPDFPageView pageView = new MuPDFPageView(mContext, 
				MainApplication.getInstance().pdf_core, 
				new Point(container.getMeasuredWidth(), container.getMeasuredHeight()));
		PointF pageSize = MainApplication.getInstance().page_sizes.get(position);
		if (pageSize != null) {
			pageView.setPage(position, pageSize);
		} else {
			pageView.blank(position);
			MuPDFPageTask task = new MuPDFPageTask(
					MainApplication.getInstance().pdf_core, pageView, position);
			task.setPDFListener(this);
			task.execute();
		}
		return pageView;
	}

	@Override
	public void onRead(MuPDFPageView pageView, int position, PointF result) {
		MainApplication.getInstance().page_sizes.put(position, result);
		if (pageView.getPage() == position) {
			pageView.setPage(position, result);
		}
	}
}


点击下载本文用到的读取PPT和PDF文件的工程代码


点此查看Android开发笔记的完整目录
作者:aqi00 发表于2017/4/17 9:26:01 原文链接
阅读:612 评论:0 查看评论

Objective C block背后的黑魔法

$
0
0

前言

block在Objective C开发中应用非常广泛,我们知道block会捕获外部对象,也知道使用block要防止循环引用。

“知其然而不知其所以然”是一件很痛苦的事情,那么block这套机制在OC中是如何实现的呢?本文通过从C/C++到汇编层面分析block的实现原理。


Clang

clang是XCode的编译器前端,编译器前端负责语法分析,语义分析,生成中间代码(intermediate representation )。

比如当你在XCode中进行build一个.m文件的时候,实际的编译命令如下

clang -x objective-c -arch x86_64
 -fmessage-length=0 
 -fobjc-arc... 
 -Wno-missing-field-initializers ... 
 -DDEBUG=1 ... 
 -isysroot iPhoneSimulator10.1.sdk 
 -fasm-blocks ... 
 -I headers.hmap 
 -F 所需要的Framework  
 -iquote 所需要的Framework  ... 
 -c ViewController.m 
 -o ViewController.o

Objective C也可以用GCC来编译,不过那超出了本文的范畴,不做讲解。

Clang除了能够进行编译之外,还有其他一些用法。比如本文分析代码的核心命令就是这个:

clang -rewrite-objc 文件.m

通过这个命令,我们可以把Objective C的代码用C++来表示。

对于想深入理解Clang命令的同学,可以用命令忙自带的工具来查看帮助文档

man clang

或者阅读官方文档:文档地址


查看汇编代码

在XCode中,对于一个源文件,我们可以通过如下方式查看其汇编代码。这对我们分析代码深层次的实现原理非常有用,这个在后面也会遇到。


Objective C对象内存模型

为了本文讲解的更清楚,我们首先来看看一个Objective C对象的内存模型。我们首先新建一个类,内容如下

DemoClass.h

@interface DemoClass : NSObject
@property (nonatomic, copy) NSString * value;
@end

DemoClass.m

@implementation DemoClass
- (void)demoFunction{
    DemoClass * obj = [[DemoClass alloc] init];
}
@end

然后,我们用上文提到的Clang命令将DemoClass.m转成C++的表示。

clang -rewrite-objc DemoClass.m

转换完毕后当前目录会多一个DemoClass.cpp文件,这个文件很大,接近十万行。

我们先搜索这个方法名称demoFunction,以方法作为切入

static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
    DemoClass * obj = ((DemoClass *(*)(id, SEL))(void *)objc_msgSend)((id)((DemoClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DemoClass"), sel_registerName("alloc")), sel_registerName("init"));
}

可以看到,转换成C++后,一个实例方法转换为一个静态方法,这个方法的内容看起来很乱,因为有各种的类型强制转换,去掉后就比较清楚了。

static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
    DemoClass * obj = objc_msgSend(objc_msgSend(objc_getClass("DemoClass"), sel_registerName("alloc")), sel_registerName("init"));
}

可以看到:

  • 转换后增加了两个参数:self_cmd
  • 方法的调用转换成了objc_msgSend,这是一个C函数,两个参数分别是ClassSEL

关于objc_msgSend内发生的事情,参见我之前的一篇博客:

到这里,我们知道了一个OC的实例方法具体是怎么实现的了。

那么,一个OC对象在内存中是如何存储的呢?我们在刚刚的方法的上下可以找到这个类的完整实现,

//类对应的结构体
struct DemoClass_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_value;
};
//demoFunction方法
static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
    DemoClass * obj = objc_msgSend(objc_msgSend(objc_getClass("DemoClass"), sel_registerName("alloc")), sel_registerName("init"));
}
//属性value的getter方法
static NSString * _I_DemoClass_value(DemoClass * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_DemoClass$_value)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

//属性value的setter方法
static void _I_DemoClass_setValue_(DemoClass * self, SEL _cmd, NSString *value) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DemoClass, _value), (id)value, 0, 1); }

我们侧重来看看类对应的结构体

struct DemoClass_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_value;
};
//我们依次查找不清楚的定义
struct NSObject_IMPL {
    Class isa;
};
typedef struct objc_class *Class;
struct objc_class {
    Class isa ;
};

可以看到,OC类实际是按照以下方式来存储对象的

  • isa指针。指向objc_class类型的结构体,这个结构体中存储了方法的列表等类相关的信息,因为objc_msgSend中,发给对象的实际是一个字符串,运行时就是通过isa找到类对象,然后通过字符串找到方法的实际执行的。
  • ivar。属性背后的存储对象,到这里也能看出来一个普通的属性就是ivar+getter+setter.

也就是说,只要有isa指针,指向一个类对象,那么这个结构就能处理OC的消息机制,也就能当成OC的对象来用。


Block的本质

我们修改DemoClass.m中的内容如下

typedef void(^VoidBlock)(void);
@implementation DemoClass

- (void)demoFunction{
    NSInteger variable = 10;
    VoidBlock temp = ^{
        NSLog(@"%ld",variable);
    };
    temp();
}
@end

然后,重新用clang转换为C++代码,有关这段代码的内容如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
struct __DemoClass__demoFunction_block_impl_0 {
  struct __block_impl impl;
  struct __DemoClass__demoFunction_block_desc_0* Desc;
  NSInteger variable;
  __DemoClass__demoFunction_block_impl_0(void *fp, struct __DemoClass__demoFunction_block_desc_0 *desc, NSInteger _variable, int flags=0) : variable(_variable) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
  NSInteger variable = __cself->variable; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hj_392p68d55td2kdxrbd9h15g40000gn_T_Test_c7592d_mi_0,variable);
}

static struct __DemoClass__demoFunction_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __DemoClass__demoFunction_block_desc_0_DATA = { 0, sizeof(struct __DemoClass__demoFunction_block_impl_0)};

static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
    NSInteger variable = 10;
    VoidBlock temp = ((void (*)())&__DemoClass__demoFunction_block_impl_0((void *)__DemoClass__demoFunction_block_func_0, &__DemoClass__demoFunction_block_desc_0_DATA, variable));
    ((void (*)(__block_impl *))((__block_impl *)temp)->FuncPtr)((__block_impl *) temp);
}

我们还是以方法作为切入点,看俺具体是怎么实现的。_I_DemoClass_demoFunctionDemoFunction转换后的方法。我们去掉一些强制转化代码,这样看起来更清楚

static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
    NSInteger variable = 10;
    VoidBlock temp = &__DemoClass__demoFunction_block_impl_0(__DemoClass__demoFunction_block_func_0, &__DemoClass__demoFunction_block_desc_0_DATA, variable));
    (temp->FuncPtr)(temp);
}

从上至下,三行的左右依次是

  • 初始化一个variable(也就是block捕获的变量)
  • 调用结构体__DemoClass__demoFunction_block_impl_0的构造函数来新建一个结构体,并且把地址赋值给temp变量(也就是初始化一个block)
  • 通过调用temp变量内的函数指针(C的函数指针)来执行实际的函数。

通过这些分析,我们知道了Block的大致实现

block背后的内存模型实际上是一个结构体,这个结构体会存储一个函数指针来指向block的实际执行代码。

接着,我们来深入的研究下block背后的结构体,也就是这个结构体__DemoClass__demoFunction_block_impl_0:

struct __block_impl {
  void *isa; //和上文提到的OC对象isa一样,指向的类对象,用来找到方法的实现
  int Flags; //标识位
  int Reserved; //保留
  void *FuncPtr; //Block对应的函数指针
};

struct __DemoClass__demoFunction_block_impl_0 {
  //结构体的通用存储结构
  struct __block_impl impl;
  //本结构体的描述信息
  struct __DemoClass__demoFunction_block_desc_0* Desc;
  //捕获的外部变量
  NSInteger variable;
  //构造函数(也就是初始化函数,用来在创建结构体实例的时候,进行必要的初始化工作)
  struct __DemoClass__demoFunction_block_impl_0 {
  struct __block_impl impl;
  struct __DemoClass__demoFunction_block_desc_0* Desc;
  NSInteger variable;
  __DemoClass__demoFunction_block_impl_0(void *fp,
                                         struct __DemoClass__demoFunction_block_desc_0 *desc,
                                         NSInteger _variable,
                                         int flags=0) : variable(_variable) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们在回头看看block初始化那句代码

//OC
VoidBlock temp = ^{
        NSLog(@"%ld",variable);
};
//C++
VoidBlock temp = &__DemoClass__demoFunction_block_impl_0(__DemoClass__demoFunction_block_func_0, 
&__DemoClass__demoFunction_block_desc_0_DATA, 
variable));

在对应之前代码块的构造函数,我们可以清楚的看到,在初始化的时候三个参数依次是

  • 函数指针__DemoClass__demoFunction_block_func_0
  • block的描述结构体(全局静态结构体)__DemoClass__demoFunction_block_desc_0_DATA
  • 捕获的变量variable

接着,我们来看看block背后的C函数__DemoClass__demoFunction_block_func_0

static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
  NSInteger variable = __cself->variable; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_hj_392p68d55td2kdxrbd9h15g40000gn_T_DemoClass_c7592d_mi_0,variable);
}

Tips:

内存中存储区域可分为以下几个区域:

  • TEXT 代码区
  • DATA 数据区
  • Stack 栈区
  • HEAP 堆区

上文的字符串@”%ld”,对应C++代码是)&__NSConstantStringImpl__var_folders_hj_392p68d55td2kdxrbd9h15g40000gn_T_DemoClass_c7592d_mi_0,是存储在数据区的。这样即使程序中有多个@”%ld”,也不会创建多个实例。

可以看到,这个C函数的参数是__DemoClass__demoFunction_block_impl_0,也就是一个block类型。然后在方法体内部,使用这个block类型的参数。

最后,我们分析下block的描述信息,也就是这段代码

static struct __DemoClass__demoFunction_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __DemoClass__demoFunction_block_desc_0_DATA = { 0, sizeof(struct __DemoClass__demoFunction_block_impl_0)};

这段代码不难理解,就是声明一个描述信息的结构体,然后初始化这个结构体类型的全局静态变量。

分析到这里,上面代码的大多数内容我们都理解了,但是有一点我们还没有搞清楚,就是isa指向的内容_NSConcreteStackBlock

impl.isa = &_NSConcreteStackBlock;

但是,到这里我们知道了为什么Block可以当作OC对象来用的原因:就是这个指向类对象的isa指针。


Block的类型

上文提到了_NSConcreteStackBlock是Block一种,block一共有三种类型

  • NSConcreteStackBlock 栈上分配,作用域结束后自动释放
  • NSConcreteGlobalBlock 全局分配,类似全局变量,存储在数据段,内存中只有一份
  • NSConcreteHeapBlock 堆上分配

我们仍然尝试用Clang转换的方式,来验证我们的理论。将DemoClass.m内容修修改为

#import "DemoClass.h"

typedef void(^VoidBlock)(void);

@interface DemoClass()
@property (copy, nonatomic) VoidBlock heapBlock;

@end
VoidBlock globalBlock = ^{};

@implementation DemoClass

- (void)demoFunction{
    VoidBlock stackBlock = ^{};
    stackBlock();
    _heapBlock = ^{};
}

@end

然后,转成C++后,分别对应如下

全局globalBlock

impl.isa = &_NSConcreteGlobalBlock;

栈上stackBlock

impl.isa = &_NSConcreteStackBlock;

属性Block

impl.isa = &_NSConcreteStackBlock;

What the fuck! 怎么属性的block是栈类型的,难道不该是堆类型的吗?

到这里,C/C++层面的代码已经无法满足我们的需求了。我们试着把代码转成汇编,一探究竟:

方便分析属性block究竟是怎么实现的,我们修改.m文件

#import "DemoClass.h"
typedef void(^VoidBlock)(void);
@interface DemoClass()
@property (copy, nonatomic) VoidBlock heapBlock;
@end
@implementation DemoClass
- (void)demoFunction{
    _heapBlock = ^{};
}
@end

转换成汇编后,在方法demoFunction部分,我们能看到类似汇编代码

bl  _objc_retainBlock
    adrp    x8, _OBJC_IVAR_$_DemoClass._heapBlock@PAGE
    add x8, x8, _OBJC_IVAR_$_DemoClass._heapBlock@PAGEOFF
    .loc    1 0 0                   ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:0:0
    ldr x1, [sp, #8]
    .loc    1 21 5                  ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:21:5
    ldrsw       x8, [x8]
    add     x8, x1, x8
    .loc    1 21 16 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:21:16
    ldr     x1, [x8]
    str     x0, [x8]
    .loc    1 21 16 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:21:16
    mov  x0, x1
    bl  _objc_release

也就是说,在方法返回之前,依次调用了

_objc_retainBlock
_objc_release

那么,_objc_retainBlock就是block从栈到堆的黑魔法。

我们通过Runtime的源码来分析这个方法的实现:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

到这里我们就清楚了,编译器为我们自动插入了_objc_retainBlock,而这个函数会把栈上的block拷贝到堆上。

Tips: 通常在写属性的时候,block都会声明为copy。这是显式的表示,即使block是栈上的,也会拷贝到堆上。其实在赋值的时候,编译器已经自动帮我们做了这些,所以其实使用strong也可以。

那么,一个临时变量的block会被拷贝到堆上么?

修改demoFunction:

- (void)demoFunction{
    VoidBlock stackBlock = ^{};
}

继续查看汇编:

Ltmp7:
    .loc    1 23 15 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:23:15
    mov  x0, x8
    bl  _objc_retainBlock
    mov x8, #0
    add x1, sp, #8              ; =8
    str x0, [sp, #8]
    .loc    1 24 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:24:1
    mov  x0, x1
    mov  x1, x8
    bl  _objc_storeStrong
    ldp x29, x30, [sp, #32]     ; 8-byte Folded Reload
    add sp, sp, #48             ; =48
    ret

我们仍然看到了_objc_retainBlock,也就是说即使是一个在函数中的block,在ARC开启的情况下,仍然会拷贝到堆上。


__block

通过之前的讲解,我们知道了block如何捕获外部变量,也知道了block的几种类型。那么block如何修改外部变量呢?

block是不可以直接修改外部变量的,比如

NSInteger variable = 0;
_heapBlock = ^{
    variable = 1;
};

直接这么写,编译器是不会通过的,想想也很简单,因为变量可能在block执行之前就被释放掉了,直接这么赋值会导致野指针。

在OC层面,我们可以通过增加__block关键字,那么加了这个关键字后,实际的C++层面代码是什么样的呢?

- (void)demoFunction{
    __block NSInteger variable = 0;
    VoidBlock stackBlock = ^{
        variable = 1;
    };
}

在转换成C++代码后,如下:

static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
    __Block_byref_variable_0 variable = {0,&variable, 0, sizeof(__Block_byref_variable_0), 0};
    VoidBlock stackBlock = &__DemoClass__demoFunction_block_impl_0(( __DemoClass__demoFunction_block_func_0,
                                                                    &__DemoClass__demoFunction_block_desc_0_DATA,
                                                                    (__Block_byref_variable_0 *)&variable,
                                                                    570425344);
}

可以看到,__block NSInteger variable = 0转换成了一个结构体

__Block_byref_variable_0 variable = {0,&variable, 0, sizeof(__Block_byref_variable_0), 0};

这个结构体定义如下:

struct __Block_byref_variable_0 {
  void *__isa;
__Block_byref_variable_0 *__forwarding;
  int __flags;
  int __size;
  NSInteger variable; //这个是要修改的变量
};

通过初始化我们可以看到

  • __isa指向0
  • __forwarding 指向__Block_byref_variable_0自身
  • __flags为0
  • __size就是结构题的大小
  • variable是我们定义的原始值0

到这里,我们有一点疑惑

  • 为什么要存在一个__forwarding来指向自身呢?

我们来看看block的方法体,也就是这部分

^{
   variable = 1;
 }

转换成C++后:

static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
  __Block_byref_variable_0 *variable = __cself->variable; // bound by ref
    variable->__forwarding->variable) = 1;
}

也就是说__forwarding存在的意义就是通过它来访问到变量的地址,如果这个指针一直指向自身,那么它也就没有存在的意义,也就是在将来的某一个时间点,它一定会指向另外一个数据结构。

我们在上文中讲到,ARC开启的时候,栈上的block会被复制到堆上。

在没有复制之前:

复制之后

这样,我们就清楚原因了:

即使发生了复制,只要修改__forwarding的指向,我们就能够保证栈上和堆上的block都访问同一个对象。


Block对对象的捕获

到这里,我们分析的block都是捕获一个外部值,并不是对象。值和对象最大的区别就是对象有生命周期,对象我们需要考虑引用计数。

修改DemoFunction

- (void)demoFunction{
    NSObject * obj = [[NSObject alloc] init];
    VoidBlock stackBlock = ^{
        [obj description];
    };
    stackBlock();
}

再转换成C++后,我们对比之前捕获NSInteger,发现多了两个生命周期管理函数

static void __DemoClass__demoFunction_block_copy_0(struct __DemoClass__demoFunction_block_impl_0*dst, struct __DemoClass__demoFunction_block_impl_0*src)
{
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __DemoClass__demoFunction_block_dispose_0(struct __DemoClass__demoFunction_block_impl_0*src)
{
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

我们再查看下Block_object_assignBlock_object_dispose的定义

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int);

也就是说,编译器通过这两个函数来管理Block捕获对象的生命周期。其中

  • _Block_object_assign相当于ARC中的reatain,在block从栈上拷贝到堆上的时候调用
  • _Block_object_dispose相当于ARC中的release,在block堆上废弃的时候调用

总结

  • block在C语言层面就是结构体,结构体存储了函数指针和捕获的变量列表
  • block分为全局,栈上,堆上三种,ARC开启的时候,会自动把栈上的block拷贝到堆上
  • __block变量在C语言层面也是一个结构体
  • block捕获对象的时候会增加对象的引用计数。

本文的Github地址:LeoMobileDeveloper,如有问题欢迎issue。也可以在我的CSDN博客下评论,我会及时更正。

作者:Hello_Hwc 发表于2017/4/17 14:24:44 原文链接
阅读:1569 评论:0 查看评论

Android4.4之后休眠状态下Alarm不准时的问题

$
0
0

Android4.4及之后休眠状态下Alarm不准时的问题

为了减轻功耗,延长电池使用时间,Android 4.4及之后的版本采用非精准闹钟机制,以及休眠状态下的wakeup类型的alarm不会实时唤醒设备,而会等到机器被物理唤醒时才触发alarmAndroid 6.0提供了新的api:setExactAndAllowWhileIdle()部分解决这个问题,但依然不能在休眠状态下精准唤醒。

关于alarm api 的支持与使用请参考下图:
alarm api 的支持与使用
(图片来源:https://plus.google.com/+AndroidDevelopers/posts/GdNrQciPwqo)

此外,应用层面不要使用不持有wakelockBroadcastReceiver,而要使用WakefulBroadcastReceiver

为了修复这个问题,以5.0.2版本为例,需要修改内核下的alarm-dev.c以及framework下的AlarmManagerService。

  • 内核 alarm-dev.c:其原因是使用普通的static struct wakeup_source alarm_wake_lock;,而非带有WAKE_LOCK_SUSPEND类别信息的struct wake_lock,并且需要使用带有android_前缀的wakeup lock相关函数。即:

    android_wake_lock_init
    android_wake_lock_destroy
    android_wake_lock
    android_wake_lock_timeout
    android_wake_unlock

    而非普通的wake lock操作函数:

    wake_lock_init
    wake_lock_destroy
    wake_lock
    wake_lock_timeout
    wake_unlock
  • framework AlarmManagerService.java:需要将wakeup类型的alarm特殊处理:即精准闹铃。在setImpl中添加如下代码:

    public boolean isWakeup(int type)
    {
        return (type & TYPE_NONWAKEUP_MASK) == 0;
    }
    
    void setImpl(int type, long triggerAtTime, long windowLength, long interval,
            PendingIntent operation, boolean isStandalone, WorkSource workSource,
            AlarmManager.AlarmClockInfo alarmClock) {
        if (operation == null) {
            Slog.w(TAG, "set/setRepeating ignored because there is no intent");
            return;
        }
    
        if (isWakeup(type)) {
            windowLength = AlarmManager.WINDOW_EXACT;
            isStandalone = true;
        }

    并在alarm被触发时多取几个满足条件的batch做处理:

    boolean triggerAlarmsLocked(ArrayList<Alarm> triggerList, final long nowELAPSED,
            final long nowRTC) {
        boolean hasWakeup = false;
        // batches are temporally sorted, so we need only pull from the
        // start of the list until we either empty it or hit a batch
        // that is not yet deliverable
    
        ArrayList<Alarm> repeatList = new ArrayList<Alarm>();
        ListIterator<Batch> it = mAlarmBatches.listIterator();
        while (it.hasNext()) {
            Batch batch = it.next();
            if (batch.start > nowELAPSED) {
                // Everything else is scheduled for the future
                break;
            }
    
            // We will (re)schedule some alarms now; don't let that interfere
            // with delivery of this current batch
            it.remove();
    
            final int N = batch.size();
            for (int i = 0; i < N; i++) {
                Alarm alarm = batch.get(i);
                alarm.count = 1;
                triggerList.add(alarm);
    
                // Recurring alarms may have passed several alarm intervals while the
                // phone was asleep or off, so pass a trigger count when sending them.
                if (alarm.repeatInterval > 0) {
                    // this adjustment will be zero if we're late by
                    // less than one full repeat interval
                    alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
    
                    // Also schedule its next recurrence
                    repeatList.add(alarm);
                }
    
                if (alarm.wakeup) {
                    hasWakeup = true;
                    mNextWakeup = 0;
                }
    
                // We removed an alarm clock. Let the caller recompute the next alarm clock.
                if (alarm.alarmClock != null) {
                    mNextAlarmClockMayChange = true;
                }
            }
        }
    
        if (repeatList.size() > 0) {
            for (Alarm alarm : repeatList) {
                final long delta = alarm.count * alarm.repeatInterval;
                final long nextElapsed = alarm.whenElapsed + delta;
                setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength,
                        maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
                        alarm.repeatInterval, alarm.operation, alarm.windowLength == AlarmManager.WINDOW_EXACT, true,
                        alarm.workSource, alarm.alarmClock, alarm.userId);
            }
        }
    
        // This is a new alarm delivery set; bump the sequence number to indicate that
        // all apps' alarm delivery classes should be recalculated.
        mCurrentSeq++;
        calculateDeliveryPriorities(triggerList);
        Collections.sort(triggerList, mAlarmDispatchComparator);
    
        return hasWakeup;
    }
  • 应用层面使用WakefulBroadcastReceiver:

    import android.support.v4.content.WakefulBroadcastReceiver;
    
    public class AutoUpdateAlarmReceiver extends WakefulBroadcastReceiver {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            // Start the service, keeping the device awake while the service is
            // launching. This is the Intent to deliver to the service.
            Intent service = new Intent(context, AutoUpdateIntentService.class);
            service.setAction(intent.getAction());
            startWakefulService(context, service);
        }
    }

    相应的IntentService如下所示:

    public class AutoUpdateIntentService extends IntentService {
        public AutoUpdateIntentService() {
            super("AutoUpdateIntentService");
        }
    
        @Override
        protected void onHandleIntent(Intent intent) {
            String action = intent.getAction();
    
            // do your work here.
            // ...
    
            // Release the wake lock provided by the WakefulBroadcastReceiver.
            AutoUpdateAlarmReceiver.completeWakefulIntent(intent);
        }
    }
作者:kesalin 发表于2017/4/17 15:21:43 原文链接
阅读:640 评论:0 查看评论

Android逆向之旅---应用的"反调试"方案解析(附加修改IDA调试端口和修改内核信息)

$
0
0

一、前言

在前一篇文章中详细介绍了Android现阶段可以采用的几种反调试方案策略,我们在破解逆向应用的时候,一般现在第一步都回去解决反调试,不然后续步骤无法进行,当然如果你是静态分析的话获取就没必要了。但是有时候必须要借助动态调试方可破解,就需要进行操作了。现阶段反调试策略主要包括以下几种方式:

第一、自己附加进程,先占坑,ptrace(PTRACE_TRACEME, 0, 0, 0)!
第二、签名校验不可或缺的一个选择,本地校验和服务端校验双管齐下!
第三、借助系统api判断应用调试状态和调试属性,最基础的防护!
第四、轮训检查android_server调试端口信息和进程信息,防护IDA的一种有效方式!
第五、轮训检查自身status中的TracerPid字段值,防止被其他进程附加调试的一种有效方式!

所以本文就来一一讲解如何解决这几种方式的反调试方案。


二、方法总结

第一种:找到关键方法,注释代码

这种方式需要采用静态方式分析代码,找到关键方法进行反调试代码功能注释,这种方式可以应对与上面所有的反调试方案,但是对于轮训检查这种方式就不太适合了,为什么?因为大家如果看过这篇文章:脱掉360加固应用保护壳;操作过的同学会发现,在去除反调试功能的时候那种痛苦了。所以这种注释代码,个人觉得只适用于以下几种反调试:

第一、自己附加进程

这个可以IDA打开关键so代码,找到这段代码处:ptrace(PTRACE_TRACEME, 0, 0, 0),直接nop掉即可。这个没什么难度,因为就一行代码,说白了就几条arm指令罢了。IDA静态分析so也是无压力的。

第二、签名校验

这个在之前的文章中介绍过了,不了解的同学可以查看这篇文章:Android中破解某应用的签名校验逻辑;最后总结了一个比较简单的过滤签名校验的方法:先在Jadx中打开应用之后,全局搜字符串内容:”signatures”,这个就可以定位到获取应用签名信息的地方了,然后可以依次跟踪校验的地方了。找到具体地方代码直接注释即可。

但是如果服务端交互信息中携带了签名校验,而签名校验又在so中,那么就需要另外操作了,这部分知识点将在后面单独一篇文章详细介绍如何破解。

第三、调用系统api判断当前应用是否处于调试状态

这种方式看到我们实现的逻辑还是比较简单的,直接调用系统的android.os.Debug.isDebuggerConnected()方法和判断当前应用属性:ApplicationInfo.FLAG_DEBUGGABLE,那么可以依然采用全局搜索反编译之后的应用内容,找到这部分内容,然后直接注释代码即可。


第二种:修改IDA通信端口

上面分析完了,直接使用静态方式+注释代码功能解决了之前提到的三种反调试方案。但是还有两种没有解决,下面就会详细介绍一种非常靠谱方便永久的方法。而这部分内容才是本文的重点。首先来看看如何解决之前提到的利用检查IDA调试端口23946这个反调试方案。这个其实思路很简单,因为你检查的端口号是默认的23946,所以我们如果能把这个端口号改成其他值,那么其实就解决了。

修改这个端口号,也比较简单:网上有一种方案就是android_server本身支持自定义端口号的,命令很简单:

./android_server -p12345;直接加上-p参数即可,注意端口号和参数之间没空格:


有的人说,这方法这么简单,那下面就不介绍了,当然不是,我写文章的目的不是为了简单,而是为了让大家了解更多的知识,宁愿多走弯路,走多条路出来。而且上面的这种方式每次都加-p比较麻烦,我想用另外一种方式去一次性解决问题,同时我更想在这个过程中熟悉一下IDA的使用,使用IDA打开android_server文件,其实他是elf格式的,打开无压力,打开之后使用shift+12查看字符串内容界面:


找到这三处关键字符串内容,我们可以通过以往运行过android_server之后的提示信息察觉:


找到这三处字符串内容,下面就简单了,一处一处进行修改,双击字符串条目内容:


选中按X键,进行切换:


选择第一个跳转到arm指令处:


这是graph视图,可以使用空格键进行切换:


看到arm指令了,LDR R5,=0x5D8A;其中0x5D8A就是十进制的23946,也就是默认端口号,所以这里我们只需要将这个arm指令,改成MOVS R5,#0xDD;可对R5进行重新赋值,这里赋值为DD,也就是221;这里有个小问题就是如何进行修改,IDA中可以切换到Hex View视图进行修改编辑二进制,但是这样修改不会生效到源文件中,所以我们这里还得借助一个二进制编辑工具010Editor,我们使用这个软件打开android_server之后,使用Ctrl+G可以直接跳转到指定地址,使用Ctrl+F可以跳转到搜索内容处,记住以下这两个快捷键。


这里看到了99 4D就是:LDR R5, =0x5D8A 对应的指令十六进制值,关于指令和十六进制值之间转化可以去网上搜一个小工具即可。我们想将其变成 MOVS R5,#0xDD 指令,对应的十六进制是:DD 25,其中DD就是立即数值,25表示MOVS R5指令。所以下面就可以直接进行修改即可:


修改完成之后,进行保存即可,这样我们就修改好了一处,还有两处操作一模一样:


继续修改init_sockets处,命令都是一样的,记住地址:B98A,去010Editor中进行修改即可:


然后继续修改IDA Android 32-bit...处

记住地址:B64C,去010Editor进行修改即可:



这样我们就全部改好了,保存android_server文件,再次使用IDA打开,找到一个地方查看修改是否成功:


的确修改成功了,下面我们把android_server拷贝到设备中运行,看看端口是否为221(0xDD):


看到了,这里成功的修改了,android_server监听端口了,主要当打开IDA进行连接的时候需要注意端口是221,而不是23946了,或者你可以用adb forward tcp:221...命令进行转发也可以!


第三种:修改boot.img文件,跳过反调试

这种方式是为了解决现在常用的反调试策略,就是轮训检查进程的TracerPid值,所以我们需要修改设备的boot.img文件,将这个值直接写死为0即可。关于如何修改操作,看雪上已经有大神讲解了非常详细的过程,我就是按照这个流程进行操作的:http://bbs.pediy.com/thread-207538.htm,因为每个设备的boot.img都不一样,所以在操作的过程中可能遇到很多问题,所以下面就把我操作的过程中遇到的问题讲解一下,顺便精炼的说一下步骤:

第一步,你得有一个可以折腾的root手机

因为现在是在玩boot.img了,后面得刷机,所以你得搞一个你觉得没多大意义的设备,即使成砖头了也无妨。当然一般不会成为砖头。


第二步:root环境下提取zImage内核文件

这里我用的是三星note2设备,自己刷了一个CM4.4系统,按照大神的贴中先去找到系统boot的文件位置,这个路径一定要注意:/dev/block/platform/[每个设备目录不一样]/by-name;其中platform目录中的子目录因为每个设备都不一样,所以需要注意,查看自己设备目录名称,然后进入到by-name之后,使用 ls -l 命令查看详情,找到一项BOOT,记住link的路径地址,这里是/dev/block/mmcblk0p8,然后使用命令,将boot导出为boot.img
dd if=/dev/block/mmcblk0p8 of=/data/local/boot.img
adb pull /data/local/boot.img boot.img


这里可能有人会遇到一个问题就是,看到多个BOOT,比如BOOT1,BOOT2,这里可以选择BOOT即可,也有的人会发现没这个选项,那么只能在刷个其他系统进行操作了。


第三步:借助bootimg.exe工具解压boot.img文件

这个工具我会在后面一起给出压缩包,命令用法很简单,

解包是:bootimg.exe --unpack-boot boot.img 

压包是:bootimg.exe --repack-boot

这里有一个坑,我找到两个版本,第一个版本工具操作之后刷机总是黑屏启动失败,最后找到了第二个版本工具才成功的。其实这些工具原理很简单,就是解析boot.img文件格式罢了,因为boot和recovery映像并不是一个完整的文件系统,它们是一种android自定义的文件格式,该格式包括了2K的文件头,后面紧跟着是用gzip压缩过的内核,再后面是一个ramdisk内存盘,然后紧跟着第二阶段的载入器程序(这个载入器程序是可选的,在某些映像中或许没有这部分):


我们想要的是kernel内核信息。所以用这个工具进行操作之后,会发现有这么几个目录和文件:


解压之后有一个kernel文件,这个就是内核文件,而ramdisk.gz就是释放到设备目录内容,也就是initrd目录,进入查看内容:


看到了吧,这就是最终设备的目录结构,可以看到这里有init.rc启动文件,default.prop配置文件等。

接下来我们就要对kernel内核文件进行特别处理了:将kernel文件复制为文件名为zImage.gz的文件,并使用010Editor工具,Ctrl+F快捷键查找十六进制内容1F 8B 08 00,找到后把前面的数据全删掉,使zImage.gz文件变成标准的gzip压缩文件,这样子就可以使用gunzip解压了。命令:gunzip zImage.gz;这时候获取到了解压之后的zImage才是我们要处理的最终文件。



第四步:IDA打开zImage内核文件进行修改

有了上面一步得到的内核文件zImage,直接使用IDA打开,但是打开的时候需要注意设置选项:


然后设置开始地址为0xC0008000:


这里为什么要设置成这个起始地址,因为Linux启动内核的地址为0xC0008000;打开之后,我们可以直接shift+F12,查看字符串内容,因为我们想改TracerPid值,所以直接搜字符串"TracerPid"值


双击进入,这时候我们可以记下这个地址,然后减去刚刚我们那个偏移地址0xC0008000:


也就是0xC0A3853C-0xC0008000=0xA3053C,这里没有像看雪大神操作那么复杂,先去定位函数位置,修改指令,因为每个设备不一样,指令代码就不一样,不具备通用性,所以这里有一个更好的方案,就是直接改TracerPid的格式字符串值,原始格式化字符串内容为:

\t%s\nTgid:\t%d\nPid:\t%d\nPPid:\t%d\nTracerPid:\t0\t\nUid:\t%d\t%d\t%d\t%d\nGid:\t%d\t%d\t%d\t%d\n

这里应该用到了C语言中的占位符%d,来进行值的填充,那么我们可以把TracerPid那一项的占位符%d,改成'0',但是'%d'是两个字符,所以我们可以改成'00',或者'0\t',或者'0\n';只要保证修改后的字符串内容对其就好。这样TracerPid这一项的值占位符就失效,值永远都是0了。而上面计算的地址就是我们要去010Editor中操作的地址,用010Editor打开zImage文件,Ctrl+G跳转到0xA3053C处:


这里我们将其改成'0\t'值,对应的十六进制就是:30 09;这样我们就修改成功了。


第五步:生成修改后的boot.img文件

这里操作其实就是一个相反的过程,首先使用gzip命令压缩上面修改好的内核文件zImage:gzip -n -f -9 zImage;然后
使用010Editor将压缩好的zImage.gz的二进制数据覆盖到原kernel文件的1F 8B 08 00处的位置(回写回去时不能改变原kernel文件的大小及修改原kernel文件后面的内容,否则会很麻烦),这时得到了新的kernel文件内容。这里需要特别强调一下,也就是我踩过的坑:比如kernel原来是10M大小,1F8B0800之前删除的是1M,我们修改之后的zImage.gz大小是8M,那么我们回写覆盖的时候一定是1M~9M的位置,而kernel的前面1M内容和后面1M内容不能有任何改动,搞错的话,刷机会出现启动失败的情况。下面用我操作的案例讲解一下:


这是我修改之后的压缩好的zImage.gz文件,最后一个数据是0x65E18D,然后全选内容复制好,记住之后,再去原来的kernel内容:


在kernel中的1F8B0800位置是0x47A0,那么我们就需要把刚刚赋值的内容从这里开始替换,到哪里结束呢?将这两个地址相加即可:0x65E18D+0x47A0=0x66292D;也就是到0x66292D结束:


这样原来的kernel内容大小肯定不会发生变化了,始终都是0x662967,所以在替换内容的时候内容一定不能发生变化。替换完成之后,将新的kernel文件替换原来的kernel文件,在使用之前提到的bootimg.exe工具生成新的boot.img文件即可。


第六步:刷机boot.img文件

这里有一个坑,在刷机的时候用到的是fastboot命令,但是遇到最多的问题就是这个错误:

这个是因为设备还没有启动fastboot,关于每个设备启动fastboot不一样操作,比如小米是电源键+音量减,三星是音量减+HOME键+电源键;具体设备可以自行网上搜索即可。到了fastboot界面再次运行fastboot就可以了:

fastboot flash boot boot-new.img


然后在运行fastboot reboot重启设备即可。有的同学在操作的时候,始终进入fastboot失败,导致fastboot命令运行错误,这个真解决不了那就换个手机试一下吧。

这时候我们启动设备,然后调试一个app,发现他的TracerPid值永远都是0了,因为我之前将TracerPid改成'00'字符串了,也是可以的:


因为感觉不正规,所以就有重新改成了'0\t'值了。都是可以的。


注意:一定要保存原始提取的内核文件boot.img,当你把设备弄成砖头启动失败的时候,可以在把这个原始的boot.img刷回去就可!


三、内容延展

不知道大家以前在看:脱360加固应用的保护壳 文章的时候当时说到了一个工具mprop,他的作用就是能够改写系统的内存中的ro.debuggable这个属性值,这样我们就没必要每次反编译app,然后在AndroidManifest.xml中添加android:debuggable="true",让应用可调试了。

当时说到这个工具有一个弊端就是他只能修改内存中的值,当设备重启就会失效,那么现在我们可以让他永久有效,其实这个属性值,是在系统根目录下的default.prop文件中的,设备启动就会解析存入内存中。所以如果我们能够把这个文件中的值改成1,那么就永久有效了。在上面解包boot.img的时候,说到了有一个initrd目录,其实default.prop就是在这个目录下:


这里我们直接将其改成1,因为我们现在已经进行了修改boot.img操作,那就顺便把这个功能也给改了。多方便呀!


四、提取内核操作总结

第一步:设备root之后,查看设备的内核文件路径: cd /dev/block/platform/[具体设备具体查看]/by-name,然后使用命令ls -l 查看boot属性的,记住路径

第二步:dd if=/dev/block/[你的内核路径] of=/data/local/boot.img

adb pull /data/local/boot.img boot.img

第三步:使用bootimg.exe工具进行boot.img解包;得到kernel文件,将kernel文件复制为文件名为zImage.gz的文件,并使用010Editor工具,Ctrl+F快捷键查找十六进制内容1F 8B 08 00,找到后把前面的数据全删掉,使zImage.gz文件变成标准的gzip压缩文件,这样子就可以使用gunzip解压了。命令:gunzip zImage.gz

第四步:使用IDA打开zImage内核文件,记得设置选项和起始地址:0xC0008000;打开之后,使用shift+F12查找到字符串“TracerPid”值,记住文件起始地址,然后减去0xC0008000;在使用010Editor打开内核文件,Ctrl+G跳转到这个地址,进行内容修改,将TracerPid那个占位符‘%d’改成‘0\t’保存即可

第五步:首先使用gzip命令压缩上面修改好的内核文件zImage:gzip -n -f -9 zImage;然后使用010Editor将压缩好的zImage.gz的二进制数据覆盖到原kernel文件的1F 8B 08 00处的位置(回写回去时不能改变原kernel文件的大小及修改原kernel文件后面的内容,否则会很麻烦)

第六步:启动设备为fastboot模式,然后使用fastboot命令进行刷机:fastboot flash boot boot-new.img,然后在重启即可

总结一张图:



踩过的坑

坑一:一定保留最原始提取的内核文件boot.img,为了防止你刷失败了,可以还原操作。
坑二:修改TracerPid值时,只需要将‘%d’占位符改成‘0\t’即可,无需改动arm命令操作。
坑三:在还原kernel文件的使用一定要记得不能改变原始kernel文件的大小。
坑四:fastboot命令运行失败,设备必须处于fastboot模式,如果还不行那就换个手机吧。


五、技术总结

第一:关于反调试的第一种解决方案比较简单,就是静态分析代码,找到反调试的位置,然后注释代码即可。

第二:对于监听IDA端口反调试,通过修改android_server的启动端口,这里也学会了如何修改端口号操作。

第三:修改内核文件,让TracerPid始终为0,ro.debuggable属性值始终为1,这个操作过程还是有点繁琐的,遇到的问题肯定很多,而且每个人遇到的问题可能不一样,但是这是一个锻炼的过程,如果成功了意味着你学会了提取内核操作,了解内核文件结构,学会分析内核文件,修改内核文件。意义重大。比如你还可以修改设备的启动图,慢慢的你可以定制自己的rom了。

第四:在以上操作中,也熟悉了IDA工具使用,了解到了字符串内容永远都是寻找问题的最好突破口,IDA中查找字符串Shirt+F12即可,010Editor中Ctrl+G和Ctrl+F查找快捷键。


解包boot.img文件的工具下载地址:http://download.csdn.net/detail/jiangwei0910410003/9793611


六、总结

本文介绍的内容主要是如何解决反调试问题,主要是三种方案,最后一种修改手机内核文件的操作比较繁琐,遇到的问题也会比较多。但是如果要是成功了,以后进行破解逆向就方便多了。所以就努力看文章,自己手动操作一次。看完文章之后,记得多多点赞和分享扩散,要是有打赏就最好啦啦!


更多内容:点击这里

关注微信公众号,最新技术干货实时推送


编码美丽技术圈
微信扫一扫进入我的"技术圈"世界


扫一扫加小编微信
添加时请注明:“编码美丽”非常感谢!

作者:jiangwei0910410003 发表于2017/4/17 18:52:24 原文链接
阅读:1791 评论:1 查看评论

性能优化十七之图片压缩优化

$
0
0

前言

        Android开发中遇到最多的问题,遇到最多的问题,面试被问的最多的问题就是关于图片的处理,防止内存溢出等。博客中也转载了很多大牛写的关于如何加载大图的文章,写的也很好,这里不做过多介绍,这里就简单的谈下如何去使用系统的API进行压缩优化,相关的知识大牛都介绍了很多。这里做些关于图片加载优化的博客总结,想要学习相关图片压缩优化可以看以下博客:


Android压缩图片到100K以下并保持不失真的高效方法
        http://blog.csdn.net/jdsjlzx/article/details/44229169
android图片压缩总结
        http://blog.csdn.net/jdsjlzx/article/details/44947811
Android图片压缩(质量压缩和尺寸压缩)&Bitmap转成字符串上传
        http://blog.csdn.net/jdsjlzx/article/details/44228935
bitmap的六种压缩方式,Android图片压缩
        http://blog.csdn.net/harryweasley/article/details/51955467
Android完美加载大图
        http://blog.csdn.net/hpc19950723/article/details/64923022
高效加载图片防止OOM–总结
        http://blog.csdn.net/hpc19950723/article/details/53175443

作者:hpc19950723 发表于2017/4/17 20:50:26 原文链接
阅读:185 评论:0 查看评论

dwc3验证套件 day6:conclusion

$
0
0

自由固不是钱所能买到的,但能够为钱而卖掉。 —— 鲁迅

usb mass storage设备能顺利读写了,这样的话,对于ip来讲,从链路层到协议层起码是能正常工作的了。关于后面文章的安排:阶段性工作总结的文章就不写了,后面可能针对某些技术点重点罗嗦几句。

一、mass storage设备读写支持

代码下载,驱动还是参考自uboot,通过msg_writemsg_read接口对设备进行读写,都能正常工作。

设备端具有一个bulk-in端点和一个bulk-out端点,协议是BBB(也就是Bulk Only Mass Storage)。

到这一步就能证明控制器从链路层到协议层都能正常工作了,其实也没必要做gadget驱动测试了,因为都是ip内部的东西,肯定是可以正常工作的。就算要测试,在现有的框架下也是很简单的事情,完成不会超过两天时间。

二、总结

其实整个验证过程前前后后大概有两个星期了,还算是比较顺利的,毕竟代码都有现成的,发扬一下拿来主义,再结合文档进行整理,在解决问题的过程中摸清整个工作原理。说实话,当前这份代码称为验证框架还不够格,因为验证最重要的是覆盖率的问题,可是现在没有重点考虑这个问题。

作者:abcamus 发表于2017/4/17 23:28:44 原文链接
阅读:187 评论:0 查看评论

XCode 8.3.1 打包ipa 解决不能收到推送消息问题

$
0
0

XCode8.3的时候, 后台使用的测试环境推送证书, iOS 端使用XCode的自动配置证书打包, 这个推送流程是没有问题的.

自动配置证书如下
这里写图片描述

升级XCode8.3.1后, 在debug模式下, 打包的ipa是不能推送的.

我的分析由于XCode自动配置证书, 如果你打包ipa后, 不管你选择debug还是release的模式, XCode的自动配置过程, 都是匹配dis(发布)的证书, 所以在后台配置测试证书的情况下, 与App端的匹配就造成了不一致的情况.

正确的做法: 后台配置dis(发布使用的证书), XCode修改成release状态打包, 就没有问题了.

我分析可能是XCode自动匹配的设计就要求这样.

关于新版XCode 打包的问题可以和我交流!

作者:sinat_30162391 发表于2017/4/18 0:22:24 原文链接
阅读:150 评论:0 查看评论

Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程

$
0
0

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53939176

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新。

在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API。还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法

在多数情况下,我们想要在界面上加载并展示一张图片只需要一行代码就能实现,如下所示:

Glide.with(this).load(url).into(imageView);

虽说只有这简简单单的一行代码,但大家可能不知道的是,Glide在背后帮我们默默执行了成吨的工作。这个形容词我想了很久,因为我觉得用非常多这个形容词不足以描述Glide背后的工作量,我查到的英文资料是用tons of work来进行形容的,因此我觉得这里使用成吨来形容更加贴切一些。

虽说我们在平时使用Glide的时候格外地简单和方便,但是知其然也要知其所以然。那么今天我们就来解析一下Glide的源码,看看它在这些简单用法的背后,到底执行了多么复杂的工作。

如何阅读源码

在开始解析Glide源码之前,我想先和大家谈一下该如何阅读源码,这个问题也是我平时被问得比较多的,因为很多人都觉得阅读源码是一件比较困难的事情。

那么阅读源码到底困难吗?这个当然主要还是要视具体的源码而定。比如同样是图片加载框架,我读Volley的源码时就感觉酣畅淋漓,并且对Volley的架构设计和代码质量深感佩服。读Glide的源码时却让我相当痛苦,代码极其难懂。当然这里我并不是说Glide的代码写得不好,只是因为Glide和复杂程度和Volley完全不是在一个量级上的。

那么,虽然源码的复杂程度是外在的不可变条件,但我们却可以通过一些技巧来提升自己阅读源码的能力。这里我和大家分享一下我平时阅读源码时所使用的技巧,简单概括就是八个字:抽丝剥茧、点到即止。应该认准一个功能点,然后去分析这个功能点是如何实现的。但只要去追寻主体的实现逻辑即可,千万不要试图去搞懂每一行代码都是什么意思,那样很容易会陷入到思维黑洞当中,而且越陷越深。因为这些庞大的系统都不是由一个人写出来的,每一行代码都想搞明白,就会感觉自己是在盲人摸象,永远也研究不透。如果只是去分析主体的实现逻辑,那么就有比较明确的目的性,这样阅读源码会更加轻松,也更加有成效。

而今天带大家阅读的Glide源码就非常适合使用这个技巧,因为Glide的源码太复杂了,千万不要试图去搞明白它每行代码的作用,而是应该只分析它的主体实现逻辑。那么我们本篇文章就先确立好一个目标,就是要通过阅读源码搞明白下面这行代码:

Glide.with(this).load(url).into(imageView);

到底是如何实现将一张网络图片展示到ImageView上面的。先将Glide的一整套图片加载机制的基本流程梳理清楚,然后我们再通过后面的几篇文章具体去了解Glide源码方方面面的细节。

准备好了吗?那么我们现在开始。

源码下载

既然是要阅读Glide的源码,那么我们自然需要先将Glide的源码下载下来。其实如果你是使用在build.gradle中添加依赖的方式将Glide引入到项目中的,那么源码自动就已经下载下来了,在Android Studio中就可以直接进行查看。

不过,使用添加依赖的方式引入的Glide,我们只能看到它的源码,但不能做任何的修改,如果你还需要修改它的源码的话,可以到GitHub上面将它的完整源码下载下来。

Glide的GitHub主页的地址是:https://github.com/bumptech/glide

不过在这个地址下载到的永远都是最新的源码,有可能还正在处于开发当中。而我们整个系列都是使用Glide 3.7.0这个版本来进行讲解的,因此如果你需要专门去下载3.7.0版本的源码,可以到这个地址进行下载:https://github.com/bumptech/glide/tree/v3.7.0

开始阅读

我们在上一篇文章中已经学习过了,Glide最基本的用法就是三步走:先with(),再load(),最后into()。那么我们开始一步步阅读这三步走的源码,先从with()看起。

1. with()

with()方法是Glide类中的一组静态方法,它有好几个方法重载,我们来看一下Glide类中所有with()方法的方法重载:

public class Glide {

    ...

    public static RequestManager with(Context context) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(context);
    }

    public static RequestManager with(Activity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }

    public static RequestManager with(FragmentActivity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public static RequestManager with(android.app.Fragment fragment) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(fragment);
    }

    public static RequestManager with(Fragment fragment) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(fragment);
    }
}

可以看到,with()方法的重载种类非常多,既可以传入Activity,也可以传入Fragment或者是Context。每一个with()方法重载的代码都非常简单,都是先调用RequestManagerRetriever的静态get()方法得到一个RequestManagerRetriever对象,这个静态get()方法就是一个单例实现,没什么需要解释的。然后再调用RequestManagerRetriever的实例get()方法,去获取RequestManager对象。

而RequestManagerRetriever的实例get()方法中的逻辑是什么样的呢?我们一起来看一看:

public class RequestManagerRetriever implements Handler.Callback {

    private static final RequestManagerRetriever INSTANCE = new RequestManagerRetriever();

    private volatile RequestManager applicationManager;

    ...

    /**
     * Retrieves and returns the RequestManagerRetriever singleton.
     */
    public static RequestManagerRetriever get() {
        return INSTANCE;
    }

    private RequestManager getApplicationManager(Context context) {
        // Either an application context or we're on a background thread.
        if (applicationManager == null) {
            synchronized (this) {
                if (applicationManager == null) {
                    // Normally pause/resume is taken care of by the fragment we add to the fragment or activity.
                    // However, in this case since the manager attached to the application will not receive lifecycle
                    // events, we must force the manager to start resumed using ApplicationLifecycle.
                    applicationManager = new RequestManager(context.getApplicationContext(),
                            new ApplicationLifecycle(), new EmptyRequestManagerTreeNode());
                }
            }
        }
        return applicationManager;
    }

    public RequestManager get(Context context) {
        if (context == null) {
            throw new IllegalArgumentException("You cannot start a load on a null Context");
        } else if (Util.isOnMainThread() && !(context instanceof Application)) {
            if (context instanceof FragmentActivity) {
                return get((FragmentActivity) context);
            } else if (context instanceof Activity) {
                return get((Activity) context);
            } else if (context instanceof ContextWrapper) {
                return get(((ContextWrapper) context).getBaseContext());
            }
        }
        return getApplicationManager(context);
    }

    public RequestManager get(FragmentActivity activity) {
        if (Util.isOnBackgroundThread()) {
            return get(activity.getApplicationContext());
        } else {
            assertNotDestroyed(activity);
            FragmentManager fm = activity.getSupportFragmentManager();
            return supportFragmentGet(activity, fm);
        }
    }

    public RequestManager get(Fragment fragment) {
        if (fragment.getActivity() == null) {
            throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");
        }
        if (Util.isOnBackgroundThread()) {
            return get(fragment.getActivity().getApplicationContext());
        } else {
            FragmentManager fm = fragment.getChildFragmentManager();
            return supportFragmentGet(fragment.getActivity(), fm);
        }
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public RequestManager get(Activity activity) {
        if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return get(activity.getApplicationContext());
        } else {
            assertNotDestroyed(activity);
            android.app.FragmentManager fm = activity.getFragmentManager();
            return fragmentGet(activity, fm);
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    private static void assertNotDestroyed(Activity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) {
            throw new IllegalArgumentException("You cannot start a load for a destroyed activity");
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public RequestManager get(android.app.Fragment fragment) {
        if (fragment.getActivity() == null) {
            throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");
        }
        if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return get(fragment.getActivity().getApplicationContext());
        } else {
            android.app.FragmentManager fm = fragment.getChildFragmentManager();
            return fragmentGet(fragment.getActivity(), fm);
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    RequestManagerFragment getRequestManagerFragment(final android.app.FragmentManager fm) {
        RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
        if (current == null) {
            current = pendingRequestManagerFragments.get(fm);
            if (current == null) {
                current = new RequestManagerFragment();
                pendingRequestManagerFragments.put(fm, current);
                fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
                handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
            }
        }
        return current;
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    RequestManager fragmentGet(Context context, android.app.FragmentManager fm) {
        RequestManagerFragment current = getRequestManagerFragment(fm);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }

    SupportRequestManagerFragment getSupportRequestManagerFragment(final FragmentManager fm) {
        SupportRequestManagerFragment current = (SupportRequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
        if (current == null) {
            current = pendingSupportRequestManagerFragments.get(fm);
            if (current == null) {
                current = new SupportRequestManagerFragment();
                pendingSupportRequestManagerFragments.put(fm, current);
                fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
                handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget();
            }
        }
        return current;
    }

    RequestManager supportFragmentGet(Context context, FragmentManager fm) {
        SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }

    ...
}

上述代码虽然看上去逻辑有点复杂,但是将它们梳理清楚后还是很简单的。RequestManagerRetriever类中看似有很多个get()方法的重载,什么Context参数,Activity参数,Fragment参数等等,实际上只有两种情况而已,即传入Application类型的参数,和传入非Application类型的参数。

我们先来看传入Application参数的情况。如果在Glide.with()方法中传入的是一个Application对象,那么这里就会调用带有Context参数的get()方法重载,然后会在第44行调用getApplicationManager()方法来获取一个RequestManager对象。其实这是最简单的一种情况,因为Application对象的生命周期即应用程序的生命周期,因此Glide并不需要做什么特殊的处理,它自动就是和应用程序的生命周期是同步的,如果应用程序关闭的话,Glide的加载也会同时终止。

接下来我们看传入非Application参数的情况。不管你在Glide.with()方法中传入的是Activity、FragmentActivity、v4包下的Fragment、还是app包下的Fragment,最终的流程都是一样的,那就是会向当前的Activity当中添加一个隐藏的Fragment。具体添加的逻辑是在上述代码的第117行和第141行,分别对应的app包和v4包下的两种Fragment的情况。那么这里为什么要添加一个隐藏的Fragment呢?因为Glide需要知道加载的生命周期。很简单的一个道理,如果你在某个Activity上正在加载着一张图片,结果图片还没加载出来,Activity就被用户关掉了,那么图片还应该继续加载吗?当然不应该。可是Glide并没有办法知道Activity的生命周期,于是Glide就使用了添加隐藏Fragment的这种小技巧,因为Fragment的生命周期和Activity是同步的,如果Activity被销毁了,Fragment是可以监听到的,这样Glide就可以捕获这个事件并停止图片加载了。

这里额外再提一句,从第48行代码可以看出,如果我们是在非主线程当中使用的Glide,那么不管你是传入的Activity还是Fragment,都会被强制当成Application来处理。不过其实这就属于是在分析代码的细节了,本篇文章我们将会把目光主要放在Glide的主线工作流程上面,后面不会过多去分析这些细节方面的内容。

总体来说,第一个with()方法的源码还是比较好理解的。其实就是为了得到一个RequestManager对象而已,然后Glide会根据我们传入with()方法的参数来确定图片加载的生命周期,并没有什么特别复杂的逻辑。不过复杂的逻辑还在后面等着我们呢,接下来我们开始分析第二步,load()方法。

2. load()

由于with()方法返回的是一个RequestManager对象,那么很容易就能想到,load()方法是在RequestManager类当中的,所以说我们首先要看的就是RequestManager这个类。不过在上一篇文章中我们学过,Glide是支持图片URL字符串、图片本地路径等等加载形式的,因此RequestManager中也有很多个load()方法的重载。但是这里我们不可能把每个load()方法的重载都看一遍,因此我们就只选其中一个加载图片URL字符串的load()方法来进行研究吧。

RequestManager类的简化代码如下所示:

public class RequestManager implements LifecycleListener {

    ...

    /**
     * Returns a request builder to load the given {@link String}.
     * signature.
     *
     * @see #fromString()
     * @see #load(Object)
     *
     * @param string A file path, or a uri or url handled by {@link com.bumptech.glide.load.model.UriLoader}.
     */
    public DrawableTypeRequest<String> load(String string) {
        return (DrawableTypeRequest<String>) fromString().load(string);
    }

    /**
     * Returns a request builder that loads data from {@link String}s using an empty signature.
     *
     * <p>
     *     Note - this method caches data using only the given String as the cache key. If the data is a Uri outside of
     *     your control, or you otherwise expect the data represented by the given String to change without the String
     *     identifier changing, Consider using
     *     {@link GenericRequestBuilder#signature(Key)} to mixin a signature
     *     you create that identifies the data currently at the given String that will invalidate the cache if that data
     *     changes. Alternatively, using {@link DiskCacheStrategy#NONE} and/or
     *     {@link DrawableRequestBuilder#skipMemoryCache(boolean)} may be appropriate.
     * </p>
     *
     * @see #from(Class)
     * @see #load(String)
     */
    public DrawableTypeRequest<String> fromString() {
        return loadGeneric(String.class);
    }

    private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
        ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
        ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
                Glide.buildFileDescriptorModelLoader(modelClass, context);
        if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
            throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
                    + " which there is a registered ModelLoader, if you are using a custom model, you must first call"
                    + " Glide#register with a ModelLoaderFactory for your custom model class");
        }
        return optionsApplier.apply(
                new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                        glide, requestTracker, lifecycle, optionsApplier));
    }

    ...

}

RequestManager类的代码是非常多的,但是经过我这样简化之后,看上去就比较清爽了。在我们只探究加载图片URL字符串这一个load()方法的情况下,那么比较重要的方法就只剩下上述代码中的这三个方法。

那么我们先来看load()方法,这个方法中的逻辑是非常简单的,只有一行代码,就是先调用了fromString()方法,再调用load()方法,然后把传入的图片URL地址传进去。而fromString()方法也极为简单,就是调用了loadGeneric()方法,并且指定参数为String.class,因为load()方法传入的是一个字符串参数。那么看上去,好像主要的工作都是在loadGeneric()方法中进行的了。

其实loadGeneric()方法也没几行代码,这里分别调用了Glide.buildStreamModelLoader()方法和Glide.buildFileDescriptorModelLoader()方法来获得ModelLoader对象。ModelLoader对象是用于加载图片的,而我们给load()方法传入不同类型的参数,这里也会得到不同的ModelLoader对象。不过buildStreamModelLoader()方法内部的逻辑还是蛮复杂的,这里就不展开介绍了,要不然篇幅实在收不住,感兴趣的话你可以自己研究。由于我们刚才传入的参数是String.class,因此最终得到的是StreamStringLoader对象,它是实现了ModelLoader接口的。

最后我们可以看到,loadGeneric()方法是要返回一个DrawableTypeRequest对象的,因此在loadGeneric()方法的最后又去new了一个DrawableTypeRequest对象,然后把刚才获得的ModelLoader对象,还有一大堆杂七杂八的东西都传了进去。具体每个参数的含义和作用就不解释了,我们只看主线流程。

那么这个DrawableTypeRequest的作用是什么呢?我们来看下它的源码,如下所示:

public class DrawableTypeRequest<ModelType> extends DrawableRequestBuilder<ModelType> implements DownloadOptions {
    private final ModelLoader<ModelType, InputStream> streamModelLoader;
    private final ModelLoader<ModelType, ParcelFileDescriptor> fileDescriptorModelLoader;
    private final RequestManager.OptionsApplier optionsApplier;

    private static <A, Z, R> FixedLoadProvider<A, ImageVideoWrapper, Z, R> buildProvider(Glide glide,
            ModelLoader<A, InputStream> streamModelLoader,
            ModelLoader<A, ParcelFileDescriptor> fileDescriptorModelLoader, Class<Z> resourceClass,
            Class<R> transcodedClass,
            ResourceTranscoder<Z, R> transcoder) {
        if (streamModelLoader == null && fileDescriptorModelLoader == null) {
            return null;
        }

        if (transcoder == null) {
            transcoder = glide.buildTranscoder(resourceClass, transcodedClass);
        }
        DataLoadProvider<ImageVideoWrapper, Z> dataLoadProvider = glide.buildDataProvider(ImageVideoWrapper.class,
                resourceClass);
        ImageVideoModelLoader<A> modelLoader = new ImageVideoModelLoader<A>(streamModelLoader,
                fileDescriptorModelLoader);
        return new FixedLoadProvider<A, ImageVideoWrapper, Z, R>(modelLoader, transcoder, dataLoadProvider);
    }

    DrawableTypeRequest(Class<ModelType> modelClass, ModelLoader<ModelType, InputStream> streamModelLoader,
            ModelLoader<ModelType, ParcelFileDescriptor> fileDescriptorModelLoader, Context context, Glide glide,
            RequestTracker requestTracker, Lifecycle lifecycle, RequestManager.OptionsApplier optionsApplier) {
        super(context, modelClass,
                buildProvider(glide, streamModelLoader, fileDescriptorModelLoader, GifBitmapWrapper.class,
                        GlideDrawable.class, null),
                glide, requestTracker, lifecycle);
        this.streamModelLoader = streamModelLoader;
        this.fileDescriptorModelLoader = fileDescriptorModelLoader;
        this.optionsApplier = optionsApplier;
    }

    /**
     * Attempts to always load the resource as a {@link android.graphics.Bitmap}, even if it could actually be animated.
     *
     * @return A new request builder for loading a {@link android.graphics.Bitmap}
     */
    public BitmapTypeRequest<ModelType> asBitmap() {
        return optionsApplier.apply(new BitmapTypeRequest<ModelType>(this, streamModelLoader,
                fileDescriptorModelLoader, optionsApplier));
    }

    /**
     * Attempts to always load the resource as a {@link com.bumptech.glide.load.resource.gif.GifDrawable}.
     * <p>
     *     If the underlying data is not a GIF, this will fail. As a result, this should only be used if the model
     *     represents an animated GIF and the caller wants to interact with the GIfDrawable directly. Normally using
     *     just an {@link DrawableTypeRequest} is sufficient because it will determine whether or
     *     not the given data represents an animated GIF and return the appropriate animated or not animated
     *     {@link android.graphics.drawable.Drawable} automatically.
     * </p>
     *
     * @return A new request builder for loading a {@link com.bumptech.glide.load.resource.gif.GifDrawable}.
     */
    public GifTypeRequest<ModelType> asGif() {
        return optionsApplier.apply(new GifTypeRequest<ModelType>(this, streamModelLoader, optionsApplier));
    }

    ...
}

这个类中的代码本身就不多,我只是稍微做了一点简化。可以看到,最主要的就是它提供了asBitmap()和asGif()这两个方法。这两个方法我们在上一篇文章当中都是学过的,分别是用于强制指定加载静态图片和动态图片。而从源码中可以看出,它们分别又创建了一个BitmapTypeRequest和GifTypeRequest,如果没有进行强制指定的话,那默认就是使用DrawableTypeRequest。

好的,那么我们再回到RequestManager的load()方法中。刚才已经分析过了,fromString()方法会返回一个DrawableTypeRequest对象,接下来会调用这个对象的load()方法,把图片的URL地址传进去。但是我们刚才看到了,DrawableTypeRequest中并没有load()方法,那么很容易就能猜想到,load()方法是在父类当中的。

DrawableTypeRequest的父类是DrawableRequestBuilder,我们来看下这个类的源码:

public class DrawableRequestBuilder<ModelType>
        extends GenericRequestBuilder<ModelType, ImageVideoWrapper, GifBitmapWrapper, GlideDrawable>
        implements BitmapOptions, DrawableOptions {

    DrawableRequestBuilder(Context context, Class<ModelType> modelClass,
            LoadProvider<ModelType, ImageVideoWrapper, GifBitmapWrapper, GlideDrawable> loadProvider, Glide glide,
            RequestTracker requestTracker, Lifecycle lifecycle) {
        super(context, modelClass, loadProvider, GlideDrawable.class, glide, requestTracker, lifecycle);
        // Default to animating.
        crossFade();
    }

    public DrawableRequestBuilder<ModelType> thumbnail(
            DrawableRequestBuilder<?> thumbnailRequest) {
        super.thumbnail(thumbnailRequest);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> thumbnail(
            GenericRequestBuilder<?, ?, ?, GlideDrawable> thumbnailRequest) {
        super.thumbnail(thumbnailRequest);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> thumbnail(float sizeMultiplier) {
        super.thumbnail(sizeMultiplier);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> sizeMultiplier(float sizeMultiplier) {
        super.sizeMultiplier(sizeMultiplier);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> decoder(ResourceDecoder<ImageVideoWrapper, GifBitmapWrapper> decoder) {
        super.decoder(decoder);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> cacheDecoder(ResourceDecoder<File, GifBitmapWrapper> cacheDecoder) {
        super.cacheDecoder(cacheDecoder);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> encoder(ResourceEncoder<GifBitmapWrapper> encoder) {
        super.encoder(encoder);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> priority(Priority priority) {
        super.priority(priority);
        return this;
    }

    public DrawableRequestBuilder<ModelType> transform(BitmapTransformation... transformations) {
        return bitmapTransform(transformations);
    }

    public DrawableRequestBuilder<ModelType> centerCrop() {
        return transform(glide.getDrawableCenterCrop());
    }

    public DrawableRequestBuilder<ModelType> fitCenter() {
        return transform(glide.getDrawableFitCenter());
    }

    public DrawableRequestBuilder<ModelType> bitmapTransform(Transformation<Bitmap>... bitmapTransformations) {
        GifBitmapWrapperTransformation[] transformations =
                new GifBitmapWrapperTransformation[bitmapTransformations.length];
        for (int i = 0; i < bitmapTransformations.length; i++) {
            transformations[i] = new GifBitmapWrapperTransformation(glide.getBitmapPool(), bitmapTransformations[i]);
        }
        return transform(transformations);
    }

    @Override
    public DrawableRequestBuilder<ModelType> transform(Transformation<GifBitmapWrapper>... transformation) {
        super.transform(transformation);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> transcoder(
            ResourceTranscoder<GifBitmapWrapper, GlideDrawable> transcoder) {
        super.transcoder(transcoder);
        return this;
    }

    public final DrawableRequestBuilder<ModelType> crossFade() {
        super.animate(new DrawableCrossFadeFactory<GlideDrawable>());
        return this;
    }

    public DrawableRequestBuilder<ModelType> crossFade(int duration) {
        super.animate(new DrawableCrossFadeFactory<GlideDrawable>(duration));
        return this;
    }

    public DrawableRequestBuilder<ModelType> crossFade(int animationId, int duration) {
        super.animate(new DrawableCrossFadeFactory<GlideDrawable>(context, animationId,
                duration));
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> dontAnimate() {
        super.dontAnimate();
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> animate(ViewPropertyAnimation.Animator animator) {
        super.animate(animator);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> animate(int animationId) {
        super.animate(animationId);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> placeholder(int resourceId) {
        super.placeholder(resourceId);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> placeholder(Drawable drawable) {
        super.placeholder(drawable);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> fallback(Drawable drawable) {
        super.fallback(drawable);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> fallback(int resourceId) {
        super.fallback(resourceId);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> error(int resourceId) {
        super.error(resourceId);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> error(Drawable drawable) {
        super.error(drawable);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> listener(
            RequestListener<? super ModelType, GlideDrawable> requestListener) {
        super.listener(requestListener);
        return this;
    }
    @Override
    public DrawableRequestBuilder<ModelType> diskCacheStrategy(DiskCacheStrategy strategy) {
        super.diskCacheStrategy(strategy);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> skipMemoryCache(boolean skip) {
        super.skipMemoryCache(skip);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> override(int width, int height) {
        super.override(width, height);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> sourceEncoder(Encoder<ImageVideoWrapper> sourceEncoder) {
        super.sourceEncoder(sourceEncoder);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> dontTransform() {
        super.dontTransform();
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> signature(Key signature) {
        super.signature(signature);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> load(ModelType model) {
        super.load(model);
        return this;
    }

    @Override
    public DrawableRequestBuilder<ModelType> clone() {
        return (DrawableRequestBuilder<ModelType>) super.clone();
    }

    @Override
    public Target<GlideDrawable> into(ImageView view) {
        return super.into(view);
    }

    @Override
    void applyFitCenter() {
        fitCenter();
    }

    @Override
    void applyCenterCrop() {
        centerCrop();
    }
}

DrawableRequestBuilder中有很多个方法,这些方法其实就是Glide绝大多数的API了。里面有不少我们在上篇文章中已经用过了,比如说placeholder()方法、error()方法、diskCacheStrategy()方法、override()方法等。当然还有很多暂时还没用到的API,我们会在后面的文章当中学习。

到这里,第二步load()方法也就分析结束了。为什么呢?因为你会发现DrawableRequestBuilder类中有一个into()方法(上述代码第220行),也就是说,最终load()方法返回的其实就是一个DrawableTypeRequest对象。那么接下来我们就要进行第三步了,分析into()方法中的逻辑。

3. into()

如果说前面两步都是在准备开胃小菜的话,那么现在终于要进入主菜了,因为into()方法也是整个Glide图片加载流程中逻辑最复杂的地方。

不过从刚才的代码来看,into()方法中并没有任何逻辑,只有一句super.into(view)。那么很显然,into()方法的具体逻辑都是在DrawableRequestBuilder的父类当中了。

DrawableRequestBuilder的父类是GenericRequestBuilder,我们来看一下GenericRequestBuilder类中的into()方法,如下所示:

public Target<TranscodeType> into(ImageView view) {
    Util.assertMainThread();
    if (view == null) {
        throw new IllegalArgumentException("You must pass in a non null View");
    }
    if (!isTransformationSet && view.getScaleType() != null) {
        switch (view.getScaleType()) {
            case CENTER_CROP:
                applyCenterCrop();
                break;
            case FIT_CENTER:
            case FIT_START:
            case FIT_END:
                applyFitCenter();
                break;
            //$CASES-OMITTED$
            default:
                // Do nothing.
        }
    }
    return into(glide.buildImageViewTarget(view, transcodeClass));
}

这里前面一大堆的判断逻辑我们都可以先不用管,等到后面文章讲transform的时候会再进行解释,现在我们只需要关注最后一行代码。最后一行代码先是调用了glide.buildImageViewTarget()方法,这个方法会构建出一个Target对象,Target对象则是用来最终展示图片用的,如果我们跟进去的话会看到如下代码:

<R> Target<R> buildImageViewTarget(ImageView imageView, Class<R> transcodedClass) {
    return imageViewTargetFactory.buildTarget(imageView, transcodedClass);
}

这里其实又是调用了ImageViewTargetFactory的buildTarget()方法,我们继续跟进去,代码如下所示:

public class ImageViewTargetFactory {

    @SuppressWarnings("unchecked")
    public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
        if (GlideDrawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new GlideDrawableImageViewTarget(view);
        } else if (Bitmap.class.equals(clazz)) {
            return (Target<Z>) new BitmapImageViewTarget(view);
        } else if (Drawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new DrawableImageViewTarget(view);
        } else {
            throw new IllegalArgumentException("Unhandled class: " + clazz
                    + ", try .as*(Class).transcode(ResourceTranscoder)");
        }
    }
}

可以看到,在buildTarget()方法中会根据传入的class参数来构建不同的Target对象。那如果你要分析这个class参数是从哪儿传过来的,这可有得你分析了,简单起见我直接帮大家梳理清楚。这个class参数其实基本上只有两种情况,如果你在使用Glide加载图片的时候调用了asBitmap()方法,那么这里就会构建出BitmapImageViewTarget对象,否则的话构建的都是GlideDrawableImageViewTarget对象。至于上述代码中的DrawableImageViewTarget对象,这个通常都是用不到的,我们可以暂时不用管它。

也就是说,通过glide.buildImageViewTarget()方法,我们构建出了一个GlideDrawableImageViewTarget对象。那现在回到刚才into()方法的最后一行,可以看到,这里又将这个参数传入到了GenericRequestBuilder另一个接收Target对象的into()方法当中了。我们来看一下这个into()方法的源码:

public <Y extends Target<TranscodeType>> Y into(Y target) {
    Util.assertMainThread();
    if (target == null) {
        throw new IllegalArgumentException("You must pass in a non null Target");
    }
    if (!isModelSet) {
        throw new IllegalArgumentException("You must first set a model (try #load())");
    }
    Request previous = target.getRequest();
    if (previous != null) {
        previous.clear();
        requestTracker.removeRequest(previous);
        previous.recycle();
    }
    Request request = buildRequest(target);
    target.setRequest(request);
    lifecycle.addListener(target);
    requestTracker.runRequest(request);
    return target;
}

这里我们还是只抓核心代码,其实只有两行是最关键的,第15行调用buildRequest()方法构建出了一个Request对象,还有第18行来执行这个Request。

Request是用来发出加载图片请求的,它是Glide中非常关键的一个组件。我们先来看buildRequest()方法是如何构建Request对象的:

private Request buildRequest(Target<TranscodeType> target) {
    if (priority == null) {
        priority = Priority.NORMAL;
    }
    return buildRequestRecursive(target, null);
}

private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
    if (thumbnailRequestBuilder != null) {
        if (isThumbnailBuilt) {
            throw new IllegalStateException("You cannot use a request as both the main request and a thumbnail, "
                    + "consider using clone() on the request(s) passed to thumbnail()");
        }
        // Recursive case: contains a potentially recursive thumbnail request builder.
        if (thumbnailRequestBuilder.animationFactory.equals(NoAnimation.getFactory())) {
            thumbnailRequestBuilder.animationFactory = animationFactory;
        }

        if (thumbnailRequestBuilder.priority == null) {
            thumbnailRequestBuilder.priority = getThumbnailPriority();
        }

        if (Util.isValidDimensions(overrideWidth, overrideHeight)
                && !Util.isValidDimensions(thumbnailRequestBuilder.overrideWidth,
                        thumbnailRequestBuilder.overrideHeight)) {
          thumbnailRequestBuilder.override(overrideWidth, overrideHeight);
        }

        ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
        Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);
        // Guard against infinite recursion.
        isThumbnailBuilt = true;
        // Recursively generate thumbnail requests.
        Request thumbRequest = thumbnailRequestBuilder.buildRequestRecursive(target, coordinator);
        isThumbnailBuilt = false;
        coordinator.setRequests(fullRequest, thumbRequest);
        return coordinator;
    } else if (thumbSizeMultiplier != null) {
        // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
        ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
        Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);
        Request thumbnailRequest = obtainRequest(target, thumbSizeMultiplier, getThumbnailPriority(), coordinator);
        coordinator.setRequests(fullRequest, thumbnailRequest);
        return coordinator;
    } else {
        // Base case: no thumbnail.
        return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
    }
}

private Request obtainRequest(Target<TranscodeType> target, float sizeMultiplier, Priority priority,
        RequestCoordinator requestCoordinator) {
    return GenericRequest.obtain(
            loadProvider,
            model,
            signature,
            context,
            priority,
            target,
            sizeMultiplier,
            placeholderDrawable,
            placeholderId,
            errorPlaceholder,
            errorId,
            fallbackDrawable,
            fallbackResource,
            requestListener,
            requestCoordinator,
            glide.getEngine(),
            transformation,
            transcodeClass,
            isCacheable,
            animationFactory,
            overrideWidth,
            overrideHeight,
            diskCacheStrategy);
}

可以看到,buildRequest()方法的内部其实又调用了buildRequestRecursive()方法,而buildRequestRecursive()方法中的代码虽然有点长,但是其中90%的代码都是在处理缩略图的。如果我们只追主线流程的话,那么只需要看第47行代码就可以了。这里调用了obtainRequest()方法来获取一个Request对象,而obtainRequest()方法中又去调用了GenericRequest的obtain()方法。注意这个obtain()方法需要传入非常多的参数,而其中很多的参数我们都是比较熟悉的,像什么placeholderId、errorPlaceholder、diskCacheStrategy等等。因此,我们就有理由猜测,刚才在load()方法中调用的所有API,其实都是在这里组装到Request对象当中的。那么我们进入到这个GenericRequest的obtain()方法瞧一瞧:

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {

    ...

    public static <A, T, Z, R> GenericRequest<A, T, Z, R> obtain(
            LoadProvider<A, T, Z, R> loadProvider,
            A model,
            Key signature,
            Context context,
            Priority priority,
            Target<R> target,
            float sizeMultiplier,
            Drawable placeholderDrawable,
            int placeholderResourceId,
            Drawable errorDrawable,
            int errorResourceId,
            Drawable fallbackDrawable,
            int fallbackResourceId,
            RequestListener<? super A, R> requestListener,
            RequestCoordinator requestCoordinator,
            Engine engine,
            Transformation<Z> transformation,
            Class<R> transcodeClass,
            boolean isMemoryCacheable,
            GlideAnimationFactory<R> animationFactory,
            int overrideWidth,
            int overrideHeight,
            DiskCacheStrategy diskCacheStrategy) {
        @SuppressWarnings("unchecked")
        GenericRequest<A, T, Z, R> request = (GenericRequest<A, T, Z, R>) REQUEST_POOL.poll();
        if (request == null) {
            request = new GenericRequest<A, T, Z, R>();
        }
        request.init(loadProvider,
                model,
                signature,
                context,
                priority,
                target,
                sizeMultiplier,
                placeholderDrawable,
                placeholderResourceId,
                errorDrawable,
                errorResourceId,
                fallbackDrawable,
                fallbackResourceId,
                requestListener,
                requestCoordinator,
                engine,
                transformation,
                transcodeClass,
                isMemoryCacheable,
                animationFactory,
                overrideWidth,
                overrideHeight,
                diskCacheStrategy);
        return request;
    }

    ...
}

可以看到,这里在第33行去new了一个GenericRequest对象,并在最后一行返回,也就是说,obtain()方法实际上获得的就是一个GenericRequest对象。另外这里又在第35行调用了GenericRequest的init(),里面主要就是一些赋值的代码,将传入的这些参数赋值到GenericRequest的成员变量当中,我们就不再跟进去看了。

好,那现在解决了构建Request对象的问题,接下来我们看一下这个Request对象又是怎么执行的。回到刚才的into()方法,你会发现在第18行调用了requestTracker.runRequest()方法来去执行这个Request,那么我们跟进去瞧一瞧,如下所示:

/**
 * Starts tracking the given request.
 */
public void runRequest(Request request) {
    requests.add(request);
    if (!isPaused) {
        request.begin();
    } else {
        pendingRequests.add(request);
    }
}

这里有一个简单的逻辑判断,就是先判断Glide当前是不是处理暂停状态,如果不是暂停状态就调用Request的begin()方法来执行Request,否则的话就先将Request添加到待执行队列里面,等暂停状态解除了之后再执行。

暂停请求的功能仍然不是这篇文章所关心的,这里就直接忽略了,我们重点来看这个begin()方法。由于当前的Request对象是一个GenericRequest,因此这里就需要看GenericRequest中的begin()方法了,如下所示:

@Override
public void begin() {
    startTime = LogTime.getLogTime();
    if (model == null) {
        onException(null);
        return;
    }
    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        onSizeReady(overrideWidth, overrideHeight);
    } else {
        target.getSize(this);
    }
    if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
        target.onLoadStarted(getPlaceholderDrawable());
    }
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("finished run method in " + LogTime.getElapsedMillis(startTime));
    }
}

这里我们来注意几个细节,首先如果model等于null,model也就是我们在第二步load()方法中传入的图片URL地址,这个时候会调用onException()方法。如果你跟到onException()方法里面去看看,你会发现它最终会调用到一个setErrorPlaceholder()当中,如下所示:

private void setErrorPlaceholder(Exception e) {
    if (!canNotifyStatusChanged()) {
        return;
    }
    Drawable error = model == null ? getFallbackDrawable() : null;
    if (error == null) {
      error = getErrorDrawable();
    }
    if (error == null) {
        error = getPlaceholderDrawable();
    }
    target.onLoadFailed(e, error);
}

这个方法中会先去获取一个error的占位图,如果获取不到的话会再去获取一个loading占位图,然后调用target.onLoadFailed()方法并将占位图传入。那么onLoadFailed()方法中做了什么呢?我们看一下:

public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z> implements GlideAnimation.ViewAdapter {

    ...

    @Override
    public void onLoadStarted(Drawable placeholder) {
        view.setImageDrawable(placeholder);
    }

    @Override
    public void onLoadFailed(Exception e, Drawable errorDrawable) {
        view.setImageDrawable(errorDrawable);
    }

    ...
}

很简单,其实就是将这张error占位图显示到ImageView上而已,因为现在出现了异常,没办法展示正常的图片了。而如果你仔细看下刚才begin()方法的第15行,你会发现它又调用了一个target.onLoadStarted()方法,并传入了一个loading占位图,在也就说,在图片请求开始之前,会先使用这张占位图代替最终的图片显示。这也是我们在上一篇文章中学过的placeholder()和error()这两个占位图API底层的实现原理。

好,那么我们继续回到begin()方法。刚才讲了占位图的实现,那么具体的图片加载又是从哪里开始的呢?是在begin()方法的第10行和第12行。这里要分两种情况,一种是你使用了override() API为图片指定了一个固定的宽高,一种是没有指定。如果指定了的话,就会执行第10行代码,调用onSizeReady()方法。如果没指定的话,就会执行第12行代码,调用target.getSize()方法。这个target.getSize()方法的内部会根据ImageView的layout_width和layout_height值做一系列的计算,来算出图片应该的宽高。具体的计算细节我就不带着大家分析了,总之在计算完之后,它也会调用onSizeReady()方法。也就是说,不管是哪种情况,最终都会调用到onSizeReady()方法,在这里进行下一步操作。那么我们跟到这个方法里面来:

@Override
public void onSizeReady(int width, int height) {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
    }
    if (status != Status.WAITING_FOR_SIZE) {
        return;
    }
    status = Status.RUNNING;
    width = Math.round(sizeMultiplier * width);
    height = Math.round(sizeMultiplier * height);
    ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
    final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
    if (dataFetcher == null) {
        onException(new Exception("Failed to load model: \'" + model + "\'"));
        return;
    }
    ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
    }
    loadedFromMemoryCache = true;
    loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
            priority, isMemoryCacheable, diskCacheStrategy, this);
    loadedFromMemoryCache = resource != null;
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
    }
}

从这里开始,真正复杂的地方来了,我们需要慢慢进行分析。先来看一下,在第12行调用了loadProvider.getModelLoader()方法,那么我们第一个要搞清楚的就是,这个loadProvider是什么?要搞清楚这点,需要先回到第二步的load()方法当中。还记得load()方法是返回一个DrawableTypeRequest对象吗?刚才我们只是分析了DrawableTypeRequest当中的asBitmap()和asGif()方法,并没有仔细看它的构造函数,现在我们重新来看一下DrawableTypeRequest类的构造函数:

public class DrawableTypeRequest<ModelType> extends DrawableRequestBuilder<ModelType> implements DownloadOptions {

    private final ModelLoader<ModelType, InputStream> streamModelLoader;
    private final ModelLoader<ModelType, ParcelFileDescriptor> fileDescriptorModelLoader;
    private final RequestManager.OptionsApplier optionsApplier;

    private static <A, Z, R> FixedLoadProvider<A, ImageVideoWrapper, Z, R> buildProvider(Glide glide,
            ModelLoader<A, InputStream> streamModelLoader,
            ModelLoader<A, ParcelFileDescriptor> fileDescriptorModelLoader, Class<Z> resourceClass,
            Class<R> transcodedClass,
            ResourceTranscoder<Z, R> transcoder) {
        if (streamModelLoader == null && fileDescriptorModelLoader == null) {
            return null;
        }
        if (transcoder == null) {
            transcoder = glide.buildTranscoder(resourceClass, transcodedClass);
        }
        DataLoadProvider<ImageVideoWrapper, Z> dataLoadProvider = glide.buildDataProvider(ImageVideoWrapper.class,
                resourceClass);
        ImageVideoModelLoader<A> modelLoader = new ImageVideoModelLoader<A>(streamModelLoader,
                fileDescriptorModelLoader);
        return new FixedLoadProvider<A, ImageVideoWrapper, Z, R>(modelLoader, transcoder, dataLoadProvider);
    }

    DrawableTypeRequest(Class<ModelType> modelClass, ModelLoader<ModelType, InputStream> streamModelLoader,
            ModelLoader<ModelType, ParcelFileDescriptor> fileDescriptorModelLoader, Context context, Glide glide,
            RequestTracker requestTracker, Lifecycle lifecycle, RequestManager.OptionsApplier optionsApplier) {
        super(context, modelClass,
                buildProvider(glide, streamModelLoader, fileDescriptorModelLoader, GifBitmapWrapper.class,
                        GlideDrawable.class, null),
                glide, requestTracker, lifecycle);
        this.streamModelLoader = streamModelLoader;
        this.fileDescriptorModelLoader = fileDescriptorModelLoader;
        this.optionsApplier = optionsApplier;
    }

    ...
}

可以看到,这里在第29行,也就是构造函数中,调用了一个buildProvider()方法,并把streamModelLoader和fileDescriptorModelLoader等参数传入到这个方法中,这两个ModelLoader就是之前在loadGeneric()方法中构建出来的。

那么我们再来看一下buildProvider()方法里面做了什么,在第16行调用了glide.buildTranscoder()方法来构建一个ResourceTranscoder,它是用于对图片进行转码的,由于ResourceTranscoder是一个接口,这里实际会构建出一个GifBitmapWrapperDrawableTranscoder对象。

接下来在第18行调用了glide.buildDataProvider()方法来构建一个DataLoadProvider,它是用于对图片进行编解码的,由于DataLoadProvider是一个接口,这里实际会构建出一个ImageVideoGifDrawableLoadProvider对象。

然后在第20行,new了一个ImageVideoModelLoader的实例,并把之前loadGeneric()方法中构建的两个ModelLoader封装到了ImageVideoModelLoader当中。

最后,在第22行,new出一个FixedLoadProvider,并把刚才构建的出来的GifBitmapWrapperDrawableTranscoder、ImageVideoModelLoader、ImageVideoGifDrawableLoadProvider都封装进去,这个也就是onSizeReady()方法中的loadProvider了。

好的,那么我们回到onSizeReady()方法中,在onSizeReady()方法的第12行和第18行,分别调用了loadProvider的getModelLoader()方法和getTranscoder()方法,那么得到的对象也就是刚才我们分析的ImageVideoModelLoader和GifBitmapWrapperDrawableTranscoder了。而在第13行,又调用了ImageVideoModelLoader的getResourceFetcher()方法,这里我们又需要跟进去瞧一瞧了,代码如下所示:

public class ImageVideoModelLoader<A> implements ModelLoader<A, ImageVideoWrapper> {
    private static final String TAG = "IVML";

    private final ModelLoader<A, InputStream> streamLoader;
    private final ModelLoader<A, ParcelFileDescriptor> fileDescriptorLoader;

    public ImageVideoModelLoader(ModelLoader<A, InputStream> streamLoader,
            ModelLoader<A, ParcelFileDescriptor> fileDescriptorLoader) {
        if (streamLoader == null && fileDescriptorLoader == null) {
            throw new NullPointerException("At least one of streamLoader and fileDescriptorLoader must be non null");
        }
        this.streamLoader = streamLoader;
        this.fileDescriptorLoader = fileDescriptorLoader;
    }

    @Override
    public DataFetcher<ImageVideoWrapper> getResourceFetcher(A model, int width, int height) {
        DataFetcher<InputStream> streamFetcher = null;
        if (streamLoader != null) {
            streamFetcher = streamLoader.getResourceFetcher(model, width, height);
        }
        DataFetcher<ParcelFileDescriptor> fileDescriptorFetcher = null;
        if (fileDescriptorLoader != null) {
            fileDescriptorFetcher = fileDescriptorLoader.getResourceFetcher(model, width, height);
        }

        if (streamFetcher != null || fileDescriptorFetcher != null) {
            return new ImageVideoFetcher(streamFetcher, fileDescriptorFetcher);
        } else {
            return null;
        }
    }

    static class ImageVideoFetcher implements DataFetcher<ImageVideoWrapper> {
        private final DataFetcher<InputStream> streamFetcher;
        private final DataFetcher<ParcelFileDescriptor> fileDescriptorFetcher;

        public ImageVideoFetcher(DataFetcher<InputStream> streamFetcher,
                DataFetcher<ParcelFileDescriptor> fileDescriptorFetcher) {
            this.streamFetcher = streamFetcher;
            this.fileDescriptorFetcher = fileDescriptorFetcher;
        }

        ...
    }
}

可以看到,在第20行会先调用streamLoader.getResourceFetcher()方法获取一个DataFetcher,而这个streamLoader其实就是我们在loadGeneric()方法中构建出的StreamStringLoader,调用它的getResourceFetcher()方法会得到一个HttpUrlFetcher对象。然后在第28行new出了一个ImageVideoFetcher对象,并把获得的HttpUrlFetcher对象传进去。也就是说,ImageVideoModelLoader的getResourceFetcher()方法得到的是一个ImageVideoFetcher。

那么我们再次回到onSizeReady()方法,在onSizeReady()方法的第23行,这里将刚才获得的ImageVideoFetcher、GifBitmapWrapperDrawableTranscoder等等一系列的值一起传入到了Engine的load()方法当中。接下来我们就要看一看,这个Engine的load()方法当中,到底做了什么?代码如下所示:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    ...    

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }

        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }

        EngineJob current = jobs.get(key);
        if (current != null) {
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

    ...
}

load()方法中的代码虽然有点长,但大多数的代码都是在处理缓存的。关于缓存的内容我们会在下一篇文章当中学习,现在只需要从第45行看起就行。这里构建了一个EngineJob,它的主要作用就是用来开启线程的,为后面的异步加载图片做准备。接下来第46行创建了一个DecodeJob对象,从名字上来看,它好像是用来对图片进行解码的,但实际上它的任务十分繁重,待会我们就知道了。继续往下看,第48行创建了一个EngineRunnable对象,并且在51行调用了EngineJob的start()方法来运行EngineRunnable对象,这实际上就是让EngineRunnable的run()方法在子线程当中执行了。那么我们现在就可以去看看EngineRunnable的run()方法里做了些什么,如下所示:

@Override
public void run() {
    if (isCancelled) {
        return;
    }
    Exception exception = null;
    Resource<?> resource = null;
    try {
        resource = decode();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Exception decoding", e);
        }
        exception = e;
    }
    if (isCancelled) {
        if (resource != null) {
            resource.recycle();
        }
        return;
    }
    if (resource == null) {
        onLoadFailed(exception);
    } else {
        onLoadComplete(resource);
    }
}

这个方法中的代码并不多,但我们仍然还是要抓重点。在第9行,这里调用了一个decode()方法,并且这个方法返回了一个Resource对象。看上去所有的逻辑应该都在这个decode()方法执行的了,那我们跟进去瞧一瞧:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}

decode()方法中又分了两种情况,从缓存当中去decode图片的话就会执行decodeFromCache(),否则的话就执行decodeFromSource()。本篇文章中我们不讨论缓存的情况,那么就直接来看decodeFromSource()方法的代码吧,如下所示:

private Resource<?> decodeFromSource() throws Exception {
    return decodeJob.decodeFromSource();
}

这里又调用了DecodeJob的decodeFromSource()方法。刚才已经说了,DecodeJob的任务十分繁重,我们继续跟进看一看吧:

class DecodeJob<A, T, Z> {

    ...

    public Resource<Z> decodeFromSource() throws Exception {
        Resource<T> decoded = decodeSource();
        return transformEncodeAndTranscode(decoded);
    }

    private Resource<T> decodeSource() throws Exception {
        Resource<T> decoded = null;
        try {
            long startTime = LogTime.getLogTime();
            final A data = fetcher.loadData(priority);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Fetched data", startTime);
            }
            if (isCancelled) {
                return null;
            }
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }

    ...
}

主要的方法就这些,我都帮大家提取出来了。那么我们先来看一下decodeFromSource()方法,其实它的工作分为两部,第一步是调用decodeSource()方法来获得一个Resource对象,第二步是调用transformEncodeAndTranscode()方法来处理这个Resource对象。

那么我们先来看第一步,decodeSource()方法中的逻辑也并不复杂,首先在第14行调用了fetcher.loadData()方法。那么这个fetcher是什么呢?其实就是刚才在onSizeReady()方法中得到的ImageVideoFetcher对象,这里调用它的loadData()方法,代码如下所示:

@Override
public ImageVideoWrapper loadData(Priority priority) throws Exception {
    InputStream is = null;
    if (streamFetcher != null) {
        try {
            is = streamFetcher.loadData(priority);
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Exception fetching input stream, trying ParcelFileDescriptor", e);
            }
            if (fileDescriptorFetcher == null) {
                throw e;
            }
        }
    }
    ParcelFileDescriptor fileDescriptor = null;
    if (fileDescriptorFetcher != null) {
        try {
            fileDescriptor = fileDescriptorFetcher.loadData(priority);
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Exception fetching ParcelFileDescriptor", e);
            }
            if (is == null) {
                throw e;
            }
        }
    }
    return new ImageVideoWrapper(is, fileDescriptor);
}

可以看到,在ImageVideoFetcher的loadData()方法的第6行,这里又去调用了streamFetcher.loadData()方法,那么这个streamFetcher是什么呢?自然就是刚才在组装ImageVideoFetcher对象时传进来的HttpUrlFetcher了。因此这里又会去调用HttpUrlFetcher的loadData()方法,那么我们继续跟进去瞧一瞧:

public class HttpUrlFetcher implements DataFetcher<InputStream> {

    ...

    @Override
    public InputStream loadData(Priority priority) throws Exception {
        return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
    }

    private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
            throws IOException {
        if (redirects >= MAXIMUM_REDIRECTS) {
            throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
        } else {
            // Comparing the URLs using .equals performs additional network I/O and is generally broken.
            // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
            try {
                if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
                    throw new IOException("In re-direct loop");
                }
            } catch (URISyntaxException e) {
                // Do nothing, this is best effort.
            }
        }
        urlConnection = connectionFactory.build(url);
        for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
          urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
        }
        urlConnection.setConnectTimeout(2500);
        urlConnection.setReadTimeout(2500);
        urlConnection.setUseCaches(false);
        urlConnection.setDoInput(true);

        // Connect explicitly to avoid errors in decoders if connection fails.
        urlConnection.connect();
        if (isCancelled) {
            return null;
        }
        final int statusCode = urlConnection.getResponseCode();
        if (statusCode / 100 == 2) {
            return getStreamForSuccessfulRequest(urlConnection);
        } else if (statusCode / 100 == 3) {
            String redirectUrlString = urlConnection.getHeaderField("Location");
            if (TextUtils.isEmpty(redirectUrlString)) {
                throw new IOException("Received empty or null redirect url");
            }
            URL redirectUrl = new URL(url, redirectUrlString);
            return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
        } else {
            if (statusCode == -1) {
                throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
            }
            throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
        }
    }

    private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection)
            throws IOException {
        if (TextUtils.isEmpty(urlConnection.getContentEncoding())) {
            int contentLength = urlConnection.getContentLength();
            stream = ContentLengthInputStream.obtain(urlConnection.getInputStream(), contentLength);
        } else {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Got non empty content encoding: " + urlConnection.getContentEncoding());
            }
            stream = urlConnection.getInputStream();
        }
        return stream;
    }

    ...
}

经过一层一层地跋山涉水,我们终于在这里找到网络通讯的代码了!之前有朋友跟我讲过,说Glide的源码实在是太复杂了,甚至连网络请求是在哪里发出去的都找不到。我们也是经过一段一段又一段的代码跟踪,终于把网络请求的代码给找出来了,实在是太不容易了。

不过也别高兴得太早,现在离最终分析完还早着呢。可以看到,loadData()方法只是返回了一个InputStream,服务器返回的数据连读都还没开始读呢。所以我们还是要静下心来继续分析,回到刚才ImageVideoFetcher的loadData()方法中,在这个方法的最后一行,创建了一个ImageVideoWrapper对象,并把刚才得到的InputStream作为参数传了进去。

然后我们回到再上一层,也就是DecodeJob的decodeSource()方法当中,在得到了这个ImageVideoWrapper对象之后,紧接着又将这个对象传入到了decodeFromSourceData()当中,来去解码这个对象。decodeFromSourceData()方法的代码如下所示:

private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    if (diskCacheStrategy.cacheSource()) {
        decoded = cacheAndDecodeSourceData(data);
    } else {
        long startTime = LogTime.getLogTime();
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Decoded from source", startTime);
        }
    }
    return decoded;
}

可以看到,这里在第7行调用了loadProvider.getSourceDecoder().decode()方法来进行解码。loadProvider就是刚才在onSizeReady()方法中得到的FixedLoadProvider,而getSourceDecoder()得到的则是一个GifBitmapWrapperResourceDecoder对象,也就是要调用这个对象的decode()方法来对图片进行解码。那么我们来看下GifBitmapWrapperResourceDecoder的代码:

public class GifBitmapWrapperResourceDecoder implements ResourceDecoder<ImageVideoWrapper, GifBitmapWrapper> {

    ...

    @SuppressWarnings("resource")
    // @see ResourceDecoder.decode
    @Override
    public Resource<GifBitmapWrapper> decode(ImageVideoWrapper source, int width, int height) throws IOException {
        ByteArrayPool pool = ByteArrayPool.get();
        byte[] tempBytes = pool.getBytes();
        GifBitmapWrapper wrapper = null;
        try {
            wrapper = decode(source, width, height, tempBytes);
        } finally {
            pool.releaseBytes(tempBytes);
        }
        return wrapper != null ? new GifBitmapWrapperResource(wrapper) : null;
    }

    private GifBitmapWrapper decode(ImageVideoWrapper source, int width, int height, byte[] bytes) throws IOException {
        final GifBitmapWrapper result;
        if (source.getStream() != null) {
            result = decodeStream(source, width, height, bytes);
        } else {
            result = decodeBitmapWrapper(source, width, height);
        }
        return result;
    }

    private GifBitmapWrapper decodeStream(ImageVideoWrapper source, int width, int height, byte[] bytes)
            throws IOException {
        InputStream bis = streamFactory.build(source.getStream(), bytes);
        bis.mark(MARK_LIMIT_BYTES);
        ImageHeaderParser.ImageType type = parser.parse(bis);
        bis.reset();
        GifBitmapWrapper result = null;
        if (type == ImageHeaderParser.ImageType.GIF) {
            result = decodeGifWrapper(bis, width, height);
        }
        // Decoding the gif may fail even if the type matches.
        if (result == null) {
            // We can only reset the buffered InputStream, so to start from the beginning of the stream, we need to
            // pass in a new source containing the buffered stream rather than the original stream.
            ImageVideoWrapper forBitmapDecoder = new ImageVideoWrapper(bis, source.getFileDescriptor());
            result = decodeBitmapWrapper(forBitmapDecoder, width, height);
        }
        return result;
    }

    private GifBitmapWrapper decodeBitmapWrapper(ImageVideoWrapper toDecode, int width, int height) throws IOException {
        GifBitmapWrapper result = null;
        Resource<Bitmap> bitmapResource = bitmapDecoder.decode(toDecode, width, height);
        if (bitmapResource != null) {
            result = new GifBitmapWrapper(bitmapResource, null);
        }
        return result;
    }

    ...
}

首先,在decode()方法中,又去调用了另外一个decode()方法的重载。然后在第23行调用了decodeStream()方法,准备从服务器返回的流当中读取数据。decodeStream()方法中会先从流中读取2个字节的数据,来判断这张图是GIF图还是普通的静图,如果是GIF图就调用decodeGifWrapper()方法来进行解码,如果是普通的静图就用调用decodeBitmapWrapper()方法来进行解码。这里我们只分析普通静图的实现流程,GIF图的实现有点过于复杂了,无法在本篇文章当中分析。

然后我们来看一下decodeBitmapWrapper()方法,这里在第52行调用了bitmapDecoder.decode()方法。这个bitmapDecoder是一个ImageVideoBitmapDecoder对象,那么我们来看一下它的代码,如下所示:

public class ImageVideoBitmapDecoder implements ResourceDecoder<ImageVideoWrapper, Bitmap> {
    private final ResourceDecoder<InputStream, Bitmap> streamDecoder;
    private final ResourceDecoder<ParcelFileDescriptor, Bitmap> fileDescriptorDecoder;

    public ImageVideoBitmapDecoder(ResourceDecoder<InputStream, Bitmap> streamDecoder,
            ResourceDecoder<ParcelFileDescriptor, Bitmap> fileDescriptorDecoder) {
        this.streamDecoder = streamDecoder;
        this.fileDescriptorDecoder = fileDescriptorDecoder;
    }

    @Override
    public Resource<Bitmap> decode(ImageVideoWrapper source, int width, int height) throws IOException {
        Resource<Bitmap> result = null;
        InputStream is = source.getStream();
        if (is != null) {
            try {
                result = streamDecoder.decode(is, width, height);
            } catch (IOException e) {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Failed to load image from stream, trying FileDescriptor", e);
                }
            }
        }
        if (result == null) {
            ParcelFileDescriptor fileDescriptor = source.getFileDescriptor();
            if (fileDescriptor != null) {
                result = fileDescriptorDecoder.decode(fileDescriptor, width, height);
            }
        }
        return result;
    }

    ...
}

代码并不复杂,在第14行先调用了source.getStream()来获取到服务器返回的InputStream,然后在第17行调用streamDecoder.decode()方法进行解码。streamDecode是一个StreamBitmapDecoder对象,那么我们再来看这个类的源码,如下所示:

public class StreamBitmapDecoder implements ResourceDecoder<InputStream, Bitmap> {

    ...

    private final Downsampler downsampler;
    private BitmapPool bitmapPool;
    private DecodeFormat decodeFormat;

    public StreamBitmapDecoder(Downsampler downsampler, BitmapPool bitmapPool, DecodeFormat decodeFormat) {
        this.downsampler = downsampler;
        this.bitmapPool = bitmapPool;
        this.decodeFormat = decodeFormat;
    }

    @Override
    public Resource<Bitmap> decode(InputStream source, int width, int height) {
        Bitmap bitmap = downsampler.decode(source, bitmapPool, width, height, decodeFormat);
        return BitmapResource.obtain(bitmap, bitmapPool);
    }

    ...
}

可以看到,它的decode()方法又去调用了Downsampler的decode()方法。接下来又到了激动人心的时刻了,Downsampler的代码如下所示:

public abstract class Downsampler implements BitmapDecoder<InputStream> {

    ...

    @Override
    public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
        final ByteArrayPool byteArrayPool = ByteArrayPool.get();
        final byte[] bytesForOptions = byteArrayPool.getBytes();
        final byte[] bytesForStream = byteArrayPool.getBytes();
        final BitmapFactory.Options options = getDefaultOptions();
        // Use to fix the mark limit to avoid allocating buffers that fit entire images.
        RecyclableBufferedInputStream bufferedStream = new RecyclableBufferedInputStream(
                is, bytesForStream);
        // Use to retrieve exceptions thrown while reading.
        // TODO(#126): when the framework no longer returns partially decoded Bitmaps or provides a way to determine
        // if a Bitmap is partially decoded, consider removing.
        ExceptionCatchingInputStream exceptionStream =
                ExceptionCatchingInputStream.obtain(bufferedStream);
        // Use to read data.
        // Ensures that we can always reset after reading an image header so that we can still attempt to decode the
        // full image even when the header decode fails and/or overflows our read buffer. See #283.
        MarkEnforcingInputStream invalidatingStream = new MarkEnforcingInputStream(exceptionStream);
        try {
            exceptionStream.mark(MARK_POSITION);
            int orientation = 0;
            try {
                orientation = new ImageHeaderParser(exceptionStream).getOrientation();
            } catch (IOException e) {
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "Cannot determine the image orientation from header", e);
                }
            } finally {
                try {
                    exceptionStream.reset();
                } catch (IOException e) {
                    if (Log.isLoggable(TAG, Log.WARN)) {
                        Log.w(TAG, "Cannot reset the input stream", e);
                    }
                }
            }
            options.inTempStorage = bytesForOptions;
            final int[] inDimens = getDimensions(invalidatingStream, bufferedStream, options);
            final int inWidth = inDimens[0];
            final int inHeight = inDimens[1];
            final int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
            final int sampleSize = getRoundedSampleSize(degreesToRotate, inWidth, inHeight, outWidth, outHeight);
            final Bitmap downsampled =
                    downsampleWithSize(invalidatingStream, bufferedStream, options, pool, inWidth, inHeight, sampleSize,
                            decodeFormat);
            // BitmapFactory swallows exceptions during decodes and in some cases when inBitmap is non null, may catch
            // and log a stack trace but still return a non null bitmap. To avoid displaying partially decoded bitmaps,
            // we catch exceptions reading from the stream in our ExceptionCatchingInputStream and throw them here.
            final Exception streamException = exceptionStream.getException();
            if (streamException != null) {
                throw new RuntimeException(streamException);
            }
            Bitmap rotated = null;
            if (downsampled != null) {
                rotated = TransformationUtils.rotateImageExif(downsampled, pool, orientation);
                if (!downsampled.equals(rotated) && !pool.put(downsampled)) {
                    downsampled.recycle();
                }
            }
            return rotated;
        } finally {
            byteArrayPool.releaseBytes(bytesForOptions);
            byteArrayPool.releaseBytes(bytesForStream);
            exceptionStream.release();
            releaseOptions(options);
        }
    }

    private Bitmap downsampleWithSize(MarkEnforcingInputStream is, RecyclableBufferedInputStream  bufferedStream,
            BitmapFactory.Options options, BitmapPool pool, int inWidth, int inHeight, int sampleSize,
            DecodeFormat decodeFormat) {
        // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
        Bitmap.Config config = getConfig(is, decodeFormat);
        options.inSampleSize = sampleSize;
        options.inPreferredConfig = config;
        if ((options.inSampleSize == 1 || Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) && shouldUsePool(is)) {
            int targetWidth = (int) Math.ceil(inWidth / (double) sampleSize);
            int targetHeight = (int) Math.ceil(inHeight / (double) sampleSize);
            // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
            setInBitmap(options, pool.getDirty(targetWidth, targetHeight, config));
        }
        return decodeStream(is, bufferedStream, options);
    }

    /**
     * A method for getting the dimensions of an image from the given InputStream.
     *
     * @param is The InputStream representing the image.
     * @param options The options to pass to
     *          {@link BitmapFactory#decodeStream(InputStream, android.graphics.Rect,
     *              BitmapFactory.Options)}.
     * @return an array containing the dimensions of the image in the form {width, height}.
     */
    public int[] getDimensions(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream,
            BitmapFactory.Options options) {
        options.inJustDecodeBounds = true;
        decodeStream(is, bufferedStream, options);
        options.inJustDecodeBounds = false;
        return new int[] { options.outWidth, options.outHeight };
    }

    private static Bitmap decodeStream(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream,
            BitmapFactory.Options options) {
         if (options.inJustDecodeBounds) {
             // This is large, but jpeg headers are not size bounded so we need something large enough to minimize
             // the possibility of not being able to fit enough of the header in the buffer to get the image size so
             // that we don't fail to load images. The BufferedInputStream will create a new buffer of 2x the
             // original size each time we use up the buffer space without passing the mark so this is a maximum
             // bound on the buffer size, not a default. Most of the time we won't go past our pre-allocated 16kb.
             is.mark(MARK_POSITION);
         } else {
             // Once we've read the image header, we no longer need to allow the buffer to expand in size. To avoid
             // unnecessary allocations reading image data, we fix the mark limit so that it is no larger than our
             // current buffer size here. See issue #225.
             bufferedStream.fixMarkLimit();
         }
        final Bitmap result = BitmapFactory.decodeStream(is, null, options);
        try {
            if (options.inJustDecodeBounds) {
                is.reset();
            }
        } catch (IOException e) {
            if (Log.isLoggable(TAG, Log.ERROR)) {
                Log.e(TAG, "Exception loading inDecodeBounds=" + options.inJustDecodeBounds
                        + " sample=" + options.inSampleSize, e);
            }
        }

        return result;
    }

    ...
}

可以看到,对服务器返回的InputStream的读取,以及对图片的加载全都在这里了。当然这里其实处理了很多的逻辑,包括对图片的压缩,甚至还有旋转、圆角等逻辑处理,但是我们目前只需要关注主线逻辑就行了。decode()方法执行之后,会返回一个Bitmap对象,那么图片在这里其实也就已经被加载出来了,剩下的工作就是如果让这个Bitmap显示到界面上,我们继续往下分析。

回到刚才的StreamBitmapDecoder当中,你会发现,它的decode()方法返回的是一个Resource<Bitmap>对象。而我们从Downsampler中得到的是一个Bitmap对象,因此这里在第18行又调用了BitmapResource.obtain()方法,将Bitmap对象包装成了Resource<Bitmap>对象。代码如下所示:

public class BitmapResource implements Resource<Bitmap> {
    private final Bitmap bitmap;
    private final BitmapPool bitmapPool;

    /**
     * Returns a new {@link BitmapResource} wrapping the given {@link Bitmap} if the Bitmap is non-null or null if the
     * given Bitmap is null.
     *
     * @param bitmap A Bitmap.
     * @param bitmapPool A non-null {@link BitmapPool}.
     */
    public static BitmapResource obtain(Bitmap bitmap, BitmapPool bitmapPool) {
        if (bitmap == null) {
            return null;
        } else {
            return new BitmapResource(bitmap, bitmapPool);
        }
    }

    public BitmapResource(Bitmap bitmap, BitmapPool bitmapPool) {
        if (bitmap == null) {
            throw new NullPointerException("Bitmap must not be null");
        }
        if (bitmapPool == null) {
            throw new NullPointerException("BitmapPool must not be null");
        }
        this.bitmap = bitmap;
        this.bitmapPool = bitmapPool;
    }

    @Override
    public Bitmap get() {
        return bitmap;
    }

    @Override
    public int getSize() {
        return Util.getBitmapByteSize(bitmap);
    }

    @Override
    public void recycle() {
        if (!bitmapPool.put(bitmap)) {
            bitmap.recycle();
        }
    }
}

BitmapResource的源码也非常简单,经过这样一层包装之后,如果我还需要获取Bitmap,只需要调用Resource<Bitmap>的get()方法就可以了。

然后我们需要一层层继续向上返回,StreamBitmapDecoder会将值返回到ImageVideoBitmapDecoder当中,而ImageVideoBitmapDecoder又会将值返回到GifBitmapWrapperResourceDecoder的decodeBitmapWrapper()方法当中。由于代码隔得有点太远了,我重新把decodeBitmapWrapper()方法的代码贴一下:

private GifBitmapWrapper decodeBitmapWrapper(ImageVideoWrapper toDecode, int width, int height) throws IOException {
    GifBitmapWrapper result = null;
    Resource<Bitmap> bitmapResource = bitmapDecoder.decode(toDecode, width, height);
    if (bitmapResource != null) {
        result = new GifBitmapWrapper(bitmapResource, null);
    }
    return result;
}

可以看到,decodeBitmapWrapper()方法返回的是一个GifBitmapWrapper对象。因此,这里在第5行,又将Resource<Bitmap>封装到了一个GifBitmapWrapper对象当中。这个GifBitmapWrapper顾名思义,就是既能封装GIF,又能封装Bitmap,从而保证了不管是什么类型的图片Glide都能从容应对。我们顺便来看下GifBitmapWrapper的源码吧,如下所示:

public class GifBitmapWrapper {
    private final Resource<GifDrawable> gifResource;
    private final Resource<Bitmap> bitmapResource;

    public GifBitmapWrapper(Resource<Bitmap> bitmapResource, Resource<GifDrawable> gifResource) {
        if (bitmapResource != null && gifResource != null) {
            throw new IllegalArgumentException("Can only contain either a bitmap resource or a gif resource, not both");
        }
        if (bitmapResource == null && gifResource == null) {
            throw new IllegalArgumentException("Must contain either a bitmap resource or a gif resource");
        }
        this.bitmapResource = bitmapResource;
        this.gifResource = gifResource;
    }

    /**
     * Returns the size of the wrapped resource.
     */
    public int getSize() {
        if (bitmapResource != null) {
            return bitmapResource.getSize();
        } else {
            return gifResource.getSize();
        }
    }

    /**
     * Returns the wrapped {@link Bitmap} resource if it exists, or null.
     */
    public Resource<Bitmap> getBitmapResource() {
        return bitmapResource;
    }

    /**
     * Returns the wrapped {@link GifDrawable} resource if it exists, or null.
     */
    public Resource<GifDrawable> getGifResource() {
        return gifResource;
    }
}

还是比较简单的,就是分别对gifResource和bitmapResource做了一层封装而已,相信没有什么解释的必要。

然后这个GifBitmapWrapper对象会一直向上返回,返回到GifBitmapWrapperResourceDecoder最外层的decode()方法的时候,会对它再做一次封装,如下所示:

@Override
public Resource<GifBitmapWrapper> decode(ImageVideoWrapper source, int width, int height) throws IOException {
    ByteArrayPool pool = ByteArrayPool.get();
    byte[] tempBytes = pool.getBytes();
    GifBitmapWrapper wrapper = null;
    try {
        wrapper = decode(source, width, height, tempBytes);
    } finally {
        pool.releaseBytes(tempBytes);
    }
    return wrapper != null ? new GifBitmapWrapperResource(wrapper) : null;
}

可以看到,这里在第11行,又将GifBitmapWrapper封装到了一个GifBitmapWrapperResource对象当中,最终返回的是一个Resource<GifBitmapWrapper>对象。这个GifBitmapWrapperResource和刚才的BitmapResource是相似的,它们都实现的Resource接口,都可以通过get()方法来获取封装起来的具体内容。GifBitmapWrapperResource的源码如下所示:

public class GifBitmapWrapperResource implements Resource<GifBitmapWrapper> {
    private final GifBitmapWrapper data;

    public GifBitmapWrapperResource(GifBitmapWrapper data) {
        if (data == null) {
            throw new NullPointerException("Data must not be null");
        }
        this.data = data;
    }

    @Override
    public GifBitmapWrapper get() {
        return data;
    }

    @Override
    public int getSize() {
        return data.getSize();
    }

    @Override
    public void recycle() {
        Resource<Bitmap> bitmapResource = data.getBitmapResource();
        if (bitmapResource != null) {
            bitmapResource.recycle();
        }
        Resource<GifDrawable> gifDataResource = data.getGifResource();
        if (gifDataResource != null) {
            gifDataResource.recycle();
        }
    }
}

经过这一层的封装之后,我们从网络上得到的图片就能够以Resource接口的形式返回,并且还能同时处理Bitmap图片和GIF图片这两种情况。

那么现在我们可以回到DecodeJob当中了,它的decodeFromSourceData()方法返回的是一个Resource<T>对象,其实也就是Resource<GifBitmapWrapper>对象了。然后继续向上返回,最终返回到decodeFromSource()方法当中,如下所示:

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}

刚才我们就是从这里跟进到decodeSource()方法当中,然后执行了一大堆一大堆的逻辑,最终得到了这个Resource<T>对象。然而你会发现,decodeFromSource()方法最终返回的却是一个Resource<Z>对象,那么这到底是怎么回事呢?我们就需要跟进到transformEncodeAndTranscode()方法来瞧一瞧了,代码如下所示:

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = transform(decoded);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Transformed resource from source", startTime);
    }
    writeTransformedToCache(transformed);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Transcoded transformed from source", startTime);
    }
    return result;
}

private Resource<Z> transcode(Resource<T> transformed) {
    if (transformed == null) {
        return null;
    }
    return transcoder.transcode(transformed);
}

首先,这个方法开头的几行transform还有cache,这都是我们后面才会学习的东西,现在不用管它们就可以了。需要注意的是第9行,这里调用了一个transcode()方法,就把Resource<T>对象转换成Resource<Z>对象了。

而transcode()方法中又是调用了transcoder的transcode()方法,那么这个transcoder是什么呢?其实这也是Glide源码特别难懂的原因之一,就是它用到的很多对象都是很早很早之前就初始化的,在初始化的时候你可能完全就没有留意过它,因为一时半会根本就用不着,但是真正需要用到的时候你却早就记不起来这个对象是从哪儿来的了。

那么这里我来提醒一下大家吧,在第二步load()方法返回的那个DrawableTypeRequest对象,它的构建函数中去构建了一个FixedLoadProvider对象,然后我们将三个参数传入到了FixedLoadProvider当中,其中就有一个GifBitmapWrapperDrawableTranscoder对象。后来在onSizeReady()方法中获取到了这个参数,并传递到了Engine当中,然后又由Engine传递到了DecodeJob当中。因此,这里的transcoder其实就是这个GifBitmapWrapperDrawableTranscoder对象。那么我们来看一下它的源码:

public class GifBitmapWrapperDrawableTranscoder implements ResourceTranscoder<GifBitmapWrapper, GlideDrawable> {
    private final ResourceTranscoder<Bitmap, GlideBitmapDrawable> bitmapDrawableResourceTranscoder;

    public GifBitmapWrapperDrawableTranscoder(
            ResourceTranscoder<Bitmap, GlideBitmapDrawable> bitmapDrawableResourceTranscoder) {
        this.bitmapDrawableResourceTranscoder = bitmapDrawableResourceTranscoder;
    }

    @Override
    public Resource<GlideDrawable> transcode(Resource<GifBitmapWrapper> toTranscode) {
        GifBitmapWrapper gifBitmap = toTranscode.get();
        Resource<Bitmap> bitmapResource = gifBitmap.getBitmapResource();
        final Resource<? extends GlideDrawable> result;
        if (bitmapResource != null) {
            result = bitmapDrawableResourceTranscoder.transcode(bitmapResource);
        } else {
            result = gifBitmap.getGifResource();
        }
        return (Resource<GlideDrawable>) result;
    }

    ...
}

这里我来简单解释一下,GifBitmapWrapperDrawableTranscoder的核心作用就是用来转码的。因为GifBitmapWrapper是无法直接显示到ImageView上面的,只有Bitmap或者Drawable才能显示到ImageView上。因此,这里的transcode()方法先从Resource<GifBitmapWrapper>中取出GifBitmapWrapper对象,然后再从GifBitmapWrapper中取出Resource<Bitmap>对象。

接下来做了一个判断,如果Resource<Bitmap>为空,那么说明此时加载的是GIF图,直接调用getGifResource()方法将图片取出即可,因为Glide用于加载GIF图片是使用的GifDrawable这个类,它本身就是一个Drawable对象了。而如果Resource<Bitmap>不为空,那么就需要再做一次转码,将Bitmap转换成Drawable对象才行,因为要保证静图和动图的类型一致性,不然逻辑上是不好处理的。

这里在第15行又进行了一次转码,是调用的GlideBitmapDrawableTranscoder对象的transcode()方法,代码如下所示:

public class GlideBitmapDrawableTranscoder implements ResourceTranscoder<Bitmap, GlideBitmapDrawable> {
    private final Resources resources;
    private final BitmapPool bitmapPool;

    public GlideBitmapDrawableTranscoder(Context context) {
        this(context.getResources(), Glide.get(context).getBitmapPool());
    }

    public GlideBitmapDrawableTranscoder(Resources resources, BitmapPool bitmapPool) {
        this.resources = resources;
        this.bitmapPool = bitmapPool;
    }

    @Override
    public Resource<GlideBitmapDrawable> transcode(Resource<Bitmap> toTranscode) {
        GlideBitmapDrawable drawable = new GlideBitmapDrawable(resources, toTranscode.get());
        return new GlideBitmapDrawableResource(drawable, bitmapPool);
    }

    ...
}

可以看到,这里new出了一个GlideBitmapDrawable对象,并把Bitmap封装到里面。然后对GlideBitmapDrawable再进行一次封装,返回一个Resource<GlideBitmapDrawable>对象。

现在再返回到GifBitmapWrapperDrawableTranscoder的transcode()方法中,你会发现它们的类型就一致了。因为不管是静图的Resource<GlideBitmapDrawable>对象,还是动图的Resource<GifDrawable>对象,它们都是属于父类Resource<GlideDrawable>对象的。因此transcode()方法也是直接返回了Resource<GlideDrawable>,而这个Resource<GlideDrawable>其实也就是转换过后的Resource<Z>了。

那么我们继续回到DecodeJob当中,它的decodeFromSource()方法得到了Resource<Z>对象,当然也就是Resource<GlideDrawable>对象。然后继续向上返回会回到EngineRunnable的decodeFromSource()方法,再回到decode()方法,再回到run()方法当中。那么我们重新再贴一下EngineRunnable run()方法的源码:

@Override
public void run() {
    if (isCancelled) {
        return;
    }
    Exception exception = null;
    Resource<?> resource = null;
    try {
        resource = decode();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Exception decoding", e);
        }
        exception = e;
    }
    if (isCancelled) {
        if (resource != null) {
            resource.recycle();
        }
        return;
    }
    if (resource == null) {
        onLoadFailed(exception);
    } else {
        onLoadComplete(resource);
    }
}

也就是说,经过第9行decode()方法的执行,我们最终得到了这个Resource<GlideDrawable>对象,那么接下来就是如何将它显示出来了。可以看到,这里在第25行调用了onLoadComplete()方法,表示图片加载已经完成了,代码如下所示:

private void onLoadComplete(Resource resource) {
    manager.onResourceReady(resource);
}

这个manager就是EngineJob对象,因此这里实际上调用的是EngineJob的onResourceReady()方法,代码如下所示:

class EngineJob implements EngineRunnable.EngineRunnableManager {

    private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper(), new MainThreadCallback());

    private final List<ResourceCallback> cbs = new ArrayList<ResourceCallback>();

    ...

    public void addCallback(ResourceCallback cb) {
        Util.assertMainThread();
        if (hasResource) {
            cb.onResourceReady(engineResource);
        } else if (hasException) {
            cb.onException(exception);
        } else {
            cbs.add(cb);
        }
    }

    @Override
    public void onResourceReady(final Resource<?> resource) {
        this.resource = resource;
        MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
    }

    private void handleResultOnMainThread() {
        if (isCancelled) {
            resource.recycle();
            return;
        } else if (cbs.isEmpty()) {
            throw new IllegalStateException("Received a resource without any callbacks to notify");
        }
        engineResource = engineResourceFactory.build(resource, isCacheable);
        hasResource = true;
        engineResource.acquire();
        listener.onEngineJobComplete(key, engineResource);
        for (ResourceCallback cb : cbs) {
            if (!isInIgnoredCallbacks(cb)) {
                engineResource.acquire();
                cb.onResourceReady(engineResource);
            }
        }
        engineResource.release();
    }

    @Override
    public void onException(final Exception e) {
        this.exception = e;
        MAIN_THREAD_HANDLER.obtainMessage(MSG_EXCEPTION, this).sendToTarget();
    }

    private void handleExceptionOnMainThread() {
        if (isCancelled) {
            return;
        } else if (cbs.isEmpty()) {
            throw new IllegalStateException("Received an exception without any callbacks to notify");
        }
        hasException = true;
        listener.onEngineJobComplete(key, null);
        for (ResourceCallback cb : cbs) {
            if (!isInIgnoredCallbacks(cb)) {
                cb.onException(exception);
            }
        }
    }

    private static class MainThreadCallback implements Handler.Callback {

        @Override
        public boolean handleMessage(Message message) {
            if (MSG_COMPLETE == message.what || MSG_EXCEPTION == message.what) {
                EngineJob job = (EngineJob) message.obj;
                if (MSG_COMPLETE == message.what) {
                    job.handleResultOnMainThread();
                } else {
                    job.handleExceptionOnMainThread();
                }
                return true;
            }
            return false;
        }
    }

    ...
}

可以看到,这里在onResourceReady()方法使用Handler发出了一条MSG_COMPLETE消息,那么在MainThreadCallback的handleMessage()方法中就会收到这条消息。从这里开始,所有的逻辑又回到主线程当中进行了,因为很快就需要更新UI了。

然后在第72行调用了handleResultOnMainThread()方法,这个方法中又通过一个循环,调用了所有ResourceCallback的onResourceReady()方法。那么这个ResourceCallback是什么呢?答案在addCallback()方法当中,它会向cbs集合中去添加ResourceCallback。那么这个addCallback()方法又是哪里调用的呢?其实调用的地方我们早就已经看过了,只不过之前没有注意,现在重新来看一下Engine的load()方法,如下所示:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    ...    

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder, Priority priority, 
            boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {

        ...

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

    ...
}

这次把目光放在第18行上面,看到了吗?就是在这里调用的EngineJob的addCallback()方法来注册的一个ResourceCallback。那么接下来的问题就是,Engine.load()方法的ResourceCallback参数又是谁传过来的呢?这就需要回到GenericRequest的onSizeReady()方法当中了,我们看到ResourceCallback是load()方法的最后一个参数,那么在onSizeReady()方法中调用load()方法时传入的最后一个参数是什么?代码如下所示:

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {

    ...

    @Override
    public void onSizeReady(int width, int height) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
        if (status != Status.WAITING_FOR_SIZE) {
            return;
        }
        status = Status.RUNNING;
        width = Math.round(sizeMultiplier * width);
        height = Math.round(sizeMultiplier * height);
        ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
        final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
        if (dataFetcher == null) {
            onException(new Exception("Failed to load model: \'" + model + "\'"));
            return;
        }
        ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
        }
        loadedFromMemoryCache = true;
        loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, 
                transcoder, priority, isMemoryCacheable, diskCacheStrategy, this);
        loadedFromMemoryCache = resource != null;
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
    }

    ...
}

请将目光锁定在第29行的最后一个参数,this。没错,就是this。GenericRequest本身就实现了ResourceCallback的接口,因此EngineJob的回调最终其实就是回调到了GenericRequest的onResourceReady()方法当中了,代码如下所示:

public void onResourceReady(Resource<?> resource) {
    if (resource == null) {
        onException(new Exception("Expected to receive a Resource<R> with an object of " + transcodeClass
                + " inside, but instead got null."));
        return;
    }
    Object received = resource.get();
    if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) {
        releaseResource(resource);
        onException(new Exception("Expected to receive an object of " + transcodeClass
                + " but instead got " + (received != null ? received.getClass() : "") + "{" + received + "}"
                + " inside Resource{" + resource + "}."
                + (received != null ? "" : " "
                    + "To indicate failure return a null Resource object, "
                    + "rather than a Resource object containing null data.")
        ));
        return;
    }
    if (!canSetResource()) {
        releaseResource(resource);
        // We can't set the status to complete before asking canSetResource().
        status = Status.COMPLETE;
        return;
    }
    onResourceReady(resource, (R) received);
}

private void onResourceReady(Resource<?> resource, R result) {
    // We must call isFirstReadyResource before setting status.
    boolean isFirstResource = isFirstReadyResource();
    status = Status.COMPLETE;
    this.resource = resource;
    if (requestListener == null || !requestListener.onResourceReady(result, model, target, loadedFromMemoryCache,
            isFirstResource)) {
        GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
        target.onResourceReady(result, animation);
    }
    notifyLoadSuccess();
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("Resource ready in " + LogTime.getElapsedMillis(startTime) + " size: "
                + (resource.getSize() * TO_MEGABYTE) + " fromCache: " + loadedFromMemoryCache);
    }
}

这里有两个onResourceReady()方法,首先在第一个onResourceReady()方法当中,调用resource.get()方法获取到了封装的图片对象,也就是GlideBitmapDrawable对象,或者是GifDrawable对象。然后将这个值传入到了第二个onResourceReady()方法当中,并在第36行调用了target.onResourceReady()方法。

那么这个target又是什么呢?这个又需要向上翻很久了,在第三步into()方法的一开始,我们就分析了在into()方法的最后一行,调用了glide.buildImageViewTarget()方法来构建出一个Target,而这个Target就是一个GlideDrawableImageViewTarget对象。

那么我们去看GlideDrawableImageViewTarget的源码就可以了,如下所示:

public class GlideDrawableImageViewTarget extends ImageViewTarget<GlideDrawable> {
    private static final float SQUARE_RATIO_MARGIN = 0.05f;
    private int maxLoopCount;
    private GlideDrawable resource;

    public GlideDrawableImageViewTarget(ImageView view) {
        this(view, GlideDrawable.LOOP_FOREVER);
    }

    public GlideDrawableImageViewTarget(ImageView view, int maxLoopCount) {
        super(view);
        this.maxLoopCount = maxLoopCount;
    }

    @Override
    public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
        if (!resource.isAnimated()) {
            float viewRatio = view.getWidth() / (float) view.getHeight();
            float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight();
            if (Math.abs(viewRatio - 1f) <= SQUARE_RATIO_MARGIN
                    && Math.abs(drawableRatio - 1f) <= SQUARE_RATIO_MARGIN) {
                resource = new SquaringDrawable(resource, view.getWidth());
            }
        }
        super.onResourceReady(resource, animation);
        this.resource = resource;
        resource.setLoopCount(maxLoopCount);
        resource.start();
    }

    @Override
    protected void setResource(GlideDrawable resource) {
        view.setImageDrawable(resource);
    }

    @Override
    public void onStart() {
        if (resource != null) {
            resource.start();
        }
    }

    @Override
    public void onStop() {
        if (resource != null) {
            resource.stop();
        }
    }
}

在GlideDrawableImageViewTarget的onResourceReady()方法中做了一些逻辑处理,包括如果是GIF图片的话,就调用resource.start()方法开始播放图片,但是好像并没有看到哪里有将GlideDrawable显示到ImageView上的逻辑。

确实没有,不过父类里面有,这里在第25行调用了super.onResourceReady()方法,GlideDrawableImageViewTarget的父类是ImageViewTarget,我们来看下它的代码吧:

public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z> implements GlideAnimation.ViewAdapter {

    ...

    @Override
    public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
        if (glideAnimation == null || !glideAnimation.animate(resource, this)) {
            setResource(resource);
        }
    }

    protected abstract void setResource(Z resource);

}

可以看到,在ImageViewTarget的onResourceReady()方法当中调用了setResource()方法,而ImageViewTarget的setResource()方法是一个抽象方法,具体的实现还是在子类那边实现的。

那子类的setResource()方法是怎么实现的呢?回头再来看一下GlideDrawableImageViewTarget的setResource()方法,没错,调用的view.setImageDrawable()方法,而这个view就是ImageView。代码执行到这里,图片终于也就显示出来了。

那么,我们对Glide执行流程的源码分析,到这里也终于结束了。

总结

真是好长的一篇文章,这也可能是我目前所写过的最长的一篇文章了。如果你之前没有读过Glide的源码,真的很难相信,这短短一行代码:

Glide.with(this).load(url).into(imageView);

背后竟然蕴藏着如此极其复杂的逻辑吧?

不过Glide也并不是有意要将代码写得如此复杂,实在是因为Glide的功能太强大了,而上述代码只是使用了Glide最最基本的功能而已。

现在通过两篇文章,我们已经掌握了Glide的基本用法,并且通过阅读源码了解了Glide总的执行流程。接下来的几篇文章,我会带大家深入到Glide源码的某一处细节,学习Glide更多的高级使用技巧,敬请期待。

关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

微信扫一扫下方二维码即可关注:

        

作者:sinyu890807 发表于2017/4/18 7:51:05 原文链接
阅读:3070 评论:29 查看评论

ReactNative调用Android原生模块

$
0
0

有时候App需要访问平台API,但React Native可能还没有相应的模块包装;或者你需要复用一些Java代码,而不是用Javascript重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
我们把React Native设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。要想实现访问Android原生API,总结一下,主要有以下几个步骤:

1. 创建一个原生模块

这个原生模块是一个继承ReactContextBaseJavaModule的Java类,它可以实现一些JavaScript所调用的原生功能。我们的目标是可以在JavaScript里写ToastAndroid.show(‘Awesome’, ToastAndroid.SHORT);,来调起一个Toast通知。例如:

public class RnTest extends ReactContextBaseJavaModule {
  public RnTest(ReactApplicationContext reactContext) {
    super(reactContext);
  }
  // ReactContextBaseJavaModule要求派生类实现getName方法。这个函数用于返回一个字符串
  // 这个字符串用于在JavaScript端标记这个原生模块
  @Override
  public String getName() {
    return "ToastByAndroid";
  }
  // 获取应用包名
  // 要导出一个方法给JavaScript使用,Java方法需要使用注解@ReactMethod
   @ReactMethod
   public void getPackageName() {
     String name = getReactApplicationContext().getPackageName();
     Toast.makeText(getReactApplicationContext(),name,Toast.LENGTH_LONG).show();
    }
}

ReactContextBaseJavaModule要求派生类实现getName方法。这个函数用于返回一个字符串名字,这个名字在JavaScript端标记这个模块。这里我们把这个模块叫做ToastByAndroid,这样就可以在JavaScript中通过React.NativeModules.ToastByAndroid访问到这个模块。
注意:模块名前的RCT前缀会被自动移除。所以如果返回的字符串为”RCTToastAndroid”,在JavaScript端依然通过React.NativeModules.ToastByAndroid访问到这个模块。

2. 注册模块

要使JavaScript端调用到原生模块还需注册这个原生模块,需要实现一个类实现ReactPackage接口,并实现其中的抽象方法。例如:

public class ExampleReactPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
      List<NativeModule> modules = new ArrayList<>();
      modules.add(new RnTest(reactContext));
      return modules;
    }

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
      return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
      return Collections.emptyList();
    }
}

MainApplication声明

除了上面的步骤外,还需在MainApplication.java文件中的getPackages方法中,实例化上面的注册类。例如:

 @Override
  protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
      new MainReactPackage(),
      // 实例化注册类
      new ExampleReactPackage());
    }
  };

3. JS调用android原生方法

3.1 引入NativeModules模块

import { NativeModules } from 'react-native';

3.2 调用Android原生方法

var rnToastAndroid = NativeModules.ToastByAndroid;
rnToastAndroid.getPackageName();

4. 获取android返回值

提供给js调用的原生android方法的返回类型必须是void,React Native的跨语言访问是异步进行的,所以想要给JavaScript返回一个值的唯一办法是使用回调函数或者发送事件。

4.1 回调函数

Callback是React.bridge中的一个接口,它作为ReactMethod的一个传参,用来映射JavaScript的回调函数(function)。Callback接口只定义了一个方法invoke,invoke接受多个参数,这个参数必须是react.bridge中支持的参数。
Android端代码:

 @ReactMethod
  public void tryCallBack(String name,String psw,Callback errorCallback,Callback successCallback){
    try{
      if(TextUtils.isEmpty(name)&&TextUtils.isEmpty(psw)){
        // 失败时回调
        errorCallback.invoke("user or psw  is empty");
      }
      // 成功时回调
      successCallback.invoke("add user success");
     }catch(IllegalViewOperationException e){
        // 失败时回调
        errorCallback.invoke(e.getMessage());
      }
    }

rn端代码:

var rnToastAndroid = NativeModules.ToastByAndroid;
rnToastAndroid.tryCallBack("luo","131",(errorCallback)=>{alert(errorCallback)},(successCallback)=>{alert(successCallback);});

5.调用测试

android主动向rn发送消息。

5.1 android端代码

public  static void sendEvent(ReactContext reactContext, String eventName, int status)
    {
        System.out.println("reactContext="+reactContext);

        reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName,status);
    }

5.2 RN端代码

// eventName为5.1中的eventName,reminder为5.1中的status
DeviceEventEmitter.addListener(eventName, (reminder) => {
      console.log(reminder):
    });

RN调用Android原生模块的代码如下:

const RNBridgeModule = NativeModules.RNBridgeModule;
nativeLanuchApp(message) {
    RNBridgeModule.nativePlayVideo(message);
  }

  <TouchableOpacity onPress={() => {
                            this.nativeLanuchApp("111");
                        }} >
      <Text >
        try
      </Text>
    </TouchableOpacity>
作者:xiangzhihong8 发表于2017/4/18 12:31:31 原文链接
阅读:299 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>