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

从头开始学 RecyclerView(三) 封装简化

$
0
0

前言


上一篇的代码,也是基于这些封装的。
RV的封装,跟以前的listView之类的封装,大同小异。
这里,从@devwiki 处,将代码搬过来,基本无修改

BaseHolder的优化


  1. 使ViewHolder只用来缓存View。
  2. 添加SparseArray,使之来缓存View。
  3. 添加BaseHolder(View view)构造器,外部更方便控制View。
  4. 保留getContext()方法,方便获取Context对象。
  5. getView(resid),简化itemView.findviewById()
/**
 * from http://blog.devwiki.net/index.php/2016/07/17/Recycler-View-Adapter-ViewHolder-optimized.html
 * 基础的ViewHolder</br>
 * ViewHolder只作View的缓存,不关心数据内容
 * Created by DevWiki on 2016/5/17.
 */
public class BaseHolder extends RecyclerView.ViewHolder {

    private SparseArray<View> mViewArray;

    /**
     * 构造ViewHolder
     * @param parent 父类容器
     * @param resId 布局资源文件id
     */
    public BaseHolder(ViewGroup parent, @LayoutRes int resId) {
        super(LayoutInflater.from(parent.getContext()).inflate(resId, parent, false));
        mViewArray = new SparseArray<>();
    }

    /**
     * 构建ViewHolder
     * @param view 布局View
     */
    public BaseHolder(View view) {
        super(view);
        mViewArray = new SparseArray<>();
    }

    /**
     * 获取布局中的View
     * @param viewId view的Id
     * @param <T> View的类型
     * @return view
     */
    public <T extends View> T getView(@IdRes int viewId){
        View view = mViewArray.get(viewId);
        if (view == null) {
            view = itemView.findViewById(viewId);
            mViewArray.put(viewId, view);
        }
        return (T) view;
    }

    /**
     * 获取Context实例
     * @return context
     */
    public Context getContext() {
        return itemView.getContext();
    }
}

Adapter部分的优化


Adapter拆分为两个抽象类:AbsAdapter与BaseAdapter,其中:
AbsAdapter:封装了和ViewHolder和HeaderView,FooterView相关的方法。
BaseAdapter:继承AbsAdapter,封装了数据相关的方法。
各自聚焦于不同的方面,方面日后扩展。

AbsAdapter的代码如下:

/**
 * from http://blog.devwiki.net/index.php/2016/07/17/Recycler-View-Adapter-ViewHolder-optimized.html
 * RecyclerView.Adapter的扩展,包含headerView/footerView等
 * Created by DevWiki on 2016/7/13.
 */

public abstract class AbsAdapter<VH extends BaseHolder> extends RecyclerView.Adapter<BaseHolder> {

    private static final String TAG = "AbsAdapter";

    public static final int VIEW_TYPE_HEADER = 1024;
    public static final int VIEW_TYPE_FOOTER = 1025;

    protected View headerView;
    protected View footerView;

    protected Context context;

    public AbsAdapter(Context context) {
        this.context = context;
    }

    @Override
    public final BaseHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == VIEW_TYPE_HEADER) {
            return new BaseHolder(headerView);
        } else if (viewType == VIEW_TYPE_FOOTER) {
            return new BaseHolder(footerView);
        } else {
            return createCustomViewHolder(parent, viewType);
        }
    }

    /**
     * 创建自定义的ViewHolder
     *
     * @param parent 父类容器
     * @param viewType view类型{@link #getItemViewType(int)}
     * @return ViewHolder
     */
    public abstract VH createCustomViewHolder(ViewGroup parent, int viewType);

    @Override
    public final void onBindViewHolder(BaseHolder holder, int position) {
        switch (holder.getItemViewType()) {
            case VIEW_TYPE_HEADER:
            case VIEW_TYPE_FOOTER:
                break;
            default:
                bindCustomViewHolder((VH) holder, position);
                break;
        }
    }

    @Override
    public void onBindViewHolder(BaseHolder holder, int position, List<Object> payloads) {
        super.onBindViewHolder(holder, position, payloads);
    }

    /**
     * 绑定自定义的ViewHolder
     *
     * @param holder ViewHolder
     * @param position 位置
     */
    public abstract void bindCustomViewHolder(VH holder, int position);

    /**
     * 添加HeaderView
     *
     * @param headerView 顶部View对象
     */
    public void addHeaderView(View headerView) {
        if (headerView == null) {
            Log.w(TAG, "add the header view is null");
            return ;
        }
        this.headerView = headerView;
        notifyDataSetChanged();
    }

    /**
     * 移除HeaderView
     */
    public void removeHeaderView() {
        if (headerView != null) {
            headerView = null;
            notifyDataSetChanged();
        }
    }

    /**
     * 添加FooterView
     *
     * @param footerView View对象
     */
    public void addFooterView(View footerView) {
        if (footerView == null) {
            Log.w(TAG, "add the footer view is null");
            return;
        }
        this.footerView = footerView;
        notifyDataSetChanged();
    }

    /**
     * 移除FooterView
     */
    public void removeFooterView() {
        if (footerView != null) {
            footerView = null;
            notifyDataSetChanged();
        }
    }

    /**
     * 获取附加View的数量,包括HeaderView和FooterView
     *
     * @return 数量
     */
    public int getExtraViewCount() {
        int extraViewCount = 0;
        if (headerView != null) {
            extraViewCount++;
        }
        if (footerView != null) {
            extraViewCount++;
        }
        return extraViewCount;
    }

    /**
     * 获取顶部附加View数量,即HeaderView数量
     * @return 数量
     */
    public int getHeaderExtraViewCount() {
        return headerView == null ? 0 : 1;
    }

    /**
     * 获取底部附加View数量,即FooterView数量
     * @return 数量,0或1
     */
    public int getFooterExtraViewCount() {
        return footerView == null ? 0 : 1;
    }

    @Override
    public abstract long getItemId(int position);

}

BaseAdapter的代码如下:

/**
 * from http://blog.devwiki.net/index.php/2016/07/17/Recycler-View-Adapter-ViewHolder-optimized.html
 * 基础的Adapter
 *
 * Created by DevWiki on 2016/7/13.
 */

public abstract class BaseAdapter<M, VH extends BaseHolder> extends AbsAdapter<VH> {

    private List<M> dataList;

    public BaseAdapter(Context context) {
        super(context);
        this.dataList = new ArrayList<>();
    }

    public BaseAdapter(Context context, List<M> list) {
        super(context);
        this.dataList = new ArrayList<>();
        this.dataList.addAll(list);
    }

    /**
     * 填充数据,此操作会清除原来的数据
     *
     * @param list 要填充的数据
     * @return true:填充成功并调用刷新数据
     */
    public boolean fillList(List<M> list) {
        dataList.clear();
        boolean result = dataList.addAll(list);
        if (result) {
            notifyDataSetChanged();
        }
        return result;
    }

    /**
     * 追加一条数据
     *
     * @param data 要追加的数据
     * @return true:追加成功并刷新界面
     */
    public boolean appendItem(M data) {
        boolean result = dataList.add(data);
        if (result) {
            if (getHeaderExtraViewCount() == 0) {
                notifyItemInserted(dataList.size() - 1);
            } else {
                notifyItemInserted(dataList.size());
            }
        }
        return result;
    }

    /**
     * 追加集合数据
     *
     * @param list 要追加的集合数据
     * @return 追加成功并刷新
     */
    public boolean appendList(List<M> list) {
        boolean result = dataList.addAll(list);
        if (result) {
            notifyDataSetChanged();
        }
        return result;
    }

    /**
     * 在最顶部前置数据
     *
     * @param data 要前置的数据
     */
    public void proposeItem(M data) {
        dataList.add(0, data);
        if (getHeaderExtraViewCount() == 0) {
            notifyItemInserted(0);
        } else {
            notifyItemInserted(getHeaderExtraViewCount());
        }
    }

    /**
     * 在顶部前置数据集合
     *
     * @param list 要前置的数据集合
     */
    public void proposeList(List<M> list) {
        dataList.addAll(0, list);
        notifyDataSetChanged();
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public final int getItemViewType(int position) {
        if (headerView != null && position == 0) {
            return VIEW_TYPE_HEADER;
        } else if (footerView != null && position == dataList.size() + getHeaderExtraViewCount()) {
            return VIEW_TYPE_FOOTER;
        } else {
            return getCustomViewType(position);
        }
    }

    /**
     * 获取自定义View的类型
     * 
     * @param position 位置
     * @return View的类型
     */
    public abstract int getCustomViewType(int position);

    @Override
    public int getItemCount() {
        return dataList.size() + getExtraViewCount();
    }

    /**
     * 根据位置获取一条数据
     * 
     * @param position View的位置
     * @return 数据
     */
    public M getItem(int position) {
        if (headerView != null && position == 0
                || position >= dataList.size() + getHeaderExtraViewCount()) {
            return null;
        }
        return headerView == null ? dataList.get(position) : dataList.get(position - 1);
    }

    /**
     * 根据ViewHolder获取数据
     *
     * @param holder ViewHolder
     * @return 数据
     */
    public M getItem(VH holder) {
        return getItem(holder.getAdapterPosition());
    }

    public void updateItem(M data) {
        int index = dataList.indexOf(data);
        if (index < 0) {
            return;
        }
        dataList.set(index, data);
        if (headerView == null) {
            notifyItemChanged(index);
        } else {
            notifyItemChanged(index + 1);
        }
    }

    /**
     * 移除一条数据
     *
     * @param position 位置
     */
    public void removeItem(int position) {
        if (headerView == null) {
            dataList.remove(position);
        } else {
            dataList.remove(position - 1);
        }
        notifyItemRemoved(position);
    }

    /**
     * 移除一条数据
     *
     * @param data 要移除的数据
     */
    public void removeItem(M data) {
        int index = dataList.indexOf(data);
        if (index < 0) {
            return;
        }
        dataList.remove(index);
        if (headerView == null) {
            notifyItemRemoved(index);
        } else {
            notifyItemRemoved(index + 1);
        }
    }
}

参考


http://blog.devwiki.net/index.php/2016/07/17/Recycler-View-Adapter-ViewHolder-optimized.html 《RecyclerView的ViewHolder和Adapter的封装优化》

作者:jjwwmlp456 发表于2017/4/1 15:59:07 原文链接
阅读:143 评论:0 查看评论

WebRTC AppRTC(一)环境配置详细步骤与坑总结

$
0
0

弄webrtc确实不是很好弄,目前仅调通了pc端的网页与手机端网页的视频。不过感觉还有些问题1、两者都必须要使用火狐浏览器2、感觉pc端摄像头拍出来的画面还可以,手机端稍微有点花3、进入房间接通后过一段时间才显示两个视频画面~~~~apprtc的demo还没有调通,问题出在turnserver,后面弄好了再发文章。网上有很多关于apprtc的搭建的步骤,有的详细有的不详细,不管怎样中间还是有很多的坑。
这里写图片描述
(ps:这图片画风越看越诡异)

之前是按照这篇文章配置的,也给大家作为参考。步骤我还会再写一遍。
http://blog.csdn.net/s569646547/article/details/50780682

Prepare

前提说明:在Window下的VM中安装的Ubuntu进行的。Ubuntu不要用16.04!16.04不稳定并且网络是个大坑巨坑!Ubuntu网络连接选择桥连接。桥连接相当于你当前的Ubuntu作为一台独立的主机存在于局域网中,这样其他主机就可以访问它了。
先把主机的VM桥接模式允许的权限打开。
这里写图片描述
这里写图片描述
这里写图片描述
之后按照下面图片或网址中的方式设置下自己的静态ip,可以先ifconfig命令看一下自己的ip(重要的在后面所以这里就不啰嗦了)
这里写图片描述
http://www.cnblogs.com/vincedotnet/p/4013099.html
此外还需要将VM的防火墙关闭 ufw disable和自己电脑的防火墙。
前提准备:
1、安装Vmware-tools,用于windows与vm文件、文本复制。
这里写图片描述
2、安装jdk后面有命令会涉及(先执行步骤(3)、(4),当时好像是先执行的(1)、(2)但是没有安装成功,用(3)、(4)命令行很简单就安装好了)
(1)去官网下载,jdk我下载的jdk-8u121-linux-x64.tar.gz
http://www.oracle.com/technetwork/java/javase/downloads/index.html
(2)找到你的download文件夹
找到你下载的文件,进行解压

tar zxvf file.name

这里写图片描述

解压后使用root权限sudo -i然后找到你的download目录,cd /home/ubuntu/download(我的用户名取的ubuntu,这里路径替换成自己的)然后将解压的jdk文件移动到新的目录下,mkdir jdk1.8.21 /usr/java下面,再修改文件名为mv jdk1.8.21 java-8-sun
接下来进行jdk环境配置,用终端输入sudo vi /etc/profile编辑profile文件,按i,在最后输入
这里写图片描述

#set java environment

JAVA_HOME=/usr/java/java-7-sun
export JRE_HOME=/usr/java/java-7-sun/jre
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH

按esc键然后按冒号键输入wq进行保存。
(3)最后,输入 java -version 命令测试jdk版本,查看jdk版本。
(4)如果没有出现jdk的版本号,可以再试下命令行安装jdk。

sudo apt-cache search openjdk
sudo apt-get update
sudo apt-get install openjdk-8-jdk

如果上面的也不行可以按照这篇文章弄一下。
http://blog.csdn.net/y999666/article/details/51685761
3、lantern需要翻墙,下载后直接双击运行安装即可。
http://download.csdn.net/detail/danfengw/9801021
4、

Action

三个服务器

房间服务器、 信令服务器、穿透服务器

1、AppRTC 房间服务器 https://github.com/webrtc/apprtc

2、Collider 信令服务器 上边源码里自带

3、coTurn 穿透服务器 https://github.com/coturn/coturn

4、需要自己实现coTurn连接信息接口,主要返回用户名、密码和turn配置信息,通常叫做TURN REST API,不实现这个接口的话AppRTCDemo连不上服务器,浏览器访问的话可以正常访问。

AppRTC房间服务器

  1、下载代码

  2、安装依赖    
sudo apt-get install nodejs
sudo npm install -g npm
sudo apt-get install nodejs-legacy
sudo npm -g install grunt-cli 

切换到源码目录

cd apprtc

npm install

sudo apt-get install python-webtest(之前是大写p安装不上)

grunt build(需要root权限 `sudo -i`再进入文件夹运行该命令)

编译之后会多出out目录
运行还依赖 Google App Engine SDK for Python 需翻墙
这里不要下载错了,我当时就下载错了以为要我下载gclund浪费了一些时间。找到python点击页面跳转之后找到下图的蓝色字点击。
这里写图片描述
下载完后设置环境变量

      sudo gedit /etc/profile

      export PATH=$PATH:/home/google_appengine

      source /etc/profile

3、修改配置文件

ifconfig查看自己的ip,修改配置主要是src/app_engine目录下的apprtc.py和constants.py

首先是constants.py:

修改TURN_BASE_URL = ‘http://192.168.1.107:80’ 这个是上边提到的连接信息接口的地址

   TURN_URL_TEMPLATE = '%s/turn.PHP?username=%s&key=%s'

             CEOD_KEY = 和coturn turnserver.conf static-auth-secret一致

             WSS_INSTANCES = [{
                   WSS_INSTANCE_HOST_KEY: '192.168.214.129:8089',
                   WSS_INSTANCE_NAME_KEY: 'wsserver-std',
                   WSS_INSTANCE_ZONE_KEY: 'us-central1-a'
                  }, {
                   WSS_INSTANCE_HOST_KEY: '192.168.214.129:8089',
                   WSS_INSTANCE_NAME_KEY: 'wsserver-std-2',
                   WSS_INSTANCE_ZONE_KEY: 'us-central1-f'
            }]

apprtc.py:
修改get_wss_parameters(request) 下的

  if wss_tls and wss_tls == 'false':
  wss_url = 'ws://' + wss_host_port_pair + '/ws'
  wss_post_url = 'http://' + wss_host_port_pair
  else:
  wss_url = 'ws://' + wss_host_port_pair + '/ws'
  wss_post_url = 'http://' + wss_host_port_pair

主要是把原来的wss和https的scheme都改为ws和http,不要让客户端和浏览器去使用ssl连接,如果有第三 方根证书的签名机构颁发的证书就不需要这样了。
修改完后重新grunt build下。

4、启动(只要下载对了google_appengine这句就应该能执行过去)

dev_appserver.py –host=0.0.0.0 ./out/app_engine

Collider信令服务器

这部分还挺顺利的就一步步照做就行了
1、安装依赖

     sudo apt-get install golang-go(之前那篇文章是大写G,安装不上)

2、在home目下创建文件夹

   mkdir -p ~/collider_root        并在collider_root目录下创建src目录

设置GOPATH环境变量 export GOPATH=~/collider_root

将apprtc/src/collider目录下的三个文件夹都拷贝到collider_root/src下

进入到collider_root/src,开始编译安装collider,准备好翻墙

     go get collidermain

     go install collidermain

成功编译后会在collider_root目录下生成bin和pkg目录,执行文件在bin下。

3、运行

修改collider_root/src/collidermain/main.go填上自己ip地址

      var roomSrv = flag.String("room-server", "http://192.168.1.107:8080/", "The origin of the room server")

启动

      ~/collider_root/bin/collidermain -port=8089 -tls=false

coTurn 打洞服务器

1、下载http://turnserver.open-sys.org/downloads/

找个适合自己Linux系统的,我这里是ubuntu32位所以选了turnserver-4.2.1.2-debian-wheezy-ubuntu-mint-x86-32bits.tar.gz

下载完后解压进入解压目录

      cat INSTALL     查看安装须知

      sudo apt-get install gdebi-core

      sudo gdebi coturn_4.2.2.2-1_i386.deb

2、编辑配置文件

       sudo gedit /etc/turnserver.conf

       listening-device=eth0

       listening-port=3478

       relay-device=eth1

       min-port=49152
       max-port=65535

       Verbose

       fingerprint

       lt-cred-mech

       use-auth-secret

       static-auth-secret=填写自己的密钥可不修改

       stale-nonce

       cert=/usr/local/etc/turn_server_cert.pem

       pkey=/usr/local/etc/turn_server_pkey.pem

       no-loopback-peers

       no-multicast-peers

       mobility

       no-cli

3、生成签名证书

       sudo openssl req -x509 -newkey  rsa:2048 -keyout/usr/local/etc/turn_server_pkey.pem -out /usr/local/etc/turn_server_cert.pem -days 99999 -nodes       

4、启动

      service coturn start

coTurn连接信息接口

拿nginx部署下php页面
1.安装Nginx

apt-get install nginx

2、启动Nginx

service nginx start

3.访问服务器IP
如果看到“Welcome to nginx!”说明安装好了。
4.安装PHP

apt-get install php7

5.配置Nginx

vi /etc/nginx/sites-available/default

找到下列代码,去掉相应注释

location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php7-fpm.sock;
}

重启服务

service nginx restart

6.默认的网站根目录在/var/www/html

vi /var/www/html/test.php

输入以下内容,并保存

<?php  

    $request_username = $_GET["username"];  
    if(empty($request_username)) {  
        echo "username == null";  
        exit;  
    }  
    $request_key = $_GET["key"];  
    $time_to_live = 600;  
    $timestamp = time() + $time_to_live;//失效时间  
    $response_username = $timestamp.":".$_GET["username"];  
    $response_key = $request_key;  
    if(empty($response_key))  
    $response_key = "密钥";//constants.py中CEOD_KEY  408……

    $response_password = getSignature($response_username, $response_key);  

    $jsonObj = new Response();  
    $jsonObj->username = $response_username;  
    $jsonObj->password = $response_password;  
    $jsonObj->ttl = 86400;  
    $jsonObj->uris = array("turn:192.168.1.107:3478?transport=udp","turn:192.168.1.107:3478?transport=tcp","turn:192.168.1.107:3479?transport=udp","turn:192.168.21.107:3479?transport=tcp");  

    echo json_encode($jsonObj);  

    /**   
         * 使用HMAC-SHA1算法生成签名值   
         *   
     * @param $str 源串   
         * @param $key 密钥   
         *   
         * @return 签名值   
         */    
    function getSignature($str, $key) {    
        $signature = "";    
        if (function_exists('hash_hmac')) {    
            $signature = base64_encode(hash_hmac("sha1", $str, $key, true));    
        } else {    
            $blocksize = 64;    
            $hashfunc = 'sha1';    
            if (strlen($key) > $blocksize) {    
                $key = pack('H*', $hashfunc($key));    
            }    
            $key = str_pad($key, $blocksize, chr(0x00));    
            $ipad = str_repeat(chr(0x36), $blocksize);    
            $opad = str_repeat(chr(0x5c), $blocksize);    
            $hmac = pack(    
                    'H*', $hashfunc(    
                            ($key ^ $opad) . pack(    
                                    'H*', $hashfunc(    
                                            ($key ^ $ipad) . $str    
                                    )    
                            )    
                    )    
            );    
            $signature = base64_encode($hmac);    
        }    
        return $signature;    
       }    

    class Response {  
        public $username = "";  
        public $password = "";  
        public $ttl = "";  
        public $uris = array("");  
    }  

?>  

测试

   部署成功后可在浏览器输入http://192.168.1.107:8080创建房间

   appRTCDemo连接也改成http://192.168.1.107:8080即可

补充

下面好像是中间遇到的一点问题,忘记是什么问题了,就先补充在这里吧
1.打开网页,复制setuptools的下载链接,利用wget下载安装包

wget http://pypi.python.org/packages/source/s/setuptools/setuptools-0.6c11.tar.gz#md5=7df2a529a074f613b509fb44feefe74e

2.将下载的setuptools-0.6c11.tar.gz解压缩

tar -zxvf setuptools-0.6c11.tar.gz

3.进入setuptools-0.6c11.tar.gz的源文件中,利用安装的python的可执行命令来进行setuptools的安装
(1)编译

python2.7 setup.py build

(2)安装

python2.7 setup.py install

http://blog.csdn.net/s569646547/article/details/50780682

sudo apt-get install openjdk-8-jre-headless

gclound未发现命令:重启vm

作者:danfengw 发表于2017/4/1 16:30:44 原文链接
阅读:117 评论:0 查看评论

深入 Android 源码系列(一)

$
0
0

   首先,每天看到不断有新人关注着这个公众号,心情很是愉悦。一种认可,一种信任,也是我前进的动力。感谢大家的支持与鼓励。

  本文讲解内容有
    loadLibrary流程 
    linker 
    ELF
    ndk开发以及配置调试版本
    ndk-gdb –start调试so
    gdb 调试bin文件
 gdb调试android apk方案

关于本文讲解使用的代码,都在网盘提供了,可以自行下载。
    链接: https://pan.baidu.com/s/1hrIxJdq 密码: jfwz
00
    开发android 应用,主要围绕着java语言,可是,如果我们需要追求性能,或者需要调用之前我们已经写好的c c++ so库的时候,或者和硬件打交道的时候,那么我们就会接触到JNI(java native interface)。
    我们知道,java是在其虚拟机里面运行。我们简单举个例子吧。我让你使用任何一个语言,写一段代码:打开一个文件,读取每一行,如果这一行内容是1,我们就在窗口显示生活真美好。 想一下,是否都能写出来?
    这里举的例子,简单的解释了虚拟机的动作,打开一个文件(输入),读取每一行(内容),如果这一行内容是1(解析),我们就在窗口显示生活真美好(输出),比起这个来说,java虚拟机比这个复杂,但是基础核心的原理就是这个了。
    我们本节去讲一个内容,System.loadLibrary(XXXX) 的执行过程。此过程完成将so库加载进来,打通java和c c++本地库的桥梁,实现相互调用。(此文牵扯概念 javaVM  JNI  ELF 动态库静态库)

   我们要做JNI,少不了使用

嗯,我们这节,就是展看loadLibrary,来看这个方法都做了哪些事情。

我们这里看下参数:libname 将加载的库名字,比如我们库为libtest_jni.so 这里则写为test_jni,其余的系统会帮我们拼接。
继续向下看,发现调用了Runtime类里面的loadLibrary0方法,我们看下:


我们看到有两个参数:第一个为Classloader,这个为我们的类加载器,我们这里的参数为VMStack.getCallingClassLoader(),于是我们看下这段代码。

看到这里为native,于是它本身是使用c或者c++本地语言编写的了,我们找下位置。通过搜索getCallingClassLoader,我们找到了本地实现的地方在dalvik_system_VMStack.cc里面,于是我们截图,来看下。

这里NATIVE_METHOD是个宏定义

于是

会转化为

这个就是jni编写中,需要配置的对应表,主要完成java和c语言函数对应,参数和返回值对应的关系,给了这些,虚拟机才会在java和c之间建立起来关系,知道哪个java函数调用的真正正确的c语言函数,同时c也是可以反向调用java的,更多可以百度jni的编写。
关于getCallingClassLoader这个是如何加入到系统的,就是上面的register_dalvik_system_VMStack方法了。


我们不对这里展开了,此方法是在runtime.cc的InitNativeMethods方法里面的RegisterRuntimeNativeMethods完成。有兴趣的可以去看看。我们继续跟踪system.loadLibrary,这里继续看VMStack.getCallingClassLoader()。
01
通过上面的展看,我们知道了这个对应的c方法为:VMStack_getCallingClassLoader,于是我们看到:


这里因为不熟悉,就不讲了。
loadLibrary0 里面主要调用的方法为:

loader.findLibrary(libraryName); 去查找是否存在此动态库,没有就报找不到异常。
然后我们调用doLoad去加载。


doLoad主要完成,传入设置的librarySearchPath,然后调用本地代码nativeLoad方法。搜索nativeLoad,我们找到了它对应的实现,在Runtime.c
里面

根据之前展开的方式,此函数为:Runtime_nativeLoad,于是我们看到:


在OpenjdkJvm.cc里面:

关键方法,通过拿到当前的虚拟机vm,调用对应的LoadNativeLibrary(java_vm_ext.cc)方法,去真正加载对应的so。

我们来到java_vm_ext.cc里面,去看下LoadNativeLibrary真正的执行过程:


这里我们关系的是高亮的几个函数:OpenNativeLibrary,完成加载so的过程。
FindSymbol(“JNI_OnLoad”)完成找出so里面的JNI_OnLoad方法,如果有,使用(*jni_on_load)(this, nullptr)调用,返回so使用的java版本。这个JNI_OnLoad就是我们加载so的时候,会主动触发的一个初始化方法了。在这里主要完成java和c的对应关系方法,然后使用RegisterNativeMethods将此关系注册进入vm,以便后续调用能够找到。
扩展:
用于Android ART虚拟机JNI调用的NativeBridge介绍,地址为:

http://www.aichengxu.com/android/1473706.htm

02

我们停一下,完成一个简单的测试demo代码,以便我们调试使用。
参考http://blog.csdn.net/a332324956/article/details/8703286 来写一个JNI搭配着eclipse去编译出来一个libtest_jni.so。(后续此工程会直接提供下载)

工程目录为:这里jni就是需要编出来so的地方。我们右键jnidemo选择properties,然后选择下Builders,点击new,创建一个编译规则。


编写一个调试:

这里Location指的是ndk-build脚本位置Working Directory 指的是当前项目的src/jni,我们要使用ndk-build将jni目录下的android.mk执行,完成生成so的动作。
最后生成出来libtest_jni.so我们在java工程使用下。(我们要在此基础上进行调试,所以我们使用的是自己load,不是写在static语句里面)

完整代码,文章最后提供,可以看着代码然后阅读。
我们在loadLibrary0上面打断点,然后看下流程:

我们可以看到看到,这里的loader为PathClassLoader.java,所以此处的findLibrary就是PathClassLoader.java文件里面的了。然后发现PathClassLoader继承自 BaseDexClassLoader,于是我们关心BaseDexClassLoader代码。

此段代码,完成在此app的本地so库的搜索路径下,查找我们的test_jni动态库,找到后path返回此so的绝对路径,以使后面的dlopen去动态打开此库。在此处,libname就是/data/app/com.example.jnidemo-2/lib/arm/libtest_jni.so,这个就是我们的jni动态库真正的位置了。
关于动态库dlopen dlsym 的用法,参照http://blog.csdn.net/edonlii/article/details/8445239 主要就是打开so,然后找到对应函数,然后执行。
按照这个文档,去调试so(需要下载android的ndk)
http://blog.csdn.net/kaiqiangzhang001/article/details/21108857
打上断点的截图为:

我们这里提供一个Android 的加载/链接器linker 的讲解
http://blog.csdn.net/dinuliang/article/details/5509009
关于android linker的代码位置 bionic/linker,可以去阅读。

03

我们回到之前的讲解,来找下LoadNativeLibrary调用的OpenNativeLibrary方法。在native_loader.cpp文件内找到此文件。

这里android调用了android_dlopen_ext方法,来实现动态库的加载,返回dlextinfo,而非android的,则是调用dlopen加载的。
我们搜索android_dlopen_ext,发现在 /bionic/libdl/
里面的/bionic/libdl/libdl.c 里面有


看,是个空方法,没有实际动作,看到这里的注释,意思是我们的dynamic linker 实现了这个方法,我们找到linker(手机里面的/system/bin/linker),我们在linker的源码里面dlfcn.cpp找到android_dlopen_ext


但是在最终编译出来的linker里面是被修改成了__dl_android_dlopen_ext
找到linker文件里面的方法,具体的操作是:
将linker提取出来


然后运行,导出来内容

然后看到了__dl_android_dlopen_ext方法的实现体:


关于linker的启动,可以参考http://www.myexception.cn/android/1930690.html 阅读。同时adnroid源码也是提供了一个简单解释:
在/development/ndk/platforms/下面的README.CRT.TXT文件,有如下内容:

完整的我提交网盘了,可以去下载阅读。

04

在bionic/linker里面的Android.mk文件,发现了一段注释,可以解释__dl_android_dlopen_ext和android_dlopen_ext 怎么变化的。


这里的–prefix-symbols=_dl 就是给名字的符号上面加入一个前缀,于是我们的android_dlopen_ext 就变成了__dl_android_dlopen_ext。想找到编译linker的所有编译规则,参数,去mmm bionic/linker,就会在out下面生成一个-mmm-._bionic_linker_Android.mk.ninja文件,这个就是我们生成linker的所有规则,从里面去找–prefix-symbols,能看到


生成linker的时候,使用了objcopy修改了方法名。
我们调试linker的代码,我们因为加载的是__dl_android_dlopen_ext ,于是我们gdb下断点 b __dl_android_dlopen_ext ,这样子我们打断点,运行时候会在加载动态库时候,停下来:


可以看到,断点成功。
info sharedlibrary 查看当前需要的so。
info breakpoints   查看断点信息
bt 查看堆栈
b 方法 下断点
delete num 删除对应断点。
file XXX.so (有调试信息的库,然后我们调试,就会变成有效信息)

05

关于gdb的使用,可以参考
http://blog.csdn.net/ghostyu/article/details/8083228
关于solib-absolute-prefix 和solib-search-path的区别 ,可以参考:
http://blog.csdn.net/caspiansea/article/details/16798735         
我们这里看到了一个地址信息,又没有显示出来,这里为0xaafceefa,我们想找到这个地址,对应的代码,该如何找呢?
adb shell
ps | grep demo (这里demo是我们包名)

我们关心的是10171(进程id),然后我们查找/proc/10171/maps 
cat /proc/10171/maps  ,找到aafc是在这个位置:
aada1000-ab1f4000 r-xp 00000000 103:08 1377      /system/lib/libart.so
于是我们file加载下这个libart.so
然后重新调试,看效果:

看#2,是不是出来了。
我们打断点,发现b android_dlopen_ext 和 b __dl_android_dlopen_ext 是一个位置(bionic/linker/dlfcn.cpp line 82).所以我们实际的android_dlopen_ext就是__dl_android_dlopen_ext,也就是dlfcn.cpp文件内容了。
我们将bionic放置到我们调试的ndk-gdb –start目录,再次调试,代码就检索出来了。


漫漫长路,我们又可以启程了,我们当前需要阅读的代码,就围绕着android_dlopen_ext(dlfcn.cpp)函数开展了。先开心看一个内容,这里我将编译出来的所有so加载进来了,我们看到调试栈就会变成:


看到没,调用信息一目了然。
我们看下追踪这条代码线,可以找到我们的调用关系:
android::OpenNativeLibrary –>dlopen_ext–>do_dlopen–>find_library–>find_libraries.再追下去就没完没了了,这些方法,都是有源码的,于是我们从源码去看看吧。

从这里开始。我们主要关注find_libraries函数,这里此方法完成扫描此so需要依赖的其他so,加入到tasklist里面,然后依据每个task,完成load的动作。

我们去看下task,这个类型为LoadTask:看下load方法:

再看个ElfReader的load即可(更深层次的自行学习了),参考链接器与加载器那本书

主要就是找空间(mmap)解析出来的ELF的格式,加载load段到内存空间。关于FindPhdr的方法含义,看下它本身的注释

嗯,我们就讲到这里,主要就是学习如何开发ndk,跟踪loadlibrary的流程,调试so,linker的具体含义。

06

我们延伸一个内容:
我们加载nativehelper库,这个是在手机/system/lib下的一个核心库。我们测试下:

运行报错,错误为:

意思就是这个动态库是系统核心的,不能单独加载起来,系统不允许。这段代码位置在:linker.cc里面的 load_library函数:


于是我们看下is_greylisted,便是判断灰名单的方法:

这里更详细的不看,只需要关注我们>23之后,直接返回出错,禁止调用系统这些库。
07
如何使用gdb调试android c可执行文件方案呢?
其中hello-jni是测试代码,操作如下:

可以看到调试结果如下:

调试成功。

08

如何使用gdb调试android apk方案呢?
手机端adb shell切入
ps | grep demo   找到我们的进程号
gdbserver :1234 –attach 8481 这里8481为进程号

电脑端输入:
shell adb forward tcp:1234 tcp:1234****
target** remote localhost:1234**
然后我们需要加载下app_process32程序,这个是从你手机/system/bin  下面导出来的。
adb pull /system/bin/app_process32 ~/
file ~/app_process32
set solib-search-path  /home/user/workspace/jnidemo/obj/local/armeabi 
将符号表导入。可以多次操作set solib-search-path
然后我们看下当前符号信息
info sharedlib
缺少某个库的符号,使用set solib-search-path继续导入

当没有打上断点的时候,使用set solib-search-path将对应的so加载上来,然后就可以了。**

更多精彩,敬请期待。

更多内容,关注微信公众号:code_gg_home。
加微信 code_gg_boy  进入代码GG交流群

作者:a332324956 发表于2017/4/1 20:47:43 原文链接
阅读:47 评论:0 查看评论

Android性能优化系列之Bitmap图片优化

$
0
0

在Android开发过程中,Bitmap往往会给开发者带来一些困扰,因为对Bitmap操作不慎,就容易造成OOM(Java.lang.OutofMemoryError - 内存溢出),本篇博客,我们将一起探讨Bitmap的性能优化。

为什么Bitmap会导致OOM?

1.每个机型在编译ROM时都设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,用来限定每个应用可用的最大内存,超出这个最大值将会报OOM。这个阀值,一般根据手机屏幕dpi大小递增,dpi越小的手机,每个应用可用最大内存就越低。所以当加载图片的数量很多时,就很容易超过这个阀值,造成OOM。

2.图片分辨率越高,消耗的内存越大,当加载高分辨率图片的时候,将会非常占用内存,一旦处理不当就会OOM。例如,一张分辨率为:1920x1080的图片。如果Bitmap使用 ARGB_8888 32位来平铺显示的话,占用的内存是1920x1080x4个字节,占用将近8M内存,可想而知,如果不对图片进行处理的话,就会OOM。

3.在使用ListView, GridView等这些大量加载view的组件时,如果没有合理的处理缓存,大量加载Bitmap的时候,也将容易引发OOM

Bitmap基础知识

一张图片Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数
而Bitmap.Config,正是指定单位像素占用的字节数的重要参数。
这里写图片描述

其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。

ALPHA_8
表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度
ARGB_4444
表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节
ARGB_8888
表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节
RGB_565
表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节

一张图片Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数

根据以上的算法,可以计算出图片占用的内存,以100*100像素的图片为例

这里写图片描述

BitmapFactory解析Bitmap的原理

BitmapFactory提供的解析Bitmap的静态工厂方法有以下五种:

Bitmap decodeFile(...)
Bitmap decodeResource(...)
Bitmap decodeByteArray(...)
Bitmap decodeStream(...)
Bitmap decodeFileDescriptor(...)

其中常用的三个:decodeFile、decodeResource、decodeStream。
decodeFile和decodeResource其实最终都是调用decodeStream方法来解析Bitmap

decodeFile方法代码:

   public static Bitmap decodeFile(String pathName, Options opts) {
        Bitmap bm = null;
        InputStream stream = null;
        try {
            stream = new FileInputStream(pathName);
            bm = decodeStream(stream, null, opts);
        } catch (Exception e) {
            /*  do nothing.
                If the exception happened on open, bm will be null.
            */
            Log.e("BitmapFactory", "Unable to decode stream: " + e);
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    // do nothing here
                }
            }
        }

decodeResource方法的代码:

public static Bitmap decodeResource(Resources res, int id, Options opts) {
        Bitmap bm = null;
        InputStream is = null; 

        try {
            final TypedValue value = new TypedValue();
            is = res.openRawResource(id, value);

            bm = decodeResourceStream(res, value, is, null, opts);
        } catch (Exception e) {
            /*  do nothing.
                If the exception happened on open, bm will be null.
                If it happened on close, bm is still valid.
            */
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // Ignore
            }
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        return bm;
    }

decodeStream的逻辑如下:

 public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
        // we don't throw in this case, thus allowing the caller to only check
        // the cache, and not force the image to be decoded.
        if (is == null) {
            return null;
        }

        Bitmap bm = null;

        Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
        try {
            if (is instanceof AssetManager.AssetInputStream) {
                final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
                bm = nativeDecodeAsset(asset, outPadding, opts);
            } else {
                bm = decodeStreamInternal(is, outPadding, opts);
            }

            if (bm == null && opts != null && opts.inBitmap != null) {
                throw new IllegalArgumentException("Problem decoding into existing bitmap");
            }

            setDensityFromOptions(bm, opts);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
        }

        return bm;
    }
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
        // ASSERT(is != null);
        byte [] tempStorage = null;
        if (opts != null) tempStorage = opts.inTempStorage;
        if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
        return nativeDecodeStream(is, tempStorage, outPadding, opts);
    }

从上面的代码可以看出,decodeStream的代码最终会调用以下两个native方法之一

nativeDecodeAsset()
nativeDecodeStream()

这两个native方法只是对应decodeFile和decodeResource、decodeStream来解析的,像decodeByteArray、decodeFileDescriptor也有专门的native方法负责解析Bitmap。

decodeFile、decodeResource的区别在于他们方法的调用路径不同:

decodeFile->decodeStream
decodeResource->decodeResourceStream->decodeStream

decodeResource在解析时多调用了一个decodeResourceStream方法,而这个decodeResourceStream方法代码如下:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {

        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }

        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }

        return decodeStream(is, pad, opts);
    }

其中对Options进行处理了,在得到opts.inDensity属性的前提下,如果我们没有对该属性设定值,那么将opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;赋定这个默认的Density值,这个默认值为160,为标准的dpi比例,即在Density=160的设备上1dp=1px,这个方法中还有这么一行

opts.inTargetDensity = res.getDisplayMetrics().densityDpi;

对opts.inTargetDensity进行了赋值,该值为当前设备的densityDpi值,所以说在decodeResourceStream方法中主要做了两件事:

1.对opts.inDensity赋值,没有则赋默认值160
2.对opts.inTargetDensity赋值,没有则赋当前设备的densityDpi值

之后参数将传入decodeStream方法,该方法中在调用native方法进行解析Bitmap后会调用这个方法setDensityFromOptions(bm, opts);:

private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
        if (outputBitmap == null || opts == null) return;

        final int density = opts.inDensity;
        if (density != 0) {
            outputBitmap.setDensity(density);
            final int targetDensity = opts.inTargetDensity;
            if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
                return;
            }

            byte[] np = outputBitmap.getNinePatchChunk();
            final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
            if (opts.inScaled || isNinePatch) {
                outputBitmap.setDensity(targetDensity);
            }
        } else if (opts.inBitmap != null) {
            // bitmap was reused, ensure density is reset
            outputBitmap.setDensity(Bitmap.getDefaultDensity());
        }
    }

主要就是把刚刚赋值过的两个属性inDensity和inTargetDensity给Bitmap进行赋值,不过并不是直接赋给Bitmap就完了,中间有个判断,当inDensity的值与inTargetDensity或与设备的屏幕Density不相等时,则将应用inTargetDensity的值,如果相等则应用inDensity的值。

所以总结来说,setDensityFromOptions方法就是把inTargetDensity的值赋给Bitmap,不过前提是opts.inScaled = true;

进过上面的分析,结论如下:

在不配置Options的情况下:
1.decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图
2.decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大

Bitmap的优化策略

经过上面的分析,我们可以得出Bitmap优化的思路:
1、BitmapConfig的配置
2、使用decodeFile、decodeResource、decodeStream进行解析Bitmap时,配置inDensity和inTargetDensity,两者应该相等,值可以等于屏幕像素密度*0.75f
3、使用inJustDecodeBounds预判断Bitmap的大小及使用inSampleSize进行压缩
4、对Density>240的设备进行Bitmap的适配(缩放Density)
5、2.3版本inNativeAlloc的使用
6、4.4以下版本inPurgeable、inInputShareable的使用
7、Bitmap的回收

所以我们根据以上的思路,我们将Bitmap优化的策略总结为以下3种:
1.对图片质量进行压缩
2.对图片尺寸进行压缩
3.使用libjpeg.so库进行压缩

对图片质量进行压缩


        public static Bitmap compressImage(Bitmap bitmap){  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中  
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
            int options = 100;  
            //循环判断如果压缩后图片是否大于50kb,大于继续压缩  
            while ( baos.toByteArray().length / 1024>50) {  
                //清空baos  
                baos.reset();  
                bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);  
                options -= 10;//每次都减少10  
            }  
            //把压缩后的数据baos存放到ByteArrayInputStream中  
            ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());  
            //把ByteArrayInputStream数据生成图片  
            Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);  
            return newBitmap;  
        }  

对图片尺寸进行压缩

    /**
     * 按图片尺寸压缩 参数是bitmap
     * @param bitmap
     * @param pixelW
     * @param pixelH
     * @return
     */
    public static Bitmap compressImageFromBitmap(Bitmap bitmap, int pixelW, int pixelH) {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
        if( os.toByteArray().length / 1024>512) {//判断如果图片大于0.5M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
            os.reset();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中
        }
        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        BitmapFactory.decodeStream(is, null, options);
        options.inJustDecodeBounds = false;
        options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH );
        is = new ByteArrayInputStream(os.toByteArray());
        Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options);
        return newBitmap;
    }


    /**
     * 动态计算出图片的inSampleSize
     * @param options
     * @param minSideLength
     * @param maxNumOfPixels
     * @return
     */
    public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);
        int roundedSize;
        if (initialSize <= 8) {
            roundedSize = 1;
            while (roundedSize < initialSize) {
                roundedSize <<= 1;
            }
        } else {
            roundedSize = (initialSize + 7) / 8 * 8;
        }
        return roundedSize;
    }

    private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        double w = options.outWidth;
        double h = options.outHeight;
        int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
        int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
        if (upperBound < lowerBound) {
            return lowerBound;
        }
        if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
            return 1;
        } else if (minSideLength == -1) {
            return lowerBound;
        } else {
            return upperBound;
        }
    }

使用libjpeg.so库进行压缩

除了通过设置simpleSize根据图片尺寸压缩图片和通过Bitmap.compress方法通过压缩图片质量两种方法外,我们还可以使用libjpeg.so这个库来进行压缩。

libjpeg是广泛使用的开源JPEG图像库,Android所用的是skia的压缩算法,而Skia对libjpeg进行了的封装。
libjpeg在压缩图像时,有一个参数叫optimize_coding,关于这个参数,libjpeg.doc有如下解释:

boolean optimize_coding 
TRUE causes the compressor to compute optimal Huffman coding tables 
for the image. This requires an extra pass over the data and 
therefore costs a good deal of space and time. The default is 
FALSE, which tells the compressor to use the supplied or default 
Huffman tables. In most cases optimal tables save only a few percent 
of file size compared to the default tables. Note that when this is 
TRUE, you need not supply Huffman tables at all, and any you do 
supply will be overwritten.

如果设置optimize_coding为TRUE,将会使得压缩图像过程中基于图像数据计算哈弗曼表,由于这个计算会显著消耗空间和时间,默认值被设置为FALSE。

谷歌的Skia项目工程师们最终没有设置这个参数,optimize_coding在Skia中默认的等于了FALSE,但是问题就随之出现了,如果我们想在FALSE和TRUE时压缩成相同大小的JPEG 图片,FALSE的品质将大大逊色于TRUE的,尽管谷歌工程师没有将该值设置为true,但是我们可以自己编译libjpeg进行图片的压缩。

libjpeg的官网下载地址:http://www.ijg.org/
从官网下载之后,我们必须自己对其进行编译。

编译libjpeg
下载最新的源码,解压后将所有文件放到jni目录中,准备用ndk编译
1、新建config.sh,将ndk中的交叉编译工具加入其中,内容如下:

NDK=/opt/ndk/android-ndk-r10e/
PLATFORM=$NDK/platforms/android-9/arch-arm/
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/
CC=$PREBUILT/bin/arm-linux-androideabi-gcc
./configure --prefix=/home/linc/jpeg-9b/jni/dist --host=arm CC="$CC --sysroot=$PLATFORM"

2、执行此脚本

$ sh config.sh 
...
checking whether to build shared libraries... no
checking whether to build static libraries... yes
...
config.status: creating Makefile
config.status: creating jconfig.h

首先,它生成了Makefile,我们可以直接使用此Makefile进行编译;其次,它生成了重要的头文件,jconfig.h.
但是这个Makefile是编译static库而不是共享库的。
此时,我们可以执行构建命令进行编译:

jni$ make install-libLTLIBRARIES
libtool: install: ranlib /home/linc/jpeg-9b/jni/dist/lib/libjpeg.a

3、Android.mk
使用ndk-build指令编译,需要手动编写Android.mk文件,内容如下:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_ARM_MODE := arm

LOCAL_SRC_FILES :=jaricom.c jcapimin.c jcapistd.c jcarith.c jccoefct.c jccolor.c \
        jcdctmgr.c jchuff.c jcinit.c jcmainct.c jcmarker.c jcmaster.c \
        jcomapi.c jcparam.c jcprepct.c jcsample.c jctrans.c jdapimin.c \
        jdapistd.c jdarith.c jdatadst.c jdatasrc.c jdcoefct.c jdcolor.c \
        jddctmgr.c jdhuff.c jdinput.c jdmainct.c jdmarker.c jdmaster.c \
        jdmerge.c jdpostct.c jdsample.c jdtrans.c jerror.c jfdctflt.c \
        jfdctfst.c jfdctint.c jidctflt.c jidctfst.c jidctint.c jquant1.c \
        jquant2.c jutils.c jmemmgr.c jmemnobs.c

LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays \
    -DANDROID -DANDROID_TILE_BASED_DECODE -DENABLE_ANDROID_NULL_CONVERT


LOCAL_MODULE := libjpeg

LOCAL_MODULE_TAGS := optional

# unbundled branch, built against NDK.
LOCAL_SDK_VERSION := 17

include $(BUILD_SHARED_LIBRARY)

其中LOCAL_SRC_FILES后面的源文件可以参考刚刚生成的Makefile。
在jni目录上一级使用ndk-build编译即可。

$ ndk-build
[armeabi] Compile arm    : jpeg <= jaricom.c
...
[armeabi] Compile arm    : jpeg <= jmemnobs.c
[armeabi] SharedLibrary  : libjpeg.so
[armeabi] Install        : libjpeg.so => libs/armeabi/libjpeg.so

在Android项目引入编译好的libjpeg
首先把so库加载到libs中,然后将编译好的头文件拷贝到项目的jni文件夹下,就可以使用Android的具体函数了,具体使用分为如下几步:

1、将Android的bitmap解码并转换为RGB数据
2、为JPEG对象分配空间并初始化
3、指定压缩数据源
4、获取文件信息
5、为压缩设定参数,包括图像大小,颜色空间
6、开始压缩
7、压缩完毕
8、释放资源

#include <string.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <jni.h>
#include <stdio.h>
#include <setjmp.h>
#include <math.h>
#include <stdint.h>
#include <time.h>
#include "jpeglib.h"
#include "cdjpeg.h"     /* Common decls for cjpeg/djpeg applications */
#include "jversion.h"       /* for version message */
#include "config.h"

#define LOG_TAG "jni"
#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define true 1
#define false 0

typedef uint8_t BYTE;

char *error;
struct my_error_mgr {
  struct jpeg_error_mgr pub;
  jmp_buf setjmp_buffer;
};

typedef struct my_error_mgr * my_error_ptr;

METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
  my_error_ptr myerr = (my_error_ptr) cinfo->err;
  (*cinfo->err->output_message) (cinfo);
  error=myerr->pub.jpeg_message_table[myerr->pub.msg_code];
  LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
 // LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
//  LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
//  LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
  longjmp(myerr->setjmp_buffer, 1);
}
//图片压缩方法
int generateJPEG(BYTE* data, int w, int h, int quality,
        const char* outfilename, jboolean optimize) {
    int nComponent = 3;

    struct jpeg_compress_struct jcs;

    struct my_error_mgr jem;

    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
        if (setjmp(jem.setjmp_buffer)) {
            return 0;
         }
     //为JPEG对象分配空间并初始化
    jpeg_create_compress(&jcs);
    //获取文件信息
    FILE* f = fopen(outfilename, "wb");
    if (f == NULL) {
        return 0;
    }
    //指定压缩数据源
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;
    jcs.image_height = h;
    if (optimize) {
        LOGI("optimize==ture");
    } else {
        LOGI("optimize==false");
    }

    jcs.arith_code = false;
    jcs.input_components = nComponent;
    if (nComponent == 1)
        jcs.in_color_space = JCS_GRAYSCALE;
    else
        jcs.in_color_space = JCS_RGB;

    jpeg_set_defaults(&jcs);
    jcs.optimize_coding = optimize;
    //为压缩设定参数,包括图像大小,颜色空间
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩
    jpeg_start_compress(&jcs, TRUE);

    JSAMPROW row_pointer[1];
    int row_stride;
    row_stride = jcs.image_width * nComponent;
    while (jcs.next_scanline < jcs.image_height) {
        row_pointer[0] = &data[jcs.next_scanline * row_stride];
        //写入数据
        jpeg_write_scanlines(&jcs, row_pointer, 1);
    }

    if (jcs.optimize_coding) {
            LOGI("optimize==ture");
        } else {
            LOGI("optimize==false");
        }
    //压缩完毕
    jpeg_finish_compress(&jcs);
    //释放资源
    jpeg_destroy_compress(&jcs);
    fclose(f);

    return 1;
}

typedef struct {
    uint8_t r;
    uint8_t g;
    uint8_t b;
} rgb;

//将java string转换为char*
char* jstrinTostring(JNIEnv* env, jbyteArray barr) {
    char* rtn = NULL;
    jsize alen = (*env)->GetArrayLength(env, barr);
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, 0);
    if (alen > 0) {
        rtn = (char*) malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0);
    return rtn;
}
//jni方法入口
jstring Java_net_bither_util_NativeUtil_compressBitmap(JNIEnv* env,
        jobject thiz, jobject bitmapcolor, int w, int h, int quality,
        jbyteArray fileNameStr, jboolean optimize) {

    AndroidBitmapInfo infocolor;
    BYTE* pixelscolor;
    int ret;
    BYTE * data;
    BYTE *tmpdata;
    char * fileName = jstrinTostring(env, fileNameStr);
    //解码Android bitmap信息,并存储值infocolor中
    if ((ret = AndroidBitmap_getInfo(env, bitmapcolor, &infocolor)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return (*env)->NewStringUTF(env, "0");;
    }
    if ((ret = AndroidBitmap_lockPixels(env, bitmapcolor, &pixelscolor)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }

    BYTE r, g, b;
    data = NULL;
    data = malloc(w * h * 3);
    tmpdata = data;
    int j = 0, i = 0;
    int color;
    //将bitmap转换为rgb数据
    for (i = 0; i < h; i++) {
        for (j = 0; j < w; j++) {
            color = *((int *) pixelscolor);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = color & 0x000000FF;
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            data = data + 3;
            pixelscolor += 4;

        }

    }
    AndroidBitmap_unlockPixels(env, bitmapcolor);
    //进行压缩
    int resultCode= generateJPEG(tmpdata, w, h, quality, fileName, optimize);
    free(tmpdata);
    if(resultCode==0){
        jstring result=(*env)->NewStringUTF(env, error);
        error=NULL;
        return result;
    }
    return (*env)->NewStringUTF(env, "1"); //success
}

新建Android.mk,生成可执行文件:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_SRC_FILES:= jpeg_compress.cpp

LOCAL_MODULE:= jtest

LOCAL_LDLIBS :=-llog
LOCAL_LDLIBS += $(LOCAL_PATH)/libjpeg.so
LOCAL_C_INCLUDES := $(LOCAL_PATH)

LOCAL_MODULE_PATH := $(TARGET_OUT_OPTIONAL_EXECUTABLES)
LOCAL_MODULE_TAGS := debug 

include $(BUILD_EXECUTABLE)

本篇博客总结了3种图片压缩的方法,大家可以根据自己的情况进行相应的使用,如有错漏,欢迎留言。

参考文章:
http://blog.csdn.net/lincyang/article/details/51085737

作者:u012124438 发表于2017/4/1 0:26:23 原文链接
阅读:737 评论:1 查看评论

还有一鲜为人知的单例写法-ThreadLocal

$
0
0

还有一鲜为人知的单例写法-ThreadLocal

源码范例

当我阅读FocusFinder和Choreographer的时候,我发现这两类的单例实现和我们平常用双重检查锁很不一样。而是用来一个ThreadLocal,这个也可以实现单例啊,那这个与双重检查锁实现的单例有什么区别呢?

1.FocusFinder

/**
 * The algorithm used for finding the next focusable view in a given direction
 * from a view that currently has focus.
 */
public class FocusFinder {

    private static final ThreadLocal<FocusFinder> tlFocusFinder =
            new ThreadLocal<FocusFinder>() {
                @Override
                protected FocusFinder initialValue() {
                    return new FocusFinder();
                }
            };

    /**
     * Get the focus finder for this thread.
     */
    public static FocusFinder getInstance() {
        return tlFocusFinder.get();
    }


    // enforce thread local access
    private FocusFinder() {}
}

2.Choreographer

public final class Choreographer {
    // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            return new Choreographer(looper);
        }
    };

    private Choreographer(Looper looper) {
        mLooper = looper;
        mHandler = new FrameHandler(looper);
        mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
        mLastFrameTimeNanos = Long.MIN_VALUE;

        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
        }
    }

    /**
     * Gets the choreographer for the calling thread.  Must be called from
     * a thread that already has a {@link android.os.Looper} associated with it.
     *
     * @return The choreographer for this thread.
     * @throws IllegalStateException if the thread does not have a looper.
     */
    public static Choreographer getInstance() {
        return sThreadInstance.get();
    }

}

理论分析

ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

public class ThreadLocal{

    /**
     * Provides the initial value of this variable for the current thread.
     * The default implementation returns {@code null}.
     *
     * @return the initial value of the variable.
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Returns the value of this variable for the current thread. If an entry
     * doesn't yet exist for this variable on this thread, this method will
     * create an entry, populating the value with the result of
     * {@link #initialValue()}.
     *
     * @return the current value of the variable for the calling thread.
     */
    @SuppressWarnings("unchecked")
    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

    /**
     * Gets Values instance for this thread and variable type.
     */
    Values values(Thread current) {
        return current.localValues;
    }

    /**
     * Sets the value of this variable for the current thread. If set to
     * {@code null}, the value will be set to null and the underlying entry will
     * still be present.
     *
     * @param value the new value of the variable for the caller thread.
     */
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

}

实现步骤

//1.initialValue,创建ThreadLocal对象
//2.get(),获取当前线程里的values
//3.如果不存在则初始化一个空的values
//4.如果存在,则复用values

还有一处经典应用

在Looper中使用ThreadLocal,使之每个Thread都有一个Looper与之对应.

public class Looper{
    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    /** Initialize the current thread as a looper.
     * This gives you a chance to create handlers that then reference
     * this looper, before actually starting the loop. Be sure to call
     * {@link #loop()} after calling this method, and end it by calling
     * {@link #quit()}.
     */
    public static void prepare() {
       prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
       if (sThreadLocal.get() != null) {
           throw new RuntimeException("Only one Looper may be created per thread");
       }
       sThreadLocal.set(new Looper(quitAllowed));
    } 
    /**
    * Return the Looper object associated with the current thread.  Returns
    * null if the calling thread is not associated with a Looper.
    */
    public static @Nullable Looper myLooper() {
       return sThreadLocal.get();
    }
}

自己也写

public class Manager {

    private static final ThreadLocal<Manager> sManager = new ThreadLocal<Manager>() {
        @Override
        protected Manager initialValue() {
            return new Manager();
        }
    };

    private Manager() {

    }

    public static Manager getInstance() {
        return sManager.get();
    }
}

参考

作者:ihrthk 发表于2017/4/1 18:46:47 原文链接
阅读:234 评论:0 查看评论

Android弹幕实现:基于B站弹幕开源系统(4)-重构

$
0
0


Android弹幕实现:基于B站弹幕开源系统(4)-重构

弹幕在视频播放的APP中比较常见,但是逻辑比较复杂,现在在附录1,2,3的基础上,我再次对弹幕进行抽象和重构,把弹幕从底向上抽象成不同的层,便于复用。

第一步,抽象数据层。
通常弹幕的来源是来源于后台的数据接口请求,在实时直播时候,是通过网络的轮询机制获取数据,那么,我把这部分代码抽出来设计成一个MGDanmakuHttpController,该类专注于数据的获取与分发:

package zhangphil.danmaku;

import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableObserver;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * Created by Phil on 2017/3/31.
 */

public class MGDanmakuHttpController {

    //private final String TAG = getClass().getName() + String.valueOf(UUID.randomUUID());

    private int msgId = 0;

    private DataMessageListener mDataMessageListener = null;
    private OkHttpClient mOkHttpClient;

    public MGDanmakuHttpController() {
        mOkHttpClient = new OkHttpClient();
    }

    private final int WHAT_START = 0xff0a;
    //private final int WHAT_STOP = WHAT_START + 1;

    private boolean promise = false;

    private int interval = 0;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            if (msg.what == WHAT_START) {
                handler.removeMessages(WHAT_START);

                try {
                    if (promise)
                        startRequestDanmaku();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    };


    public void startRequestDanmaku() throws Exception {
        promise = true;

        Observable mObservable = Observable.fromCallable(new Callable<List<DanmakuMsg>>() {
            @Override
            public List<DanmakuMsg> call() throws Exception {
                //同步方法返回观察者需要的数据结果
                //在这里处理线程化的操作
                return fetchData();
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());

        mObservable.subscribe(new DisposableObserver<List<DanmakuMsg>>() {

            @Override
            public void onNext(@NonNull List<DanmakuMsg> lists) {
                if (mDataMessageListener != null && promise) {
                    mDataMessageListener.onDataMessageListener(lists);
                }
            }

            @Override
            public void onComplete() {
                fireRequest();
            }

            @Override
            public void onError(Throwable e) {
                fireRequest();
            }
        });
    }

    public void stopRequestDanmaku() {
        promise = false;
    }

    /**
     * 设置轮询的间隔时间
     *
     * @param interval 单位毫秒 默认是0
     */
    public void setHttpRequestInterval(int interval) {
        this.interval = interval;
    }

    private void fireRequest() {
        //这里将触发重启数据请求,在这里可以调节重启数据请求的节奏。
        //比如可以设置一定的时延
        handler.sendEmptyMessageDelayed(WHAT_START, interval);
    }

    private List<DanmakuMsg> fetchData() {
        //同步方法返回观察者需要的数据结果
        //在这里处理线程化的操作
//        String url = "http://blog.csdn.net/zhangphil";
//        try {
//            Request request = new Request.Builder().url(url).build();
//            Response response = mOkHttpClient.newCall(request).execute();
//            if (response.isSuccessful()) {
//                byte[] bytes = response.body().bytes();
//                String data = new String(bytes, 0, bytes.length);


        try {
            Thread.sleep((int) (Math.random() * 500));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        int count = (int) (Math.random() * 10);

        //装配模拟数据
        List<DanmakuMsg> danmakuMsgs = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            DanmakuMsg danmakuMsg = new DanmakuMsg();
            danmakuMsg.msg = String.valueOf(msgId++);
            danmakuMsgs.add(danmakuMsg);
        }

        return danmakuMsgs;
//            }
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//
//        return null;
    }


    public interface DataMessageListener {
        void onDataMessageListener(@NonNull List<DanmakuMsg> lists);
    }

    public void setDataMessageListener(DataMessageListener listener) {
        mDataMessageListener = listener;
    }
}


第二步,通过一个模型把弹幕的view和数据用胶水粘合在一起,我写了一个MGDanmaku:

package zhangphil.danmaku;

import android.graphics.Color;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;

import master.flame.danmaku.danmaku.model.BaseDanmaku;
import master.flame.danmaku.danmaku.model.DanmakuTimer;
import master.flame.danmaku.danmaku.model.IDisplayer;
import master.flame.danmaku.danmaku.model.android.DanmakuContext;
import master.flame.danmaku.ui.widget.DanmakuView;

/**
 * Created by Phil on 2017/4/1.
 */

public class MGDanmaku {
    private final String TAG = getClass().getName() + UUID.randomUUID();

    private MGDanmakuHttpController mMGDanmakuHttpController;
    private DanmakuView mDanmakuView;
    private AcFunDanmakuParser mParser;

    private DanmakuContext mDanmakuContext;

    private final int MAX_DANMAKU_LINES = 8; //弹幕在屏幕显示的最大行数

    private ConcurrentLinkedQueue<DanmakuMsg> mQueue = null; //所有的弹幕数据存取队列,在这里做线程的弹幕取和存
    private ArrayList<DanmakuMsg> danmakuLists = null;//每次请求最新的弹幕数据后缓存list

    private final int WHAT_GET_LIST_DATA = 0xffab01;
    private final int WHAT_DISPLAY_SINGLE_DANMAKU = 0xffab02;

    /**
     * 每次弹幕的各种颜色从这里面随机的选一个
     */
    private final int[] colors = {
            Color.RED,
            Color.YELLOW,
            Color.BLUE,
            Color.GREEN,
            Color.CYAN,
            Color.DKGRAY};

    //弹幕开关总控制
    // true正常显示和请求
    // false则取消
    private boolean isDanmukuEnable = false;

    public MGDanmaku(@NonNull DanmakuView view, @NonNull MGDanmakuHttpController controller) {
        this.mDanmakuView = view;
        this.mMGDanmakuHttpController = controller;

        initDanmaku();

        danmakuLists = new ArrayList<>();
        mQueue = new ConcurrentLinkedQueue<>();

        mMGDanmakuHttpController.setDataMessageListener(new MGDanmakuHttpController.DataMessageListener() {
            @Override
            public void onDataMessageListener(@NonNull List<DanmakuMsg> lists) {
                danmakuLists = (ArrayList<DanmakuMsg>) lists;
                //for (int i = 0; i < danmakuLists.size(); i++) {
                    //Log.d("获得数据", danmakuLists.get(i).msg);
                //}

                addListData();
            }
        });

        Log.d(getClass().getName(), TAG);
    }

    private Handler mDanmakuHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            switch (msg.what) {
                case WHAT_GET_LIST_DATA:
                    addListData();
                    break;

                case WHAT_DISPLAY_SINGLE_DANMAKU:
                    mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU);
                    displayDanmaku();
                    break;
            }
        }
    };

    private void addListData() {
        if (danmakuLists != null && !danmakuLists.isEmpty()) {
            mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA);

            mQueue.addAll(danmakuLists);
            danmakuLists.clear();

            mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU);
        }
    }


    private void initDanmaku() {
        // 设置最大显示行数
        HashMap<Integer, Integer> maxLinesPair = new HashMap<>();
        maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, MAX_DANMAKU_LINES); // 滚动弹幕最大显示5行

        // 设置是否禁止重叠
        HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<>();
        overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
        overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);

        mDanmakuContext = DanmakuContext.create();

        //普通文本弹幕也描边设置样式
        //如果是图文混合编排编排,最后不要描边
        mDanmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 10) //描边的厚度
                .setDuplicateMergingEnabled(false)
                .setScrollSpeedFactor(1.2f) //弹幕的速度。注意!此值越小,速度越快!值越大,速度越慢。// by phil
                .setScaleTextSize(1.2f)  //缩放的值
//        .setCacheStuffer(new BackgroundCacheStuffer())  // 绘制背景使用BackgroundCacheStuffer
                .setMaximumLines(maxLinesPair)
                .preventOverlapping(overlappingEnablePair);

        mParser = new AcFunDanmakuParser();
        mDanmakuView.prepare(mParser, mDanmakuContext);

        //mDanmakuView.showFPS(true);
        mDanmakuView.enableDanmakuDrawingCache(true);

        if (mDanmakuView != null) {
            mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
                @Override
                public void updateTimer(DanmakuTimer timer) {
                }

                @Override
                public void drawingFinished() {

                }

                @Override
                public void danmakuShown(BaseDanmaku danmaku) {
                    Log.d("弹幕文本", "显示 text=" + danmaku.text);
                }

                @Override
                public void prepared() {
                    mDanmakuView.start();
                }
            });
        }
    }

    /**
     * 驱动弹幕显示机制重新运作起来
     */
    private void startDanmaku() {
        mDanmakuView.show();
        //mDanmakuView.start();

        mDanmakuHandler.sendEmptyMessage(WHAT_GET_LIST_DATA);
        mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU);
    }

    private void stopDanmaku() {
        if (mDanmakuView != null) {
            mDanmakuView.hide();
            mDanmakuView.clearDanmakusOnScreen();
            mDanmakuView.clear();
        }

        mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA);
        mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU);

        danmakuLists.clear();
        mQueue.clear();
    }

    public void setDanmakuRunning(boolean enable) {
        //如果是重复设置,则跳过
        if (isDanmukuEnable == enable) {
            return;
        }

        this.isDanmukuEnable = enable;

        //Log.d("isDanmukuEnable", String.valueOf(isDanmukuEnable));

        if (isDanmukuEnable) {
            startDanmaku();

            try {
                mMGDanmakuHttpController.startRequestDanmaku();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            stopDanmaku();
            mMGDanmakuHttpController.stopRequestDanmaku();
        }
    }


    public boolean getDanmakuRunning() {
        return isDanmukuEnable;
    }

    public void sendMsg(@NonNull DanmakuMsg danmakuMsg) {
        displayDanmaku(danmakuMsg);
    }

    public void onResume() {
        if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
            mDanmakuView.resume();
        }
    }

    public void onPause() {
        if (mDanmakuView != null && mDanmakuView.isPrepared()) {
            mDanmakuView.pause();
        }
    }

    public void onDestroy() {
        if (mDanmakuView != null) {
            // dont forget release!
            mDanmakuView.release();
            mDanmakuView = null;
        }

        stopDanmaku();
    }

    private void displayDanmaku(@NonNull DanmakuMsg dm) {
        //如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据
        //要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据
        if (isDanmukuEnable) {
            if (!TextUtils.isEmpty(dm.msg)) {
                addDanmaku(dm.msg, dm.islive);
            }
        }
    }

    private void displayDanmaku() {
        //如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据
        //要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据
        boolean b = !mQueue.isEmpty() && getDanmakuRunning();
        if (b) {
            DanmakuMsg dm = mQueue.poll();
            if (!TextUtils.isEmpty(dm.msg)) {
                addDanmaku(dm.msg, dm.islive);
            }
        }
    }

    private void addDanmaku(CharSequence cs, boolean islive) {
        BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
        if (danmaku == null || mDanmakuView == null) {
            return;
        }

        danmaku.text = cs;
        danmaku.padding = 5;
        danmaku.priority = 0;  // 可能会被各种过滤器过滤并隐藏显示
        danmaku.isLive = islive;
        danmaku.setTime(mDanmakuView.getCurrentTime());
        danmaku.textSize = 20f * (mParser.getDisplayer().getDensity() - 0.6f); //文本弹幕字体大小
        danmaku.textColor = getRandomColor(); //文本的颜色
        danmaku.textShadowColor = getRandomColor(); //文本弹幕描边的颜色
        //danmaku.underlineColor = Color.DKGRAY; //文本弹幕下划线的颜色
        danmaku.borderColor = getRandomColor(); //边框的颜色

        mDanmakuView.addDanmaku(danmaku);
    }

    /**
     * 从一系列颜色中随机选择一种颜色
     *
     * @return
     */
    private int getRandomColor() {
        int i = ((int) (Math.random() * 10)) % colors.length;
        return colors[i];
    }
}



第三步,直接拿来在上层的activity用:

package zhangphil.danmaku;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;

import master.flame.danmaku.ui.widget.DanmakuView;

public class MainActivity extends Activity {
    private MGDanmaku mMGDanmaku;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(getClass().getName(),"onCreate");

        DanmakuView mDanmakuView = (DanmakuView) findViewById(R.id.danmakuView);

        MGDanmakuHttpController mMGDanmakuHttpController = new MGDanmakuHttpController();
        mMGDanmakuHttpController.setHttpRequestInterval(0);
        mMGDanmaku = new MGDanmaku(mDanmakuView, mMGDanmakuHttpController);

        CheckBox checkBox = (CheckBox) findViewById(R.id.checkBox);
        checkBox.setChecked(mMGDanmaku.getDanmakuRunning());
        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                mMGDanmaku.setDanmakuRunning(isChecked);
            }
        });

        Button sendText = (Button) findViewById(R.id.sendText);
        sendText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DanmakuMsg msg = new DanmakuMsg();
                msg.msg = "zhangphil: " + System.currentTimeMillis();

                mMGDanmaku.sendMsg(msg);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        mMGDanmaku.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mMGDanmaku.onPause();
    }

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



一个简单的弹幕数据消息封装包:

package zhangphil.danmaku;

/**
 * Created by Phil on 2017/3/31.
 */

import java.io.Serializable;

/**
 * 弹幕数据封装的类(bean)
 */
public class DanmakuMsg implements Serializable {
    public String id = "";
    public String msg = null;
    public boolean islive = true;
    public String point = "";
}



测试的MainActivity布局文件:

<?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="match_parent"
    android:orientation="vertical">

    <CheckBox
        android:id="@+id/checkBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="弹幕开关" />

    <Button
        android:id="@+id/sendText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="发送文本弹幕" />

    <master.flame.danmaku.ui.widget.DanmakuView
        android:id="@+id/danmakuView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>


注意!需要配置Activity在AndroidManifest.xml的属性configChanges和launchMode,以适应弹幕在横竖屏切换时的状态正确,配置如:

 <activity android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|screenSize|fontScale"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>


代码运行结果如图:


附录:
1,《Android弹幕实现:基于B站弹幕开源系统(1)》链接:http://blog.csdn.net/zhangphil/article/details/68067100 
2,《Android弹幕实现:基于B站弹幕开源系统(2)》链接:http://blog.csdn.net/zhangphil/article/details/68114226 
3,《Android弹幕实现:基于B站弹幕开源系统(3)-文本弹幕的完善和细节调整》链接:http://blog.csdn.net/zhangphil/article/details/68485505
4,《Java ConcurrentLinkedQueue队列线程安全操作》链接:http://blog.csdn.net/zhangphil/article/details/65936066

作者:zhangphil 发表于2017/4/1 18:58:58 原文链接
阅读:238 评论:0 查看评论

Unity3D引擎之渲染技术系列一

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

在学习Unity引擎或者是其他3D游戏引擎时,经常遇到的问题就是各个坐标空间,在以前的博客中也给读者介绍过,本篇博客里用通俗易懂的语言再给读者揭露一下为什么需要这么多坐标空间?以及如何产生矩阵变换或者说利用矩阵变换解决问题。

在生活中我们经常遇到问路的,有的人方向感比较强,而有的人方向感比较弱,针对这些人指路时就要注意了,方向感强的人,你可以告诉他向东走大约300米,再向北走100米,你给他说的这些就是以东南西北为坐标轴介绍的,方向感弱的人,你就不能这么说了,你只能说向向前走300米,再向左走100米,这时你使用的是以这个路人围原点的坐标空间,通过这个案例,你可以看到不同的指路方式对应的就是我们不同的坐标轴向。由此可见,我们在生活中在不同的情况下使用不同的坐标空间,这个跟游戏中使用不同的空间是类似的。

Unity3D引擎开发3D游戏时,也会面临着各个坐标空间的变换,在这里给读者普及一下,这样开发者再做一些空间变换时更容易上手。Unity3D引擎使用的是扩展名为FBX的模型,模型的制作是美术人员用建模工具Max制作完成导出的,除了模型还包括图片,比如高光,法线等,关于材质的渲染可以使用Unity3D自带的Shader或者自己编写Shader。制作的模型相对于它自身而言就是局部空间也称为模型空间,也称为局部空间,每个模型都有自己独立的坐标空间。当它移动或者旋转时,模型空间也会跟着它移动和旋转。打个形象比喻,我们在公司的房间里面移动时,我们自身的空间也在跟着移动,当我们转身时,我们的坐标空间也在跟随变化。

在模型制作时,美术人员会给模型一个坐标,默认的情况下都是设置成(0,0,0),这个坐标是作为父节点的坐标,模型会有自己的孩子节点,假设带有孩子节点的物体如下图所示:

孩子节点的坐标是(2,2,0),它是子物体的局部坐标,下面会通过矩阵转换的方式,计算一下它在Unity中的世界坐标,也是高速读者如何利用矩阵变换获取到在世界空间的世界坐标。在Unity3D引擎使用的是左手坐标系也就是Z轴朝里,X轴朝右,Y轴朝上。将我们制作好的模型拖入到Unity编辑器后的效果如下所示:

红色箭头表示的是X轴,绿色的箭头表示的是Y轴,蓝色的箭头表示的是Z轴。它在世界场景中的坐标是(0,0,0)换句话说就是它自身的局部坐标与世界坐标重合。它的孩子节点是我们上面提到过的,下面我们将模型进行位移,旋转,缩放进行操作,我们进行缩放(2,2,2),旋转(0,150,0),位移(5,0,25),在Unity的表现效果如下所示:


我们可以构建出模型变换矩阵,在这里要注意一个问题,它们变换的顺序是一定的,不能改变。先进行缩放,在进行旋转,最后进行位移操作。相应的矩阵变换如下所示:


接下来我们对模型的子物体进行模型变换如下:


也就是说,在世界空间下,模型的子孩子的位置是(9,4,18.0)这样我们通过矩阵变换就得到了子孩子的世界坐标,其实这些基本运算我们在游戏开发中也是经常使用的,比如我们可以自己利用矩阵变换实现实时阴影,Unity3D自带的阴影由于消耗比较大,不容易在移动端实现,我们自己可以利用矩阵变换实现,效率得到提升。

后面继续介绍观察空间的矩阵实现。













作者:jxw167 发表于2017/4/2 10:57:12 原文链接
阅读:269 评论:0 查看评论

手把手教你写Linux设备驱动---定时器(一)(基于友善之臂4412开发板)

$
0
0

这个专题我们来说下Linux中的定时器。

Linux内核中,有这样的一个定时器,叫做内核定时器,内核定时器用于控制某个函数,也就是定时器将要处理的函数在未来的某个特定的时间内执行。内核定时器注册的处理函数只执行一次,即不是循环执行的。
如果对延迟的精度要求不高的话,最简单的实现方法如下---忙等待:
Unsigned long  j = jiffies + jit_delay * HZ;
While(jiffies  <  j)
{

         ……
}

下面来说下具体的参数代表的含义:

jiffies:全局变量,用来记录自系统启动以来产生的节拍总数。启动时内核将该变量初始化为0

此后每次时钟中断处理程序增加该变量的值。每一秒钟中断次数HZjiffies一秒内增加HZ。系统运行时间 = jiffie/HZ.

jiffies用途:计算流逝时间和时间管理

jiffies内部表示:

extern u64 jiffies_64;

extern unsigned long volatilejiffies;     //位长更系统有关32/64---->

|

|

32位:497天后溢出

64位:……

在定时器中有这样一个概念,度量时间差:

时钟中断由系统的定时硬件以周期性的时间间隔产生,这个间隔说白了其实就是频率由内核根据HZ来确定,HZ是一个与体系结构无关的常数,可以配置为(50-1200),X86平台,它的值被默认为1000 ;

定时器在内核中相关的头文件以及数据结构如下:

#include <linux/timer.h>  /*timer*/
#include <asm/uaccess.h>  /*jiffies*/

struct timer_list {
	/*
	 * All fields that change during normal runtime grouped to the
	 * same cacheline
	 */
	 //定时器可以作为链表的一个节点
	struct list_head entry;
	//定时值基于jiffies
	unsigned long expires;
	//定时器内部值
	struct tvec_base *base;
	//定时器处理函数
	void (*function)(unsigned long);
	 //定时器处理函数参数
	unsigned long data;
	int slack;
#ifdef CONFIG_TIMER_STATS
	int start_pid;
	void *start_site;
	char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

定时器最基本的使用方法可以使用下面这两个个内核提供的宏:

//初始化定时器
#define init_timer(timer)\
init_timer_key((timer), NULL, NULL)
//注册一个定时器
#define setup_timer(timer, fn, data)\
setup_timer_key((timer), NULL, NULL, (fn), (data))

还有以下两个函数:

添加一个定时器
void add_timer(struct timer_list *timer)

删除一个定时器

int del_timer(struct timer_list *timer)

那么写一个定时器的具体步骤是什么?
1、初始化内核定时器
2、设置定时器执行函数的参数(可有可无)
3、设置定时时间
4、设置定时器函数
5、启动定时器


接下来,我们结合一个简单的驱动来了解这个过程,这个驱动非常简单,就是开机后,5s钟后,开发板上的蜂鸣器就会每隔1s钟交替响。

先来看看开发板的蜂鸣器的原理图:

(1)蜂鸣器接口位于电路板的底板,看电路图可知道是高电平有效。


 (2)相对应的找到核心板的接口。由此可知,我们的蜂鸣器是GPD0_0


  接下来找数据手册,找到对应的寄存器,然后配置它就可以了。

  2、查数据手册,找到相关的寄存器,并配置生气

(1)找到GPD0CON,地址是0x114000A0,我们需要配置GPD0CON(0)为输出状态。也就是写0x1这个值到这个寄存器。

 

(2)找到GPD0DAT这个寄存器,用于配置蜂鸣器的高低电平,物理地址是0x114000A4,刚好与上一个差4个字节的偏移

我们只要对这个寄存器写1和写0,那么蜂鸣器就可以叫起来了,哈哈。是不是很简单?大笑


整个简单的驱动代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/fb.h>
#include <linux/backlight.h>
#include <linux/err.h>
#include <linux/pwm.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
#include <linux/timer.h>  /*timer*/
#include <asm/uaccess.h>  /*jiffies*/
#include <linux/delay.h>
//设备名称
#define DEVICE_NAME				"Bell"
//设备GPIO引脚
#define BUZZER_GPIO			EXYNOS4_GPD0(0)
//定义一个定时器链表
struct timer_list timer;
static void Bell_init()
{
	//1、请求gpio,相当于注册gpio
	gpio_request(BUZZER_GPIO,DEVICE_NAME);
	//2、调用板级驱动的函数,将gpio配置成输出状态
	s3c_gpio_cfgpin(BUZZER_GPIO, S3C_GPIO_OUTPUT);
        //3、设置gpio为0,表示低电平,蜂鸣器高电平就会响
	gpio_set_value(BUZZER_GPIO,0);
}
void timer_function(unsigned long value)
{
	while(value)	
	{
		//设置gpio为1,表示高电平,蜂鸣器高电平就会响
		gpio_set_value(BUZZER_GPIO,1);
		printk("BUZZER ON\n");
		mdelay(1000);
		//设置gpio为0,表示低电平,蜂鸣器高电平就会响
		gpio_set_value(BUZZER_GPIO,0);
		printk("BUZZER OFF\n");
		mdelay(1000);
	}
}
static int __init tiny4412_Bell_init(void) 
{
    //bell init
    Bell_init();	
    //初始化内核定时器
    init_timer(&timer); 
    //给执行的函数传参	
    timer.data= 1;
    //当前jiffies的值加上5秒钟之后	
    timer.expires= jiffies + (5 * HZ);
    //如果超时了就执行这个函数
    timer.function= timer_function;
    //启动定时器
    add_timer(&timer);                    		
    return 0 ;
}

static void __exit tiny4412_Bell_exit(void) 
{
    //释放gpio
    gpio_free(BUZZER_GPIO);
    //删除注册的定时器
    del_timer(&timer);
}

module_init(tiny4412_Bell_init);
module_exit(tiny4412_Bell_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YYX");
MODULE_DESCRIPTION("Exynos4 BELL Driver");
接下来,开启我们开发板串口,观察运行结果:

果然,定时器在开发板启动后的若干时间后,就周而复始的去打开和关闭我们板子上的蜂鸣器了。






作者:morixinguan 发表于2017/4/2 17:56:10 原文链接
阅读:280 评论:0 查看评论

Unity3D优化技巧系列五

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

架构设计也是优化的一种,一款游戏如果没有一个好的架构,程序出现问题很难做到及时的响应,读者可以试想一下,如果编程的时候只是为了实现功能而实现功能,到头来就是代码越写越乱,各种功能交织在一起。出现问题后到处找BUG修复,优化就更无从谈起了,谁也不希望把即将上线的项目代码,重新重构一遍,那还不如重新再做一遍,所以架构设计在游戏开发中是非常重要的,而且是必须要掌握的技术。下面重点给读者介绍MVC和FSM架构模式,效果如下所示:


MVC架构模式在前面的优化系列中给读者提过,下面就详细的告诉读者如何编写代码?给读者详细的分析一下,拿窗口的切换进行距离,游戏运行时,首先是创立界面,我们把每个界面作为一个窗口,比如LoginWindow,HeroWindow等。它们也是作为View界面,这样游戏中就会出现很多界面的设计,效果如下所示:


下面分析界面的编写,上文提到的每个UI就是一个Window,也是一个View,这么多View肯定有自己的共性,我们可以将其定义为基类,基类称为BaseWindow,代码编写如下所示:

namespace Game.View
{
    public abstract class BaseWindow
    {
        protected Transform mRoot;

        protected EScenesType mScenesType; //场景类型
        protected string mResName;         //资源名
        protected bool mResident;          //是否常驻 
        protected bool mVisible = false;   //是否可见
      

        //类对象初始化
        public abstract void Init();

        //类对象释放
        public abstract void Realse();

        //窗口控制初始化
        protected abstract void InitWidget();

        //窗口控件释放
        protected abstract void RealseWidget();

        //游戏事件注册
        protected abstract void OnAddListener();

        //游戏事件注消
        protected abstract void OnRemoveListener();

        //显示初始化
        public abstract void OnEnable();

        //隐藏处理
        public abstract void OnDisable();

        //每帧更新
        public virtual void Update(float deltaTime) { }

        //取得所以场景类型
        public EScenesType GetScenseType()
        {
            return mScenesType;
        }

        //是否已打开
        public bool IsVisible() { return mVisible;  }

        //是否常驻
        public bool IsResident() { return mResident; }

        //显示
        public void Show()
        {
            if (mRoot == null)
            {
                if (Create())
                {
                    InitWidget();
                }
            }

            if (mRoot && mRoot.gameObject.activeSelf == false)
            {
                mRoot.gameObject.SetActive(true);

                mVisible = true;

                 OnEnable();

                OnAddListener();
            }
        }

        //隐藏
        public void Hide()
        {
            if (mRoot && mRoot.gameObject.activeSelf == true)
            {
                OnRemoveListener();
                OnDisable();

                if (mResident)
                {
                    mRoot.gameObject.SetActive(false);
                }
                else
                {
                    RealseWidget();
                    Destroy();
                }
            }

            mVisible = false;
        }

        //预加载
        public void PreLoad()
        {
            if (mRoot == null)
            {
                if (Create())
                {
                    InitWidget();
                }
            }
        }

        //延时删除
        public void DelayDestory()
        {
            if (mRoot)
            {
                RealseWidget();
                Destroy();
            }
        }

        //创建窗体
        private bool Create()
        {
            if (mRoot)
            {
                Debug.LogError("Window Create Error Exist!");
                return false;
            }

            if (mResName == null || mResName == "")
            {
                Debug.LogError("Window Create Error ResName is empty!");
                return false;
            }

            if (GameMethod.GetUiCamera.transform== null)
            {
                Debug.LogError("Window Create Error GetUiCamera is empty! WindowName = " + mResName);
                return false;
            }

            GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);

            if (obj == null)
            {
                Debug.LogError("Window Create Error LoadRes WindowName = " + mResName);
                return false;
            }

            mRoot = obj.transform;

            mRoot.gameObject.SetActive(false);

            return true;
        }

        //销毁窗体
        protected void Destroy()
        {
            if (mRoot)
            {
                LoadUiResource.DestroyLoad(mRoot.gameObject);
                mRoot = null;
            }
        }

        //取得根节点
        public Transform GetRoot()
        {
            return mRoot;
        }
    }
}

以上是关于窗体的基类,实现了所有窗体需要创建的方法,创建窗体,销毁窗体,预加载,掩藏 等操作。实行按的具体操作类需要继承该窗体类,比如LoginWindow的写法如下所示:

namespace Game.View
{
    public class LobbyWindow : BaseWindow
    {
        public LobbyWindow()
        {
            mScenesType = EScenesType.EST_Login; //场景类型
            mResName = GameConstDefine.LoadGameLobbyUI; //资源名字
            mResident = false;  //是否常驻内存
        }

        ////////////////////////////继承接口/////////////////////////
        //类对象初始化
        public override void Init()
        {
            EventCenter.AddListener(EGameEvent.eGameEvent_LobbyEnter, Show);
            EventCenter.AddListener(EGameEvent.eGameEvent_LobbyExit, Hide); 
        }

        //类对象释放
        public override void Realse()
        {
            EventCenter.RemoveListener(EGameEvent.eGameEvent_LobbyEnter, Show);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_LobbyExit, Hide); 
        }

        //窗口控件初始化
        protected override void InitWidget()
        {           
            mHomepage = mRoot.FindChild("StartMenuManager/StartMenuBtn/Homepage").GetComponent<UIToggle>();
            UIGuideCtrl.Instance.AddUiGuideEventBtn(mHomepage.gameObject);
            mBattle = mRoot.FindChild("StartMenuManager/StartMenuBtn/Battle").GetComponent<UIToggle>();
            mMarket = mRoot.FindChild("StartMenuManager/StartMenuBtn/Market").GetComponent<UIToggle>();
            mInteraction = mRoot.FindChild("StartMenuManager/StartMenuBtn/Interaction").GetComponent<UIToggle>();

            UIGuideCtrl.Instance.AddUiGuideEventBtn(mMarket.gameObject);
            mDiamondText = mRoot.FindChild("Status/Diamond/Label").GetComponent<UILabel>();
            mGoldText = mRoot.FindChild("Status/Gold/Label").GetComponent<UILabel>();
            mSettingBtn = mRoot.FindChild("Status/Setting").GetComponent<UIButton>();

            UIGuideCtrl.Instance.AddUiGuideEventBtn(mBattle.gameObject);
            EventDelegate.Add(mHomepage.onChange, OnHomePageChange);
            EventDelegate.Add(mBattle.onChange, OnBattleChange);
            EventDelegate.Add(mMarket.onChange, OnMarketChange);
            EventDelegate.Add(mInteraction.onChange, OnInteractionChange);
            UIEventListener.Get(mHeadIcon.transform.parent.gameObject).onClick += InfoPersonPress;
        }
        private void InfoPersonPress(GameObject go)
        {
            GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_PersonalInfo);

            LobbyCtrl.Instance.AskPersonInfo();
            //PresonInfoCtrl.Instance.Enter();
        }
      
        //窗口控件释放
        protected override void RealseWidget()
        {
        }


        //游戏事件注册
        protected override void OnAddListener()
        {
            EventCenter.AddListener(EGameEvent.eGameEent_ChangeMoney, RefreshMoney);
            EventCenter.AddListener<bool>(EGameEvent.eGameEvent_ReceiveLobbyMsg, NewChat);
            EventCenter.AddListener(EGameEvent.eGameEvent_AddNewMailReq, NoticeNewMail);
            EventCenter.AddListener(EGameEvent.eGameEvent_ChangeNickName,ChangeNickName);
            EventCenter.AddListener(EGameEvent.eGameEvent_ChangeHeadID, ChangeHeadID);

            EventCenter.AddListener(EGameEvent.eGameEvent_ChangeUserLevel, ChangeLevel);
        }
        private void ChangeHeadID()
        {
            mHeadIcon.spriteName = GameUserModel.Instance.GameUserHead.ToString();
        }

        //游戏事件注消
        protected override void OnRemoveListener()
        {
            EventCenter.RemoveListener(EGameEvent.eGameEent_ChangeMoney, RefreshMoney);
            EventCenter.RemoveListener<bool>(EGameEvent.eGameEvent_ReceiveLobbyMsg, NewChat);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_AddNewMailReq, NoticeNewMail);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_ChangeNickName, ChangeNickName);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_ChangeHeadID, ChangeHeadID);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_ChangeUserLevel, ChangeLevel);
        }

        //显示
        public override void OnEnable()
        {

        }

        //隐藏
        public override void OnDisable()
        {

        }

        public void ChangeUserLevel()
        {
            mLevel.text = GameUserModel.Instance.UserLevel.ToString();
        }
	//回调事件
        public void OnHomePageChange()
        {
            //todo
            mLevel.text = GameUserModel.Instance.UserLevel.ToString();
            mNickName.text = GameUserModel.Instance.GameUserNick;
            mHeadIcon.spriteName = GameUserModel.Instance.GameUserHead.ToString();
            mGold.text = GameUserModel.Instance.mGameUserGold.ToString();
            mDiamond.text = GameUserModel.Instance.mGameUserDmd.ToString();
            VipSignLevel.text = "VIP "+GameUserModel.Instance.GameUserVipLevel.ToString();
            int level = GameUserModel.Instance.UserLevel;
            mLevel.text = level.ToString();
            
            LevelConfigInfo leveinfo = ConfigReader.GetLevelInfo(level);
            if (leveinfo != null)
            {
                mExp.text = GameUserModel.Instance.GameUserExp + "/" + leveinfo.LevelUpExp;
                mExp.transform.parent.GetComponent<UISprite>().fillAmount = GameUserModel.Instance.GameUserExp / leveinfo.LevelUpExp;
                if (level >= 29 && GameUserModel.Instance.GameUserExp >= leveinfo.LevelUpExp)
                {
                    level = 30;
                    mLevel.text = level.ToString();
                    mExp.gameObject.SetActive(false);
                    mExp.transform.parent.GetComponent<UISprite>().fillAmount = 1f;
                }
            }
            
            if (mHomepage.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_HomePage);
                HomePageCtrl.Instance.Enter();
            }
            else
            {
                HomePageCtrl.Instance.Exit();
            }
        }

        public void OnBattleChange()
        {
            if (mBattle.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_Battle);
                BattleCtrl.Instance.Enter();
            }
            else
            {
                BattleCtrl.Instance.Exit();
            }
        }

        public void OnMarketChange()
        {
            if (mMarket.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_Market);
                MarketCtrl.Instance.Enter();
            }
            else
            {
                MarketCtrl.Instance.Exit();
            }
        }

        public void OnInteractionChange()
        {
            if (mInteraction.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_Friend);
                SocialCtrl.Instance.Enter();
            }
            else
            {
                SocialCtrl.Instance.Exit();
            }
        }

        public void MailListBtn(GameObject obj)
        {
            mMailBtnBg.SetActive(false);

            MailCtrl.Instance.Enter();
        }
    }


}
该类主要是实现整个UI的逻辑编写,其他的窗口编写类似。下面编写Control控制类代码的编写,示例代码如下:

    public class LoginCtrl : Singleton<LoginCtrl>
    {
        public void Enter()
        {
            EventCenter.Broadcast(EGameEvent.eGameEvent_LoginEnter);
        }

        public void Exit()
        {
            EventCenter.Broadcast(EGameEvent.eGameEvent_LoginExit);
        }

        //登陆
        public void Login(string account, string pass)
        {

        }

        //登陆错误反馈
        public void LoginError(int code)
        {

        }

        //接收GateServer信息
        public void RecvGateServerInfo(Stream stream)
        {

        }

        //登陆失败
        public void LoginFail()
        {

        }

        //选择LoginServer
        public void SelectLoginServer(int i)
        {

        }
        //开始游戏
        public void GamePlay()
        {

        }
   }
控制类的主要作用是进行一些消息的传递,这也是为了解除模块之间的耦合性,该模块的主要核心是Enter函数和Exit函数。后面继续代码的编写。。。。


作者:jxw167 发表于2017/4/2 18:06:16 原文链接
阅读:266 评论:0 查看评论

Unity3D优化技巧系列六

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

继续MVC代码的封装工作,上一节封装到了Control模块,我们现在很多的窗体,如何管理?这需要我们设计一个管理类去管理所有的窗体,下面给读者展示如何编写WindowManager类,代码如下:

namespace Game.View
{
   //场景类型
    public enum EScenesType
    {
        EST_None,
        EST_Login,
        EST_Play,
    }

   //窗体类型
    public enum EWindowType
    {
        EWT_LoginWindow, //登录
        EWT_UserWindow, //用户
        EWT_LobbyWindow,
        EWT_BattleWindow,
        EWT_RoomWindow,
        EWT_HeroWindow,
    }

    public class WindowManager : Singleton<WindowManager>
    {
        public WindowManager()
        {
            mWidowDic = new Dictionary<EWindowType, BaseWindow>();

            mWidowDic[EWindowType.EWT_LoginWindow] = new LoginWindow();
            mWidowDic[EWindowType.EWT_UserWindow] = new UserInfoWindow();
            mWidowDic[EWindowType.EWT_LobbyWindow] = new LobbyWindow();
            mWidowDic[EWindowType.EWT_BattleWindow] = new BattleWindow();
            mWidowDic[EWindowType.EWT_RoomWindow] = new RoomWindow();
            mWidowDic[EWindowType.EWT_HeroWindow] = new HeroWindow();
        }

        public BaseWindow GetWindow(EWindowType type)
        {
            if (mWidowDic.ContainsKey(type))
                return mWidowDic[type];

            return null;
        }

        public void Update(float deltaTime)
        {
            foreach (BaseWindow pWindow in mWidowDic.Values)
            {
                if (pWindow.IsVisible())
                {
                    pWindow.Update(deltaTime);
                }
            }
        }

        public void ChangeScenseToPlay(EScenesType front)
        {
            foreach (BaseWindow pWindow in mWidowDic.Values)
            {
                if (pWindow.GetScenseType() == EScenesType.EST_Play)
                {
                    pWindow.Init();

                    if(pWindow.IsResident())
                    {
                        pWindow.PreLoad();
                    }
                }
                else if ((pWindow.GetScenseType() == EScenesType.EST_Login) && (front == EScenesType.EST_Login))
                {
                    pWindow.Hide();
                    pWindow.Realse();

                    if (pWindow.IsResident())
                    {
                        pWindow.DelayDestory();
                    }
                }
            }
        }

        public void ChangeScenseToLogin(EScenesType front)
        {
            foreach (BaseWindow pWindow in mWidowDic.Values)
            {
                if (front == EScenesType.EST_None && pWindow.GetScenseType() == EScenesType.EST_None)
                {
                    pWindow.Init();

                    if (pWindow.IsResident())
                    {
                        pWindow.PreLoad();
                    }
                }

                if (pWindow.GetScenseType() == EScenesType.EST_Login)
                {
                    pWindow.Init();

                    if (pWindow.IsResident())
                    {
                        pWindow.PreLoad();
                    }
                }
                else if ((pWindow.GetScenseType() == EScenesType.EST_Play) && (front == EScenesType.EST_Play))
                {
                    pWindow.Hide();
                    pWindow.Realse();

                    if (pWindow.IsResident())
                    {
                        pWindow.DelayDestory();
                    }
                }
            }
        }

        /// <summary>
        /// 隐藏该类型的所有Window
        /// </summary>
        /// <param name="front"></param>
        public void HideAllWindow(EScenesType front)
        {
            foreach (var item in mWidowDic)
            {
                if (front == item.Value.GetScenseType())
                {
                    Debug.Log(item.Key);
                    item.Value.Hide();
                    //item.Value.Realse();
                }
            }
        }

        public void ShowWindowOfType(EWindowType type)
        { 
            BaseWindow window;
            if(!mWidowDic.TryGetValue(type , out window))
            {
                return;
            }
            window.Show();
        }

        private Dictionary<EWindowType, BaseWindow> mWidowDic;
    }
}

对窗体进行注册,显示,隐藏等操作,该函数实现了对游戏中所有窗体的管理工作。窗体之间如何切换?我们使用了状态机去编写逻辑,每个窗体都有自己的状态类,状态类也有共性,我们将其抽离出来,实现代码如下:

namespace Game.GameState
{
    public interface IGameState
    {
        GameStateType GetStateType();
        void SetStateTo(GameStateType gsType);
        void Enter();
        GameStateType Update(float fDeltaTime);
        void FixedUpdate(float fixedDeltaTime);
        void Exit();
    }
}
父类实现了后,LoginState的实现如下:

namespace Game.GameState
{
    class LoginState : IGameState
    {
        GameStateType stateTo;

        GameObject mScenesRoot;
	public LoginState()
	{
	}

        public GameStateType GetStateType()
        {
            return GameStateType.GS_Login;
        }

        public void SetStateTo(GameStateType gs)
        {
            stateTo = gs;
        }

        public void Enter()
        {
            SetStateTo(GameStateType.GS_Continue);
            ResourceUnit sceneRootUnit = ResourcesManager.Instance.loadImmediate(GameConstDefine.GameLogin, ResourceType.PREFAB);
            mScenesRoot = GameObject.Instantiate(sceneRootUnit.Asset) as GameObject;

            LoginCtrl.Instance.Enter();
        
            ResourceUnit audioClipUnit = ResourcesManager.Instance.loadImmediate(AudioDefine.PATH_UIBGSOUND, ResourceType.ASSET);
            AudioClip clip = audioClipUnit.Asset as AudioClip;       

            AudioManager.Instance.PlayBgAudio(clip);

            EventCenter.AddListener<CEvent>(EGameEvent.eGameEvent_InputUserData, OnEvent);
            EventCenter.AddListener<CEvent>(EGameEvent.eGameEvent_IntoLobby, OnEvent);
            EventCenter.AddListener(EGameEvent.eGameEvent_SdkLogOff, SdkLogOff);            
        }

        public void Exit()
        {
            EventCenter.RemoveListener<CEvent>(EGameEvent.eGameEvent_InputUserData, OnEvent);
            EventCenter.RemoveListener<CEvent>(EGameEvent.eGameEvent_IntoLobby, OnEvent);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_SdkLogOff, SdkLogOff);       

            //LoadUiResource.DestroyLoad(mUIRoot);
            LoginCtrl.Instance.Exit();
            GameObject.DestroyImmediate(mScenesRoot);            
        }

        public void FixedUpdate(float fixedDeltaTime)
        {
            
        }

        public GameStateType Update(float fDeltaTime)
        {
            return stateTo;
        }

        public void OnEvent(CEvent evt)
        {
            UIPlayMovie.PlayMovie("cg.mp4", Color.black, 2/* FullScreenMovieControlMode.Hidden*/, 3/*FullScreenMovieScalingMode.Fill*/);
            switch (evt.GetEventId())
            {
                case EGameEvent.eGameEvent_InputUserData:
                    SetStateTo(GameStateType.GS_User);
                    break;
                case EGameEvent.eGameEvent_IntoLobby:
                    GameStateManager.Instance.ChangeGameStateTo(GameStateType.GS_Lobby);
                    break;
            }
        }
    }
}
以上是关于State状态机的类实现,代码如下:

namespace Game.GameState
{
    class LoginState : IGameState
    {
        GameStateType stateTo;

        GameObject mScenesRoot;

       // GameObject mUIRoot;
	
		public LoginState()
		{
		}

        public GameStateType GetStateType()
        {
            return GameStateType.GS_Login;
        }

        public void SetStateTo(GameStateType gs)
        {
            stateTo = gs;
        }

        public void Enter()
        {
            SetStateTo(GameStateType.GS_Continue);
            ResourceUnit sceneRootUnit = ResourcesManager.Instance.loadImmediate(GameConstDefine.GameLogin, ResourceType.PREFAB);
            mScenesRoot = GameObject.Instantiate(sceneRootUnit.Asset) as GameObject;

            LoginCtrl.Instance.Enter();
        
            ResourceUnit audioClipUnit = ResourcesManager.Instance.loadImmediate(AudioDefine.PATH_UIBGSOUND, ResourceType.ASSET);
            AudioClip clip = audioClipUnit.Asset as AudioClip;              
        }
        public void Exit()
        {
            EventCenter.RemoveListener<CEvent>(EGameEvent.eGameEvent_InputUserData, OnEvent);
            EventCenter.RemoveListener<CEvent>(EGameEvent.eGameEvent_IntoLobby, OnEvent);
            
            //LoadUiResource.DestroyLoad(mUIRoot);
            LoginCtrl.Instance.Exit();
            GameObject.DestroyImmediate(mScenesRoot);            
        }

        public void FixedUpdate(float fixedDeltaTime)
        {
            
        }

        public GameStateType Update(float fDeltaTime)
        {
            return stateTo;
        }

        public void OnEvent(CEvent evt)
        {
            UIPlayMovie.PlayMovie("cg.mp4", Color.black, 2/* FullScreenMovieControlMode.Hidden*/, 3/*FullScreenMovieScalingMode.Fill*/);
            switch (evt.GetEventId())
            {
                case EGameEvent.eGameEvent_InputUserData:
                    SetStateTo(GameStateType.GS_User);
                    break;
                case EGameEvent.eGameEvent_IntoLobby:
                    GameStateManager.Instance.ChangeGameStateTo(GameStateType.GS_Lobby);
                    break;
            }
        }
    }
}
在Enter函数中,使用了LoginCtrl类接口的调用。另外,我们编写的逻辑是不挂在脚本上的,资源都是动态加载的,后面会告诉读者如何使用,后面会继续给读者讲解。。。。。。




作者:jxw167 发表于2017/4/2 21:32:30 原文链接
阅读:17 评论:0 查看评论

Android 设计模式实战——单例模式

$
0
0

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

单例设计模式上应用最广的模式之一,在应用单例模式时,单例对象的类必须保证只有一个实例存在,而且可以自行实例化并向整个系统提供这个实例。一般在不能自由构造对象的情况下,就会使用单例设计模式,例如创建一个对象需要消耗资源过多,还有访问IO和数据库等资源等情况。

1.程序中使用单例模式的优缺点

1.优点

1)在内存中只有一个实例,减少了内存开支和性能开销;
2)可以避免对同一资源文件的同时写操作;
3)可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

2.缺点

1)一般没有接口,扩展困难;
2)单例模式如果持有 Context,很容易引发内存泄漏,此时需要注意传递给单例对象的 Context 最好是 Application Context。

2.单例模式的实现方式

1.饿汉式

public class Singleton {
    /**
     * 单例模式(饿汉式)
     */
    private static final Singleton instance = new Singleton(); // 句柄
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}

2.懒汉式

优点是单例只有在使用时才会被实例化;缺点是第一次加载反应稍慢,每次调用
getInstance() 都进行同步,造成不必要的同步开销,一般不建议使用

public class Singleton {
    /**
     * 单例模式(懒汉式)
     */
    private static Singleton instance; // 句柄
    private Singleton() {
    }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3.DCL方式

Double Check Lock 实现单例,优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用 getInstance() 不进行同步锁;缺点是第一次加载反应稍慢。

public class Singleton {
    /**
     * 单例模式(DCL方式)
     */
    private volatile static Singleton instance = null; // 句柄
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) { // 避免不必要的同步
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4.静态内部类方式——推荐使用

第一次加载不会初始化 Singleton,只有在第一次调用 Singleton 的 getInstance() 才会初始化,第一次调用会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,推荐使用

public class Singleton {
    /**
     * 单例模式(静态内部类的方式)
     */
    private Singleton() {
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

5.使用容器实现单例模式

在初始,将多种单例类型注入到一个统一的管理类中,在使用时根据 key 获取对象对应类型的对象。可管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了耦合度。

public class SingletonManager {
    /**
     * 单例模式(使用容器实现单例模式)
     */
    private static Map<String, Object> objMap = new HashMap<>();
    private SingletonManager() {
    }
    public static void registerInstance(String key, Object instance) {
        if (objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }
    public static Object getInstance(String key) {
        return objMap.get(key);
    }
}

不管以何种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题,选择何种取决于项目本身,并发环境是否复杂、单例对象等资源消耗等。

序列化:将一个对象的实例写到磁盘,然后再读回来,从而获得一个实例。

作者:smartbetter 发表于2017/4/2 19:17:19 原文链接
阅读:142 评论:0 查看评论

Android 设计模式实战——建造者模式

$
0
0

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

当我们遇到类似汽车的装配,需要车轮、方向盘、发动机,还有各种小零件时,为了在构建过程中隐藏实现细节,就可以使用建造者模式 (Builder模式) 将部件和组装过程分离,使得构建过程和部件都可以自由扩展,两者之间的耦合也降到最低。接下来我们看一下定义,建造者模式是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

1.建造者模式的使用场景

1)相同的方法,不同的执行顺序,产生不同的事件结果时;
2)多个部件,都可以装配到一个对象中,但是产生的运行结果又不相同时;
3)产品类非常复杂,或者产品类中的调用顺序不同产生了不同的作用时;
4)当初始化一个对象特别复杂时;

2.程序中使用建造者模式的优缺点

1.优点

1)良好的封装性,使用建造者模式可以使客户端不必知道产品内部组成的细节;
2)建造者独立,扩展性好。

2.缺点

1)会产生多余的 Builder 对象,消耗内存。

3.建造者模式的UML类图

类图

  • Product产品类:产品的抽象类
  • Builder:抽象Builder类,规范产品的组建
  • ConcreteBuilder:具体的Builder类
  • Director:统一组装过程

值得注意的是,在实际开发中,Director 对象经常会被省略,而直接使用一个 Builder 来进行对象的组装,这个 Builder 通常为链式调用,下面我们看一下链式调用的类图。

链式调用的类图

  • Product产品类:产品的抽象类
  • Builder:抽象Builder类,规范产品的组建
  • ConcreteBuilder:具体的Builder类,链式调用

下面我们给出的实现也基于链式调用的。

4.建造者模式的实现——链式调用

1.定义Product产品类(以组装计算机为例):

public abstract class Computer {
    protected String board;
    protected String display;
    protected String os;
    protected Computer() {
    }
    // 设置主板
    public void setmBoard(String board) {
        this.board = board;
    }
    // 设置显示器
    public void setmDisplay(String display) {
        this.display = display;
    }
    // 设置操作系统
    public abstract void setmOS();
    @Override
    public String toString() {
        return "Computer{" + "board='" + board + '\'' + ", display='" + display + '\'' +
                ", os='" + os + '\'' + '}';
    }
}

具体的 Computer 类:

public class MacBook extends Computer {
    protected MacBook() {
    }
    @Override
    public void setmOS() {
        os = "mac OS Sierra";
    }
}

2.定义抽象Builder类,规范产品的组建:

public abstract class Builder {
    // 设置主板
    public abstract Builder setBoard(String board);
    // 设置显示器
    public abstract Builder setDisplay(String display);
    // 设置操作系统
    public abstract Builder setOS();
    // 创建 Computer
    public abstract Computer create();
}

具体的Builder类:

public class MacBookBuilder extends Builder {
    private Computer mComputer = new MacBook();
    @Override
    public Builder setBoard(String board) {
        mComputer.setmBoard(board);
        return this;
    }
    @Override
    public Builder setDisplay(String display) {
        mComputer.setmDisplay(display);
        return this;
    }
    @Override
    public Builder setOS() {
        mComputer.setmOS();
        return this;
    }
    @Override
    public Computer create() {
        return mComputer;
    }
}

链式调用的关键点是每个 setter 方法都返回自身,也就是 return this; 这样就使得 setter 方法可以为链式调用。

3.测试代码:

@Test
public void demo() {
    // 链式调用
    Computer computer = new MacBookBuilder().setBoard("Intel").setDisplay("Retina").setOS().create();
    System.out.println("Computer info:" + computer.toString());
}

打印结果:Computer info:Computer{board=’Intel’, display=’Retina’, os=’mac OS Sierra’}

作者:smartbetter 发表于2017/4/3 0:02:51 原文链接
阅读:164 评论:0 查看评论

Android实战——RxJava2+Retrofit+RxBinding解锁各种新姿势

$
0
0

RxJava2+Retrofit+RxBinding解锁各种新姿势


本篇文章内容包含以下内容

  • 前言
  • RxJava2的基本介绍
  • RxJava2观察者模式的介绍
  • RxJava2观察者模式的使用
  • RxJava2的基本使用
    • 模拟发送验证码
  • RxJava2与Retrofit的使用
    • 模拟用户登陆获取用户数据
    • 合并本地与服务器购物车列表
  • RxJava2与RxBinding的使用
    • 优化搜索请求
    • 优化点击请求
  • 源码下载
  • 结语

前言

作为主流的第三方框架Rx系列,不学习也不行啊,对于初学者来说,可能RxJava看起来很难,用起来更难,但是你要知道,越复杂的东西往往能解决越复杂的问题,有可能你应用在项目中,也许你在面试的时候,就会和初级工程师拉开一大段距离。这门课程需要大家有Retrofit的基础,如果想学习Retrofit的同学可以查看我的博客,废话不多说,Hensen老师开车了。

RxJava2的介绍

用原话就是:RxJava2是一个在Java虚拟机上,使用可观察的序列构成基于事件的,异步的程序库。不理解没关系,可以类比成我们的AsyncTask,这样就好理解多了

RxJava传送门:https://github.com/ReactiveX/RxJava

RxJava2观察者模式的介绍

观察者模式就是RxJava使用的核心点,掌握这个模式,可以理解RxJava更简单,观察者模式简单的说就是”订阅-发布”的模式,举一例子说,当你订阅某家牛奶店的早餐奶(订阅过程),只要牛奶店生产牛奶,便会给你送过去(发布过程)。这里的牛奶店只有一家,但是订阅的人可以很多,这是一种一对多的关系,只要牛奶店发布牛奶,那么订阅的人就会收到牛奶。换做RxJava里面的话,牛奶店就是被观察者(Observable),订阅的人就是观察者(Observer)

RxJava2观察者模式的使用

这里我们举一例子学校点名的例子,首先创建我们所说的观察者和被观察者

public interface Observable {
    //订阅
    public void attach(Observer observer);
    //取消订阅
    public void detach(Observer observer);
    //发布
    public void notifyObservers(String message);
}
public interface Observer {
    //给个名字来分辨不同的观察者
    void setName(String name);
    //观察者的方法
    void say(String message);
}

各位可以思考一下,根据上面的介绍,学生和老师,谁是观察者,谁是被观察者,下面就看代码给你分析

public class Teather implements Observable {

    private List<Observer> observers = new ArrayList<>();

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.say(message);
        }
    }
}
public class Student implements Observer {

    private String name;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void say(String message) {
        System.out.println(message + ":" + this.name + "到");
    }
}

通过代码可以看到,注意分别实现的不同接口

1、老师是被观察者,他需要实现接口的方法

  • 订阅/取消订阅:往集合中存放/移除观察者
  • 发布:循环遍历观察者,调用观察者方法

2、学生是观察者,那么我们只需要给他个名字,实现观察者的方法即可

最后,我们就把观察者和被观察者关联起来,LessonStart (上课啦)

public class LessonStart {

    public static void main(String[] args){

        Observable teather = new Teather();

        Observer xiaoming = new Student();
        xiaoming.setName("小明");
        Observer xiaohong = new Student();
        xiaohong.setName("小红");

        teather.attach(xiaoming);
        teather.attach(xiaohong);

        teather.notifyObservers("点名啦");
    }
}

代码很简单,我们模拟了一个老师和小明同学和小红同学,老师已经知道看到两个人来了,那么可以开始点名了,下面通过Log打印出信息

点名啦:小明到
点名啦:小红到

RxJava2的基本使用

首先我先贴出我们后面所用到的第三方依赖库,以免后面忘记说了,大家对号入座

//retrofit
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.2.0'
//rx+retrofit
compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
//rxjava
compile "io.reactivex.rxjava2:rxjava:2.0.8"
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
//rxbinding
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-recyclerview-v7:2.0.0'

其次还需要添加联网权限

<uses-permission android:name="android.permission.INTERNET" />

最后我们回到正题,看完上面的例子,我们可以知道RxJava就是这种订阅和发布的模式,换成我们的RxJava代码应该是怎么样的?当然也是通过被观察者订阅观察者啦

//拿到被观察者
Observable<String> observable = getObservable();
//拿到观察者
Observer<String> observer = getObserver();
//订阅
observable.subscribe(observer);

我们具体被观察者和观察者的实现,当然是创建出来啦

public Observable<String> getObservable() {
       return Observable.create(new ObservableOnSubscribe<String>() {
           @Override
           public void subscribe(@NonNull ObservableEmitter<String> e) throws Exception {
               e.onNext("俊俊俊很帅");
               e.onNext("你值得拥有");
               e.onNext("取消关注");
               e.onNext("但还是要保持微笑");
               e.onComplete();
           }
       });
}

onNext方法就是我们的发布过程,我们看其观察者的创建就知道怎么回事了

public Observer<String> getObserver() {
    return new Observer<String>() {

        Disposable disposable = null;

        @Override
        public void onSubscribe(Disposable d) {
            disposable = d;
        }

        @Override
        public void onNext(String s) {
            Log.e("onNext", s);
            if (s.equals("取消关注")) {
                //断开订阅
                disposable.dispose();
            }
        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onComplete() {
            Log.e("onComplete", "onComplete");
        }
    };
}

我们可以发现,观察者的创建实现的方法,在被观察者中是对应起来的,也就是说,我们发布了什么,就可以在观察者中收到订阅信息,那么我们就可以在代码中编写我们的逻辑了,这样基本上已经使用好了RxJava了,通过Log打印出信息

1、人类就喜欢酷炫,炫耀,当然RxJava也少不了人类这种心理,就是链式编程,下面这段代码可以完美替代上面的所有代码

//创建被观察者
Observable.create(new ObservableOnSubscribe<String>() {
    @Override
    //默认在主线程里执行该方法
    public void subscribe(@NonNull ObservableEmitter<String> e) throws Exception {
        e.onNext("俊俊俊很帅");
        e.onNext("你值得拥有");
        e.onNext("取消关注");
        e.onNext("但还是要保持微笑");
        e.onComplete();
    }
})
//将被观察者切换到子线程
.subscribeOn(Schedulers.io())
//将观察者切换到主线程
.observeOn(AndroidSchedulers.mainThread())
//创建观察者并订阅
.subscribe(new Observer<String>() {
    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onNext(String s) {
        Log.e("onNext", s);
    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onComplete() {

    }
});

这里我多写了两个方法,也就是.subscribeOn(Schedulers.io())和.observeOn(AndroidSchedulers.mainThread()),这里就是RxJava的好处之一,他可以手动切换线程,这两个方法在这里表示被观察者创建实现的方法都放在io线程也就是子线程,因为在被观察者中通常会调用网络数据请求,那么网络请求必须在子线程运行,当网络请求收到后,则发布出去,在观察者中通过TextView等控件展示在界面上,那么UI的更新必须在主线程进行,也就是我们上面的代码mainThread。如果你不深造RxJava,基本上这两个方法已经成了固定的写法,这也是很多初学者忘记添加上去的方法

2、久而久之,人类喜欢简洁,喜欢定制服务,巧了,RxJava也给你满足了,下面这段代码中,实现的方法跟上面的实现方法是对应起来的,大家看参数就知道哪个对应哪个了,你可以通过new Consumer,不需要实现的方法你可以不写,看上去更简洁,这里我为了方便大家看,都new出来了,Consumer就是消费者的意思,可以理解为消费了onNext等等等事件

Observable<String> observable = getObservable();
observable.subscribe(new Consumer<String>() {
    @Override
    public void accept(@NonNull String s) throws Exception {
        Log.e("accept", s);
    }
}, new Consumer<Throwable>() {
    @Override
    public void accept(@NonNull Throwable throwable) throws Exception {

    }
}, new Action() {
    @Override
    public void run() throws Exception {

    }
}, new Consumer<Disposable>() {
    @Override
    public void accept(@NonNull Disposable disposable) throws Exception {

    }
});

哦,对了,我们还忘记打印Log信息,不能否认了我很帅这个事实

04-03 01:32:48.445 13512-13512/com.handsome.boke2 E/onNext: 俊俊俊很帅
04-03 01:32:48.446 13512-13512/com.handsome.boke2 E/onNext: 你值得拥有
04-03 01:32:48.446 13512-13512/com.handsome.boke2 E/onNext: 取消关注

当然你觉得只要夸奖我一个帅就行了,那么你也可以通过下面这几种方法发送给观察者

public Observable<String> getObservable() {
    //1、可发送对应的方法
    return Observable.create(new ObservableOnSubscribe<String>() {
        @Override
        public void subscribe(@NonNull ObservableEmitter<String> e) throws Exception {
            e.onNext("俊俊俊很帅");
            e.onNext("你值得拥有");
            e.onNext("取消关注");
            e.onNext("但还是要保持微笑");
            e.onComplete();
        }
    });
    //2、发送多个数据
    return Observable.just("俊俊俊很帅","你值得拥有","取消关注","但还是要保持微笑");
    //3、发送数组
    return Observable.fromArray("俊俊俊很帅","你值得拥有","取消关注","但还是要保持微笑");
    //4、发送一个数据
    return Observable.fromCallable(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "俊俊俊很帅";
        }
    });
}

模拟发送验证码

这里的案例使用我们平时最简单的需求,看效果图就知道(图片会卡,效果大家脑补)

这里写图片描述

这里是整个代码的实现思路,我会在代码下面注释一下需要注意的点,代码我就直接贴出来,有句话说得好,成为大神,就必须先学会阅读别人的代码,哈哈哈,我的代码阅读起来应该没问题的吧

public class ButtonEnableActivity extends AppCompatActivity implements View.OnClickListener {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_button_enable);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        final long count = 3;
        Observable.interval(0, 1, TimeUnit.SECONDS)
                .take(count + 1)
                .map(new Function<Long, Long>() {
                    @Override
                    public Long apply(@NonNull Long aLong) throws Exception {
                        return count - aLong;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(@NonNull Disposable disposable) throws Exception {
                        button.setEnabled(false);
                        button.setTextColor(Color.BLACK);
                    }
                })
                .subscribe(new Observer<Long>() {
                    @Override
                    public void onSubscribe(Disposable d) {}
                    @Override
                    public void onNext(Long aLong) {
                        button.setText("剩余" + aLong + "秒");
                    }
                    @Override
                    public void onError(Throwable e) {}
                    @Override
                    public void onComplete() {
                        button.setEnabled(true);
                        button.setTextColor(Color.RED);
                        button.setText("发送验证码");
                    }
                });
    }
}

1、操作符

  • 像这种interval、take、map、observeOn、doOnSubscribe、subscribe都是属于RxJava的操作符,简单的说就是实现某个方法,里面的功能都被包装起来了
  • RxJava支持的操作符很多,很多操作符用起来都简单,但是组合起来就很复杂,功能很强大,具体分类如图所示

这里写图片描述

2、操作符介绍

  • interval:延时几秒,每隔几秒开始执行
  • take:超过多少秒停止执行
  • map:类型转换,由于是倒计时,案例需要将倒计时的数字反过来
  • observeOn:在主线程运行
  • doOnSubscribe:在执行的过程中
  • subscribe:订阅

RxJava2与Retrofit的使用

RxJava与Retrofit的使用,更像我们的AsyncTask,通过网络获取数据然后通过Handler更新UI

模拟用户登陆获取用户数据

人类总是喜欢看图说话,巧了,我给你提供了,我只能拿出我过硬的美工技术给你们画图了

这里写图片描述

① Bean对象

public class UserParam {
    private String param1;
    private String param2;
    public UserParam(String param1, String param2) {
        this.param1 = param1;
        this.param2 = param2;
    }
    public String getParam1() {
        return param1;
    }
    public void setParam1(String param1) {
        this.param1 = param1;
    }
    public String getParam2() {
        return param2;
    }
    public void setParam2(String param2) {
        this.param2 = param2;
    }
    @Override
    public String toString() {
        return new Gson().toJson(this);
    }
}
  • 这里我们采用的是httpbin的一个post接口,各位可以在它的网站试一下,这里的NetBean是通过请求返回的数据,进行GsonFormat生成的
public class NetBean {
    private FormBean form;
    public FormBean getForm() {
        return form;
    }
    public void setForm(FormBean form) {
        this.form = form;
    }
    public static class FormBean {
        private String username;
        private String password;
        public String getPassword() {
            return password;
        }
        public void setPassword(String password) {
            this.password = password;
        }
        public String getUsername() {
            return username;
        }
        public void setUsername(String username) {
            this.username = username;
        }
    }
}
public class UserBean {
    private String username;
    private String passwrod;
    public UserBean(String passwrod, String username) {
        this.passwrod = passwrod;
        this.username = username;
    }
    public String getPasswrod() {
        return passwrod;
    }
    public void setPasswrod(String passwrod) {
        this.passwrod = passwrod;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    @Override
    public String toString() {
        return new Gson().toJson(this);
    }
}

② ApiService

  • 这里返回Observable对象,也就是我们RxJava的被观察者
public interface ApiService {
    @FormUrlEncoded
    @POST("/post")
    Observable<NetBean> getUserInfo(@Field("username")String username, @Field("password")String password);
}

③ RxJava+Retrofit的实现

public class RxLoginActivity extends AppCompatActivity {

    ApiService apiService;
    TextView tv_text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rx_login);
        tv_text = (TextView) findViewById(R.id.tv_text);

        //构建Retrofit
        apiService = new Retrofit.Builder()
                .baseUrl("https://httpbin.org/")
                //rx与Gson混用
                .addConverterFactory(GsonConverterFactory.create())
                //rx与retrofit混用
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()
                .create(ApiService.class);

        //构建RxJava
        UserParam param = new UserParam("hensen", "123456");
        //发送param参数
        Observable.just(param)
                .flatMap(new Function<UserParam, ObservableSource<NetBean>>() {
                    @Override
                    public ObservableSource<NetBean> apply(@NonNull UserParam userParam) throws Exception {
                        //第一步:发送网络请求,获取NetBean
                        return apiService.getUserInfo(userParam.getParam1(), userParam.getParam2());
                    }
                })
                .flatMap(new Function<NetBean, ObservableSource<UserBean>>() {
                    @Override
                    public ObservableSource<UserBean> apply(@NonNull NetBean netBean) throws Exception {
                        UserBean user = new UserBean(netBean.getForm().getUsername(), netBean.getForm().getPassword());
                        //第二步:转换netBean数据为我们需要的UserBean类型
                        return Observable.just(user);
                    }
                })
                //将被观察者放在子线程,将观察者放在主线程
                .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<UserBean>() {
                    @Override
                    public void accept(@NonNull UserBean userBean) throws Exception {
                        //第三步:接收第二步发送过来的数据,进行UI更新
                        tv_text.setText("用户名:" + userBean.getUsername() + "--密码:" + userBean.getPasswrod());
                    }
                });
    }
}

1、Retrofit

  • RxJava与Retrofit一起使用必须在Retrofit上加上这句话addCallAdapterFactory(RxJava2CallAdapterFactory.create())

2、RxJava

  • 我们通过just方式发送数据
  • flatMap方法是用于数据格式转换的方法,其后面的参数UserParam与ObservableSource< NetBean>,参数一表示原数据,参数二表示转换的数据,那么就是通过发送网络参数,转换成网络返回的数据,调用Retrofit

合并本地与服务器购物车列表

这个案例其实就是用户添加购物车的时候,首先会在本地存储一份,然后发现如果没有网络,那么没办法提交到服务器上,只能等下一次有网络的时候采用本地数据库和服务器数据的合并来实现上传到服务器,这里我们就贴RxJava与Retrofit的代码,不贴其他代码了,废话不多说,上图

这里写图片描述

public class CartMegerActivity extends AppCompatActivity implements View.OnClickListener {
    private Button button;
    private ApiService apiService;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart_meger);
        apiService = new Retrofit.Builder()
                .baseUrl("http://httpbin.org/")
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiService.class);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        Observable.merge(getDataForLocal(), getDataForNet()).subscribe(new Observer<List<String>>() {
            @Override
            public void onSubscribe(Disposable d) {}
            @Override
            public void onNext(List<String> strings) {
                for (String str : strings){
                    System.out.println(str);
                }
            }
            @Override
            public void onError(Throwable e) {}
            @Override
            public void onComplete() {
                System.out.println("onComplete");
            }
        });
    }
    private Observable<List<String>> getDataForLocal() {
        List<String> list = new ArrayList<>();
        list.add("购物车物品一");
        list.add("购物车物品二");
        return Observable.just(list);
    }
    private Observable<List<String>> getDataForNet() {
        return Observable.just("购物车物品三").flatMap(new Function<String, ObservableSource<NetBean>>() {
            @Override
            public ObservableSource<NetBean> apply(@NonNull String s) throws Exception {
                return apiService.getCartList(s);
            }
        }).flatMap(new Function<NetBean, ObservableSource<List<String>>>() {
            @Override
            public ObservableSource<List<String>> apply(@NonNull NetBean netBean) throws Exception {
                String shop = netBean.get_$Args257().getShopName();
                List<String> list = new ArrayList<>();
                list.add(shop);
                return Observable.just(list);
            }
        }).subscribeOn(Schedulers.io());
    }
}

这里使用到merge的操作符,其表示意思就是将两个ObservableSource合并为一个ObservableSource,最后的打印结果是

04-03 02:37:28.840 5615-5615/com.handsome.boke2 I/System.out: 购物车物品一
04-03 02:37:28.840 5615-5615/com.handsome.boke2 I/System.out: 购物车物品二
04-03 02:37:39.501 5615-6337/com.handsome.boke2 I/System.out: 购物车物品三
04-03 02:37:39.501 5615-6337/com.handsome.boke2 I/System.out: onComplete

RxJava2与RxBinding的使用

RxBinding的使用也是为了让界面看起来更简洁,剩去了传统的findViewById和setOnClickListener的方法,不用任何声明,只要添加依赖就可以直接使用了

优化搜索请求

这里的案例是说当我们在EditText打字实时搜索的时候,可能用户会打字很会快,那么我们就没有必要一直发送网络请求,请求搜索结果,我们可以通过当用户打字停止后的延时500毫秒再发送搜索请求

public class TextChangeActivity extends AppCompatActivity {
   private  EditText edittext;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_text_change);
        edittext = (EditText) findViewById(R.id.edittext);

        RxTextView.textChanges(edittext)
                //当你敲完字之后停下来的半秒就会执行下面语句
                .debounce(500, TimeUnit.MILLISECONDS)
                //下面这两个都是数据转换
                //flatMap:当同时多个网络请求访问的时候,前面的网络数据会覆盖后面的网络数据
                //switchMap:当同时多个网络请求访问的时候,会以最后一个发送请求为准,前面网路数据会被最后一个覆盖
                .switchMap(new Function<CharSequence, ObservableSource<List<String>>>() {
                    @Override
                    public ObservableSource<List<String>> apply(@NonNull CharSequence charSequence) throws Exception {
                        //网络操作,获取我们需要的数据
                        List<String> list = new ArrayList<String>();
                        list.add("2017年款最新帅哥俊俊俊");
                        list.add("找不到2017年比俊俊俊更帅的人");
                        return Observable.just(list);
                    }
                })
                //网络请求是在子线程的
                .subscribeOn(Schedulers.io())
                //界面更新在主线程
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<String>>() {
                    @Override
                    public void accept(@NonNull List<String> strings) throws Exception {
                        //界面更新,这里用打印替代
                        System.out.println(strings.toString());
                    }
                });
    }
}

操作符

  • RxTextView.textChanges(edittext):Rxbinding用法
  • debounce:表示延时多少秒后执行
  • switchMap:也是数据转换,与flatMap的区别在注释中解释很清楚了

演示效果图

这里写图片描述

优化点击请求

这个案例很简单,当用户一直点击一个按钮的时候,我们不应该一直调用访问网络请求,而是 1秒内,只执行一次网络请求

public class ButtonClickActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_button_click);
        button = (Button) findViewById(R.id.button);
        RxView.clicks(button).throttleFirst(1, TimeUnit.SECONDS)
                .subscribe(new Observer<Object>() {
                    @Override
                    public void onSubscribe(Disposable d) {}
                    @Override
                    public void onNext(Object o) {
                        System.out.println("俊俊俊点击了按钮");
                    }
                    @Override
                    public void onError(Throwable e) {}
                    @Override
                    public void onComplete() {}
                });
    }
}

源码下载

源码下载

结语

听说认真看的同学都会学的很多哦,对于RxJava的操作符还是建议大家通过官网的wiki去深造,只有当RxJava用在项目中的时候,你才会体会它的好处,同时也让你与初级工程师有了一定的距离,像很多RxBus、RxPermission、RxAndroid,很多人会疑问要不要去学习它,毫无疑问是必须学习的,技术是不断更新,只有当你的技术跟上时代的时候,你才有可能和大神聊的津津有味,以上纯属瞎掰,当然有时间我也会去学习Rx系列,如果喜欢我的朋友可以关注我的博客,或者加群大家一起学习吧

作者:qq_30379689 发表于2017/4/3 15:15:20 原文链接
阅读:179 评论:3 查看评论

Unity3D优化技巧系列七

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

上篇博客实现了LoginState的封装,其实作为UI的各个窗口,每个窗口都对应着自己的State也就是状态类。这么多状态类,不能鼓孤立的存在着,它也需要我们封装一个状态管理类去处理所有的状态,它的主要功能是注册状态, 状态之间的切换等操作,该函数继承于单例模式,代码如下所示:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Game.GameState
{
    public enum GameStateType
    {
        GS_Continue,
        GS_Login,
        GS_User,
        GS_Lobby,
        GS_Room,
        GS_Hero,
        GS_Loading,
        GS_Play,
        GS_Over,
    }

    public class GameStateManager : Singleton<GameStateManager>
    {
        Dictionary<GameStateType, IGameState> gameStates;
        IGameState currentState;

        public GameStateManager()
        {
            gameStates = new Dictionary<GameStateType, IGameState>();

            IGameState gameState;

            gameState = new LoginState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new UserState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new LobbyState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new RoomState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new HeroState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new LoadingState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new PlayState();
            gameStates.Add(gameState.GetStateType(), gameState);

            gameState = new OverState();
            gameStates.Add(gameState.GetStateType(), gameState);
        }

        public IGameState GetCurState()
        {
            return currentState;
        }

        public void ChangeGameStateTo(GameStateType stateType)
        {
            if (currentState != null && currentState.GetStateType() != GameStateType.GS_Loading && currentState.GetStateType() == stateType)
                return;

            if (gameStates.ContainsKey(stateType))
            {
                if (currentState != null)
                {
                    currentState.Exit();
                }

                currentState = gameStates[stateType];
                currentState.Enter();
            }
        }

        public void EnterDefaultState()
        {
            ChangeGameStateTo(GameStateType.GS_Login);
        }

        public void FixedUpdate(float fixedDeltaTime)
        {
            if (currentState != null)
            {
                currentState.FixedUpdate(fixedDeltaTime);
            }
        }

        public void Update(float fDeltaTime)
        {
            GameStateType nextStateType = GameStateType.GS_Continue;
            if (currentState != null)
            {
                nextStateType = currentState.Update(fDeltaTime);
            }

            if (nextStateType > GameStateType.GS_Continue)
            {
                ChangeGameStateTo(nextStateType);
            }
        }

        public IGameState getState(GameStateType type)
        {
            if (!gameStates.ContainsKey(type))
            {
                return null;
            }
            return gameStates[type];
        }
    }
}

该类首先定一个枚举用于表示各个状态,然后实现了状态之间的切换功能。接下来我们开始使用我们编写的脚步,实现起来比较简单,新建一个i额场景用于逻辑的编写,定义一个类比如GameLogic,在它的Awake函数和Start函数中分别调用接口如下:

void Awake(){
        WindowManager.Instance.ChangeScenseToLogin(EScenesType.EST_None);
    }

	// Use this for initialization
	void Start () {
        GameStateManager.Instance.EnterDefaultState();
	}

另外在它的Update函数中要进行状态的更新,函数代码如下所示:

       //更新游戏状态机
        GameStateManager.Instance.Update(Time.deltaTime);
        //UI更新
        WindowManager.Instance.Update(Time.deltaTime);

这样就可以实现MVC模式,并将其应用到项目开发中去,各个模块之间使用的是封装的消息传递,降低了模块之间的耦合性。我们使用该模式实现了多款游戏,总体上感觉还是不错的。
作者:jxw167 发表于2017/4/3 20:45:43 原文链接
阅读:17 评论:0 查看评论

【iOS沉思录】NSThread、GCD、NSOperation多线程编程总结

$
0
0

OC中的多线程

OC中多线程根据封装程度可以分为三个层次:NSThreadGCDNSOperation,另外由于OC兼容C语言,因此仍然可以使用C语言的POSIX接口来实现多线程,只需引入相应的头文件:#include <pthread.h>

NSThread

NSThread是封装程度最小最轻量级的,使用更灵活,但要手动管理线程的生命周期、线程同步和线程加锁等,开销较大;

NSThread的基本使用比较简单,可以动态创建初始化NSThread对象,对其进行设置然后启动;也可以通过NSThread的静态方法快速创建并启动新线程;此外NSObject基类对象还提供了隐式快速创建NSThread线程的performSelector系列类别扩展工具方法;NSThread还提供了一些静态工具接口来控制当前线程以及获取当前线程的一些信息。

下面以在一个UIViewController中为例展示NSThread的使用方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    /** NSThread静态工具方法 **/
    /* 1 是否开启了多线程 */
    BOOL isMultiThreaded = [NSThread isMultiThreaded];
    /* 2 获取当前线程 */
    NSThread *currentThread = [NSThread currentThread];
    /* 3 获取主线程 */
    NSThread *mainThread = [NSThread mainThread];
    NSLog(@"main thread");
    /* 4 睡眠当前线程 */
    /* 4.1 线程睡眠5s钟 */
    [NSThread sleepForTimeInterval:5];
    /* 4.2 线程睡眠到指定时间,效果同上 */
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    /* 5 退出当前线程,注意不要在主线程调用,防止主线程被kill掉 */
    //[NSThread exit];
    NSLog(@"main thread");

    /** NSThread线程对象基本创建,target为入口函数所在的对象,selector为线程入口函数 **/
    /* 1 线程实例对象创建与设置 */
    NSThread *newThread= [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    /* 设置线程优先级threadPriority(0~1.0),即将被抛弃,将使用qualityOfService代替 */
    newThread.threadPriority = 1.0;
    newThread.qualityOfService = NSQualityOfServiceUserInteractive;
    /* 开启线程 */
    [newThread start];
    /* 2 静态方法快速创建并开启新线程 */
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"block run...");
    }];

    /** NSObejct基类隐式创建线程的一些静态工具方法 **/
    /* 1 在当前线程上执行方法,延迟2s */
    [self performSelector:@selector(run) withObject:nil afterDelay:2.0];
    /* 2 在指定线程上执行方法,不等待当前线程 */
    [self performSelector:@selector(run) onThread:newThread withObject:nil waitUntilDone:NO];
    /* 3 后台异步执行函数 */
    [self performSelectorInBackground:@selector(run) withObject:nil];
    /* 4 在主线程上执行函数 */
    [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
}

- (void)run {
    NSLog(@"run...");
}

GCD大中央调度

GCD(Grand Central Dispatch),又叫大中央调度,对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口,使用更加简单高效,也是苹果推荐的方式。

对于GCD多线程编程的理解需要结合实例和实践去体会、总结,网上有一篇国外的GCD详细教程,结合案例通俗易懂,可以帮助快速掌握,文章地址为:https://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1,另外国内有业界人士对其做了翻译,中文版地址为:https://github.com/nixzhu/dev-blog/blob/master/2014-04-19-grand-central-dispatch-in-depth-part-1.md,教程分两部分,此处给出第一部分地址,第二部分可因此找到。这里对其进行进一步的总结提炼,整理出如下必会内容,帮助快速掌握使用:

  • 同步dispatch_sync与异步dispatch_async任务派发
  • 串行队列与并发队列dispatch_queue_t
  • dispatch_once_t只执行一次
  • dispatch_after延后执行
  • dispatch_group_t组调度

两个关键概念

串行与并发(Serial和Concurrent):

这个概念在创建操作队列的时候有宏定义参数,用来指定创建的是串行队列还是并行队列。

串行指的是队列内任务一个接一个的执行,任务之间要依次等待不可重合,且添加的任务按照先进先出FIFO的顺序执行,但并不是指这就是单线程,只是同一个串行队列内的任务需要依次等待排队执行避免出现竞态条件,但仍然可以创建多个串行队列并行的执行任务,也就是说,串行队列内是串行的,串行队列之间仍然是可以并行的,同一个串行队列内的任务的执行顺序是确定的(FIFO),且可以创建任意多个串行队列;

并行指的是同一个队列先后添加的多个任务可以同时并列执行,任务之间不会相互等待,且这些任务的执行顺序执行过程不可预测。

**同步和异步任务派发(Synchronous和Asynchronous):**GCD多线程编程时经常会使用dispatch_async和dispatch_sync函数往指定队列中添加任务块,区别就是同步和异步。同步指的是阻塞当前线程,要等添加的耗时任务块block完成后,函数才能返回,后面的代码才可以继续执行。如果在主线上,则会发生阻塞,用户会感觉应用不响应,这是要避免的。而有时需要使用同步任务的原因是想保证先后添加的任务要按照编写的逻辑顺序依次执行;异步指的是将任务添加到队列后函数立刻返回,后面的代码不用等待添加的任务完成返回即可继续执行。

dispatch_syncdispatch_async

通过下面的代码比较异步和同步任务的区别:

/* 1. 提交异步任务 */
NSLog(@"开始提交异步任务:");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /* 耗时任务... */
    [NSThread sleepForTimeInterval:10];
});
NSLog(@"异步任务提交成功!");

/* 2. 提交同步任务 */
NSLog(@"开始提交同步任务:");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /* 耗时任务... */
    [NSThread sleepForTimeInterval:10];
});
NSLog(@"同步任务提交成功!");

打印结果:

2017-02-28 16:01:44.643 SingleView[19100:708069] 开始提交异步任务:
2017-02-28 16:01:44.643 SingleView[19100:708069] 异步任务提交成功!
2017-02-28 16:01:44.644 SingleView[19100:708069] 开始提交同步任务:
2017-02-28 16:01:54.715 SingleView[19100:708069] 同步任务提交成功!

通过打印结果的时间可以看出,异步任务提交后立即就执行下一步打印提交成功了,不会阻碍当前线程,提交的任务会在后台去执行;而提交同步任务要等到提交的后台任务结束后才可以继续执行当前线程的下一步。此处在主线程上添加的同步任务就会阻塞主线程,导致后面界面的显示要延迟,影响用户体验。

dispatch_queue_t
操作队列主要有两种,并发队列和串行队列,它们的区别上面已经提到,具体创建的方法很简单,要提供两个参数,一个是标记该自定义队列的唯一字符串,另一个是指定串行队列还是并发队列的宏参数:

/* 创建一个并发队列 */
dispatch_queue_t concurrent_queue = dispatch_queue_create("demo.gcd.concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
/* 创建一个串行队列 */
dispatch_queue_t serial_queue = dispatch_queue_create("demo.gcd.serial_queue", DISPATCH_QUEUE_SERIAL);

另外GCD还提供了几个常用的全局队列以及主队列,获取方法如下:

// 获取主队列(在主线程上执行)
dispatch_queue_t main_queue = dispatch_get_main_queue();
// 获取不同优先级的全局队列(优先级从高到低)
dispatch_queue_t queue_high = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue_low = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t queue_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_once_t
这个函数控制指定代码只会被执行一次,常用来实现单例模式,这里以单例模式实现的模板代码为例展示dispatch_once_t的用法,其中的实例化语句只会被执行一次:

+ (instancetype *)sharedInstance {
    static dispatch_once_t once = 0;
    static id sharedInstance = nil;
    dispatch_once(&once, ^{
        // 只实例化一次
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

dispatch_after
通过该函数可以让要提交的任务在从提交开始后的指定时间后执行,也就是定时延迟执行提交的任务,使用方法很简单:

    // 定义延迟时间:3s
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
    dispatch_after(delay, dispatch_get_main_queue(), ^{
        // 要执行的任务...
    });

dispatch_group_t

组调度可以实现等待一组操作都完成后执行后续操作,典型的例子是大图片的下载,例如可以将大图片分成几块同时下载,等各部分都下载完后再后续将图片拼接起来,提高下载的效率。使用方法如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*操作1 */ });
dispatch_group_async(group, queue, ^{ /*操作2 */ });
dispatch_group_async(group, queue, ^{ /*操作3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 后续操作...
});

同步代码到主线程

对于UI的更新代码,必须要在主线程上执行才会及时有效,当当前代码不在主线程时,需要将UI更新的部分代码单独同步到主线程,同步的方法有三种,可以使用NSThread类的performSelectorOnMainThread方法或者NSOperationQueue类的mainQueue主队列来进行同步,但推荐直接使用GCD方法:

dispatch_async(dispatch_get_main_queue(), ^{
        // UI更新代码...
    });

NSOperation

NSOperation是基于GCD的一个抽象基类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,但比GCD可控性更强,例如可以加入操作依赖(addDependency)、设置操作队列最大可并发执行的操作个数(setMaxConcurrentOperationCount)、取消操作(cancel)等。NSOperation作为抽象基类不具备封装我们的操作的功能,需要使用两个它的实体子类:NSBlockOperation和NSInvocationOperation,或者继承NSOperation自定义子类。

NSBlockOperation和NSInvocationOperation用法的主要区别是:前者执行指定的方法,后者执行代码块,相对来说后者更加灵活易用。NSOperation操作配置完成后便可调用start函数在当前线程执行,如果要异步执行避免阻塞当前线程则可以加入NSOperationQueue中异步执行。他们的简单用法如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    /* NSInvocationOperation初始化 */
    NSInvocationOperation *invoOpertion = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [invoOpertion start];

    /* NSBlockOperation初始化 */
    NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSBlockOperation");
    }];
    [blkOperation start];
}

- (void)run {
    NSLog(@"NSInvocationOperation");
}

另外NSBlockOperation可以后续继续添加block执行块,操作启动后会在不同线程并发的执行这些执行快:

/* NSBlockOperation初始化 */
NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperationA");
}];
[blkOperation addExecutionBlock:^{
    NSLog(@"NSBlockOperationB");
}];
[blkOperation addExecutionBlock:^{
    NSLog(@"NSBlockOperationC");
}];
[blkOperation start];
2017-04-04 11:27:02.805 SingleView[12008:3666657] NSBlockOperationB
2017-04-04 11:27:02.805 SingleView[12008:3666742] NSBlockOperationC
2017-04-04 11:27:02.805 SingleView[12008:3666745] NSBlockOperationA

另外说了NSOperation的可控性比GCD要强,其中一个非常重要的特性是可以设置各操作之间的依赖,即强行规定操作A要在操作B完成之后才能开始执行,成为操作A依赖于操作B:

/* 获取主队列(主线程) */
NSOperationQueue *queue = [NSOperationQueue mainQueue];
/* 创建a、b、c操作 */
NSOperation *c = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"OperationC");
}];
NSOperation *a = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"OperationA");
}];
NSOperation *b = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"OperationB");
}];
/* 添加操作依赖,c依赖于a和b,这样c一定会在a和b完成后才执行,即顺序为:A、B、C */
[c addDependency:a];
[c addDependency:b];
/* 添加操作a、b、c到操作队列queue(特意将c在a和b之前添加) */
[queue addOperation:c];
[queue addOperation:a];
[queue addOperation:b];

NSBlockOperation和NSInvocationOperation可以满足多数情况下的编程需求,如果需求特殊则需要继承NSOperation类自定义子类来更加灵活的实现。

常见面试题


问题:什么是线程?它与进程有什么区别?为什么要使用多线程?

线程是指程序在执行过程中,能够执行程序代码的一个执行单元。线程主要有四种状态:运行、就绪、挂起、结束。

进程是指一段正在执行的程序。而线程有时候也被称为轻量级进程,是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间,进程与线程的关系如下图所示:

这里写图片描述

在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行,那么为什么要使用多线程呢?其实,多线程的使用为程序研发带来了巨大的便利,具体而言,有以下几个方面的内容:

  1. 使用多线程可以减少程序的响应时间。在单线程(单线程指的是程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序)的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,使得程序具备了更好的交互性。
  2. 与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
  3. 多CPU或多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
  4. 使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。

问题: 列举Cocoa中常见的几种多线程的实现,并谈谈多线程安全的几种解决办法,一般什么地方会用到多线程?

问的是三个层次的多线程编程实现;线程锁的使用;

  • 只在主线程刷新访问UI
  • 如果要防止资源抢夺,得用synchronized进行加锁保护
  • 如果异步操作要保证线程安全等问题, 尽量使用GCD(有些函数默认 就是安全的)

问题: OC中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么?


问题: 在iphone上有两件事情要做,请问是在一个线程里按顺序做效率高还是两个线程里做效率高?为什么?

这里的效率高指的是时间上效率高,也就是希望在最短的时间内完成所有任务,两件事情按顺序做意味着串行执行,第二件事情要等待第一件事情结束后才可开始,效率相对很低。但如果利用两个线程让两件事情能够并发执行,则时间上效率会大大提高。


问题: runloop是什么?在主线程中的某个函数里调用了异步函数,怎样block当前线程,且还能响应当前线程的timer事件,touch事件等?


问题: 对比在OS X和iOS中实现并发性的不同方法。

在iOS中有三种方法可以实现并发性:threads、dispatch queues和operation queues。

threads的缺点是开发者要自己创建合适的并发解决方案,要决定创建多少线程并且要根据情况动态调整线程的数量。并且应用还要为创建和维护线程承担主要代价, 因此OS X和IOS系统并不是依靠线程来解决并发问题的,而是采用了异步设计的途径。

其中一个开启异步任务的技术是GCD(Grand Central Dispatch),让系统层次来对线程进行管理。开发者要做的就是定义要执行的任务并将之添加到合适的dispatch分派队列。GCD会负责创建需要的线程并安排任务在这些线程上运行。所有的dispatch队列都是先进先出(FIFO)队列结构,所以任务的执行顺序是和添加顺序是一致的。

operation queues和并发dispatch队列一样都是通过Cocoa框架的NSOperationQueue类实现的,不过它并不一定是按照先进先出的顺序执行任务的,而是支持创建更复杂的执行顺序图来管理任务的执行顺序。


问题: 操作队列(NSOperation queue)是什么?解释NSOperationQueue串行操作队列和并行操作队列的区别。


问题: 用户下载一个图片,图片很大,需要分成很多份进行下载,使用GCD应该如何实现?使用什么队列?

使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合并图片
});

问题:Dispatch_barrier_(a)sync的作用?

通过Dispatch_barrier_async添加的操作会暂时阻塞当前队列,即等待前面的并发操作都完成后执行该阻塞操作,待其完成后后面的并发操作才可继续。可以将其比喻为一座霸道的独木桥,是并发队列中的一个并发障碍点,临时阻塞并独占。

可见使用Dispatch_barrier_async可以实现类似dispatch_group_t组调度的效果,同时主要的作用是避免数据竞争,高效访问数据。

/* 创建并发队列 */
dispatch_queue_t concurrentQueue = dispatch_queue_create("test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
/* 添加两个并发操作A和B,即A和B会并发执行 */
dispatch_async(concurrentQueue, ^(){
    NSLog(@"OperationA");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"OperationB");
});
/* 添加barrier障碍操作,会等待前面的并发操作结束,并暂时阻塞后面的并发操作直到其完成 */
dispatch_barrier_async(concurrentQueue, ^(){
    NSLog(@"OperationBarrier!");
});
/* 继续添加并发操作C和D,要等待barrier障碍操作结束才能开始 */
dispatch_async(concurrentQueue, ^(){
    NSLog(@"OperationC");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"OperationD");
});
2017-04-04 12:25:02.344 SingleView[12818:3694480] OperationB
2017-04-04 12:25:02.344 SingleView[12818:3694482] OperationA
2017-04-04 12:25:02.345 SingleView[12818:3694482] OperationBarrier!
2017-04-04 12:25:02.345 SingleView[12818:3694482] OperationD
2017-04-04 12:25:02.345 SingleView[12818:3694480] OperationC

问题: 用过NSOperationQueue吗?如果用过或者了解的话,为什么要使用 NSOperationQueue?实现了什么?简述它和GCD的区别和类似的地方(提示:可以从两者的实现机制和适用范围来述)。

使用NSOperationQueue用来管理子类化的NSOperation对象,控制其线程并发数目。GCD和NSOperation都可以实现对线程的管理,区别是NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中使用NSOperation的优点是 NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化 NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会使代码更为易读,建议在简单项目中使用。

  • GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本封装

  • GCD只支持FIFO的队列,NSOperationQueue可以很方便地调整执行顺 序、设置最大并发数量

  • NSOperationQueue可以在轻松在Operation间设置依赖关系,而GCD 需要写很多的代码才能实现

  • NSOperationQueue支持KVO,可以监测operation是否正在执行 (isExecuted)、是否结束(isFinished),是否取消(isCanceld) 5> GCD的执行速度比NSOperationQueue快 任务之间不太互相依赖:GCD 任务之间有依赖\或者要监听任务的执行情况:NSOperationQueue


问题: 在使用GCD以及block时要注意些什么?它们两是一回事儿么?block在ARC中和传统的MRC中的行为和用法有没有什么区别,需要注意些什么?

使用block是要注意,若将block做函数参数时,需要把它放到最后,GCD是Grand Central Dispatch,是一个对线程开源类库,而Block是闭包,是能够读取其他函数内部变量的函数。


问题: 在项目什么时候选择使用GCD,什么时候选择 NSOperation?

项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现多线程支持,而接口简单,建议在复杂项目中使用。

项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议在简单项目中使用。


问题:对于a,b,c三个线程,如何使用用NSOpertion和NSOpertionQueue实现执行完a,b后再执行c?

可以通过NSOpertion的依赖特性实现该需求,即让c依赖于a和b,这样只有a和b都执行完后,c才可以开始执行:

/* 获取主队列(主线程) */
NSOperationQueue *queue = [NSOperationQueue mainQueue];
/* 创建a、b、c操作 */
NSOperation *c = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation C Start!");
    // ... ...
}];
NSOperation *a = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation A Start!");
    [NSThread sleepForTimeInterval:3.0];
    NSLog(@"Operation A Done!");
}];
NSOperation *b = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation B Start!");
    [NSThread sleepForTimeInterval:3.0];
    NSLog(@"Operation B Done!");
}];
/* 添加操作依赖,c依赖于a和b */
[c addDependency:a];
[c addDependency:b];
/* 添加操作a、b、c到操作队列queue(特意将c在a和b之前添加) */
[queue addOperation:c];
[queue addOperation:a];
[queue addOperation:b];

打印结果:

2017-03-18 13:51:37.770 SingleView[15073:531745] Operation A Start!
2017-03-18 13:51:40.772 SingleView[15073:531745] Operation A Done!
2017-03-18 13:51:40.775 SingleView[15073:531745] Operation B Start!
2017-03-18 13:51:43.799 SingleView[15073:531745] Operation B Done!
2017-03-18 13:51:43.800 SingleView[15073:531745] Operation C Start!

**问题:**GCD内部是怎么实现的?

  • iOS和OS X的核心是XNU内核,GCD是基于XNU内核实现的

  • GCD的API全部在libdispatch库中

  • GCD的底层实现主要有Dispatch Queue和Dispatch Source

  • Dispatch Queue :管理block(操作)

  • Dispatch Source :处理事件


问题:既然提到GCD,那么问一下在使用GCD以及 block 时要注意些什么?它们两是一回事儿么? block 在 ARC 中和传统的 MRC 中的行为和用法有 没有什么区别,需要注意些什么?

Block的使用注意:

  1. block的内存管理
    • 防止循环retian
    • 非ARC(MRC):__block
    • ARC:__weak__unsafe_unretained

问题:下面代码有什么问题?

int main(int argc, const char * argv[]) {
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
    return 0;
}

main函数中第二句代码在主线程上使用了dispatch_sync同步向主线程派发任务,而同步派发要等到任务完成后才能返回,阻塞当前线程。也就是说执行到此处,主线程被阻塞,同时又要等主线程执行完成该任务,造成主线程自身的等待循环,也就是死锁。程序运行到此处会崩溃。将dispatch_sync改为dispatch_async异步派发任务即可避免死锁,或者将任务派发到其他队列上而不是主队列。


问题: 简单介绍下NSURLConnection类,以及+sendSynchronousRequest:returningResponse:error:与– initWithRequest:delegate:两个方法的区别?

NSURLConnection主要用于网络链接请求,提供了异步和同步两种请求方式,异步请求会新创建一个线程单独用于之后的下载,不会阻塞当前调用的线程;同步请求会阻塞当前调用线程,等待它下载结束,如果在主线程上进行同步请求则会阻塞主线程,造成界面卡顿。

+sendSynchronousRequest:returningResponse:error:是同步请求数据,会阻塞当前的线程,直到request返回response;

–initWithRequest:delegate:是异步请求数据,不会足阻塞当前线程,当数据请求结束后会通过代理回到主线程,并通知它委托的对象。


问题:
UIKit类要在哪一个应用线程上使用?

UIKit的界面类只能在主线程上使用,对界面进行更新,多线程环境中要对界面进行更新必须要切换到主线程上。


问题: 以下代码有什么问题?如何修复?

@interface TTWaitController : UIViewController

@property (strong, nonatomic) UILabel *alert;

@end

@implementation TTWaitController

- (void)viewDidLoad
{
    CGRect frame = CGRectMake(20, 200, 200, 20);
    self.alert = [[UILabel alloc] initWithFrame:frame];
    self.alert.text = @"Please wait 10 seconds...";
    self.alert.textColor = [UIColor whiteColor];
    [self.view addSubview:self.alert];

    NSOperationQueue *waitQueue = [[NSOperationQueue alloc] init];
    [waitQueue addOperationWithBlock:^{
        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
        self.alert.text = @"Thanks!";
    }];
}

@end

@implementation TTAppDelegate

- (BOOL)application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[TTWaitController alloc] init];
    [self.window makeKeyAndVisible];
    return YES;
}

这段代码是想提醒用户等待10s,10s后在标签上显示“Thanks”,但多线程代码部分NSOperationQueue的addOperationWithBlock函数不能保证block里面的语句是在主线程中运行的,UILabel显示文字属于UI更新,必须要在主线程进行,否则会有未知的操作,无法在界面上及时正常显示。

解决方法是将UI更新的代码写在主线程上即可,代码同步到主线程上主要有三种方法:NSThread、NSOperationQueue和GCD,三个层次的多线程都可以获取主线程并同步。

NSThread级主线程同步:

NSOperationQueue *waitQueue = [[NSOperationQueue alloc] init];
[waitQueue addOperationWithBlock:^{
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    // 同步到主线程
    [self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
}];

/**
 * UI更新函数
 */
- (void)updateUI {
    self.alert.text = @"Thanks!";
}

NSOperationQueue级主线程同步:

NSOperationQueue *waitQueue = [[NSOperationQueue alloc] init];
[waitQueue addOperationWithBlock:^{
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    // 同步到主线程
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.alert.text = @"Thanks!";
    }];
}];

GCD级主线程同步:

NSOperationQueue *waitQueue = [[NSOperationQueue alloc] init];
[waitQueue addOperationWithBlock:^{
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    // 同步到主线程
    dispatch_async(dispatch_get_main_queue(), ^{
        self.alert.text = @"Thanks!";
    });
}];
作者:cordova 发表于2017/4/4 12:34:49 原文链接
阅读:212 评论:0 查看评论

Unity3D优化技巧系列八

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

下面给读者讲解在游戏开发中经常使用的FSM有限状态机的实现,有限状态机在状态切换中使用的非常多,比如足球游戏,动作游戏等。角色在游戏场景中经常需要动作的切换,比如Idle,Run,Attack,Skill等,这些技能状态之间的切换,我们通常会使用FSM去处理。我们在游戏中将动作切换和技能切换放在一起使用FSM处理,下面通过代码的方式给读者进行封装处理:

	public interface EntityFSM
	{
		bool CanNotStateChange{set;get;}
		FsmState State { get; }
		void Enter(Ientity entity , float stateLast);
		bool StateChange(Ientity entity , EntityFSM state);
		void Execute(Ientity entity);
		void Exit(Ientity Ientity);
	}

先定义一个抽象类用于具体状态的实现,游戏中的具体状态,我们通过枚举的方式定义如下:

	public enum FsmState 
	{
		FSM_STATE_FREE,
		FSM_STATE_RUN,
       		 FSM_STATE_SING,
		FSM_STATE_RELEASE,
        	FSM_STATE_LEADING,
        	FSM_STATE_LASTING,
		FSM_STATE_DEAD,
       	 	FSM_STATE_ADMOVE,
        	FSM_STATE_FORCEMOVE,
        	FSM_STATE_RELIVE,
        	FSM_STATE_IDLE,
	}
下面告诉大家如何使用我们具体的动作状态切换,具体实现就是继承上面定义的接口:

 public class EntityIdleFSM : EntityFSM
	{
        public static readonly EntityFSM Instance = new EntityIdleFSM();
		
		public FsmState State
		{
			get
            		{
               		 	return FsmState.FSM_STATE_IDLE;
			}
		}
		
		public bool CanNotStateChange{
			set;get;
		}
		
		public bool StateChange(Ientity entity , EntityFSM fsm)
        	{
			return CanNotStateChange;
		}
		
		public void Enter(Ientity entity , float last)
        	{
            		entity.OnEnterIdle();
		}
		
		public void Execute(Ientity entity)
        	{
            		if (EntityStrategyHelper.IsTick(entity, 3.0f))
           	 	{
                		entity.OnFSMStateChange(EntityFreeFSM.Instance);
            		}
		}
		
		public void Exit(Ientity entity){
			
		}
	}
在这个函数中使用了一个接口Ientity,其实它主要实现的是状态之间的切换,下面就把该类的主要功能实现如下所示:

 /// <summary>
        /// 状态改变
        /// </summary>
        /// <param name="fsm"></param>
        /// <param name="last"></param>
        public void OnFSMStateChange(EntityFSM fsm, float last)
        {
            if (this.FSM != null && this.FSM.StateChange(this, fsm))
            {
                return;
            }

            if (this.FSM == fsm && this.FSM != null && this.FSM.State == FsmState.FSM_STATE_DEAD)
            {
                return;
            }
            if (this.FSM != null)
            {
                this.FSM.Exit(this);
            }

            if (this.FSM != null)
                this.RealEntity.FSMStateName = fsm.ToString();

            this.FSM = fsm;
            StrategyTick = Time.time;
            this.FSM.Enter(this, last);
        }

        public void OnFSMStateChange(EntityFSM fsm)
        {
            if (this.FSM != null && this.FSM.StateChange(this, fsm))
            {
                return;
            }

            if (this.FSM == fsm && this.FSM != null && (this.FSM.State == FsmState.FSM_STATE_DEAD))
            {
                return;
            }

            if (this.FSM != null)
            {
                this.FSM.Exit(this);
            }

            this.FSM = fsm;
            if (this.FSM != null)
                this.RealEntity.FSMStateName = fsm.ToString();
            StrategyTick = Time.time;
            this.FSM.Enter(this, 0.0f);
        }

该类还提供了各个动作的接口,实现如下所示:

	public virtual void OnEnterIdle()
        {
            this.RealEntity.PlayerAnimation("idle");
        }

        /// <summary>
        /// Run状态进入时调用
        /// </summary>
        public virtual void OnEnterMove()
        {
        }

这个RealEntity类实现的是具体的动作或者是新动画触发函数封装事例代码如下所示:

	public void PlayerIdleAnimation()
    	{
        	if (this.animation == null)
        	{
           	 	return;
       		 }

        PlayerAnimation("idle");
        this.animation.PlayQueued("free");
    }

	public void PlayerFreeAnimation()
    {
        if (this.animation == null)
        {
            return;
        }

        PlayerAnimation("free");
	}

	public void PlayerRunAnimation(){
		if (this.animation == null) {
			return;
		}
        PlayerAnimation("walk");
	}

技能的接口实现跟这个类似,代码如下所示:

	public class EntityReleaseSkillFSM : EntityFSM
	{
		public static readonly EntityFSM Instance = new EntityReleaseSkillFSM();
		public FsmState State{
			get
			{
				return FsmState.FSM_STATE_RELEASE;
			}
		}
		public bool CanNotStateChange{
			set;get;
		}
		
		public bool StateChange(Ientity entity , EntityFSM fsm){
			return CanNotStateChange;
		}
		
		public void Enter(Ientity entity , float last){
			//Debug.LogError("prepareplayskill enter!");
			entity.OnEntityReleaseSkill();
		}
		
		public void Execute(Ientity entity){
            //entity.OnEntityPrepareAttack ();
		}

		public void Exit(Ientity entity){
		    
		}
	}
这样我们的FSM状态机就实现完成了,使用该框架开发了多款游戏,效果如下:






作者:jxw167 发表于2017/4/4 12:49:16 原文链接
阅读:122 评论:0 查看评论

Android:简单动画效果-淡入淡出播放

$
0
0

 淡入淡出的切换效果很常见呢!

无论是Html5,JQuery,都经常用到呢,

这首Android的动画效果:一个深入,一个淡入淡出




实现起来也很简单:


import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.AlphaAnimation;
import android.widget.Button;
import android.widget.ImageView;

public class MainActivity extends Activity implements OnClickListener {

    private ImageView iv_alpha;
    private AlphaAnimation alphaAnimation;
    private TransitionDrawable transitionDrawable;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btn_play1 = (Button) findViewById(R.id.btn_play1);
        Button btn_play2 = (Button) findViewById(R.id.btn_play2);
        btn_play1.setOnClickListener(this);
        btn_play2.setOnClickListener(this);
        iv_alpha = (ImageView) findViewById(R.id.iv_alpha);
        //一开始先设置透明,这样图片不会显示,等点击按钮时再显示
        iv_alpha.setAlpha(0.0f);
        alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
        alphaAnimation.setDuration(3000);    //深浅动画持续时间
        alphaAnimation.setFillAfter(true);   //动画结束时保持结束的画面
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_play1) {
            iv_alpha.setImageResource(R.drawable.a1);
            iv_alpha.setAlpha(1.0f);
            iv_alpha.setAnimation(alphaAnimation);
            alphaAnimation.start();
        } else if (v.getId() == R.id.btn_play2) {
            //淡入淡出动画需要先设置一个Drawable数组,用于变换图片
            Drawable[] drawableArray = {
                    getResources().getDrawable(R.drawable.a1),
                    getResources().getDrawable(R.drawable.a2)
            };
            transitionDrawable = new TransitionDrawable(drawableArray);
            iv_alpha.setImageDrawable(transitionDrawable);
            transitionDrawable.startTransition(3000);
        }

    }

}


xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="example.com.cartoon_danrudanchu.MainActivity">

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <Button
        android:id="@+id/btn_play1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="播放深入动画"/>
    <Button
        android:id="@+id/btn_play2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="淡入淡出动画"/>

</LinearLayout>
    <ImageView
        android:id="@+id/iv_alpha"
        android:layout_width="260dp"
        android:layout_height="320dp"
        android:layout_gravity="center"
        android:scaleType="fitXY"
        android:layout_marginTop="10dp"/>

</LinearLayout>

感谢:“湖前琴亭” 博主 !



作者:ssh159 发表于2017/4/4 15:33:10 原文链接
阅读:113 评论:0 查看评论

Android设计模式(十七)-代理模式

$
0
0

代理模式也叫委托模式,是结构型设计模式。代理就是让别人帮你做事,比如帮你带饭,请律师打官司什么的。

喵了个呜的小宇宙

定义

为其他对象提供一种代理以控制对这个对象的访问。

使用场景

  • 当一个对象不能或者不想直接访问另一个对象时,可以通过一个代理对象来间接访问。为保证客户端使用的透明性,委托对象和代理对象要实现同样的接口。
  • 被访问的对象不想暴露全部内容时,可以通过代理去掉不想被访问的内容。

UML

  • Subject: 抽象主题类,声明真是主体与代理主题的共同接口方法。
  • RealSubject: 真实主题类,定义了代理所表示的真是对象,执行具体的业务方法。客户端通过代理类来间接的调动这个真实主题中的方法。
  • ProxySubject: 代理类,持有一个真实类的引用,在接口方法中调用真实主题相应的方法,达到代理的作用。

简单实现

就以打官司为例。我们一般人要打官司都要找个律师来代理。

静态代理

先建立一个起诉类的接口:

public interface ILawsuit {
    void submit();//提交申请
    void burden();//进行举证
    void defend();//开始辩护
    void finish();//诉讼完成
}

真正的起诉者:

public class Civilian implements ILawsuit {
    @Override
    public void submit() {
        System.out.println("起诉");
    }

    @Override
    public void burden() {
        System.out.println("举证");
    }

    @Override
    public void defend() {
        System.out.println("辩护");
    }

    @Override
    public void finish() {
        System.out.println("胜诉");
    }
}

找的律师:

public class Lawyer implements ILawsuit {
    private ILawsuit civilian;

    public Lawyer(ILawsuit civilian) {
        this.civilian = civilian;
    }

    @Override
    public void submit() {
        civilian.submit();
    }

    @Override
    public void burden() {
        civilian.burden();
    }

    @Override
    public void defend() {
        civilian.defend();
    }

    @Override
    public void finish() {
        civilian.finish();
    }
}

客户端调用,调用律师的方法,通过律师调用真正的su起诉者的方法。

public class Client {
    public static void main(String[] args) {
        ILawsuit civilian = new Civilian();
        ILawsuit lawyer = new Lawyer(civilian);
        lawyer.submit();
        lawyer.burden();
        lawyer.defend();
        lawyer.finish();
    }
}

输出:

一个代理可以代理多个类,就像这个律师可以给很多人打官司,只需要在实现一个具体的ILawsuit就行了。代理会根据传进来的被代理者调用传进来的被代理者的方法。

动态代理

代理模式大致分为两大部分:静态代理和动态代理。

上面是是一种静态代理,代理者的代码时先生成写好,然后再对其进行编译,在代码运行前,代理类的class编译文件就已经存在了。

动态代理是相反的,通过反射动态的生成代理者对象,也就是说在写代码的时候根本不知道要代理谁,具体代理谁会在执行阶段决定。

Java提供了一个便捷的动态代理接口InvocationHandler,动态代理类只要实现这个接口就行:

public class DynamicProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

看一下动态代理的用法:

public class DynamicProxy implements InvocationHandler {
    private Object object;

    public DynamicProxy(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //当然这里可以对方法名进行判断过滤 if(method.getName().equals("***"))
        Object result = method.invoke(object,args);
        return result;
    }
}

客户端调用:

public class Main {
    public static void main(String[] args) {
        ILawsuit lawsuit = new Civilian();
        DynamicProxy proxy = new DynamicProxy(lawsuit);
        ClassLoader loader = lawsuit.getClass().getClassLoader();
        //动态创建代理类,需要传入一个类加载器ClassLoader;一个你希望这个代理实现的接口列表,这里要代理ILawsuit接口;
        //和一个InvocationHandler的实现,也就是前面创建的proxy。
        ILawsuit lawyer = (ILawsuit) Proxy.newProxyInstance(loader,new Class[]{ILawsuit.class},proxy);
        lawyer.submit();
        lawyer.burden();
        lawyer.defend();
        lawyer.finish();
    }
}

输出和上面一毛一样:

动态代理并不局限与代理一个接口的实现,可以根据运行时传入的接口,动态的生成代理类,然后通过Method的invoke方法来执行被代理类的真实方法。非常灵活。

其他分类

静态代理和动态代理是从code方便进行分类的。这两个分类根据适用范围来分都可以分为下面几种:

  • 远程代理:为摸个对象在不同的内存地址空间提供局部代理,是系统Server部分隐藏,以便Client不用考虑Server的存在。
  • 虚拟代理:如果要创建一个资源消耗较大的对象,可以先用一个代理对象表示,在真正需要的时候才真正创建。
  • 保护代理:用代理对象控制对一个对象的访问,给不同的用户提供不同的访问权限。
  • 智能引用:在引用原始对象的时候附加额外操作,并对指向原始对象的引用增加引用计数。

总结

代理模式使用非常广泛,从分类就能感觉出来,而且其他的设计模式中也会有代理模式的影子。

优点

优点可以从他的适用范围看出来
- 协调调用者和被调用者,降低系统耦合度。
- 用小对象代表大对象,减少系统资源消耗,提高系统运行速度,如虚拟代理。
- 控制用户对呗调用者的使用权限,如保护代理。

缺点

  • 首先当然是比直接调用原始对象多了一个中间者,会让结构有点复杂。
  • 调用原始对象的方法要通过代理来调用,可能会造成请求处理速度变慢。
作者:qq_25806863 发表于2017/4/7 11:51:42 原文链接
阅读:78 评论:0 查看评论

Android打印机--小票打印格式及模板设置

$
0
0

小票打印就是向打印设备发送控制打印格式的指令集,而这些打印格式需要去查询对应打印机的API文档,这里我把常用的api给封装了一下

  • 文字对齐方式
  • 打印字体大小
  • 字体是否加粗
  • 打印二维码
  • 打印条形码
  • 切纸
  • 打开钱箱
  • 字符串转字节数组
  • 字符拼接

PrintFormatUtils.java

/**
 * 打印格式
 * Created by john on 17-3-23.
 */

public class PrintFormatUtils {
    // 对齐方式
    public static final int ALIGN_LEFT = 0;     // 靠左
    public static final int ALIGN_CENTER = 1;   // 居中
    public static final int ALIGN_RIGHT = 2;    // 靠右

    //字体大小
    public static final int FONT_NORMAL = 0;    // 正常
    public static final int FONT_MIDDLE = 1;    // 中等
    public static final int FONT_BIG = 2;       // 大

    //加粗模式
    public static final int FONT_BOLD = 0;              // 字体加粗
    public static final int FONT_BOLD_CANCEL = 1;       // 取消加粗

    /**
     * 打印二维码
     * @param qrCode
     * @return
     */
    public static String getQrCodeCmd(String qrCode) {
        byte[] data;
        int store_len = qrCode.length() + 3;
        byte store_pL = (byte) (store_len % 256);
        byte store_pH = (byte) (store_len / 256);

        // QR Code: Select the model
        //              Hex     1D      28      6B      04      00      31      41      n1(x32)     n2(x00) - size of model
        // set n1 [49 x31, model 1] [50 x32, model 2] [51 x33, micro qr code]
        // https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=140
        byte[] modelQR = {(byte)0x1d, (byte)0x28, (byte)0x6b, (byte)0x04, (byte)0x00, (byte)0x31, (byte)0x41, (byte)0x32, (byte)0x00};

        // QR Code: Set the size of module
        // Hex      1D      28      6B      03      00      31      43      n
        // n depends on the printer
        // https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=141
        byte[] sizeQR = {(byte)0x1d, (byte)0x28, (byte)0x6b, (byte)0x03, (byte)0x00, (byte)0x31, (byte)0x43, (byte)0x08};

        //          Hex     1D      28      6B      03      00      31      45      n
        // Set n for error correction [48 x30 -> 7%] [49 x31-> 15%] [50 x32 -> 25%] [51 x33 -> 30%]
        // https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=142
        byte[] errorQR = {(byte)0x1d, (byte)0x28, (byte)0x6b, (byte)0x03, (byte)0x00, (byte)0x31, (byte)0x45, (byte)0x31};

        // QR Code: Store the data in the symbol storage area
        // Hex      1D      28      6B      pL      pH      31      50      30      d1...dk
        // https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=143
        //                        1D          28          6B         pL          pH  cn(49->x31) fn(80->x50) m(48->x30) d1…dk
        byte[] storeQR = {(byte)0x1d, (byte)0x28, (byte)0x6b, store_pL, store_pH, (byte)0x31, (byte)0x50, (byte)0x30};

        // QR Code: Print the symbol data in the symbol storage area
        // Hex      1D      28      6B      03      00      31      51      m
        // https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=144
        byte[] printQR = {(byte)0x1d, (byte)0x28, (byte)0x6b, (byte)0x03, (byte)0x00, (byte)0x31, (byte)0x51, (byte)0x30};

        data = byteMerger(modelQR, sizeQR);
        data = byteMerger(data, errorQR);
        data = byteMerger(data, storeQR);
        data = byteMerger(data, qrCode.getBytes());
        data = byteMerger(data, printQR);

        return new String(data);
    }

    /**
     * 打印条码
     * @param barcode
     * @return
     */
    public static String getBarcodeCmd(String barcode) {
        // 打印 Code-128 条码时需要使用字符集前缀
        // "{A" 表示大写字母
        // "{B" 表示所有字母,数字,符号
        // "{C" 表示数字,可以表示 00 - 99 的范围


        byte[] data;

        String btEncode;

        if (barcode.length() < 18) {
            // 字符长度小于15的时候直接输出字符串
            btEncode = "{B" + barcode;
        } else {
            // 否则做一点优化

            int startPos = 0;
            btEncode = "{B";

            for (int i = 0; i < barcode.length(); i++) {
                char curChar = barcode.charAt(i);

                if (curChar < 48 || curChar > 57 || i == (barcode.length() - 1)) {
                    // 如果是非数字或者是最后一个字符

                    if (i - startPos >= 10) {
                        if (startPos == 0) {
                            btEncode = "";
                        }

                        btEncode += "{C";

                        boolean isFirst = true;
                        int numCode = 0;

                        for (int j = startPos; j < i; j++) {

                            if (isFirst) { // 处理第一位
                                numCode = (barcode.charAt(j) - 48) * 10;
                                isFirst = false;
                            } else { // 处理第二位
                                numCode += (barcode.charAt(j) - 48);
                                btEncode += (char) numCode;
                                isFirst = true;
                            }

                        }

                        btEncode += "{B";

                        if (!isFirst) {
                            startPos = i - 1;
                        } else {
                            startPos = i;
                        }
                    }

                    for (int k = startPos; k <= i; k++) {
                        btEncode += barcode.charAt(k);
                    }
                    startPos = i + 1;
                }

            }
        }


        // 设置 HRI 的位置,02 表示下方
        byte[] hriPosition = {(byte) 0x1d, (byte) 0x48, (byte) 0x02};
        // 最后一个参数表示宽度 取值范围 1-6 如果条码超长则无法打印
        byte[] width = {(byte) 0x1d, (byte) 0x77, (byte) 0x02};
        byte[] height = {(byte) 0x1d, (byte) 0x68, (byte) 0xfe};
        // 最后两个参数 73 : CODE 128 || 编码的长度
        byte[] barcodeType = {(byte) 0x1d, (byte) 0x6b, (byte) 73, (byte) btEncode.length()};
        byte[] print = {(byte) 10, (byte) 0};

        data = PrintFormatUtils.byteMerger(hriPosition, width);
        data = PrintFormatUtils.byteMerger(data, height);
        data = PrintFormatUtils.byteMerger(data, barcodeType);
        data = PrintFormatUtils.byteMerger(data, btEncode.getBytes());
        data = PrintFormatUtils.byteMerger(data, print);

        return new String(data);
    }

    /**
     * 切纸
     * @return
     */
    public static String getCutPaperCmd() {
        // 走纸并切纸,最后一个参数控制走纸的长度
        byte[] data = {(byte) 0x1d, (byte) 0x56, (byte) 0x42, (byte) 0x15};

        return new String(data);
    }

    /**
     * 对齐方式
     * @param alignMode
     * @return
     */
    public static String getAlignCmd(int alignMode) {
        byte[] data = {(byte) 0x1b, (byte) 0x61, (byte) 0x0};
        if (alignMode == ALIGN_LEFT) {
            data[2] = (byte) 0x00;
        } else if (alignMode == ALIGN_CENTER) {
            data[2] = (byte) 0x01;
        } else if (alignMode == ALIGN_RIGHT) {
            data[2] = (byte) 0x02;
        }

        return new String(data);
    }

    /**
     * 字体大小
     * @param fontSize
     * @return
     */
    public static String getFontSizeCmd(int fontSize) {
        byte[] data = {(byte) 0x1d, (byte) 0x21, (byte) 0x0};
        if (fontSize == FONT_NORMAL) {
            data[2] = (byte) 0x00;
        } else if (fontSize == FONT_MIDDLE) {
            data[2] = (byte) 0x01;
        } else if (fontSize == FONT_BIG) {
            data[2] = (byte) 0x11;
        }

        return new String(data);
    }

    /**
     * 加粗模式
     * @param fontBold
     * @return
     */
    public static String getFontBoldCmd(int fontBold) {
        byte[] data = {(byte) 0x1b, (byte) 0x45, (byte) 0x0};

        if (fontBold == FONT_BOLD) {
            data[2] = (byte) 0x01;
        } else if (fontBold == FONT_BOLD_CANCEL) {
            data[2] = (byte) 0x00;
        }

        return new String(data);
    }

    /**
     * 打开钱箱
     * @return
     */
    public static String getOpenDrawerCmd() {
        byte[] data = new byte[4];
        data[0] = 0x10;
        data[1] = 0x14;
        data[2] = 0x00;
        data[3] = 0x00;

        return new String(data);
    }

    /**
     * 字符串转字节数组
     * @param str
     * @return
     */
    public static byte[] stringToBytes(String str) {
        byte[] data = null;

        try {
            byte[] strBytes = str.getBytes("utf-8");

            data = (new String(strBytes, "utf-8")).getBytes("gbk");
        } catch (UnsupportedEncodingException exception) {
            exception.printStackTrace();
        }

        return data;
    }

    /**
     * 字节数组合并
     * @param bytesA
     * @param bytesB
     * @return
     */
    public static byte[] byteMerger(byte[] bytesA, byte[] bytesB) {
        byte[] bytes = new byte[bytesA.length + bytesB.length];
        System.arraycopy(bytesA, 0, bytes, 0, bytesA.length);
        System.arraycopy(bytesB, 0, bytes, bytesA.length, bytesB.length);
        return bytes;
    }
}

有了打印格式,还要对具体的打印小票设置打印模板,主要就是利用上面的打印格式工具类,进行字符或字符串拼接,设置文字间空格的长度,以及使用换行符换行等。

有些小票打印的内容有可能是通用的,比如底部结束语–可能是公司宣传语或广告语,这些内容是否展示需要根据具体需求加以控制,还有二维码、条形码打印,是否切纸等需要根据实际场景取舍,所以最好封装一个打印配置类,以控制打印内容显示。

/**
 * 打印模板
 */
public class PrintContract {

    /**
     * 打印内容
     */
    public static StringBuilder createXxTxt(String ...) {

        StringBuilder builder = new StringBuilder();

        //设置大号字体以及加粗
        builder.append(PrintFormatUtils.getFontSizeCmd(PrintFormatUtils.FONT_BIG));
        builder.append(PrintFormatUtils.getFontBoldCmd(PrintFormatUtils.FONT_BOLD));

        // 标题
        builder.append("Title");
       //换行,调用次数根据换行数来控制
        addLineSeparator(builder);

        //设置普通字体大小、不加粗
        builder.append(PrintFormatUtils.getFontSizeCmd(PrintFormatUtils.FONT_NORMAL));
        builder.append(PrintFormatUtils.getFontBoldCmd(PrintFormatUtils.FONT_BOLD_CANCEL));

        //内容
        ......

        //设置某两列文字间空格数, x需要计算出来
        addIdenticalStrToStringBuilder(builder, x, " ");

        //切纸
        builder.append(PrintFormatUtils.getCutPaperCmd());

        return builder;
    }

    /**
     * 向StringBuilder中添加指定数量的相同字符
     *
     * @param printCount   添加的字符数量
     * @param identicalStr 添加的字符
     */

    private static void addIdenticalStrToStringBuilder(StringBuilder builder, int printCount, String identicalStr) {
        for (int i = 0; i < printCount; i++) {
            builder.append(identicalStr);
        }
    }

    /**
     * 根据字符串截取前指定字节数,按照GBK编码进行截取
     *
     * @param str 原字符串
     * @param len 截取的字节数
     * @return 截取后的字符串
     */
    private static String subStringByGBK(String str, int len) {
        String result = null;
        if (str != null) {
            try {
                byte[] a = str.getBytes("GBK");
                if (a.length <= len) {
                    result = str;
                } else if (len > 0) {
                    result = new String(a, 0, len, "GBK");
                    int length = result.length();
                    if (str.charAt(length - 1) != result.charAt(length - 1)) {
                        if (length < 2) {
                            result = null;
                        } else {
                            result = result.substring(0, length - 1);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 添加换行符
     */
    private static void addLineSeparator(StringBuilder builder) {
        builder.append("\n");
    }

    /**
     * 在GBK编码下,获取其字符串占据的字符个数
     */
    private static int getCharCountByGBKEncoding(String text) {
        try {
            return text.getBytes("GBK").length;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


   /**
    * 打印相关配置
    */
    public static class PrintConfig {
        public int maxLength = 30;

        public boolean printBarcode = false;  // 打印条码
        public boolean printQrCode = false;   // 打印二维码
        public boolean printEndText = true;   // 打印结束语
        public boolean needCutPaper = false;  // 是否切纸
    }

}

有了打印模板,接下来就是调用打印设备打印方法发送打印指令

//调用打印机打印方法,传入上面某个小票打印模板返回的字符串
String str = PrintContract.createXxTxt(...);
printer.print(str, null);

//打开钱箱方法
printer.print(PrintFormatUtils.getOpenDrawerCmd(), null);
作者:johnWcheung 发表于2017/4/7 16:00:10 原文链接
阅读:364 评论:2 查看评论

Android设计模式(十八)-组合模式

$
0
0

组合模式,也称作部分整体模式。是结构型设计模式之一。组合模式画成图就是数据结构中的树结构,有一个根节点,然后有很多分支。将最顶部的根节点叫做根结构件,将有分支的节点叫做枝干构件,将没有分支的末端节点叫做叶子构件.

定义

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

使用场景

  • 想表示对象的部分-整体层次结构时。
  • 希望用户忽略单个对象和组合对象的不同,对对象使用具有统一性时。
  • 从一个整体中能够独立出部分模块或功能时。

UML

安全的组合模式

  • Component:抽象节点,为组合中的对象声明接口,适当的时候实现所有类的公有接口方法的默认行为。
  • Composite:定义所有枝干节点的行为,存储子节点,实现相关操作。
  • Leaf:叶子节点,没有子节点,实现相关对象的行为。

看一下这个模式的通用代码

抽象的节点:

public abstract class Component {
    protected String name;

    public Component(String name) {
        this.name = name;
    }
    public abstract void doSonthing();
}

枝干节点:

public class Composite extends Component {
    private List<Component> components = new ArrayList<>();
    public Composite(String name) {
        super(name);
    }

    @Override
    public void doSonthing() {
        System.out.println(name);
        if (null!=components){
            for (Component c:components) {
                c.doSonthing();
            }
        }
    }

    public void addChild(Component child){
        components.add(child);
    }
    public void removeChild(Component child){
        components.remove(child);
    }
    public Component getChild(int index){
        return components.get(index);
    }

}

叶子节点:

public class Leaf extends Component {
    public Leaf(String name) {
        super(name);
    }

    @Override
    public void doSonthing() {
        System.out.println(name);
    }
}

客户端调用:

public class CLient {
    public static void main(String[] args) {
        Composite root = new Composite("root");
        Composite branch1 = new Composite("branch1");
        Composite branch2 = new Composite("branch2");
        Composite branch3 = new Composite("branch3");

        Leaf leaf1 = new Leaf("leaf1");
        Leaf leaf2 = new Leaf("leaf2");
        Leaf leaf3 = new Leaf("leaf3");

        branch1.addChild(leaf1);
        branch3.addChild(leaf2);
        branch3.addChild(leaf3);

        root.addChild(branch1);
        root.addChild(branch2);
        root.addChild(branch3);

        root.doSonthing();
    }
}

输出:

我们可以发现在Client使用的时候,根本没用到接口Component。违反了依赖倒置原则。

因为接口中没有定义公共方法,必须使用对应搞得实现节点才能完成相应的操作,叫安全的组合模式。

透明的组合模式

所以就有一种透明的组合模式,所有的节点都包含有同样的结构

抽象的节点:

public abstract class Component {
    protected String name;

    public Component(String name) {
        this.name = name;
    }
    public abstract void doSonthing();

    public abstract void addChild(Component child);
    public abstract void removeChild(Component child);
    public abstract Component getChild(int index);
}

枝干节点:

public class Composite extends Component {
    private List<Component> components = new ArrayList<>();
    public Composite(String name) {
        super(name);
    }

    @Override
    public void doSonthing() {
        System.out.println(name);
        if (null!=components){
            for (Component c:components) {
                c.doSonthing();
            }
        }
    }

    public void addChild(Component child){
        components.add(child);
    }
    public void removeChild(Component child){
        components.remove(child);
    }
    public Component getChild(int index){
        return components.get(index);
    }

}

叶子节点:

public class Leaf extends Component {
    public Leaf(String name) {
        super(name);
    }

    @Override
    public void doSonthing() {
        System.out.println(name);
    }

    @Override
    public void addChild(Component child) {
        throw new UnsupportedOperationException("叶子节点没有子节点");
    }

    @Override
    public void removeChild(Component child) {
        throw new UnsupportedOperationException("叶子节点没有子节点");
    }

    @Override
    public Component getChild(int index) {
        throw new UnsupportedOperationException("叶子节点没有子节点");
    }
}

客户端调用:

public class CLient {
    public static void main(String[] args) {
        Component root = new Composite("root");
        Component branch1 = new Composite("branch1");
        Component branch2 = new Composite("branch2");
        Component branch3 = new Composite("branch3");

        Component leaf1 = new Leaf("leaf1");
        Component leaf2 = new Leaf("leaf2");
        Component leaf3 = new Leaf("leaf3");

        branch1.addChild(leaf1);
        branch3.addChild(leaf2);
        branch3.addChild(leaf3);

        root.addChild(branch1);
        root.addChild(branch2);
        root.addChild(branch3);

        root.doSonthing();
    }
}

输出:

简单实现

以文件夹系统举个例子:

抽象的文件系统:

public abstract class Dir {
    protected List<Dir> dirs = new ArrayList<>();
    private String name;

    public Dir(String name) {
        this.name = name;
    }

    public abstract void addDir(Dir dir);
    public abstract void rmDir(Dir dir);//删除文件或文件夹
    public abstract void clear();//清空所有元素
    public abstract void print();//打印文件夹系统结构
    public abstract List<Dir> getFiles();
    public  String getName(){
        return name;
    }
}

文件夹:

public class Folder extends Dir {
    public Folder(String name) {
        super(name);
    }

    @Override
    public void addDir(Dir dir) {
        dirs.add(dir);
    }

    @Override
    public void rmDir(Dir dir) {
        dirs.remove(dir);
    }

    @Override
    public void clear() {
        dirs.clear();
    }

    @Override
    public void print() {
        //利用递归来输出文件夹结构
        System.out.print(getName()+"(");
        Iterator<Dir> i = dirs.iterator();
        while (i.hasNext()){
            Dir dir = i.next();
            dir.print();
            if (i.hasNext()){
                System.out.print(", ");
            }
        }
        System.out.print(")");
    }

    @Override
    public List<Dir> getFiles() {
        return dirs;
    }
}

文件:

public class File extends Dir {
    public File(String name) {
        super(name);
    }

    @Override
    public void addDir(Dir dir) {
        throw new UnsupportedOperationException("文件不支持此操作");
    }

    @Override
    public void rmDir(Dir dir) {
        throw new UnsupportedOperationException("文件不支持此操作");
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException("文件不支持此操作");
    }

    @Override
    public void print() {
        System.out.print(getName());
    }

    @Override
    public List<Dir> getFiles() {
        throw new UnsupportedOperationException("文件不支持此操作");
    }
}

客户端调用:

public class Client {
    public static void main(String[] args) {
        //创建根目录 root
        Dir root = new Folder("root");
        //root下有个文件log.txt和三个文件夹 system,user,lib;
        root.addDir(new File("log.txt"));
        Dir system = new Folder("system");
        system.addDir(new File("systemlog.txt"));
        root.addDir(system);
        Dir user = new Folder("user");
        user.addDir(new File("usernamelist.txt"));
        root.addDir(user);
        Dir lib = new Folder("lib");
        lib.addDir(new File("libs.txt"));
        root.addDir(lib);
        root.print();
    }
}

输出:

Android源码中的组合模式

组合模式在Android中太常用了,View和ViewGroup就是一种很标准的组合模式:

在Android的视图树中,容器一定是ViewGroup,只有ViewGroup才能包含其他View和ViewGroup。View是没有容器的。者是一种安全的组合模式。

总结

在Android开发中用到组合模式并不很多,组合模式更多的用于界面UI的架构设计上,而这部分让开发者去实现的并不多。

优点

  • 可以清楚定义分层次的复杂对象,表示全部或部分层次,让高层忽略层次的差异,方便对整个层次结构进行控制。
  • 高层模块可以一致的使用一个组合结构或其中的单个对象,不必挂心处理的是单个对象还是整个组合结构,简化了高层模块的代码。
  • 增加新的枝干和叶子构件都很方便,无需对现有类进行任何修改,就像增加一个自定义View一样。
  • 将对象之间的关系形成树形结构,便于控制。

缺点

  • 设计变得更加抽象,因此很难限制组合中的组件,因为他们都来自相同的抽象层。所以必须在运行时进行类型检查才能实现。
作者:qq_25806863 发表于2017/4/7 16:02:34 原文链接
阅读:81 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>