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

hunterliy小作品之 HunterMusic音乐播放器(Day2-后台播放服务实现)

$
0
0

1.1完成音乐播放布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/play_layout"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#e0e0e0"
    tools:context="com.example.administrator.huntermusic.activity.AudioPlayActivity">
    <TextView
        android:id="@+id/music_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_gravity="center"
        android:text="歌曲名"
        android:textSize="20sp"
        android:layout_marginTop="30dp"/>
    <TextView
        android:id="@+id/singer_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="歌手"
        android:layout_margin="5dp"
        android:layout_gravity="center"
        android:gravity="center"/>
    <com.example.administrator.huntermusic.utils.CDView
        android:id="@+id/CD_view"
        android:layout_marginBottom="80dp"
        android:layout_marginTop="50dp"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center">
    </com.example.administrator.huntermusic.utils.CDView>
    <TextView
        android:id="@+id/tv_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="00:00/00:00"
        android:textSize="15sp"
        android:layout_margin="10dp"
        />
    <SeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_margin="20dp"
        android:progress="0"
        android:thumbOffset="0dp"
        />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        >
        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
            <ImageView
                android:id="@+id/play_mode"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                />
            <ImageView
                android:id="@+id/pre"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:background="@drawable/pre"/>
            <ImageView
                android:id="@+id/play"
                android:layout_width="64dp"
                android:layout_height="wrap_content"
                android:layout_margin="10dp"
                />
            <ImageView
                android:id="@+id/next"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:background="@drawable/next"/>
            <ImageView
                android:id="@+id/playlist"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:background="@drawable/playlist"/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

其中的CDView可以先使用一个ImageView来代替,之后我们再慢慢完善。最终效果如下:
这里写图片描述

1.2播放音乐

播放音乐需要在服务中播放,因为在退出界面的时候,音乐还在播放,这导致音乐的不可控制。所以为了便于控制我们将播放的逻辑放在服务中。那么就需要将数据也传递到服务中。

AudioPlayActivity .java

package com.example.administrator.huntermusic.activity;

import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.res.Resources;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.Window;


public class AudioPlayActivity extends AppCompatActivity implements View.OnClickListener{

    private AudioServiceConnection audioServiceConnection;
    private AudioPlayService.AudioBinder binder;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio_play);
        initData();
        initView();

    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
        unbindService(audioServiceConnection);

    }

    public void initData(){
        Intent service = new Intent(getIntent());
        service.setClass(this, AudioPlayService.class);
        audioServiceConnection = new AudioServiceConnection();
        bindService(service, audioServiceConnection, BIND_AUTO_CREATE);
        startService(service);
    }





    public class AudioServiceConnection implements ServiceConnection {
        @Override
        public void onServiceConnected(ComponentName name, IBinder iBinder) {
               binder = (AudioPlayService.AudioBinder) iBinder;
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    }
}

在服务中播放音乐

AudioPlayService .java

package com.example.administrator.huntermusic.service;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import com.example.administrator.huntermusic.bean.AudioItem;
import java.io.IOException;


public class AudioPlayService extends Service {
    private AudioBinder binder;
    private List<AudioItem> audioItemList;
    private int position;

    @Override
    public IBinder onBind(Intent intent) {
        binder = new AudioBinder();
        return binder;
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        audioItemList = (List<AudioItem>) intent.getSerializableExtra("audioItems");
        position = intent.getIntExtra("position", -1);
        binder.play();
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }



    public class AudioBinder extends Binder{
        private MediaPlayer media_player;
        public void play(){
            if (media_player!=null){
                media_player.reset();
                media_player.release();
                media_player  = null;

            }
            AudioItem audioItem = audioItemList.get(position);
            media_player = new MediaPlayer();
            backIsPlay = true;
            try {
                media_player.setDataSource(AudioPlayService.this, Uri.parse(audioItem.getPath()));
                media_player.prepare();
                media_player.setOnPreparedListener(new OnAudioPreparedListener(media_player.getCurrentPosition()));
                media_player.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

1.3 完成界面功能

1.3.1在 AudioBinder 中提供音乐暂停开始的方法。在AudioPlayActivity 中点击开始按钮调用暂停开始的方法。

public void pause(){
            media_player.pause();
            backIsPlay = false;
        }
public void start(){
            media_player.start();
            backIsPlay = true;
        }
public boolean isPlaying(){
            return media_player.isPlaying();
        }

AudioPlayActivity 继承View .OnClickListener接口,在 AudioPlayActivity 中调用开始暂停播放的方法

@Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.play :
                switchPlayState();
                break;
    }
    private void switchPlayState() {
        if (binder.isPlaying()){
            binder.pause();
            }else{
            binder.start();
        }
        updataPlayButton();
         }

更新播放按钮

private void updataPlayButton(){
        if (binder.isPlaying()) {
            play_button.setImageResource(R.drawable.pause_shadow);
        } else {
            play_button.setImageResource(R.drawable.play_shadow);
        }
    }

当音乐准备完成后需要改变界面按钮的状态为暂停按钮,并且初始化接收到的歌曲名和歌手名。在服务中发送广播,在界面中注册广播之后更新界面。

public class AudioBinder extends Binder{
        private MediaPlayer media_player;
        public void play(){
            if (media_player!=null){
                media_player.reset();
                media_player.release();
                media_player  = null;

            }
            AudioItem audioItem = audioItemList.get(position);
            media_player = new MediaPlayer();
            backIsPlay = true;
            try {
                media_player.setDataSource(AudioPlayService.this, Uri.parse(audioItem.getPath()));
                media_player.prepare();
                media_player.setOnPreparedListener(new OnAudioPreparedListener(media_player.getCurrentPosition()));
                media_player.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
            public void pause(){
            media_player.pause();
            backIsPlay = false;
        }
public void start(){
            media_player.start();
            backIsPlay = true;
        }
public boolean isPlaying(){
            return media_player.isPlaying();
        }

private class OnAudioPreparedListener implements MediaPlayer.OnPreparedListener{
            @Override
            public void onPrepared(MediaPlayer mp) {
            mp.start();
                Intent intent = new Intent("com.work.huntermusic.updateplaybtn");
                //获取当前正在播放的音乐
                AudioItem audioItem = audioItemList.get(position);
                intent.putExtra("audioItem",audioItem);
                sendBroadcast(intent);
            }
        }
}

在 AudioPlayActivity 中注册广播,更新界面

IntentFilter intentFilter = new IntentFilter("com.work.huntermusic.updateplaybtn");
        registerReceiver(new AudioBroadCastReceiver(), intentFilter);

private class AudioBroadCastReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            //更新界面的按钮
            updataPlayButton();
            AudioItem audioItem = (AudioItem) intent.getSerializableExtra("audioItem");
            music_name.setText(audioItem.getName());
            singer_name.setText(audioItem.getSinger());
     }
}        

1.3.2时间进度更新
在服务中的 AudioBinder中提供获取播放总时长和当前播放进度的方法, 当 MediaPlayer准备完成后播放界面收到广播发消息开始更新

在 AudioBinder 中提供获取总时长和当前播放进度的方法

/** 获取音乐的播放总时长*/
public int getDuration(){
            return media_player.getDuration();
        }
/** 获取音乐当前播放进度*/
public int getCurrentPosition(){
            return media_player.getCurrentPosition();
        }

在播放界面收到广播之后开始发消息更新

private class AudioBroadCastReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            //更新界面的按钮
            updataPlayButton();
            AudioItem audioItem = (AudioItem) intent.getSerializableExtra("audioItem");
            music_name.setText(audioItem.getName());
            singer_name.setText(audioItem.getSinger());
            //更新播放进度
            updateCurrentPosition();
     }
} 
public void updataCurrentPosition(){
        int duration=binder.getDuration();
        int position=binder.getCurrentPosition();
        seekbar.setMax(binder.getDuration());
        String durationStr= StringUtil.formatDuration(duration);
        String positionStr=StringUtil.formatDuration(position);
        tv_position.setText(positionStr + " / " + durationStr);
       handler.sendEmptyMessageDelayed(UPDATA_POSITION,500);
    }

    private static final int UPDATA_POSITION = 0;
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case UPDATA_POSITION:
                    updataCurrentPosition();
        break;

            }
        }
    };

这里给出StringUtil中时间转换的方法formatDuration()

public static String formatDuration(int duration){
        int hour = 60*60*1000;
        int minute = 60*1000;
        int second = 1000;

        int ghour = duration/hour;
        int temp = duration%hour;

        int gminute = temp/minute;
        temp = temp%minute;

        int gsecond = temp/second;

        if (ghour==0){
            return String.format("%02d:%02d",gminute,gsecond);
        }else{
            return String.format("%02d:%02d:%2d",ghour,gminute,gsecond);
        }
    }

为了防止内存泄露,在界面销毁的时候也需要将消息都移除掉。

1.3.3时间进度条的实现

在播放页面收到广播之后给 SeekBar 进行设置最大进度,在更新播放进度的时候同时去更新当前进度并且滑动 SeekBar 的时候改变进度

设置当前播放进度

 /** 更新播放进度*/
 public void updateCurrentPosition() {
 int position=binder.getCurrentPosition();
 //及时更新进度时间
 updatePosition(position);
 handler.sendEmptyMessageDelayed(UPDATE_POSITION,500);
 }

滑动 SeekBar 的时候改变进度。在服务中提供改变进度的方法并且给 SeekBar 设置进度改变的监听

 seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (fromUser) {
                    binder.seekTo(progress);
                    updatePosition(progress);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                binder.seekTo(seekBar.getProgress());
            }
        });

服务 AudioBinder 中的改变进度的方法

/** 进度跳转*/
 public void seekTo(int progress){
     mediaPlayer.seekTo(progress);
 }

1.3.4播放上一首和下一首

在 AudioBinder 中提供播放上一首和下一首的方法,当点击按钮的时候调用播放上一首和下一首的方法。注意:在播放的时候需要将之前的 mediaplayer 进行重置

        public void previous(){
            if (position!=0){
                position--;
                media_player.reset();
                play();
            }else {
                position = audioItemList.size()-1;
            }
        }
        public void next(){
            if (position!=audioItemList.size()-1){
                position++;
                media_player.reset();
                play();
            }else {
                position = 0;
            }
        }

然后在播放页面调用binder的方法前一首和后一首的方法

@Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.play :
                switchPlayState();
                break;
            case R.id.pre :
                binder.previous();
                break;
            case R.id.next :
                binder.next();
                break;

        }
    }

1.3.5切换播放模式

在 AudioBinder 中提供切换播放模式的方法和获取当前播放模式

    public static final int PLAY_ALL_REPEAT = 0;
    public static final int PLAY_RANDOM = 1;
    public static final int PLAY_SINGLE_REPEAT = 2;

    private int playMode = PLAY_ALL_REPEAT;
        public void changePlayMode(){
            if (playMode == PLAY_ALL_REPEAT){
                    playMode = PLAY_RANDOM;
            }else if (playMode ==PLAY_RANDOM) {
                playMode = PLAY_SINGLE_REPEAT;
            }else if (playMode ==PLAY_SINGLE_REPEAT){
                    playMode = PLAY_ALL_REPEAT;
            }
        }

在播放页面点击切换播放模式的图片

@Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.play :
                switchPlayState();
                break;
            case R.id.pre :
                binder.previous();
                break;
            case R.id.next :
                binder.next();
                break;
            case R.id.play_mode:
                binder.changePlayMode();
                //及时更新播放模式
                updataPlayMode();
                break;
        }
    }

private void updataPlayMode() {
        switch (binder.getPlayMode()){
            case AudioPlayService.PLAY_ALL_REPEAT:
                play_mode.setImageResource(R.drawable.list_repeat);
                break;
            case AudioPlayService.PLAY_RANDOM:
                play_mode.setImageResource(R.drawable.random);
                break;
            case AudioPlayService.PLAY_SINGLE_REPEAT:
                play_mode.setImageResource(R.drawable.single_repeat);
                break;
        }
    }

1.3.6根据播放模式自动播放下一首音乐

在服务的 AudioBinder 的 play 方法中给 mediaplay 设置结束的监听,当音乐播放结束之后自动播放下一首

 media_player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                //歌曲播放结束
                //自动播放下一首
                    autoPlayNext();
                }
            });

private void autoPlayNext() {
            switch (playMode){
                case PLAY_ALL_REPEAT:
                    if (position == audioItemList.size()-1) {
                        position = 0;
                    }else {
                        position++;
                    }
                    break;
                case PLAY_RANDOM:
                    position = new Random().nextInt(audioItemList.size());
                    break;
                case PLAY_SINGLE_REPEAT:
                    break;
            }
            play();
        }

这里写图片描述
至此音乐播放服务和界面已经初步完成了,但是还有一些工作要作补充比如专辑图片和歌词等等,这个我会在之后进行补充,但是一个简略的MP3播放器已经成型,大家也可以根据我的代码自己加工构造出更好的作品,我只是作为抛砖引玉,同时希望也能得到大家的分享。

作者:hunterliy 发表于2016/10/16 9:48:21 原文链接
阅读:37 评论:0 查看评论

谷歌电子市场第2天

$
0
0



一线程池的使用

1.线程池的原理

public class ThreadPool {
			int maxCount = 3;
			AtomicInteger count =new AtomicInteger(0);// 当前开的线程数  count=0,atomicInteger可以保持线程同步
			LinkedList<Runnable> runnables = new LinkedList<Runnable>();
		
			public void execute(Runnable runnable) {
				runnables.add(runnable);
				if(count.incrementAndGet()<=3){
					createThread();
				}
			}
			private void createThread() {
				new Thread() {
					@Override
					public void run() {
						super.run();
						while (true) {
							// 取出来一个异步任务
							if (runnables.size() > 0) {
								Runnable remove = runnables.remove(0);
								if (remove != null) {
									remove.run();
								}
							}else{
								//  等待状态   wake();
							}
						}
					}
				}.start();
			}
		}

2.线程池的使用

public class ThreadManager {

	private ThreadManager() {

	}
	private static ThreadManager manager = new ThreadManager();
	private ThreadPoolProxy longPool ;
	private ThreadPoolProxy shortPool ;
	public static ThreadManager getInstance() {
		return manager;
	}
	
	
	/** 操作网络文件的线程*/
	// cpu的核数*2+1
	public synchronized ThreadPoolProxy createLongPool() {
		if (longPool == null) {
			 longPool = new ThreadPoolProxy(5, 5, 5000L);
		}
		return longPool;
	}
	
	/** 操作本地文件的线程*/
	public synchronized ThreadPoolProxy createshortPool() {
		if (shortPool == null) {
			shortPool = new ThreadPoolProxy(3, 3, 5000L);
		}
		return shortPool;
	}

	public class ThreadPoolProxy {
		private ThreadPoolExecutor pool;

		private int corePoolSize;
		private int maximumPoolSize;
		private long time;

		public ThreadPoolProxy(int corePoolSize, int maximumPoolSize, long time) {
			this.corePoolSize = corePoolSize;
			this.maximumPoolSize = maximumPoolSize;
			this.time = time;

		}

		public void execute(Runnable runnable) {
			/*
			 * 1.线程池里面管理的线程数2.如果排队满了额外再开的线程数3.如果线程池没有要执行的任务 存活多久4.时间的单位5.如果
			 * 线程池里管理的线程都已经用了,剩下的任务 临时存到LinkedBlockingQueue对象中 排队10个排队
			 */
			if (pool == null) {
				pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
						time, TimeUnit.MILLISECONDS,
						new LinkedBlockingQueue<Runnable>(10));
			}
			pool.execute(runnable);
		}

		public void cancel(Runnable runnable) {
			// 不为null ,没有停止,,没有崩溃
			if (pool != null && !pool.isShutdown() && !pool.isTerminated()) {
				pool.remove(runnable);
			}
		}
	}
}

二。读取网络数据

1.BaseProtocol

public abstract class BaseProtocol<T> {
	
	private FileWriter writer;
	private BufferedWriter bufferedWriter;
	private BufferedReader bufferedReader;
	private JSONArray jsonArray;
	private T parseJson;
	
	public T load(int index) {
		String json = loadLocal(index);// 1.加载本地数据
		System.out.println("加载了本地数据");

		if (json == null) {// 2.没有本地数据,则加载服务器数据
			json = loadServer(index);
			// System.out.println(json);
			if (json != null) {
				saveLocal(index, json);// 3.保存数据到本地
			}
		}

		if (json != null) {// 4.如果加载到了本地数据,解析
			parseJson = parseJson(json);
			return parseJson;
		} else {
			return null;
		}
	}
	/** 1.加载本地数据 */
	private String loadLocal(int index) {
		File dir = FileUtils.getFilePath("cache");
		File file = new File(dir, getKey() + index);// 缓存在sdcard/google1/cache文件夹中
		if (file != null) {
			try {
				bufferedReader = new BufferedReader(new FileReader(file));
				long time = Long.parseLong(bufferedReader.readLine());
				if (System.currentTimeMillis() - time > 0) {
					return null;
				} else {
					StringWriter stringWriter = new StringWriter();
					String str;
					while ((str = bufferedReader.readLine()) != null) {
						stringWriter.write(str);
					}
					return stringWriter.toString();
				}
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			} finally {
				IOUtils.closeQuietly(bufferedReader);
			}
		}
		return null;
	}
	
	/** 2.加载服务器数据 */
	private String loadServer(int index) {

		// 这里为什么不用在子线程中运行,因为调用的时候就是在子线程中调用的,用httputils的话里面涉及线程中的线程问题
		StringBuffer buffer = null;
		HttpURLConnection connection = null;
		try {
			URL url = new URL("http://127.0.0.1:8090/" + getKey() + "?index="
					+ index + getParams());
			connection = (HttpURLConnection) url.openConnection();
			connection.setRequestMethod("GET");
			connection.setReadTimeout(8000);
			connection.setConnectTimeout(8000);
			connection.connect();
			if (connection.getResponseCode() == 200) {
				InputStream stream = connection.getInputStream();
				BufferedReader reader = new BufferedReader(
						new InputStreamReader(stream));
				buffer = new StringBuffer();
				String line = "";
				while ((line = reader.readLine()) != null) {
					buffer.append(line);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		} finally {
			if (connection != null) {
				connection.disconnect();
			}
		}
		return buffer.toString();
	}
	
	private void saveLocal(int index, String json) {

		try {
			File dir = FileUtils.getFilePath("cache");
			File file = new File(dir, getKey() + index);// 缓存在sdcard/google1/cache文件夹中
			writer = new FileWriter(file);
			bufferedWriter = new BufferedWriter(writer);
			bufferedWriter.write(System.currentTimeMillis() + "");
			bufferedWriter.newLine();
			bufferedWriter.write(json);
			bufferedWriter.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			IOUtils.closeQuietly(writer);
			IOUtils.closeQuietly(bufferedWriter);
		}
	}
	
	public abstract  T parseJson(String json);
	
	public abstract String getKey();
	public abstract String getParams();
}
2.获取缓存在本地的路径

public class FileUtils {
	public static final String URL = "http://127.0.0.1:8090/";
	private static final String Root = "google1";
	public static final String ICON = "icon";
	String path = "";
	private static StringBuilder stringBuilder;
	/**
	 * 获取图片的缓存的路径
	 * @return
	 */
	public static File getIconDir(){
		return getFilePath(ICON);
		
	}
	/**
	 * 获取缓存路径
	 * @return
	 */
	public static File getCacheDir() {
		return getFilePath("");
	}
	//获取缓存的路径
	public static File getFilePath(String str) {
		if (Environment.getExternalStorageState().equals(
				Environment.MEDIA_MOUNTED)) {
			stringBuilder = new StringBuilder();
			stringBuilder.append(Environment.getExternalStorageDirectory()
					.getAbsolutePath());
			stringBuilder.append(File.separator);
			stringBuilder.append(Root);
			stringBuilder.append(File.separator);
			stringBuilder.append(str);
		}else{
			File filesDir = BaseApplication.getApplication().getFilesDir();
			stringBuilder.append(filesDir.getAbsolutePath());
			stringBuilder.append(File.separator);
			stringBuilder.append(str);
		}
		File file = new File(stringBuilder.toString());

		// 不存在或者不是一个文件夹
		if (!file.exists() || !file.isDirectory()) {
			file.mkdirs();
		}
		return file;
	}

}
3.HomeProtocol解析数据

public class HomeProtocol extends BaseProtocol<List<Appinfo>>{
	private JSONArray jsonArray;

	public List<Appinfo> parseJson(String json) {
		JSONObject jsonObject;
		List<Appinfo> appinfos = new ArrayList<Appinfo>();
		try {
			jsonObject = new JSONObject(json);
			jsonArray = jsonObject.getJSONArray("list");
			for (int i = 0; i < jsonArray.length(); i++) {
				JSONObject jsonObject2 = jsonArray.getJSONObject(i);
				long id = jsonObject2.getLong("id");
				String name = jsonObject2.getString("name");
				String packageName = jsonObject2.getString("packageName");
				String iconUrl = jsonObject2.getString("iconUrl");
				float stars = Float.parseFloat(jsonObject2.getString("stars"));
				long size = jsonObject2.getLong("size");
				String downloadUrl = jsonObject2.getString("downloadUrl");
				String des = jsonObject2.getString("des");
				Appinfo appinfo = new Appinfo(id, name, packageName, iconUrl,
						stars, size, downloadUrl, des);
				appinfos.add(appinfo);
			}
			return appinfos;
		} catch (JSONException e) {
			e.printStackTrace();
			return null;
		}

	}

	public String getKey() {
		return "home";
	}

	/** 重写这个后缀,有的有,有的没有,需要的重写 */
	public String getParams() {
		return "";
	}

}

4.设置HomeFragment页面

public class HomeFragment extends BaseFragment {

	private List<Appinfo> data;
	private ListView listView;
	private Myadapter myadapter;
	private BitmapUtils bitmapUtils;
	// 开始的时候就加载HomeFragment的页面
	@Override
	public void onActivityCreated(@Nullable Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		show();
	}

	/** 创建了成功的界面 */
	protected View createSuccessView() {
		listView = new ListView(BaseApplication.getApplication());
		myadapter = new Myadapter();
		// listView.setAdapter(myadapter);
		return listView;
	}

	class Myadapter extends BaseAdapter {
	

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

		@Override
		public Object getItem(int position) {
			return data.get(position);
		}

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

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			View view = null;
			ViewHolder holder;
			if (convertView == null) {
				holder = new ViewHolder();
				view = View.inflate(BaseApplication.getApplication(),
						R.layout.item_app, null);
				holder.item_icon = (ImageView) view
						.findViewById(R.id.item_icon);
				holder.item_title = (TextView) view
						.findViewById(R.id.item_title);
				holder.item_size = (TextView) view.findViewById(R.id.item_size);
				holder.item_bottom = (TextView) view
						.findViewById(R.id.item_bottom);
				holder.item_rating = (RatingBar) view
						.findViewById(R.id.item_rating);
				view.setTag(holder);
			} else {
				view = convertView;
				holder = (ViewHolder) view.getTag();
			}
			Appinfo appinfo = data.get(position);
			holder.item_title.setText(appinfo.getName());
			String formatFileSize = Formatter.formatFileSize(
					BaseApplication.getApplication(), appinfo.getSize());
			holder.item_size.setText(formatFileSize);
			holder.item_bottom.setText(appinfo.getDes());
			holder.item_rating.setRating(appinfo.getStarts());

			//http://127.0.0.1:8090/image?name=app/com.youyuan.yyhl/icon.jpg
			// 加载图片
			String iconUrl = appinfo.getIconUrl();
			System.out.println(iconUrl+"-------------");
			bitmapUtils = BitmapHelper.getBitmapUtils();//使用单例模式的BitmapUtils,并且可以缓存到sdcard中
			bitmapUtils.display(holder.item_icon,"http://127.0.0.1:8090" + "/image?name=" + iconUrl);
			bitmapUtils.configDefaultLoadingImage(R.drawable.ic_launcher);
			return view;
		}

	}
 
	// 获取数据从服务器
	protected int LoadDataFromServer() {
		HomeProtocol homeProtocol = new HomeProtocol();
		data = homeProtocol.load(0);

		for (Appinfo appinfo : data) {
			// System.out.println(appinfo.toString());
		}
		getActivity().runOnUiThread(new Runnable() {

			@Override
			public void run() {
				listView.setAdapter(myadapter);
			}
		});
		return checkData(data);
	}

	

	static class ViewHolder {
		ImageView item_icon;
		TextView item_title;
		TextView item_size;
		TextView item_bottom;
		RatingBar item_rating;
	}
}

5.使用BitmapUtils的方法可以将图片缓存到本地中

public class BitmapHelper {
		private BitmapHelper() {
		}
	
		private static BitmapUtils bitmapUtils;
	
		/**
		 * BitmapUtils不是单例的 根据需要重载多个获取实例的方法
		 * 
		 * @param appContext
		 *            application context
		 * @return
		 */
		public static BitmapUtils getBitmapUtils() {
			if (bitmapUtils == null) {
				// 第二个参数 缓存图片的路径 // 加载图片 最多消耗多少比例的内存 0.05-0.8f
				bitmapUtils = new BitmapUtils(UiUtils.getContext(), FileUtils
						.getIconDir().getAbsolutePath(), 0.3f);
			}
			return bitmapUtils;
		}
	}

6.设置ListView的细节处理

## BaseListView 


	public class BaseListView extends ListView {
	
		public BaseListView(Context context) {
			super(context);
			init();
		}
	
		public BaseListView(Context context, AttributeSet attrs, int defStyle) {
			super(context, attrs, defStyle);
			init();
		}
	
		public BaseListView(Context context, AttributeSet attrs) {
			super(context, attrs);
			init();
		}
	
		private void init() {
	//		setSelector  点击显示的颜色
	//		setCacheColorHint  拖拽的颜色
	//		setDivider  每个条目的间隔	的分割线	
			this.setSelector(R.drawable.nothing);  // 什么都没有
			this.setCacheColorHint(R.drawable.nothing);
			this.setDivider(UiUtils.getDrawalbe(R.drawable.nothing));
		}
	
	}

作者:qq_33645265 发表于2016/10/16 9:49:09 原文链接
阅读:36 评论:0 查看评论

Android Studio 快捷键总结

$
0
0
Alt+回车 导入包,自动修正

Ctrl+N   查找类

Ctrl+Shift+N 查找文件

Ctrl+Alt+L  格式化代码

Ctrl+Alt+O 优化导入的类和包

Alt+Insert 生成代码(如get,set方法,构造函数等)

Ctrl+E或者Alt+Shift+C  最近更改的代码

Ctrl+R 替换文本

Ctrl+F 查找文本

Ctrl+Shift+Space 自动补全代码

Ctrl+空格 代码提示

Ctrl+Alt+Space 类名或接口名提示

Ctrl+P 方法参数提示

Ctrl+Shift+Alt+N 查找类中的方法或变量

Alt+Shift+C 对比最近修改的代码

Shift+F6  重构-重命名

Ctrl+Shift+先上键

Ctrl+X 删除行

Ctrl+D 复制行

Ctrl+/ 或 Ctrl+Shift+/  注释(// 或者 )

Ctrl+J  自动代码

Ctrl+E 最近打开的文件

Ctrl+H 显示类结构图

Ctrl+Q 显示注释文档

Alt+F1 查找代码所在位置

Alt+1 快速打开或隐藏工程面板

Ctrl+Alt+ left/right 返回至上次浏览的位置

Alt+ left/right 切换代码视图

Alt+ Up/Down 在方法间快速移动定位

Ctrl+Shift+Up/Down 代码向上/下移动。

F2 或Shift+F2 高亮错误或警告快速定位

代码标签输入完成后,按Tab,生成代码。

选中文本,按Ctrl+Shift+F7 ,高亮显示所有该文本,按Esc高亮消失。

Ctrl+W 选中代码,连续按会有其他效果

选中文本,按Alt+F3 ,逐个往下查找相同文本,并高亮显示。

Ctrl+Up/Down 光标跳转到第一行或最后一行下

Ctrl+B 快速打开光标处的类或方法

 

最常用快捷键

1.Ctrl+E,可以显示最近编辑的文件列表

2.Shift+Click可以关闭文件

3.Ctrl+[或]可以跳到大括号的开头结尾

4.Ctrl+Shift+Backspace可以跳转到上次编辑的地方

5.Ctrl+F12,可以显示当前文件的结构

6.Ctrl+F7可以查询当前元素在当前文件中的引用,然后按F3可以选择

7.Ctrl+N,可以快速打开类

8.Ctrl+Shift+N,可以快速打开文件

9.Alt+Q可以看到当前方法的声明

10.Ctrl+W可以选择单词继而语句继而行继而函数

11.Alt+F1可以将正在编辑的元素在各个面板中定位

12.Ctrl+P,可以显示参数信息

13.Ctrl+Shift+Insert可以选择剪贴板内容并插入

14.Alt+Insert可以生成构造器/Getter/Setter等

15.Ctrl+Alt+V 可以引入变量。例如把括号内的SQL赋成一个变量

16.Ctrl+Alt+T可以把代码包在一块内,例如try/catch

17.Alt+Up and Alt+Down可在方法间快速移动

 

 

下面的不是很有用

18.在一些地方按Alt+Enter可以得到一些Intention Action,例如将”==”改为”equals()”

19.Ctrl+Shift+Alt+N可以快速打开符号

20.Ctrl+Shift+Space在很多时候都能够给出Smart提示

21.Alt+F3可以快速寻找

22.Ctrl+/和Ctrl+Shift+/可以注释代码

23.Ctrl+Alt+B可以跳转到抽象方法的实现

24.Ctrl+O可以选择父类的方法进行重写

25.Ctrl+Q可以看JavaDoc

26.Ctrl+Alt+Space是类名自动完成

27.快速打开类/文件/符号时,可以使用通配符,也可以使用缩写

28.Live Templates! Ctrl+J

29.Ctrl+Shift+F7可以高亮当前元素在当前文件中的使用

30.Ctrl+Alt+Up /Ctrl+Alt+Down可以快速跳转搜索结果

31.Ctrl+Shift+J可以整合两行

32.Alt+F8是计算变量值

 
IntelliJ IDEA使用技巧一览表

在使用 InelliJ IDEA 的过程中,通过查找资料以及一些自己的摸索,发现这个众多 Java 程序员喜欢的 IDE 里有许多值得一提的小窍门,如果能熟练的将它们应用于实际开发过程中,相信它会大大节省你的开发时间,而且随之而来的还会有那么一点点成就感:) Try it !

 

1 、写代码时用 Alt-Insert ( Code|Generate… )可以创建类里面任何字段的 getter 与 setter 方法。

2 、右键点击断点标记(在文本的左边栏里)激活速查菜单,你可以快速设置 enable/disable 断点或者条件它的属性。

3 、 CodeCompletion (代码完成)属性里的一个特殊的变量是,激活 Ctrl-Alt-Space 可以完成在或不在当前文件里的类名。如果类没有引入则 import 标志会自动创建。

4 、使用 Ctrl-Shift-V 快捷键可以将最近使用的剪贴板内容选择插入到文本。使用时系统会弹出一个含有剪贴内容的对话框,从中你可以选择你要粘贴的部分。

5 、利用 CodeCompletion (代码完成)属性可以快速地在代码中完成各种不同地语句,方法是先键入一个类名地前几个字母然后再用 Ctrl-Space 完成全称。如果有多个选项,它们会列在速查列表里。

6 、用 Ctrl-/ 与 Ctrl-Shift-/ 来注释 / 反注释代码行与代码块。

-/ 用单行注释标记(“ //… ”)来注释 / 反注释当前行或者选择地代码块。而 Ctrl-Shift-/ 则可以用块注释标记(“ ”)把所选块包围起来。要反注释一个代码块就在块中任何一个地方按 Ctrl-Shift-/ 即可。

7 、按 Alt-Q ( View|Context Info )可以不需要移动代码就能查看当前方法地声明。连续按两次会显示当前所编辑的类名。

8 、使用 Refactor|Copy Class… 可以创建一个所选择的类的“副本”。这一点很有用,比如,在你想要创建一个大部分内容都和已存在类相同的类时。

9 、在编辑器里 Ctrl-D 可以复制选择的块或者没有所选块是的当前行。

10 、 Ctrl-W (选择字)在编辑器里的功能是先选择脱字符处的单词,然后选择源代码的扩展区域。举例来说,先选择一个方法名,然后是调用这个方法的表达式,然后是整个语句,然后包容块,等等。

11 、如果你不想让指示事件细节的“亮球”图标在编辑器上显示,通过按 Alt-Enter 组合键打开所有事件列表然后用鼠标点击它就可以把这个事件文本附件的亮球置成非活动状态。

这样以后就不会有指示特殊事件的亮球出现了,但是你仍然可以用 Alt-Enter 快捷键使用它。

12 、在使用 CodeCompletion 时,可以用逗点( . )字符,逗号(,)分号(;),空格和其它字符输入弹出列表里的当前高亮部分。选择的名字会随着输入的字符自动输入到编辑器里。

13 、在任何工具窗口里使用 Escape 键都可以把焦点移到编辑器上。

Shift-Escape 不仅可以把焦点移到编辑器上而且还可以隐藏当前(或最后活动的)工具窗口。

F12 键把焦点从编辑器移到最近使用的工具窗口。

14 、在调试程序时查看任何表达式值的一个容易的方法就是在编辑器中选择文本(可以按几次 Ctrl-W 组合键更有效地执行这个操作)然后按 Alt-F8 。

15 、要打开编辑器脱字符处使用的类或者方法 Java 文档的浏览器,就按 Shift-F1 (右键菜单的 External JavaDoc )。
要使用这个功能须要把加入浏览器的路径,在“ General ”选项中设置( Options | IDE Settings ),另外还要把创建的 Java 文档加入到工程中( File | Project Properties )。

16 、用 Ctrl-F12 ( View | File Structure Popup )键你可以在当前编辑的文件中快速导航。
这时它会显示当前类的成员列表。选中一个要导航的元素然后按 Enter 键或 F4 键。要轻松地定位到列表中的一个条目,只需键入它的名字即可。

17 、在代码中把光标置于标记符或者它的检查点上再按 Alt-F7 (右键菜单中的 Find Usages… )会很快地查找到在整个工程中使用地某一个类、方法或者变量的位置。

18 、按 Ctrl-N ( Go to | Class… )再键入类的名字可以快速地在编辑器里打开任何一个类。从显示出来的下拉列表里选择类。
同样的方法你可以通过使用 Ctrl-Shift-N ( Go to | File… )打开工程中的非 Java 文件。

19 、要导航代码中一些地方使用到的类、方法或者变量的声明,把光标放在查看项上再按 Ctrl-B 即可。也可以通过按 Ctrl 键的同时在查看点上单击鼠标键调转到声明处。

20 、把光标放到查看点上再按 Ctrl-Alt-B 可以导航到一个抽象方法的实现代码。

21 、要看一个所选择的类的继承层次,按 Ctrl-H ( Browse Type Hierarchy )即可。也可以激活编辑器中的继承关系视图查看当前编辑类的继承关系。22 、使用 Ctrl-Shift-F7 ( Search | Highlight Usages in File )可以快速高亮显示当前文件中某一变量的使用地方。按 Escape 清除高亮显示。

23 、用 Alt-F3 ( Search | Incremental Search )在编辑器中实现快速查查找功能。

在“ Search for: ”提示工具里输入字符,使用箭头键朝前和朝后搜索。按 Escape 退出。

24 、按 Ctrl-J 组合键来执行一些你记不起来的 Live Template 缩写。比如,键“ it ”然后按 Ctrl-J 看看有什么发生。

25 、 Introduce Variable 整合帮助你简化代码中复杂的声明。举个例子,在下面的代码片断里,在代码中选择一个表达式:然后按 Ctrl-Alt-V 。

26 、 Ctrl-Shift-J 快捷键把两行合成一行并把不必要的空格去掉以匹配你的代码格式。

27 、 Ctrl-Shift-Backspace ( Go to | Last Edit Location )让你调转到代码中所做改变的最后一个地方。

多按几次 Ctrl-Shift-Backspace 查看更深的修改历史。

28 、用 Tools | Reformat Code… 根据你的代码样式参考(查看 Options | IDE Setting | Code Style )格式化代码。

使用 Tools | Optimize Imports… 可以根据设置(查看 Options | IDE Setting | Code Style | Imports )自动“优化” imports (清除无用的 imports 等)。

29 、使用 IDEA 的 Live Templates | Live Templates 让你在眨眼间创建许多典型代码。比如,在一个方法里键入

再按 Tab 键看有什么事情发生了。

用 Tab 键在不同的模板域内移动。查看 Options | Live Templates 获取更多的细节。

30 、要查看一个文件中修改的本地历史,激活右键菜单里的 Local VCS | Show History… 。也许你可以导航不同的文件版本,看看它们的不同之处再回滚到以前的任何一个版本吧。

使用同样的右键菜单条目还可以看到一个目录里修改的历史。有了这个特性你就不会丢失任何代码了。

31 、如果要了解主菜单里每一个条目的用途,把鼠标指针移到菜单条目上再应用程序框架的底部的状态栏里就会显示它们的一些简短描述,也许会对你有帮助。

32 、要在编辑器里显示方法间的分隔线,打开 Options | IDE Settings | Editor ,选中“ Show method separators ”检查盒( checkbox )。

33 、用 Alt-Up 和 Alt-Down 键可以在编辑器里不同的方法之间快速移动。

34 、用 F2/Shift-F2 键在高亮显示的语法错误间跳转。

用 Ctrl-Alt-Down/Ctrl-Alt-Up 快捷键则可以在编译器错误信息或者查找操作结果间跳转。

35 、通过按 Ctrl-O ( Code | Override Methods… )可以很容易地重载基本类地方法。

要完成当前类 implements 的(或者抽象基本类的)接口的方法,就使用 Ctrl-I ( Code | Implement Methods… )。

36 、如果光标置于一个方法调用的括号间,按 Ctrl-P 会显示一个可用参数的列表。

37 、要快速查看编辑器脱字符处使用的类或方法的 Java 文档,按 Ctrl-Q (在弹出菜单的 Show Quick JavaDoc 里)即可。

38 、像 Ctrl-Q ( Show Quick JavaDoc 显示简洁 Java 文档), Ctrl-P ( Show Parameter Info 显示参数信息), Ctrl-B ( Go to Declaration 跳转到声明), Shift-F1 ( External JavaDoc 外部 Java 文档)以及其它一些快捷键不仅可以在编辑器里使用,也可以应用在代码完成右键列表里。

39 、 Ctrl-E ( View | Recent Files )弹出最近访问的文件右键列表。选中文件按 Enter 键打开。

40 、在 IDEA 中可以很容易地对你的类,方法以及变量进行重命名并在所有使用到它们的地方自动更正。

试一下,把编辑器脱字符置于任何一个变量名字上然后按 Shift-F6 ( Refactor | Rename… )。在对话框里键入要显示地新名字再按 Enter 。你会浏览到使用这个变量地所有地方然后按“ Do Refactor ”按钮结束重命名操作。

41 、要在任何视图( Project View 工程视图, Structure View 结构视图或者其它视图)里快速

选择当前编辑地部分(类,文件,方法或者字段),按 Alt-F1 ( View | Select in… )。

42 、在“ new ”字符后实例化一个已知类型对象时也许你会用到 SmartType 代码完成这个特性。比如,键入

再按 Ctrl-Shift-Space :

作者:u013309870 发表于2016/10/16 10:03:51 原文链接
阅读:49 评论:0 查看评论

IPV6详解:Stable Privacy Address Kernel实现分析

$
0
0

主要分析Stable Privacy Address在kernel中的实现code

Kernel Version: 4.6

无状态地址配置过程

一般情况下,在对应的网卡启动的时候,如果支持IPV6,且软件的IPV6功能使能,就会自动配置一个Link local地址,这个link local地址就是以fe80开头,同时有了link local地址之后,系统就会发送一个RS的packet到网络中去,如果路由器支持IPV6,收到这个RS之后,会回复一个携带prefix信息的RA包,Host收到RA包之后,提取prefix,生成interface id,组成一个global地址,这个过程就是无状态地址配置的过程.

RS packet : 

从图中看出,属于ICMPV6协议的封包,使用link local address发送,目的地址是路由器多播组


RA packet:

Host收到RA之后,就提取prefix,组成一个global地址


Kernel 实现分析

在kernel中专门提供了这样一个patch增加了对RFC7217的支持。Patch:ipv6: generation of stable privacy addresses for link-local and autoconf

@@ -2302,6 +2305,11 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)
 				       in6_dev->token.s6_addr + 8, 8);
 				read_unlock_bh(&in6_dev->lock);
 				tokenized = true;
+			} else if (in6_dev->addr_gen_mode ==
+				   IN6_ADDR_GEN_MODE_STABLE_PRIVACY&&
+				   !ipv6_generate_stable_address(&addr, 0,
+								 in6_dev)) {
+				goto ok;
 			} else if (ipv6_generate_eui64(addr.s6_addr + 8, dev) &&
 				   ipv6_inherit_eui64(addr.s6_addr + 8, in6_dev)) {
 				in6_dev_put(in6_dev);

在收到prefix的时候,会判断下addr_gen_mode 生成地址的模式,如果是IN6_ADDR_GEN_MODE_STABLE_PRIVACY, 那么就调用ipv6_generate_stable_address 生成地址stable address. 
static int ipv6_generate_stable_address(struct in6_addr *address,
					u8 dad_count,
					const struct inet6_dev *idev)
{
	static DEFINE_SPINLOCK(lock);
	static __u32 digest[SHA_DIGEST_WORDS]; //160bit消息摘要
	static __u32 workspace[SHA_WORKSPACE_WORDS];
	
	static union {
		char __data[SHA_MESSAGE_BYTES]; //512bit data block : 128bit secret + 64bit prefix + 256bit HW + 8bit DAD_Count;
		struct {
			struct in6_addr secret;
			__be32 prefix[2];
			unsigned char hwaddr[MAX_ADDR_LEN];
			u8 dad_count;
		} __packed;
	} data;

	struct in6_addr secret;
	struct in6_addr temp;
	struct net *net = dev_net(idev->dev); // 取得网卡的device结构体

	BUILD_BUG_ON(sizeof(data.__data) != sizeof(data));

	//获取初始化的scret
	if (idev->cnf.stable_secret.initialized)
		secret = idev->cnf.stable_secret.secret;
	else if (net->ipv6.devconf_dflt->stable_secret.initialized)
		secret = net->ipv6.devconf_dflt->stable_secret.secret;
	else
		return -1; //如果没有scret,直接返回-1
retry:
	spin_lock_bh(&lock);

	sha_init(digest); //初始化SHA-1的原始消息摘要值
	memset(&data, 0, sizeof(data)); //清0,data区域
	memset(workspace, 0, sizeof(workspace));//清0,workspace
	memcpy(data.hwaddr, idev->dev->perm_addr, idev->dev->addr_len); //将设备的mac地址复制到data中
	data.prefix[0] = address->s6_addr32[0]; //prefix信息
	data.prefix[1] = address->s6_addr32[1];
	data.secret = secret; // secret 信息 到data
	data.dad_count = dad_count; //dad_count 传入的值,赋值给data

	sha_transform(digest, data.__data, workspace); //使用SHA-1算法进行加密,并产生160bit的摘要信息

	temp = *address; 
	//这个地方就是使用160bit的摘要信息的前64bit,赋值给IPV6地址的后64bit,这样就组成了一个prefix + interface id的完整地址
	temp.s6_addr32[2] = (__force __be32)digest[0];
	temp.s6_addr32[3] = (__force __be32)digest[1];

	spin_unlock_bh(&lock);

	//判断是否是保留的interface id,这些interface id不能被使用
	if (ipv6_reserved_interfaceid(temp)) {
		dad_count++;// 如果和保留interface id 一致,则dad_count加1
		//如果dad_count > idgen_retries 生成stable address失败,否则retry
		if (dad_count > dev_net(idev->dev)->ipv6.sysctl.idgen_retries)
			return -1;
		goto retry;
	}

	*address = temp;
	return 0;
}
从ipv6_generate_stable_address函数中可以总结出以下步骤:
1. 初始化消息摘要
2. 填充需要加密的512数据块:128bit secret + 64bit prefix + 256bit HW + 8bit DAD_Count  这个地方不足512字节,应该后面的补0
3. 使用SHA-1对data进行加密,并产生160bit的消息摘要
4. 将消息摘要的前64bit,放置到IPV6地址的后64bit
5. 检查生成的interface id是否保留的interface id,如果是保留的,需要重新生成或者直接失败
6. 生成整个地址,返回。

1-4步都是设计到SHA-1算法,如果不了解可以参考文章: IPv6详解:SHA1算法实现及详解    了解FIPS 180-4的算法流程
初始化消息摘要,这个地方就是将初始值复制给160bit的消息摘要
/**
 * sha_init - initialize the vectors for a SHA1 digest
 * @buf: vector to initialize
 */
void sha_init(__u32 *buf)
{
	buf[0] = 0x67452301;
	buf[1] = 0xefcdab89;
	buf[2] = 0x98badcfe;
	buf[3] = 0x10325476;
	buf[4] = 0xc3d2e1f0;
}

sha_transform 

这个函数是SHA-1的核心算法,对单个的512 block使用SHA-1算法进行转换,这个函数最后产生一个160bit的消息摘要,但是这个地方不要与文章 IPv6详解:SHA1算法实现及详解  中的FIS 180-4  SHA-1算法混淆,两者差异的地方在于Linux kernel实现的不再进行补位和补长度的操作,应该不够512个字节就会填0,剩下的步骤2者一致
1 .首先将512 data block分成 80个双字,W(0~79),如下提供了2个宏,在t为0~15的时候,使用SHA_SRC, 计算出W0~W15
在16~79的时候,使用SHA_MAX计算出W16~W79
/*
 * Where do we get the source from? The first 16 iterations get it from
 * the input data, the next mix it from the 512-bit array.
 */
#define SHA_SRC(t) get_unaligned_be32((__u32 *)data + t)
#define SHA_MIX(t) rol32(W(t+13) ^ W(t+8) ^ W(t+2) ^ W(t), 1)
2. 有了W0~W79, 分别使用函数对每个双字进行计算,更新A,B,C,D,E,计算的宏如下,看起来还是比较麻烦的,不同的T使用不同的常量
#define SHA_ROUND(t, input, fn, constant, A, B, C, D, E) do { \
	__u32 TEMP = input(t); setW(t, TEMP); \
	E += TEMP + rol32(A,5) + (fn) + (constant); \
	B = ror32(B, 2); } while (0)

#define T_0_15(t, A, B, C, D, E)  SHA_ROUND(t, SHA_SRC, (((C^D)&B)^D) , 0x5a827999, A, B, C, D, E )
#define T_16_19(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (((C^D)&B)^D) , 0x5a827999, A, B, C, D, E )
#define T_20_39(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B^C^D) , 0x6ed9eba1, A, B, C, D, E )
#define T_40_59(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, ((B&C)+(D&(B^C))) , 0x8f1bbcdc, A, B, C, D, E )
#define T_60_79(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B^C^D) ,  0xca62c1d6, A, B, C, D, E )
具体的算法如下,比较长,进行80次运算
void sha_transform(__u32 *digest, const char *data, __u32 *array)
{
	__u32 A, B, C, D, E;

	A = digest[0];
	B = digest[1];
	C = digest[2];
	D = digest[3];
	E = digest[4];

	/* Round 1 - iterations 0-16 take their input from 'data' */
	T_0_15( 0, A, B, C, D, E);
	T_0_15( 1, E, A, B, C, D);
	T_0_15( 2, D, E, A, B, C);
	T_0_15( 3, C, D, E, A, B);
	T_0_15( 4, B, C, D, E, A);
	T_0_15( 5, A, B, C, D, E);
	T_0_15( 6, E, A, B, C, D);
	T_0_15( 7, D, E, A, B, C);
	T_0_15( 8, C, D, E, A, B);
	T_0_15( 9, B, C, D, E, A);
	T_0_15(10, A, B, C, D, E);
	T_0_15(11, E, A, B, C, D);
	T_0_15(12, D, E, A, B, C);
	T_0_15(13, C, D, E, A, B);
	T_0_15(14, B, C, D, E, A);
	T_0_15(15, A, B, C, D, E);

	/* Round 1 - tail. Input from 512-bit mixing array */
	T_16_19(16, E, A, B, C, D);
	T_16_19(17, D, E, A, B, C);
	T_16_19(18, C, D, E, A, B);
	T_16_19(19, B, C, D, E, A);

	/* Round 2 */
	T_20_39(20, A, B, C, D, E);
	T_20_39(21, E, A, B, C, D);
	T_20_39(22, D, E, A, B, C);
	T_20_39(23, C, D, E, A, B);
	T_20_39(24, B, C, D, E, A);
	T_20_39(25, A, B, C, D, E);
	T_20_39(26, E, A, B, C, D);
	T_20_39(27, D, E, A, B, C);
	T_20_39(28, C, D, E, A, B);
	T_20_39(29, B, C, D, E, A);
	T_20_39(30, A, B, C, D, E);
	T_20_39(31, E, A, B, C, D);
	T_20_39(32, D, E, A, B, C);
	T_20_39(33, C, D, E, A, B);
	T_20_39(34, B, C, D, E, A);
	T_20_39(35, A, B, C, D, E);
	T_20_39(36, E, A, B, C, D);
	T_20_39(37, D, E, A, B, C);
	T_20_39(38, C, D, E, A, B);
	T_20_39(39, B, C, D, E, A);

	/* Round 3 */
	T_40_59(40, A, B, C, D, E);
	T_40_59(41, E, A, B, C, D);
	T_40_59(42, D, E, A, B, C);
	T_40_59(43, C, D, E, A, B);
	T_40_59(44, B, C, D, E, A);
	T_40_59(45, A, B, C, D, E);
	T_40_59(46, E, A, B, C, D);
	T_40_59(47, D, E, A, B, C);
	T_40_59(48, C, D, E, A, B);
	T_40_59(49, B, C, D, E, A);
	T_40_59(50, A, B, C, D, E);
	T_40_59(51, E, A, B, C, D);
	T_40_59(52, D, E, A, B, C);
	T_40_59(53, C, D, E, A, B);
	T_40_59(54, B, C, D, E, A);
	T_40_59(55, A, B, C, D, E);
	T_40_59(56, E, A, B, C, D);
	T_40_59(57, D, E, A, B, C);
	T_40_59(58, C, D, E, A, B);
	T_40_59(59, B, C, D, E, A);

	/* Round 4 */
	T_60_79(60, A, B, C, D, E);
	T_60_79(61, E, A, B, C, D);
	T_60_79(62, D, E, A, B, C);
	T_60_79(63, C, D, E, A, B);
	T_60_79(64, B, C, D, E, A);
	T_60_79(65, A, B, C, D, E);
	T_60_79(66, E, A, B, C, D);
	T_60_79(67, D, E, A, B, C);
	T_60_79(68, C, D, E, A, B);
	T_60_79(69, B, C, D, E, A);
	T_60_79(70, A, B, C, D, E);
	T_60_79(71, E, A, B, C, D);
	T_60_79(72, D, E, A, B, C);
	T_60_79(73, C, D, E, A, B);
	T_60_79(74, B, C, D, E, A);
	T_60_79(75, A, B, C, D, E);
	T_60_79(76, E, A, B, C, D);
	T_60_79(77, D, E, A, B, C);
	T_60_79(78, C, D, E, A, B);
	T_60_79(79, B, C, D, E, A);

	digest[0] += A;
	digest[1] += B;
	digest[2] += C;
	digest[3] += D;
	digest[4] += E;
}
计算完80次,生成的A,B,C,D,E,然后加上原始的,就是160bit的输出。更详细的算法参考:IPv6详解:SHA1算法实现及详解 
检查是否是保留interface id
    分配的地址如下之中情况,认为分配失败。
static bool ipv6_reserved_interfaceid(struct in6_addr address)
{
	// interface id 为0 ,认为失败
	if ((address.s6_addr32[2] | address.s6_addr32[3]) == 0)
		return true;
	
	if (address.s6_addr32[2] == htonl(0x02005eff) &&
	    ((address.s6_addr32[3] & htonl(0xfe000000)) == htonl(0xfe000000)))
		return true;

	if (address.s6_addr32[2] == htonl(0xfdffffff) &&
	    ((address.s6_addr32[3] & htonl(0xffffff80)) == htonl(0xffffff80)))
		return true;

	return false;
}

DAD失败的处理

生成地址之后,会进行DAD验证是否是重复地址,如果是重复的地址,需要重新生成
void addrconf_dad_failure(struct inet6_ifaddr *ifp)
{
	struct in6_addr addr;
	struct inet6_dev *idev = ifp->idev;
	struct net *net = dev_net(ifp->idev->dev);

	if (addrconf_dad_end(ifp)) {
		in6_ifa_put(ifp);
		return;
	}
	//进入这个函数说明DAD失败
	net_info_ratelimited("%s: IPv6 duplicate address %pI6c detected!\n",
			     ifp->idev->dev->name, &ifp->addr);

	spin_lock_bh(&ifp->lock);

	//如果地址flag有IFA_F_STABLE_PRIVACY,说明是stable address
	if (ifp->flags & IFA_F_STABLE_PRIVACY) {
		int scope = ifp->scope;
		u32 flags = ifp->flags;
		struct in6_addr new_addr;
		struct inet6_ifaddr *ifp2;
		u32 valid_lft, preferred_lft;
		int pfxlen = ifp->prefix_len;
		int retries = ifp->stable_privacy_retry + 1;  //retry 次数加一
		
		//如果重传次数大于idgen_retries,就彻底失败
		if (retries > net->ipv6.sysctl.idgen_retries) {
			net_info_ratelimited("%s: privacy stable address generation failed because of DAD conflicts!\n",
					     ifp->idev->dev->name);
			goto errdad;
		}
		
		//否则重新生成ipv6_generate_stable_address,retries 为DAD_Counter
		new_addr = ifp->addr;
		if (ipv6_generate_stable_address(&new_addr, retries,
						 idev))
			goto errdad;

		valid_lft = ifp->valid_lft;
		preferred_lft = ifp->prefered_lft;

		spin_unlock_bh(&ifp->lock);

		if (idev->cnf.max_addresses &&
		    ipv6_count_addresses(idev) >=
		    idev->cnf.max_addresses)
			goto lock_errdad;

		net_info_ratelimited("%s: generating new stable privacy address because of DAD conflict\n",
				     ifp->idev->dev->name);
		//重新设置到接口中
		ifp2 = ipv6_add_addr(idev, &new_addr, NULL, pfxlen,
				     scope, flags, valid_lft,
				     preferred_lft);
		if (IS_ERR(ifp2))
			goto lock_errdad;

		spin_lock_bh(&ifp2->lock);
		ifp2->stable_privacy_retry = retries;
		ifp2->state = INET6_IFADDR_STATE_PREDAD;
		spin_unlock_bh(&ifp2->lock);

		addrconf_mod_dad_work(ifp2, net->ipv6.sysctl.idgen_delay);
		in6_ifa_put(ifp2);
lock_errdad:
		spin_lock_bh(&ifp->lock);
	} else if (idev->cnf.accept_dad > 1 && !idev->cnf.disable_ipv6) {
		addr.s6_addr32[0] = htonl(0xfe800000);
		addr.s6_addr32[1] = 0;

		if (!ipv6_generate_eui64(addr.s6_addr + 8, idev->dev) &&
		    ipv6_addr_equal(&ifp->addr, &addr)) {
			/* DAD failed for link-local based on MAC address */
			idev->cnf.disable_ipv6 = 1;

			pr_info("%s: IPv6 being disabled!\n",
				ifp->idev->dev->name);
		}
	}

errdad:
	/* transition from _POSTDAD to _ERRDAD */
	ifp->state = INET6_IFADDR_STATE_ERRDAD;
	spin_unlock_bh(&ifp->lock);
	//再次进行dad检查
	addrconf_mod_dad_work(ifp, 0);
}
从上述代码可以看出基本流程是按照RFC7217的说明:
 如果DAD失败,就会在DAD_Counter满足idgen_retries,就会重新生成, DAD_Counter增加的地方有两个:
1. 刚生成地址时,检查是否是保留interface id,如果是,DAD_Counter 加一,重新生成stable address
2. DAD失败之后,重新生成stable address,也会加一







作者:wdscq1234 发表于2016/10/16 10:45:42 原文链接
阅读:55 评论:0 查看评论

iOS常见问题

$
0
0

以前整理在word文档里的,不过最近下重新整理资料,还是把这些东西放到博客里吧,方便查找,在顺带撑撑门面

一storyboard连线问题


产生原因:将与storyboard关联的属性删除了,但是storyboard中还保持之前所关联的属性。
解决:
1.    点击view controller
2.    点击这排最后一个按钮
3.    会出现
4.    发现感叹号。点击感叹号的左边的x,取消关联就不会报错了。

 

二 文本框中怎么输入显示类似输入密码时候的东东。

解决:勾选这个

三.输入文本的时候,怎么显示右边的X按钮

1.点击

 

2.会显示

 

 

3.选择,表示当编辑的时候,会出出现X。

效果:

 

                                                           

11.22 常见问题

一.Storyboard连线问题。

报错原因:1.没有实现btnClick这个方法。

解决方式一:添加这个btnClick这个方法

解决方式二:

1.点击view controller

2.点击这排最后一个按钮

3.会出现

4.发现感叹号没,和之前一样,x了它,就哦了。

注意点;OC中冒号也算做方法名的一部分喔,记住!

 

 

二.结构体问题

 

报错原因:OC语法规定:不允许直接修改某个对象的结构体属性的成员

_btn 是个对象

frame是个结构体。

对象和结构体是不一样的,结构体是C语言中的,里面可以定义许多属性,但是不能定义方法,而对象是即可以定义属性又可以定义方法的,是典型的面向对象语法。

如何改变对象中结构体属性的成员:

解决方法一:

    // 既然不能直接修改对象中的结构体属性成员

    // 先取出结构体

    CGRect frame = _btn.frame;

    // 修改结构体

    frame.origin.y -= 10;

    // 将修改后的结构体重新赋值回去

    _btn.frame = frame;

 

解决方法二:

    // 先取出y

    CGFloat y = _btn.frame.origin.y;

    // 修改y

    y -= 10;

    // 重新设置_btny值,其他属性和_btn保持不变

_btn.frame = CGRectMake(_btn.frame.origin.x, y, _btn.frame.size.width,_btn.frame.size.height);

三.Id问题

报错原因:id类型不能使用点语法

解决方式一:

    // 利用get方法获取tag

    NSInteger i = [sender tag];

 

解决方式二:

// id强转为UIButton

    UIButton *button = (UIButton *)sender;

    // 就能使用点语法获取tag,编译器很笨的,他只会根据当前类型,去判断是否能使用这个语法。一般强转为对应类型,就能使用对应类型的方法了。

NSInteger i = button.tag;

 

 

四.如果发现给控件设置transform属性,控件没有任何反应,或者反应了,但是效果不对。

解决方法:将这个选项取消勾选。

五.如果想让同一个控件同时即改变位置的移动,又放大。这样设置是无效果的。

    _btn.transform = CGAffineTransformMakeTranslation(0, 100);

_btn.transform = CGAffineTransformMakeScale(1.2, 1.2);

 

这样操作是创建新的transform然后赋值,给按钮的transform,第二次赋值的会把之前赋值的给覆盖,所以会达不到想要的效果

解决方法:

    _btn.transform = CGAffineTransformMakeTranslation(0, 100);

    // 在之前的transform情况下,继续添加缩放的形变。

    _btn.transform = CGAffineTransformScale(_btn.transform, 1.2, 1.2);

 

 

 

 

六.四舍五入问题。

float i = 1.7;

    // 会自动四舍五入,不保留小数

    NSLog(@"%0.f",i); // 打印结果2

    // 强转类型不会四舍五入

    int j = (int)i;

    NSLog(@"%d",j); // 打印结果1

 

 

 

 

 

 

七.优先级问题

//    int b = 2;

//    int a = 4 *(b == 2? 1:2);

//   NSLog(@"%d",a); 打印出4

   

//    int a = 4 * b == 2? 1:2;

//    NSLog(@"%d",a); 打印出2

   

//    由此得出 * == 优先级高,先算*,在算==

 

 

八.模拟器黑屏

解决方法:

九.打代码时,Xcode没提示

解决方法:

0. 点击Preferences

1. 进入Text Editing

2. 勾选

 

 

 

 

 

 

 

 

十.有的同学可能在勾选 Autolayout的时候,搞错了对象,误把控制器的View的User Interaction Enabled勾选掉了。

解决办法:User Interaction Enabled 必须勾选,否则控制器根视图中所有子控件无法进行任何操作。

 

 

11.23 常见问题

一.访问权限

错误:

会报链接错误。

 

报错原因,利用下划线访问了@package这个权限里的东西。

被@package 修饰的成员属性只能在同一个框架内部才允许访问。否则会引发link erro。

@private 实例变量只能被声明它的类访问

@protected 实例变量能被声明它的类和子类访问。

@public 实例变量可以被任何类访问。

 

11.24 常见问题

一.运行程序时,显示运行完成,但是模拟器没反应。

问题原因:有时候应用程序的标示符一样,会导致程序不能成功运行。

解决方式一:将模拟器之前的程序删除。

解决方式二:  将模拟器还原

 

二.2d表示保留两位 02d 表示不够了用0来补齐。

    NSLog(@"%02d",cols);

 

 

 

三.Plist文件读取。

 

错误原因:不要看到有很多元素的,就认为是数组。

这样解析是错误的。

_dict = [NSArray arrayWithContentsOfFile:path];

 

这个pist文件是一个字典,正确的解析此plist文件。

   

   // 2.根据文件路径加载字典

_dict = [NSDictionary dictionaryWithContentsOfFile:path];

 

四,给程序拖图片的时候,一定要注意,如下图勾选的,都要勾选中。

 

 

五.禁用UITextField的双击出现复制粘贴剪切等操作

解决方式:

有时候我们需要禁用UITextField的双击出现copy paste的功能,然而UITextField本身没有直接设置禁止用户复制粘贴剪切操作等方法,但是可以重载canPerformAction方法来实现。

 

新建一个类继承UITextField,然后实canPerformAction方法:
只需覆盖canPerformAction:withSender方法就可以,canPerformAction:withSender属于UIResponder类的。

如下:

-(BOOL)canPerformAction:(SEL)actionwithSender:(id)sender {

 

UIMenuController*menuController = [UIMenuController sharedMenuController];

if (menuController){

[UIMenuControllersharedMenuController].menuVisible = NO;

}

returnNO;

}

 

11.25 常见问题

1.将UIButton添加到UILabel,UIButton是不能点击的。

 

原因: 1.UILabel是继承UIView的,默认不能监听点击事件。UIButton是继承UIControl能够监听点击事件。

2.将UIButton添加到UILabel,他们之间的关系是UILabel是UIButton的父视图,父视图都不能监听点击事件,自然而然不会将事件传递给子视图,因此UIButton也不能监听点击事件了。

 

思维指导:有些人会认为UIControl不是继承UIView的吗,都是继承UIView,为什么单独继承UIControl可以监听点击事件,继承UIControl的父类UIView不能监听事件了,这是因为我们一般在父类里实现的都是一些共用的属性和方法,而在子类中具体实现子类特有的方法。因此在UIControl实现了监听点击的特有方法,即继承UIControl才能监听点击事件。

2.发现不少人在给成员变量初始化的时候,容易进错一个方法去初始化。

注意这个方法只有在内存发生警告的时候才会调用。

- (void)didReceiveMemoryWarning

{

    [super didReceiveMemoryWarning];

    // Dispose of any resources that can be recreated.

   

}

大部分成员属性的初始化应该在viewDidLoad里面进行。

- (void)viewDidLoad

{

    [super viewDidLoad];

}

 

 

最牛解决方法:在一开始就将didReceiveMemoryWarning这个方法删掉。删掉不会影响程序运行。

 

3.模拟器问题

当出现这个问题的时候,原因:没有选择模拟器。

解决办法:

 

 

 

11.26常见问题

1.对象方法和类方法问题(此问题基础好的可以看看,基础稍微差的,可以先放着,以后回顾的时候,看看。)

    // 创建视图的工厂方法

    + (UIView *)rowViewInitWithicon:(UIImage*)icon shuju:(NSString *)shuju

    {

        rowView *viewtext = [[NSBundlemainBundle]loadNibNamed:@"rowView" owner:nil options:nil][0];

        [viewtext.btntouxiangsetBackgroundImage:icon forState:UIControlStateNormal];

        viewtext.mingzilablexiao.text = shuju;

       

        // 重点是这句

        //  这是往通知中心添加一条通知指定通知名称为  back  当观察者self监听到 back 通知是就调用callback

        [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(callback) name:@"back" object:nil];

       

        /*   以下是报错信息:

         +[rowView callback]: unrecognizedselector sent to class 0x79d8

         2013-11-26 15:31:02.581lianxirenlianxi[1266:c07] *** Terminating app due to uncaught exception'NSInvalidArgumentException', reason: '+[rowView callback]: unrecognizedselector sent to class 0x79d8'

         */

        // reason: '+[rowView callback]: 看到报错原因里的+就想到没有实现callback这个类方法

       

        // 由于self这个观察者是在类方法中添加的,指的是一个类,所以在调用方法的时候,他会去类方法中找有没有这个方法,不会去对象方法中找。因此我们也应该实现类方法。因此这里的self也只能调用类方法

   

    //由于实现的callback为对象方法  所以会报错

        //  解决方法  callback  写成类方法   供观察者调用

        return viewtext;

    }

    //callback方法

    - (void)callback

    {

        NSLog(@"11111111111");

    }

解决方法,将callback 写成类方法供观察者调用

//callback方法

    + (void)callback

    {

        NSLog(@"11111111111");

    }

 

 

 

2.Xib是用来描述视图长什么样子,一个项目中允许有很多xib,因此我们需要给xib绑定一个标识,即他View中对应的class是谁,就代表描述哪个class。

Xib中owner的class是用来告诉xib中的View需要调用哪个对象的方法,就填谁。比如需要调用dog类中的方法,就填dog。

注意:在连线选择上别连错了,

步骤一:先考虑自己是想给视图添加控件了还是想给视图添加事件

步骤二:添加控件就跟xib中的view连线。添加一些事件就给xib中的File’s Owner 连线。

 

 

 

 

3.代码顺序问题

[UIView animateWithDuration:0.5 animations:^{

        CGRect  tmpFrame = sender.superview.frame;

        tmpFrame.origin.x = self.view.frame.size.width;

       sender.superview.frame = tmpFrame;

       sender.superview.alpha = 0;

    } completion:^(BOOL finished) {

        int index = [self.view.subviewsindexOfObject:sender.superview];

       [sender.superview removeFromSuperview];

        [UIView animateWithDuration:0.2 animations:^{

            for (int i = index; i<self.view.subviews.count; i++)

            {

                UIView *chlid = self.view.subviews[i];

                CGRect tmp = chlid.frame;

               tmp.origin.y -=kViewH+1;

               chlid.frame =tmp;

            }

           

        }];

        // 在这判断删除按钮是否允许点中,会在动画执行完毕的时候,判断。

        _removeIteam.enabled = self.view.subviews.count>1;

 

    }];

     // 而在执行代码块之外,判断删除按钮是否允许点中是不对的,因为动画是在后台运行的,所以在执行动画的时候,就已经执行完判断语句了,而这时最后一个视图还没销毁掉,因此删除按钮永远不会不允许选中,也就不能在判断删除按钮是否允许点中。

//_removeIteam.enabled = self.view.subviews.count>1;

 

 

删完最后一行之后,正确的效果。

删完最后一行之后,错误的效果。原因,判断的位置放错了。

 

 

 

4.Xib描述视图的时候,已经固定好描述视图的宽高了,外界调用视图的时候,只需要设置x,y值就好了

1.出现的问题,创建xib描述的视图时,将宽度设置为一个按钮的宽度了,导致删除按钮不能点击。

 

#pragma mark 添加联系人

- (IBAction)AddPerson:(UIBarButtonItem *)sender {

   

    NSString *imgName=[NSString stringWithFormat:@"01%d.png",arc4random_uniform(9)];

    NSString *labelName = arr[arc4random_uniform(arr.count)];

    RowView *rowView = [RowView rowViewWithIcon:imgName name:labelName];

    UIView *lastView = [self.view.subviews lastObject];

    int nextY = lastView.frame.origin.y + kSpace + kItemHW ;

    // 设置rowView的位置和尺寸

    CGRect cg =CGRectMake(0, nextY, kItemHW, kItemHW);

    rowView.frame=cg;

   

    [self.view addSubview:rowView];

}

错误原因:设置rowView的宽度为kItemHW,因此会有以上图片的出现。

 

 

 

错误会导致删除按钮不能点击,原因:父视图的尺寸不够,即父视图能接收事件的尺寸只有一点点,也就导致超出父视图尺寸的子视图不能监听点击事件。还有一点需要注意,将子视图添加到父视图尺寸之外的位置,只要还在屏幕上就会显示子视图,只不过它不能接收任何事件。

解决方法:CGRect cg =CGRectMake(0, nextY,rowView.frame.size.width , kItemHW);

  这样设置就好了,因为xib里面已经设置了rowView的尺寸了,外界不需要更改视图的宽度了,直接获取视图的宽度即可。

正确效果:

 

 

5.  UIToolBar问题

注意UIToolBar中不能使用viewWithTag这个方法,获取UIToolBar里的子视图。因为UIToolBar里的子视图都是UIBarButtonItem,而UIBarButtonItem是继承NSObject的,因此不能使用viewWithTag获取UIToolBar里的子视图,

viewWithTag:实现原理

- (UIView *)viewWithTag:(NSInteger)tag

{

   

// 1.如果当前tag和当前视图tag相同,直接返回

if (self.tag == tag) return self;

  

    // 2.如果和当前视图tag不相同,遍历当前视图的所有子控件,查找对应的tag

for (UIView *view in self.subviews) {

   // 3.如果view不是UIView类或者UIView的子类直接返回nil

if (![view isKindOfClass:[UIView class]]) return nil;

        if (tag == view.tag) {

    //  4. 返回有相匹配的视图

            return view;

        }

    }

    // 5.如果都没有找到,返回nil.

    return nil;

}

       11.27常见问题

1.结构体和对象问题

// 这样定义是错的,结构体不是对象,声明变量是不需要加*

CGRect *frame = self.view.frame;

结构体变量正确定义:

CGRect frame = self.view.frame;

CGPoint center = self.view.center;

CGSize size = self.view.frame.size;

2.内存管理问题

            错误打印:

正确打印:

当对象被销毁,一定会调用的方法,可以用这个方法,判断对象在什么时候销毁,用这个调试。

 

 

3.创建模型的时候,尽量自定义一个工厂方法供外界调用。

// 工厂方法,简化对象的实例化

+ (id)provinceWithName:(NSString *)name;

 

工厂方法好处:简化对象的实例化,快速创建对象。

4.非ARC内存管理问题。

有些同学在创建项目的时候忘记点ARC了,导致一些成员属性都莫名其妙的释放了。然后出现了一系列莫名其妙的错误。

在滚动UITableView的时候出现野指针错误。

一出现这些野指针错误,首先应该想到某些对象被释放了,然后发现代码中,并没有什么造成对象被释放的情况,这时候应该马上想到很可能是非ARC弄的。下图为怎么查看项目是否是非ARC

在非ARC中没有强引用的概念,因此下图的成员变量是没有被强引用的。

在看看下图,allPro数组没有通过alloc调用,没有调用alloc产生的对象都是自动释放的

    allPro=@[

            @{

                kCities:@[@"浦东",@"杨浦",@"闸北",@"闵行"],

                kHeader:@"上海",

                kFooter:@"上海不错"

            },

            @{

                kCities:@[@"海淀",@"昌平",@"天安门"],

                kHeader:@"北京",

                kFooter:@"北京很好"

             }

   

    ];

所以在滚动的时候会出现野指针错误,因此需要将项目改成ARC。如下图

两个勾都得选中,然后一直点确认就OK了。

6.  数据模型属性采用的策略中除了基本类型需要用assign,字符串需要用copy,其他对象类型都需要用strong。而控制器中视图采用的策略需要根据情况而定。

下图就是数据模型中属性用错了策略导致,UITableViewcell重新出现到界面时,会导致数据丢失。

 

 

11.29 常见问题

1.想在点击cell时做些操作,方法选错了。

下面两个方法太相似了,很容易选错。

// 当点击一行cell时,会调用这个方法

-  (void)tableView:(UITableView *)tableViewdidSelectRowAtIndexPath:(NSIndexPath *)indexPath

// 当取消选择一行cell时,会调用这个方法

-   (void)tableView:(UITableView *)tableViewdidDeselectRowAtIndexPath:(NSIndexPath *)indexPath

 

2.字符串小调试技巧

当把一个字符串转换成基本数据类型时,字符串打印有值,但是转换成基本数据类型为0时,这个莫名其妙的问题,首先应该想到字符串中很可能有换行符合等,导致转换不成功。

NSString *str = @"                                                                                 123";

    NSInteger i = [str integerValue];

    NSLog(@"%@",str);

NSLog(@"%d",i);

碰到这种情况,可以在打印字符串的时候在占位符两边各加一个数字.

    NSLog(@"1%@1",str);

然后看打印结果就能知道有字符串中有多少空行了

只要将空行去掉就能转换成功了。

字符串去掉空格的方法

// 此方法是通过什么字符集裁剪字符串。

- (NSString *)stringByTrimmingCharactersInSet:(NSCharacterSet *)set;

 

NSCharacterSet对象可以通过类方法创建

    // 创建空格和换行字符集

    [NSCharacterSet whitespaceAndNewlineCharacterSet];

    // 创建空格字符集

    [NSCharacterSet whitespaceCharacterSet];

3.下图的警告意思是:初始化时,类型指向不匹配,应该用NSArray * 而不是YZPerson *

 

 

找到原因后,然后看person方法是怎么声明。

解决方式:将NSArray * 改成id。

 

11.30 常见问题

 

1.UITableView数据源问题

1.1

错误原因:说YZViewController 没有实现tableView:numberOfRowsInSection:

解决方式:实现tableView:numberOfRowsInSection:

 

1.2

这里返回空,会报错。

UITableView内部实现原理:

数据源实现了这个方法

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

 

tableView内部自动会调用以下方法添加cell。

[tableView addSubview:cell];

 

如果返回的cell为空,也就意味着生成下面一行代码。

[tableView addSubview:nil];

addSubview是将右边参数添加到数组中保存起来,而数组是不能添加空值的。所有集合对象都不能出传空。例如数组,字典,NSSet

 

以上错误总结:作为tableView的数据源必须实现两个方法。

返回行数

-(NSInteger)tableView:(UITableView*)tableViewnumberOfRowsInSection:(NSInteger)section;

返回每一行显示的内容

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

另外返回每一行显示的内容不能返回nil

 

 

2.在数组删除一个模型,并不代表把这个模型给释放了。

上面步骤二,仅仅是将模型从数组中销毁,而模型并没有被销。

 

12.1 常见问题

1.stroyboard中显示的跟根视图是UIView,而stroyboard的控制器是UITableViewController就会报这个错误

原因是:UITableViewController控制器不能加载UITableView,因为它会去加载stroyboard中的UIView。

解决方式:将stroyboard中的UIView改成UITableView

 

12.2 常见问题

1.自定义视图的属性命名冲突问题

当发现自己描述的xib和运行的时候展现出来的不一样的时候,这时候已经想到自己命名的属性名称和系统命名的冲突了。

 

错误原因

系统自带的UITableViewCell中也有imageView这个属性,因此冲突了。

解决办法:将自定义视图的imageView属性名称改成iconView.

注意:以后自定义属性命名不要和系统自带的属性名称相同。

 

 

2.链接错误

以后看到duplicate这个词语,错误原因就是重复定义了类,函数方法等等。

一般都是因为导入了.m文件

错误:

解决方式:将#import "newsCell.m"这一行删掉。

 

3.注意将之前storyboard中控制器删除之后,拖入一个新的控制器的时候,stroyboard中控制器的class也要重新填入自己想要展示的控制器,告诉stroyboard去加载哪个控制器。

 

12.3常见错误

1.初始化方法命名规范问题。

看见这个错误,应该要想到初始化方法命名错误的问题。因为self只能在init开头的方法中赋值,init必须是一个独立的单词,因此init后的第一个字母必须大写。

12.4常见错误

1.重写set方法忘记赋值,以后重写set方法,第一步就先赋值。

2.strong和weak乱用

一般情况:代理和控件使用weak

其他对象使用strong

基本数据类型使用assign

12.5常见错误

1.QQ好友列表中,展开了列表,但是箭头没动画。

原因:由于代理方法中重新刷新了表格,也就意味着把之前的头部视图给替换了,因此没有动画了,不要创建新的头部,才能让旧的头部执行动画

解决方法:用一个数组或者字典保存所有的头部视图,重新刷新的时候,直接取就OK了。

12.6 常见错误

1.加载xib时,名字是分大小写的,注意名字一定要保持一致。

报错原因:xib是大写的KeyboardTool,而加载的时候名字写成小写了。

解决方式:将加载的xib的名称改成大写。



作者:jyt199011302 发表于2016/10/16 10:54:32 原文链接
阅读:25 评论:0 查看评论

java使用url读取网页资源

$
0
0
package com.susu;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

public class URLDemo2 {
	public static void main(String[] args) {
		try {
			//创建一个url实例
			URL url=new URL("http://www.baidu.com");
			//通过url的openStream获取url对象所表示资源的字节输入流
			InputStream is=url.openStream();
			//将字节输入流转换为字符输入流
			InputStreamReader isr=new InputStreamReader(is,"utf-8");
			//为字符输入流添加缓冲
			BufferedReader br=new BufferedReader(isr);
			String res=null;
			String line=null;
			//读取数据
			while((line=br.readLine())!=null){
				res+=line;
			}
			br.close();
			isr.close();
			is.close();
			System.out.println(res);
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
}


作者:su20145104009 发表于2016/10/16 11:17:29 原文链接
阅读:62 评论:0 查看评论

java/android 设计模式学习笔记(24)---访问者模式

$
0
0

  这篇博客我们来介绍访问者模式(Visitor Pattern),这也是行为型设计模式之一。访问者模式是一种将数据操作与数据结构分离的设计模式,它可以算是 23 中设计模式中最复杂的一个,但它的使用频率并不是很高,大多数情况下,你并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是需要使用它了。
  访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。
  转载请注明出处:http://blog.csdn.net/self_study/article/details/52778713
  PS:对技术感兴趣的同鞋加群544645972一起交流。

设计模式总目录

  java/android 设计模式学习笔记目录

特点

  封装一些作用于某种数据结构中的个元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
  访问者模式使用的场景

  • 对象结构比较稳定,但经常需要在此对象结构上定义新的操作;
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

UML类图

  这里写图片描述
  访问者模式角色介绍:

  • Visitor:接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素个数是一样的,因此访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式;
  • ConcreteVisitor:具体地访问者,它需要给出对每一个元素类访问时所产生的具体行为;
  • Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问;
  • ElementA,ElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法;
  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。
  我们在介绍双重分派的时候会列出这个模式的通用代码。

示例与源码

  查阅相关资料时,了解到了有一个分派的概念,总结一下:

分派的概念

  变量被声明时的类型叫做变量的静态类型(Static Type),有些人又把静态类型叫做明显类型(Apparent Type);而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。比如:

List list = null;
list = new ArrayList();

声明了一个变量list,它的静态类型(也叫明显类型)是List,而它的实际类型是ArrayList。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派动态分派静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派;动态分派(Dynamic Dispatch)发生在运行时期,动态分派动态地置换掉某个方法。接着我们来具体看看静态分派动态分派

静态分派

  Java通过方法重载支持静态分派,我们以一个例子来看一下:

public class StaticDispatch {

    public void sayHello(Human guy) {
        System.out.println("hello, guy!");
    }

    public void sayHello(Man guy) {
        System.out.println("hello, man!");
    }

    public void sayHello(Women guy) {
        System.out.println("hello, women!");
    }

    public static void main(String[] args) {

        Human man = new Man();

        Human women = new Women();

        StaticDispatch sd = new StaticDispatch();

        sd.sayHello(man);

        sd.sayHello(women);

    }
}

class Human {
}

class Man extends Human {
}

class Women extends Human {
}

最后的输出结果:

hello, guy!
hello, guy!

没错,程序就是大家熟悉的重载(Overload),而且大家也应该能知道输出结果,但是为什么输出结果会是这个呢,先来看一下代码的定义:

Human man = new Man();

我们把 Human 称为变量的 静态类型, Man 称为变量的 实际类型,其中,变量的静态类型和动态类型在程序中都可以发生变化,而区别是变量的静态类型是在编译阶段就可知的,但是动态类型要在运行期才可以确定,编译器在编译的时候并不知道变量的实际类型是什么。现在回到代码中,由于方法的接受者已经确定是 StaticDispatch 的实例 sd 了,所以最终调用的是哪个重载版本也就取决于传入参数的类型了。实际上,虚拟机(应该说是编译器)在重载时时通过参数的静态类型来当判定依据的,而且静态类型在编译期就可知,所以编译器在编译阶段就可根据静态类型来判定究竟使用哪个重载版本。于是对于例子中的两个方法的调用都是以 Human 为参数的版本,Java中,所有以静态类型来定位方法执行版本的分派动作,都称为静态分派

动态分派

  我们再来看看动态分派,Java 通过方法的重写支持动态分派,它和多态的另外一个重要体现有很大的关联,这个体现是什么,可能大家也能猜出,没错,就是重写(override),我们来看看例子:

public class DynamicDispatch {

    public static void main(String[] args) {
        Human man = new Man();
        Human women = new Women();

        man.sayHello();
        women.sayHello();

        man = new Women();
        man.sayHello();
    }

}

abstract class Human {
    protected abstract void sayHello();
}

class Man extends Human {
    @Override
    protected void sayHello() {
        System.out.println("hello man!");
    }
}

class Women extends Human {
    @Override
    protected void sayHello() {
        System.out.println("hello women!");
    }

}

输出结果很显然:

hello man!
hello women!
hello women!

其实由两次改变 man 变量的实际类型导致调用函数版本不同,我们就可以知道,虚拟机是根据变量的实际类型来调用重写方法的。我们也可以从例子中看出,变量的实际类型是在运行期确定的,重写方法的调用也是根据实际类型来调用的,而不是根据静态类型。我们把这种在运行期根据实际类型来确定方法执行版本的分派动作,称为动态分派。

分派的类型

  一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参数统称做方法的宗量。比如下面例子中的Test类:

public class Test {
    public void print(String str){
        System.out.println(str);
    }
}

在上面的类中,print() 方法属于 Test 对象,所以它的接收者也就是 Test 对象了。print()方法有一个参数是 str,它的类型是 String。
  根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言(Uni-Dispatch)多分派语言(Multi-Dispatch)。单分派语言根据一个宗量的类型进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。C++ 和 Java均是单分派语言,多分派语言的例子包括 CLOS 和 Cecil 。按照这样的区分,Java 就是动态的单分派语言,因为这种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这种语言对重载方法的分派会考虑到方法的接收者的类型以及方法的所有参数的类型。
  在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪一个操作:一是请求的名字,二是接收者的真实类型。单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。在 Java 语言里面,如果一个操作是作用于某个类型不明的对象上面,那么对这个对象的真实类型测试仅会发生一次,这就是动态的单分派的特征。

双重分派

  一个方法根据两个宗量的类型来决定执行不同的代码,这就是“双重分派”。Java 语言不支持动态的多分派,也就意味着 Java 不支持动态的双分派。但是通过使用访问者模式,也可以在 Java 语言里实现动态的双重分派。在 Java 中可以通过两次方法调用来达到两次分派的目的。类图如下所示:
这里写图片描述
在图中有两个对象,左边的叫做 West ,右边的叫做 East 。现在 West 对象首先调用 East 对象的 goEast() 方法,并将它自己传入。在 East 对象被调用时,立即根据传入的参数知道了调用者是谁,于是反过来调用“调用者”对象的 goWest() 方法。通过两次调用将程序控制权轮番交给两个对象,其时序图如下所示:
这里写图片描述
这样就出现了两次方法调用,程序控制权被两个对象像传球一样,首先由 West 对象传给了 East 对象,然后又被返传给了 West 对象。但是仅仅返传了一下球,并不能解决双重分派的问题。关键是怎样利用这两次调用,以及 Java 语言的动态单分派功能,使得在这种传球的过程中,能够触发两次单分派。
  动态单分派在 Java 语言中是在子类重写父类的方法时发生的。换言之,West 和 East 都必须分别置身于自己的类型等级结构中,就正如上面的访问者模式 uml 类图,我们写出访问者模式的通用代码:
Visitor角色:West.class

public abstract class West {

    public abstract void goWest1(SubEast1 east);
    public abstract void goWest2(SubEast2 east);
}

ConcreteVisitor角色:SubWest1.class 和 SubWest2.class

public class SubWest1 extends West{
    @Override
    public void goWest1(SubEast1 east) {
        System.out.println("SubWest1 + " + east.myName1());
    }

    @Override
    public void goWest2(SubEast2 east) {
        System.out.println("SubWest1 + " + east.myName2());
    }
}
public class SubWest2 extends West{
    @Override
    public void goWest1(SubEast1 east) {
        System.out.println("SubWest2 + " + east.myName1());
    }

    @Override
    public void goWest2(SubEast2 east) {
        System.out.println("SubWest2 + " + east.myName2());
    }
}

Element角色:East.class

public abstract class East {
    public abstract void goEast(West west);
}

ConcreteElement角色:SubEast1.class 和 SubEast2.class

public class SubEast1 extends East{
    @Override
    public void goEast(West west) {
        west.goWest1(this);
    }

    public String myName1(){
        return "SubEast1";
    }
}
public class SubEast2 extends East{
    @Override
    public void goEast(West west) {
        west.goWest2(this);
    }

    public String myName2(){
        return "SubEast2";
    }
}

Client.class

public class Client {
    public static void main(String[] args) {
        //组合1
        East east = new SubEast1();
        West west = new SubWest1();
        east.goEast(west);
        //组合2
        east = new SubEast1();
        west = new SubWest2();
        east.goEast(west);
    }
}

最后结果如下:

SubWest1 + SubEast1
SubWest2 + SubEast1

系统运行时,会首先创建 SubWest1 和 SubEast1 对象,然后客户端调用 SubEast1 的goEast() 方法,并将 SubWest1 对象传入。由于 SubEast1 对象重写了其超类 East 的 goEast() 方法,因此,这个时候就发生了一次动态的单分派。当 SubEast1 对象接到调用时,会从参数中得到 SubWest1 对象,所以它就立即调用这个对象的 goWest1() 方法,并将自己传入。由于 SubEast1 对象有权选择调用哪一个对象,因此,在此时又进行一次动态的方法分派。这个时候 SubWest1 对象就得到了 SubEast1 对象。通过调用这个对象 myName1() 方法,就可以打印出自己的名字和 SubEast 对象的名字,其时序图如下所示:
这里写图片描述
由于这两个名字一个来自East等级结构,另一个来自West等级结构中,因此,它们的组合式是动态决定的,这就是在 Java 中动态双重分派的实现机制。

总结

  在现实情况下,我们要根据具体的情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,使用访问者模式是否能够优化我们的代码,而不是使我们的代码变得更复杂。在使用一个模式之前,我们应该明确它的使用场景、它能解决什么问题等,以此来避免滥用设计模式的现象,所以,在学习设计模式时,一定要理解模式的适用性以及优缺点。
  访问者模式的优点:

  • 各角色职责分离,符合单一职责原则
  • 能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能,具有良好的扩展性;
  • 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化;
  • 灵活性。
  访问者模式的缺点:
  • 具体元素对访问者公布细节,违反了迪米特原则
  • 具体元素变更时导致修改成本大;
  • 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象。

源码下载

  https://github.com/zhaozepeng/Design-Patterns/tree/master/VisitorPattern

引用

https://en.wikipedia.org/wiki/Visitor_pattern
http://www.cnblogs.com/java-my-life/archive/2012/06/14/2545381.html
http://blog.csdn.net/jason0539/article/details/45146271
https://my.oschina.net/sel/blog/215959

作者:zhao_zepeng 发表于2016/10/16 11:35:29 原文链接
阅读:81 评论:0 查看评论

NDK开发 从入门到放弃

$
0
0

一、前言

前段时间听朋友说在学习NDK开发,说现在的普通安卓程序猿马上都要失业了,吓得我赶紧去各大招聘平台搜了下,确实有不少招聘都写着要求NDK开发经验、JNI开发经验、H5/JS开发经验、cocos2d开发经验、蓝牙等底层开发经验巴拉巴拉的,还有些招安卓的还要求同时具有ios开发经验是什么鬼... 唉,安卓程序猿不好混呀,近段时间微信小程序也是火的不要不要的,天天处在要失业的阴影中。为了赚口猿粮不容易,还是花点时间来瞅瞅NDK开发(也叫JNI开发)吧。

● NDK

Native Development Kit(NDK)是一系列工具的集合。它提供了一系列的工具,帮助开发者快速开发C/C++的动态库,并能自动将so和java应用一起打包成apk。

● JNI

Java Native Interface(JNI)标准是java平台的一部分,JNI是Java语言提供的Java和C/C++相互沟通的机制,Java可以通过JNI调用C/C++代码,C/C++的代码也可以调用java代码。

 JNI与NDK的关系

NDK可以为我们生成了C/C++的动态链接库,JNI是java和C/C++沟通的接口,两者与android没有半毛钱关系,只因为安卓是java程序语言开发,然后通过JNI又能与C/C++沟通,所以我们可以使用NDK+JNI来实现“Java+C”的开发方式。

● 为什么要NDK开发

1、项目需要调用底层的一些C/C++的一些东西(java无法直接访问到操作系统底层(如系统硬件等,或者已经在C/C++环境下实现了功能代码,直接使用即可。NDK开发常用于驱动开发、无线热点共享、数学运算、实时渲染的游戏、音视频处理、文件压缩、人脸识别等。

2、为了效率更加高效些。这个可能并不能成为NDK开发的优点,因为虽然C/C++代码是高效的,但是在java与C/C++相互调用时增大了开销;

3、基于安全性的考虑。防止代码被反编译,为了安全起见,使用C/C++语言来编写重要的部分以增大系统的安全性,最后生成so库(用过第三方库的应该都不陌生)便于给人提供方便。(任何有效的代码混淆对于会smail语法反编译你apk是分分钟的事,即使你加壳也不能幸免高手的攻击)

4、便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。

二、安装与配置

首先我们在Android Studio下新建一个安卓项目。然后打开Project Structure界面,如下:


在SDK Location目录下,有SDK和NDK的路径,而这里我们暂时还未下载配置过NDK,故我们需要点击Download Android NDK来进行下载。这里Android Studio会下载最新版本的NDK进行安装,默认会下载保存在SDK的路径。我们在上图中还能看到有一段介绍文字,说SDK以及NDK的路径配置会保存在local.properties文件内,安装完成后我们刷新Project,进local.properties文件查看也能看到SDK与NDK的路径。


NDK下载配置完成之后,需要在gradle.properties文件中加上一行:

android.useDeprecatedNdk=true
接下来,我们借助强大的Android Studio的插件功能,在External Tools下配置两个非常有用的插件。进入Settings-->Tools-->ExternalTools,点击+号增加。


javah -jni命令,是根据java文件生成.h头文件的,会自动根据java文件中的类名与方法名生成对应的C/C++里面的方法名。下面是参数配置及其含义:

1.Program:    $JDKPath$\bin\javah.exe    这里配置的是javah.exe的路径
2.Parametes:    -classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$    这里$FileClass$指的是要执行操作的类名(包名.类名),$ModuleFileDir$/src/main/jni表示生成的文件保存在这个module目录的src/main/jni目录下。
3.Working: $ModuleFileDir$\src\main\java 

使用方式:选中java文件--->右键--->External Tools--->javah-jni,将生成jni文件夹以及文件夹下的 包名.类名的.h头文件 (名字过长,我们可以自己重命名)。


ndk -build命令,是根据C/C++文件生成so文件的。下面是参数配置及其含义:

1.Program:    F:\apk\sdk\ndk-bundle\ndk-build.cmd    这里配置的是ndk下的ndk-build.cmd的路径(根据实际情况填写)
2.Working: $ModuleFileDir$\src\main\ 

使用方式:选中C/C++文件--->右键--->ExternalTools--->ndk-build,将在main文件夹下生成libs文件夹以及多个so文件,我们可以移动至jniLibs目录下去。

三、简单实例

接下来我们创建一个访问本地C/C++方法的java类。

public class JniTest {
    /**
     * 将用C++代码实现,在android代码中调用的方法:获取当前app的包名
     * @param o
     * @return
     */
    public static native String getPackname(Object o);

    /**
     * 加载so库或jni库
     */
    static {
        System.loadLibrary("JNI_ANDROID_TEST");
    }
}

注意JNI_ANDROID_TEST这个Library名字,之后还会需要用到,要保持一致。该类提供了一个static的native方法,该方法将用来获取app的包名。然后对该文件执行javah -jni操作,生成对应的.h头文件。


如图,已经根据我们的java类生成了对应的.h文件,文件名为包名_类名.h,我们可以手动改名为jnitest.h,里面只有一个方法,返回值为String(jstring),方法名为Java_类的包名_类名_方法名(包名中的分级不是用.而是_),前面两个参数是C++里面必须有的(JNIEnv代表指向JVM的指针,jclass是调用该方法的java对象),第三个就是我们java类的方法里面的参数Object。

然后我们新建一个C++文件,取名为jnitest.cpp,写上需要include的文件,从.h文件中复制方法过来(方法名、参数类型、返回值等必须一致)。


至此,.h文件和c++文件均已完成,接下来还需要在这个jni目录下增加两个文件,Android.mk和Application.mk

Android.mk,注意LOCAL_MODULE的值与之前的名字相对应,LOCAL_SRC_FILES的值写c++文件。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := JNI_ANDROID_TEST
LOCAL_SRC_FILES =: jnitest.cpp
include $(BUILD_SHARED_LIBRARY)

Application.mk,注意APP_MODULES的值与之前的名字相对应。

APP_MODULES := JNI_ANDROID_TEST
APP_ABI := all

接下来我们需要对C++文件执行ndk-build操作,生成相应的so文件。


如图,在main/libs目录下生成了多个so文件,名字为lib+我们指定的库名(同时还生成了obj文件夹,不知是什么东西)。

这时候我们可以在main目录下新建jniLibs文件夹,把生成的libs文件夹内的东西均复制过去,删除新生成的jni、libs、obj三个文件夹。然后在Activity中测试调用,在TextView上显示我们通过C++代码实现的方法getPackname获取app的包名了。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = (TextView) findViewById(R.id.tv_app_package_name);
        tv.setText("packageName: " + JniTest.getPackname(MainActivity.this));
    }
}


测试能正确得到包名,说明调用成功了。我们可以把JniTest类以及so文件给别人去使用,这样别人是看不到我们的代码实现的,能很好的保护我们的源码。

以上内容大部分来自网络,整理并测试过后,给出自己的一个理解以及详细图文操作步骤,本文仅供新手参考。本人也刚开始接触,文中可能存在错误之处,请谨慎。




      
作者:xiaoyu_93 发表于2016/10/16 12:20:51 原文链接
阅读:23 评论:0 查看评论

android那些事--点阵显示内部分析

$
0
0

看了一个点阵的实现形式,感觉挺好的.对其中的技术点总结一下.

点阵的显示原理

当字符或者图片在点阵上需要显示时,可以认为是数据源在对应的像素点上的像素是有或者没有.
如果有那么这一个像素点九显示反之就不显示.
那么需求就来了 如何得到资源数据(字符或者图片)像素值(这个像素值应该是个二位数组).

像素值的存放

在java中所有的数据底层都是byte(字节),字节数据可以存放到byte数组中.那么存放的问题就解决了

字符资源的存放

String.getBytes()
String.getBytes(Charset)
对于字符串数据可以通过上面的两个方法获取字节数组,但是不建议使用第一个,因为第一个会得到一个缺省编码的字节数组.
通过设置特定的编码格式,可以得到大小不同的字节

这里写图片描述

关于字符资源的转换还有一些其他的点,也顺便记录一下.
与getBytes对应的可以通过设置编码格式从字节数组创建一个新的字符串new String(byte[], Charset) 这里就要求以什么格式获得的字节数组在返回字符串的时候必须设置同样的编码格式,否则就会得到乱码的结果如图所示
这里写图片描述

另外对于iso8859-1编码的结果在返回的时候也会出现乱码,这个是因为在ISO8859-1编码的编码表中就不存在汉字字符
这里写图片描述

因此在通过String.getBytes(Charset)和new String(byte[],Charset)进行转换时需要注意当前的编码格式的编码表中存在目标String的码值

对于特殊需求一定需要使用ISO8859-1编码格式的中文字符怎么办呢比如(http header 要求其内容必须时iso8859-1编码)这个时候可以采用借鸡生蛋的转换方法

public static void main(String[] args) {
        String str="我";
        try {
            byte[]str_gbk=str.getBytes("gbk");
            byte[]str_utf8=str.getBytes("utf-8");
            byte[]str_iso88591=str.getBytes("iso8859-1");

            String reslut=new String(str_iso88591, "iso8859-1");
            System.out.println(reslut);
            System.out.println("----------------");

            String resource=new String(str.getBytes("utf-8"), "iso8859-1");
            String result1=new String(resource.getBytes("iso8859-1"), "utf-8");

            System.out.println(result1);
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

这里写图片描述

GB2312和GBK的关系

1.GBK 是GB2312的扩展
2.GB2312是中国规定的汉字的编码 即简体中文的编码.而GBK
  在兼容GB2312的基础上还支持繁体字的显示,同样还有日文
  的假名

HZK16字库

1.HZK16字库是符合GB2312标准的16*16的点阵字库
2.HZK16的GB2312-80支持的汉字有6763个 
  符号682个其中一级汉字3755个 二级汉字3008

到这里已经得到了字符的资源了,这里先选择使用gbk编码格式原因下面在解释.

两个字节大小的字节数组里存的是什么

1.第一个字节放的就是该汉字的区号,第二个字节放的就是该汉字的位号
2.GB2312汉字有两个字节编码,范围是0XA1A1~0XFEFE
  其中A1-A9为符号区   B0-F7为汉字区

3.以汉字  我 为例在HZK16字库中找到它对应的32个字节的子模数据.
第一个字节为区号,后一个字节为位号.
其中每个区号记录94个汉字,位号为该汉字在该区中的位置,所以要找到这个汉字就必须找到它的区码和位码.

4.区码   汉字的第一个字节(因为汉字编码是从0XA0区开始的,所以文件最前面就是从0XA0区开始, 要算出相对区码)
位码    汉字的第二个字节

这样就可以得到汉字在HZK16中的绝对位置
offset=(94*(区码-1)+(位码-1))*32
注解
1.区码减1是因为数组是以0为开始而区号是以1为开始的
2.(94*(区号-1)+(位码-1))是一个汉字字模占用的字节数
3.最后乘以32 是因为汉字字库文应从 该位置起的32字节信息记录该字的字模信息

这里写图片描述

图片资源的存放

相比字符资源的存取,图片就显得轻松多了.
主要就一个方法的使用
public void getPixels (int[] pixels, int offset, int stride, int x, int y, int width, int height)

把位图的数据拷贝到pixels[]中,每一个都由一个表示颜色值的int值来表示
幅度参数表名调用者允许的像素组行间距
参数解释

  • pixels 接收位图颜色值的数组
  • offset 写入到pixels[]中的第一个像素索引值
  • stride pixels[]中的行间距个数值
  • x 从位图中读取的第一个像素的x坐标值
  • y 从位图中读取的第一个像素的y坐标值
  • width 从每一行中读取的像素宽度
  • height 读取的行数
int[] pixels = new int[bit.getWidth()*bit.getHeight()];//保存所有的像素的数组,图片宽×高

         bit.getPixels(pixels,0,bit.getWidth(),0,0,bit.getWidth(),bit.getHeight());
         for(int i = 0; i < pixels.length; i++){
             int clr = pixels[i];
                int  red   = (clr & 0x00ff0000) >> 16;  //取高两位
                int  green = (clr & 0x0000ff00) >> 8; //取中两位
                int  blue  =  clr & 0x000000ff; //取低两位
                System.out.println("r="+red+",g="+green+",b="+blue);
         }

整理如下图
这里写图片描述

作者:lucky9322 发表于2016/10/16 12:26:54 原文链接
阅读:11 评论:0 查看评论

透过Retrofit使用看其源码设计模式

$
0
0

前言

这篇文章我将从Retrofit的基本用法出发,透过其使用步骤,一步步的探究Retrofit的实现原理及其源码的设计模式。这篇文章可能会将Retrofit中用到的设计模式和其实现原理穿插着写,所以各位同学也可以选择性的阅读。而对于Retrofit具体使用还不太清楚的同学可以去看的另一篇文章Retrofit2的使用介绍

Retrofit基本用法

我以用户登录作为示例:

声明接口

首先我们先定义一个登录服务接口LoginService,如下:

public interface LoginService {
    @FormUrlEncoded
    @POST("login")
    Call<String> login(@Field("username") String name, @Field("password") String password);
}

创建Retrofit对象

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://ittiger.cn")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

发起请求

LoginService service = retrofit.create(LoginService.class);
Call<User> call = service.login("user", "pwd");
call.execute()或call.enqueue()

Retrofit关键类

在讲Retrofit实现原理之前,我先说下Retrofit里面涉及到几个关键类都是干什么用的

  1. Retorift: 负责配置请求过程中的基本参数,如:请求地址,结果转换器,自定义OKHttpClient等,同时还会生成请求接口对象
  2. Call: 网络请求执行者(Retrofit.Call),比如:上面示例中最后调用login方法得到的Call对象就是此接口的实例
  3. OkHttpCall: 此类是Retrofit.Call接口的实现,示例中最后调用login方法得到的Call对象就是此类的实例。但是其底层网络请求执行都是通过OkHttp.Call接口间接执行的,也就是说OkHttpCall是对OkHttp.Call网络请求功能的封装。
  4. Converter & Converter.Factory: 分别负责网络请求结果转换以及生成Converter转换器
  5. CallAdapter & CallAdapter.Factory: 分别负责对Retrofit.Call实例(OkHttpCall)进行适配及生成CallAdapter适配器
  6. Platform: 确定Retrofit当前运行平台,以及确定当前平台默认的的CallAdapter.FactoryExecutor
  7. ExecutorCallAdapterFactory: Android平台下的默认CallAdapter.Factory实现
  8. ServiceMethod: 解析接口服务所有注解、生成请求对象Request、解析请求结果Response
  9. ParameterHandler: 服务接口方法(login())参数解析处理器,配合ServiceMethod进行服务接口参数注解解析
  10. RequestBuilder: 根据参数和URL构造请求需要的OkHttp.Request对象

以上就是Retrofit源码实现中比较关键的10个类及其相关作用

使用流程 >> 实现 >> 设计模式

Builder模式创建Retrofit

Retrofit场景

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://ittiger.cn")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

上面代码的对象创建方式看着是不是似曾相识,看着很眼熟,没错,Android里面的Dialog的创建就是使用的这种方式:Builder模式

Builder模式定义

将一个复杂对象的构建与它的表示分离,使得同样的构建可以创建不同的表示

Builder模式使用场景

  1. 相同的方法不同的执行顺序产生不同的结果
  2. 多个部件都可以装配到一个对象中,但是产生的结果不同

Builder模式类图

这里写图片描述

Retrofit中的Builder模式

  1. Retrofit中的Builder模式是简化版的Builder模式,省略了抽象建造者和指挥者
  2. 不同的配置会对Retrofit产生不同的影响,如果通过addCallAdapterFactory()配置CallAdapterFactory和不配置CallAdapterFactory会对Retrofit产生完全不同的影响。
  3. 如果Retrofit中使用构造方法的方式创建对象,则需要实现多个不同参数的构造方法,而使用构造方法创建对象时如果参数太多,很多时候参数代表的意思是不太理解的,总归来说就是创建过程不直观。

Builder模式优缺点

  • 优点:
    1. 不需要知道产品内部的组成细节,产品与创建过程解耦
    2. 分步组装产品,使得产品的创建过程更精细更清晰
    3. 容易扩展,新产品只需要新建一个建造者即可
  • 缺点:
    1. Builder模式创建的产品差异性小,使用范围受限制
    2. 不同的产品会产生多个建造者和指挥者

Retrofit创建流程 >> Platform

在创建Retrofit过程中有这样一行代码:

Retrofit retrofit = new Retrofit.Builder()
...
.build();

从代码可以看到在创建Retrofit时得先根据Retrofit.Builder内部类的默认构造方法Retrofit.Builder()创建一个Builder对象,所以我们来看看这个默认构造方法里都做了些什么事:

public Builder() {
  this(Platform.get());
}

OK,我们再来看看我们前面说到的Platform这个平台类的静态方法get()

//静态实例对象,类加载就确定了
private static final Platform PLATFORM = findPlatform();

  static Platform get() {
    return PLATFORM;
  }

  private static Platform findPlatform() {
    try {
      Class.forName("android.os.Build");
      if (Build.VERSION.SDK_INT != 0) {
        return new Android();
      }
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("java.util.Optional");
      return new Java8();
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("org.robovm.apple.foundation.NSObject");
      return new IOS();
    } catch (ClassNotFoundException ignored) {
    }
    return new Platform();
  }
  ...
}

通过上面的代码我们可以很明确的知道,在Platform类加载的时候它就通过反射的机制确定了当前运行的平台是属于哪一个,是Android,是Java8还是IOS,并生成对应的平台类的实例,get()方法是用来获取当前的平台类的实例。

目前,我们只关注Android平台下的Platform实例,我们也来看看Android平台类中做了些什么:

static class Android extends Platform {
    @Override public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

    @Override CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }

    static class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override public void execute(Runnable r) {
        handler.post(r);
      }
    }
  }

可以看到Android类中重写了Platform类的两个方法defaultCallbackExecutor()defaultCallAdapterFactory(Executor callbackExecutor)
1. 前者就是用来返回当前平台下默认的Executor,这Android平台下就是MainThreadExecutor这个类的实例,可以看到这个执行器主要就是用来进行线程切换的,因为我们知道安卓平台下所有的UI操作都必须在UI线程中执行。
2. 后者就是用来返回当前平台下默认的CallAdapter.Factory
3. 当然你也可以不使用这两个默认值,都可以在创建Retrofit过程中自定义配置自己需要的相关实例

Retrofit创建流程 >> ExecutorCallAdapterFactory

看完Platform之后紧接着我们再来看看Android平台下默认的CallAdapter.Factory实现ExecutorCallAdapterFactory都做了些什么,这里只贴关键代码:

public interface CallAdapter<T> {
    abstract class Factory {
        public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations,
            Retrofit retrofit);
        ...
    }
}

final class ExecutorCallAdapterFactory extends CallAdapter.Factory {
  final Executor callbackExecutor;//对应默认的MainThreadExecutor

  ExecutorCallAdapterFactory(Executor callbackExecutor) {
    this.callbackExecutor = callbackExecutor;
  }

  @Override
  public CallAdapter<Call<?>> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    ...
    return new CallAdapter<Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public <R> Call<R> adapt(Call<R> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };
  }
  ...
}

大家看源码可以发现CallAdapter.Factory工厂是通过get()方法来创建CallAdapter的,所以ExecutorCallAdapterFactory关键代码也是在get()方法的实现上,上面的代码中我们可以看到get()方法返回一个CallAdapter实例,这就是我前面介绍关键类作用时说到的CallAdapter.Factory主要负责生成CallAdapter的实现。

该类中,我们从其类的命名以及代码实现结构上来看,ExecutorCallAdapterFactory其实也使用了一种设计模式,那就是工厂方法模式,其实Retrofit中还有一个地方也使用了工厂方法模式,那就是Converter & Converter.Factory它的实现方式和CallAdapter & CallAdapter.Factory是一样样的。

工厂方式模式(创建CallAdapter & Converter)

本文我就已CallAdapter进行举例,看懂CallAdapter的创建原理之后,再看Converter的创建也就比较简单,都是一样的道理。

Retrofit场景

Retrofit中使用工厂方式模式的场景我在前面讲ExecutorCallAdapterFactory实现的时候已经讲过了,这里就不重复举例了,大家可以对照着源码看下。

工厂方法模式定义

一个用于创建对象的接口,让子类决定实例化哪个类

工厂方法模式使用场景

  1. 不需要知道其具体的类名,只需要知道生成它的工厂
  2. 一个类通过其子类来决定创建哪个对象

工厂方法模式类图

这里写图片描述

Retrofit中的工厂方法

  1. Retrofit中使用工厂方法模式可以讲CallAdapter的创建与具体实现充分解耦,对于创建我们只需要知道其工厂即可,不需要关注是如何实现
  2. 所以我们可以通过addCallAdapterFactory()addConverterFactory()很方便的自定义我们自己所需要的适配器工厂和数据转换工厂
  3. 通过addCallAdapterFactory()可以很方便的让Retrofit支持RxJava特性,而通过addConverterFactory()可以自定义配置们想要的转换器,让我们可以将请求数据结果转换成我们想要的任意类型。

这些就是Retrofit使用工厂方法模式带来的好处。

工厂方法模式优缺点

  • 优点
    1. 只关注产品工厂即可,不需要关注产品如何创建,由工厂确定如何创建
    2. 扩展性好,新增产品时,只需要新增一个具体工厂和具体产品
  • 缺点
    1. 新增产品时,需要新增具体工厂和具体产品类,使系统变得庞大
    2. 系统中加入抽象层,增加了系统的抽象性和理解难度

适配器模式 >> CallAdapter

Retrofit场景

先来看看CallAdapterRetrofit中的使用场景

public interface CallAdapter<T> {
    public Type responseType();

    public <R> Call<R> adapt(Call<R> call);
}

//ExecutorCallAdapterFactory中生成CallAdapter实例
return new CallAdapter<Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public <R> Call<R> adapt(Call<R> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };

前面讲到ExecutorCallAdapterFactory会生成一个CallAdapter实例。而CallAdapter这个名字看着是不是也很眼熟,也有种似曾相识的感觉,没错,CallAdapter与我们在Android中使用ListViewRecyclerView时经常用到的各种Adapter一样也是一个适配器。

CallAdapter是来适配什么的呢?

还记得前面介绍关键类的时候说到的OkHttpCall吗?CallAdapter就是来适配OkHttpCall实例的,结合上面的代码来说的话在调用CallAdapter.adapt方法时OkHttpCall实例会作为参数传递给adapt方法从而交给CallAdapter去进行适配。

在我前面举的登录示例中,我们调用login()方法得到的Call实例就是CallAdapter适配OkHttpCall之后得到的一个新Call实例对象,至于为什么是这样,我后面会一一讲解,各位看官不要离开

所以Retrofit在这个地方又使用了一种设计模式:适配器模式

适配器模式定义

将一个类的接口变成客户端所需要的另一个接口,从而使原本因接口不匹配而无法一起工作的两个类可以在一起工作

适配器模式使用场景

  1. 需要复用现有类,而现有类不符合系统需求
  2. 需要一个统一的输出接口,而输入端类型不可预知

适配器模式类图

这里写图片描述

Retrofit中的适配器模式

//ExecutorCallAdapterFactory中生成CallAdapter实例
return new CallAdapter<Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public <R> Call<R> adapt(Call<R> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };

Android平台下默认的CallAdapter会将OkHttpCallMainThreadExecutor两个实例对象适配成一个新的Call实例,这个新的Call实例在执行过程中就具备了切换到UI线程的功能。

Retrofit在这个地方为什么要使用适配器模式将OkHttpCall进行适配了,直接拿过来用不就可以了吗?

前面讲过OkHttpCall仅仅只是对OkHttp.Call执行网络请求操作的封装,没有其他功能,也就是说OkHttpCall也只有网络请求的功能,而Retrofit是支持多个平台的(安卓,Java8,IOS,甚至包括支持RxJava特性),而不同的平台可能具有不同的特性。

如果在请求过程中需要用到这些特性的话,那么单靠OkHttp.Call是无法完成的,而如果在其他地方柔和进这些特性的支持可能就会使得框架结构不那么严谨平台解耦性比较差,甚至有可能会增加更多的接口。

Retrofit通过使用适配器模式将平台特性与OkHttpCall适配成一个最终我们需要的Call实例,这样的话我们在使用过程中只需要关注最后拿到的Call对象,而不需要关注底层这个Call实例到底是什么样的,这也就为我们支持更多的特性提供了可能。比如对RxJava特性的支持,我们只需要提供一个支持RxJava特性的CallAdapter适配器即可,所以我们就可以通过addCallAdapterFactory()配置我们提供的支持RxJava特性的CallAdapter.Factory

适配器模式优缺点

  • 优点
    1. 复用性好,引入适配器类来重用适配者类,无需修改原有代码
    2. 增加类的透明性,将适配过程封装在适配器类中,对使用者来说相对透明
    3. 灵活性扩展性好,通过配置可以随时更换适配器
  • 缺点
    1. 使用适配器会使系统整体不好把握,调的是A接口,却被适配成了B接口的实现

静态代理模式 >> ExecutorCallbackCall

Retrofit场景

还是先来看看Retrofit中使用ExecutorCallbackCall的场景

//ExecutorCallAdapterFactory中生成CallAdapter实例
return new CallAdapter<Call<?>>() {
      ...
      @Override public <R> Call<R> adapt(Call<R> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };

在上面CallAdapter实现中,可以发现它将OkHttpCall适配成了一个新的Call实例:ExecutorCallbackCall,所以我们接着看看ExecutorCallbackCall的具体实现代码

static final class ExecutorCallbackCall<T> implements Call<T> {
    final Executor callbackExecutor;//Android平台下的Executor:MainThreadExecutor
    final Call<T> delegate;//网络实际执行者OkHttpCall实例

    ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
      this.callbackExecutor = callbackExecutor;
      this.delegate = delegate;
    }

    @Override public void enqueue(final Callback<T> callback) {
      if (callback == null) throw new NullPointerException("callback == null");

      delegate.enqueue(new Callback<T>() {
        @Override public void onResponse(Call<T> call, final Response<T> response) {
          //Android平台下此处进行了线程切换
          callbackExecutor.execute(new Runnable() {
            @Override public void run() {
              if (delegate.isCanceled()) {
                callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
              } else {
                callback.onResponse(ExecutorCallbackCall.this, response);
              }
            }
          });
        }

        @Override public void onFailure(Call<T> call, final Throwable t) {
          //Android平台下此处进行了线程切换
          callbackExecutor.execute(new Runnable() {
            @Override public void run() {
              callback.onFailure(ExecutorCallbackCall.this, t);
            }
          });
        }
      });
    }

    @Override public boolean isExecuted() {
      return delegate.isExecuted();
    }

    @Override public Response<T> execute() throws IOException {
      return delegate.execute();
    }

    @Override public void cancel() {
      delegate.cancel();
    }

    @Override public boolean isCanceled() {
      return delegate.isCanceled();
    }

    @SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone.
    @Override public Call<T> clone() {
      return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone());
    }

    @Override public Request request() {
      return delegate.request();
    }
  }
}

可以看到我们通过login()方法拿到Call实例(也就是ExecutorCallbackCall)之后,在执行网络请求时,在ExecutorCallbackCall的实现中其实都是将具体操作委托给OkHttpCall在执行。所以RetrofitExecutorCallbackCall中又使用了一种设计模式:静态代理模式

静态代理模式定义

为其他对象提供一种代理以控制对这个对象的访问

静态代理模式使用场景

无法访问或不想直接访问某个对象

静态代理模式类图

这里写图片描述

Retrofit中的静态代理

Retrofit中使用ExecutorCallbackCall代理OkHttpCall具体请求操作,可以将Call的使用与底层实现进行解耦,不用关心底层具体请求接口的实现,所以如果将来出现了一个比OkHttp更好的网络请求库,我们完全可以将OkHttp替换掉,即便这样也不会影响外部API接口在项目中的使用。

静态代理的优缺点

  • 优点
    1. 协调调用者与被调用者,降低系统耦合度
    2. 减小外部接口与内部接口实现的关联,降低耦合
  • 缺点
    1. 委托对象与代理对象需要实现相同的接口,当接口类增加方法时,除了所有实现类需要增加该方法外,所有代理类也需要实现此方法,增加了维护难度
    2. 一个代理类只能代理一种类型的对象

动态代理 >> Retrofit.create()

先看下Retrofit.create()方法的具体实现代码:

public <T> T create(final Class<T> service) {
    return (T) Proxy.newProxyInstance(service.getClassLoader(), 
        new Class<?>[] { service }, new InvocationHandler() {
      private final Platform platform = Platform.get();
      @Override 
      public Object invoke(Object proxy, Method method, Object... args)
          throws Throwable {
        ...
        ServiceMethod serviceMethod = loadServiceMethod(method);
        OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
        return serviceMethod.callAdapter.adapt(okHttpCall);
      }
    });
}

相信很多人在刚开始用Retrofit时都会有一点疑问,我们明明声明的是接口,为什么通过create()方法就能创建出一个对象实例呢?
通过上面的实现源码我们找到了答案,那就是使用了JDK中提供的动态代理机制,它会在运行过程中为我们声明的服务接口动态的创建出一个代理对象,以便实现我们的请求操作。我个人认为这是Retrofit框架得以实现的一个核心之处,另外一个核心之处就是其完善的注解机制,关于其注解本文就不说,主要就是一些注解的声明和解析,比较简单,感兴趣的可以去看看。

上面的源码中我们可以看到,运行过程中得到服务接口的代理对象之后,当我们调用login()这样的接口方法时,其实真实执行的是上面源码中写的invoke()方法,所以我们调用login()方法时其实是执行了如下三步:
1. 根据反射得到接口方法Method对象生成对应的ServiceMethod对象,该对象会对该声明方法上的所有方法注解、参数注解进行解析以得到一个请求所需要的所有信息
2. 得到ServiceMethod对象之后,会根据该对象和方法调用时传递的参数生成OkHttpCall对象,也就是具体的网络实施者
3. 将OkHttpCall作为CallAdapter适配器中adapt()方法的参数传递给CallAdapter进行适配,最后得到我们所需要的ExecutorCallbackCall对象,也就是调用login()方法得到的Call实例对象

动态代理使用场景

静态代理特点一个代理对应一种类型,如果有多个类需要代理则需要多个代理,而且维护成本高,而动态代理就是来解决此类问题

动态代理特点

运行期由JVM通过反射机制动态生成,可以代理多种类型,代码复用性高。但是只能代理Java接口,不能代理Java实现类。

Call.enqueue() & Call.execute()实现

前面从Retrofit的配置、创建、调用接口方法得到Call实例,基本用法都已经讲的差不多了,现在我们来看基本用法的最后一步Call.enqueue() & Call.execute()

前面讲过调用接口方法比如login()时,Android平台下默认得到的是ExecutorCallbackCall实例,而ExecutorCallbackCall实例中执行网络请求的实际上又是OkHttpCall,所以我们来看OkHttpCall中的Call.enqueue() & Call.execute()两个方法的实现,我以Call.enqueue()为例,另外一个大家可以自己去看看

下面是该方法实现的关键代码:

OkHttpCall.enqueue()

@Override public void enqueue(final Callback<T> callback) {
    ...
    okhttp3.Call call;
    Throwable failure;

    synchronized (this) {
      if (executed) throw new IllegalStateException("Already executed.");
      executed = true;

      call = rawCall;
      failure = creationFailure;
      if (call == null && failure == null) {
        try {
          //如果okhttp3.Call为空,则先创建该实例
          call = rawCall = createRawCall();
        } catch (Throwable t) {
          failure = creationFailure = t;
        }
      }
    }
    ...
    //又讲网络执行转交给okhttp3.Call实例来执行
    call.enqueue(new okhttp3.Callback() {
      @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
          throws IOException {
        Response<T> response;
        try {
          //将okhttp3.Response结果包装成Retrofit中的结果对象Response
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }
        callSuccess(response);
      }

      @Override public void onFailure(okhttp3.Call call, IOException e) {
        ...
      }

      private void callFailure(Throwable e) {
        ...
      }

      private void callSuccess(Response<T> response) {
        ...
      }
    });
  }

前面介绍关键类时说过OkHttpCall底层网络执行其实是OkHttp.Call在执行,从上面的代码我们就可以看出来(代码关键地方我加了注释),上面代码关键第一步是先创建一个okhttp3.call实例,所以我们同样看看创建okhttp3.call实例的代码是怎么实现的

private okhttp3.Call createRawCall() throws IOException {
    Request request = serviceMethod.toRequest(args);
    okhttp3.Call call = serviceMethod.callFactory.newCall(request);
    ...
    return call;
  }

通过上面的createRawCall()方法实现我们可以发现,它会首先通过ServiceMethod.toRequest()方法生成一个OkHttp.Request对象(这部分代码比较简单,我就不细说了),然后根据ServiceMethod中的成员变量CallFactory创建一个okhttp3.Call实例。但是这个CallFactory是怎么来的呢?其实我们可以猜到这个CallFactory实例就是OkHttpClient实例。但是我们还是看看ServiceMethod的创建过程

ServiceMethod创建

//在Retrofit.create()方法实现的第一步就是通过loadServiceMethod()方法创建ServiceMethod,这是其实现
ServiceMethod loadServiceMethod(Method method) {
    ServiceMethod result;
    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = new ServiceMethod.Builder(this, method).build();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

//下面是ServiceMethod相关关键代码
final class ServiceMethod<T> {
    final okhttp3.Call.Factory callFactory;
    ServiceMethod(Builder<T> builder) {
        this.callFactory = builder.retrofit.callFactory();
        this.callAdapter = builder.callAdapter;
        this.baseUrl = builder.retrofit.baseUrl();
        this.responseConverter = builder.responseConverter;
        ...
      }

    static final class Builder {
        public Builder(Retrofit retrofit, Method method) {
          this.retrofit = retrofit;
          this.method = method;
          ...
        }
        public ServiceMethod build() {
              //创建CallAdapter
              callAdapter = createCallAdapter();
              //得到请求结果返回类型,接口方法声明
              responseType = callAdapter.responseType();
              ...
              //创建得到Converter结果转换器
              responseConverter = createResponseConverter();
              ...
              return new ServiceMethod<>(this);
        }
    }
}

通过上面的ServiceMethod创建过程的相关代码可以看出,ServiceMethod中的实例变量callFactory其实是调用Retrofit.callFactory()方法所得,大家也可以看看上面我注释的CallAdapterConverter的创建过程,所以我们再来看看这个方法的实现

public okhttp3.Call.Factory callFactory() {
    return callFactory;
}

可以看到该方法只是返回了Retrofit中的callFactory实例,同样,我们再来看看Retrofit中的callFactory实例是怎么来的

public final class Retrofit {
    private final okhttp3.Call.Factory callFactory;
    ...

    Retrofit(okhttp3.Call.Factory callFactory, ...) {
        this.callFactory = callFactory;
        ...
    }

    public static final class Builder {
        private okhttp3.Call.Factory callFactory;
        ...
        public Builder client(OkHttpClient client) {
          return callFactory(checkNotNull(client, "client == null"));
        }

        public Builder callFactory(okhttp3.Call.Factory factory) {
          this.callFactory = checkNotNull(factory, "factory == null");
          return this;
        }

        public Retrofit build() {
          ...
          okhttp3.Call.Factory callFactory = this.callFactory;
          if (callFactory == null) {
            callFactory = new OkHttpClient();
          }
          ...
          return new Retrofit(callFactory, ...);
        }
    }
}

通过上面的代码,我们可以看出Retrofit中的callFactory实例就是我们使用的OkHttpClient实例,所以这就验证了我们前面猜测的serviceMethod.callFactory就是OkHttpClient实例的猜想。

Ok,回到我们前面将的OkHttpCall.equeue()方法的实现流程上来。

请求结果解析

call.enqueue(new okhttp3.Callback() {
      @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
          throws IOException {
        Response<T> response;
        try {
          //将okhttp3.Response结果包装成Retrofit中的结果对象Response
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }
        callSuccess(response);
      }
      ...
}

创建完okhttp3.call实例之后,调用该实现的equeue()方法开始执行网络请求,请求执行完成之后,会调用parseResponse方法,我们来看看这个方法实现的关键代码:

Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
    ResponseBody rawBody = rawResponse.body();
    ...
    //上面省略的这段代码是对请求失败时的结果处理,大家可以自行查看源码

    ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
    try {
      T body = serviceMethod.toResponse(catchingBody);
      return Response.success(body, rawResponse);
    } catch (RuntimeException e) {
      catchingBody.throwIfCaught();
      throw e;
    }
  }

这个方法中,当请求成功时,会调用serviceMethod.toResponse()这个方法,我们来看看这个方法又做了哪些事情:

T toResponse(ResponseBody body) throws IOException {
    return responseConverter.convert(body);
}

很明显,上面方法中就直接调用了ServiceMethod中的Converter实例进行结果转换,也就是说在这个地方Retrofit会帮我们把网络请求结果转换成我们所需要的类型,转换成功之后会调用Response.success(body, rawResponse)将转换后的结果包装成Retrofit中的Response对象。

获取配置的Converter

问题又来了,上面的Converter是怎么来的呢?

在前面给出的ServiceMethod创建过程的代码块中,我对ServiceMethod中创建Converter实例的代码进行了注释,我们再回过头来看看这段代码:

//ServiceMethod.Buibler中的方法
private Converter<ResponseBody, T> createResponseConverter() {
      ...
      try {
        return retrofit.responseBodyConverter(responseType, annotations);
      } catch (RuntimeException e) { // Wide exception range because factories are user code.
        throw methodError(e, "Unable to create converter for %s", responseType);
      }
    }

//Retrofit中的方法
public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
    return nextResponseBodyConverter(null, type, annotations);
  }

public <T> Converter<ResponseBody, T> nextResponseBodyConverter(Converter.Factory skipPast,
      Type type, Annotation[] annotations) {
    ...
    int start = converterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = converterFactories.size(); i < count; i++) {
      Converter<ResponseBody, ?> converter =
          converterFactories.get(i).responseBodyConverter(type, annotations, this);
      if (converter != null) {
        return (Converter<ResponseBody, T>) converter;
      }
    }
    ...
    throw new IllegalArgumentException(builder.toString());
  }

通过上面的代码我们可以看到会从RetrofitconverterFactories转换器工厂集合中去查找当前接口方法对应的转换器工厂。其实这也告诉我们可以在创建Retrofit时配置多个Converter.Factory转换器工厂,也就是说我们一个接口服务中如果声明的多个接口方法的返回值不一样时,我们可以针对性的配置多个不一样的结果转换器工厂去进行结果解析,而不用为了保持结果类型一致对接口进行其他处理。

上面创建ServiceMethod时得到CallAdapter的过程与得到Converter的过程基本一样,我就不赘述了。

到这里,Retrofit的实现原理、实现流程以及其源码实现过程中用到的设计模式就介绍完了。

内容好长,时间好长~~~~

欢迎访问我的独立博客:http://ittiger.cn

作者:huyongl1989 发表于2016/10/16 12:29:14 原文链接
阅读:11 评论:0 查看评论

ThreadLocal的理解

$
0
0

学习一个东西首先要知道为什么要引入它,就是我们能用它来干什么。所以我们先来看看ThreadLocal对我们到底有什么用,然后再来看看它的实现原理。

ThreadLocal如果单纯从名字上来看像是“本地线程"这么个意思,只能说这个名字起的确实不太好,很容易让人产生误解,ThreadLocalVariable(线程本地变量)应该是个更好的名字。我们先看一下官方对ThreadLocal的描述:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
我们从中摘出要点:


1、每个线程都有自己的局部变量

    每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的(有前提,后面解释)

2、独立于变量的初始化副本

    ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。

3、状态与某一个线程相关联

    ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

我们先看一个简单的例子


运行后结果:

Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5

我们看到,每个线程累加后的结果都是5,各个线程处理自己的本地变量值,线程之间互不影响。

我们再来看一个例子:


执行后我们发现结果如下(每次运行还都不一样):


Thread-0 : 1390
Thread-2 : 2390
Thread-4 : 4390
Thread-3 : 3491
Thread-1 : 1390

这次为什么线程本地变量又失效了呢?大家可以仔细观察上面代码自己先找一下原因。

-----------------------------------------------低调的分割线-------------------------------------------

让我们再来回味一下 “ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本” 这句话。“初始值的副本。。。”,貌似想起点什么。我们再来看一下上面代码中定义ThreadLocal的地方


 上面代码中,我们通过覆盖initialValue函数来给我们的ThreadLocal提供初始值,每个线程都会获取这个初始值的一个副本。而现在我们的初始值是一个定义好的一个对象,num是这个对象的引用.换句话说我们的初始值是一个引用。引用的副本和引用指向的不就是同一个对象吗?

如果我们想给每一个线程都保存一个Index对象应该怎么办呢?那就是创建对象的副本而不是对象引用的副本:


对象的拷贝图示:

  

现在我们应该能明白ThreadLocal本地变量的含义了吧。接下来我们就来看看ThreadLocal的源码,从内部来揭示它的神秘面纱。

ThreadLocal有一个内部类ThreadLocalMap,这个类的实现占了整个ThreadLocal类源码的一多半。这个ThreadLocalMap的作用非常关键,它就是线程真正保存线程自己本地变量的容器。每一个线程都有自己的单独的一个ThreadLocalMap实例,其所有的本地变量都会保存到这一个map中。

在上面谈到了对ThreadLocal的一些理解,那我们下面来看一下具体ThreadLocal是如何实现的。

  先了解一下ThreadLocal类提供的几个方法:

1
2
3
4
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

   get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。


现在就让我们从ThreadLocal的get和set这两个最常用的方法开始分析:


强调一下:Thread对象都有一个ThreadLocalMap类型的属性threadLocals,这个属性是专门用于保存自己所有的线程本地变量的。这个属性在线程对象初始化的时候为null。所以对一个线程对象第一次使用线程本地变量的时候,需要对这个threadLocals属性进行初始化操作。注意要区别 “线程第一次使用本地线程变量”和“第一次使用某一个线程本地线程变量”。

getMap方法:

//直接返回线程对象的threadLocals属性


setInitialValue方法:(看完后再回顾一下之前的那个例子)


我们再来看一下set方法


ThradLocal还有一个remove方法,让我们来分析一下:


到这里大家应该对ThreadLocal变量比较清晰了,至于ThradLocalMap的实现细节这里就不在说了。大家有兴趣可以自己去看ThreadLocal的源码。


总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点: 
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
 


作者:hard_working1 发表于2016/10/16 12:31:45 原文链接
阅读:16 评论:0 查看评论

swift中array数组的使用

$
0
0

数组使用有序列表存储同一类型的多个值,且相同的值可以多次出现在一个数组的不同位置中。

数组会强制检测元素的类型,如果类型不同则会报错Swift数组应该遵循像Array<Element>这样的形式,其中Element是这个数组中唯一允许存在的数据类型。

如果创建一个数组,并赋值给一个变量,则创建的集合就是可以修改的。这意味着在创建数组后,可以通过添加、删除、修改的方式改变数组里的项目。如果将一个数组赋值给常量,数组就不可更改,并且数组的大小和内容都不可以修改。

使用注意事项: 数组初始化时定义了的元素类型后,在后续使用中,元素类型必须是统一的。但如果定义了类型为"Any",或"AnyObject"时,则数组中可以使用任意类型的元素,同时"Any"类型时,还可以包含方法selector类。

[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 空数组  
  2. let arrayTmp01 = Array<String>()  
  3. print(arrayTmp01)  
  4.           
  5. let arrayTmp02 = [String]()  
  6. print(arrayTmp02)  
  7.           
  8. let arrayTmp03:Array<String> = []  
  9. print(arrayTmp03)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 数组清空  
  2. var arrayTmp04 = ["1""2"]  
  3. print(arrayTmp04)  
  4.           
  5. arrayTmp04 = []  
  6. print(arrayTmp04)  
  7.           
  8. arrayTmp04 = ["1""2"]  
  9. print(arrayTmp04)  
  10.           
  11. arrayTmp04 = Array<String>()  
  12. print(arrayTmp04)  

[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 创建数组  
  2. let array01:[Int] = [1,2,3,4,5]  
  3. print(array01)  
  4.           
  5. let array02 = ["1""2""3"]  
  6. print(array02)  
  7.           
  8. let array03 = [array01, array02]  
  9. print(array03)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 读取数组元素值  
  2. let value01 = array01[0]  
  3. print(value01)  
  4.           
  5. let value02 = array01.indexOf(1)  
  6. print(value02)  
  7.           
  8. for value in array01  
  9. {  
  10.  print("value = \(value)")  
  11. }  
  12.           
  13. for var index = 0; index < array01.count; index++  
  14. {  
  15.  let value = array01[index]  
  16.  print("index = \(index), value = \(value)")  
  17. }  
  18.           
  19. for i in 0..<array01.count  
  20. {  
  21.  print(array01[i])  
  22. }  
  23.           
  24. for (index, item) in array01.enumerate()  
  25. {  
  26.  print("在 index = \(index) 位置上的值为 \(item)")  
  27. }  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 修改数组  
  2. var array04 = [10,20,30]  
  3. // 添加元素(添加到最后)  
  4. array04.append(100// 单个元素  
  5. print(array04)  
  6. array04 = array04 + [200// 元素数组  
  7. print(array04)  
  8. array04.appendContentsOf([201,202]) // 元素集合  
  9. print(array04)  
  10. array04.appendContentsOf(203...210// 元素序列  
  11. print(array04)  

[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 添加元素(添加到指定位置)</span>  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. array04.insert(500, atIndex2)  
  2. print(array04)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 修改元素  
  2. array04[4] = 300  
  3. print(array04)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 删除元素  
  2. array04.removeFirst()  
  3. print(array04)  
  4.           
  5. array04.removeLast()  
  6. print(array04)  
  7.           
  8. array04.removeAtIndex(0)  
  9. print(array04)  
  10.           
  11. array04.removeAll()  
  12. print(array04)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 合并数组(注意:数组类型必须一致)  
  2. array04 = array01 + [100,200,300]  
  3. print(array04)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 类型判断  
  2. var isArray:Bool = array01 is Array  
  3. if isArray  
  4. {  
  5.  print("array01 is array class")  
  6. }  
  7. else  
  8. {  
  9.  print("array01 is not array class")  
  10. }  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 数组元素个数  
  2. let count01 = array04.count  
  3. print(count01)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 判断数组是否为空  
  2. let isValid = array04.isEmpty  
  3. print(isValid)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 第一个元素  
  2. let item01 = array04.first  
  3. print(item01)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 最后一个元素  
  2. let item02 = array04.last  
  3. print(item02)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 是否包含某个元素  
  2. let contant1:Bool = array01.contains(1)  
  3. print("array01 \(contant1) 包含 1")  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 数组排序   
  2. let arraySort = array04.sort({(n1:Int, n2:Int)->Bool in  
  3.  // 从小到大  
  4.  // return n2 > n1  
  5.               
  6.  // 从大到小  
  7.  return n2 < n1  
  8. })  
  9. print(arraySort)  
  10.           
  11. let arraySort02 = array04.sort({  
  12.  // 从小到大  
  13.  // return $1 > $0  
  14.               
  15.  // 从大到小  
  16.  return $1 < $0  
  17. })  
  18. print(arraySort02)  
  19.           
  20. let arraySort03 = array04.sort(){  
  21.  // 从小到大  
  22.  // $1 > $0  
  23.               
  24.  // 从大到小  
  25.  $1 < $0  
  26. }  
  27. print(arraySort03)  


补充:Any类型元素,或AnyObject类型元素的数组

[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 方法selector  
  2. func buttonClick() -> Void  
  3. {  
  4.             print("点击")  
  5. }  
  6. print(buttonClick)  
  7.           
  8. // AnyObject任意类型元素的数组(不包含方法selector)  
  9. let arrayAnyObject01:Array<AnyObject> = [Grade0ne(chinese: "86", maths"66"), GradeTwo(chinese: "78", english"92"), "DevZhang", 30]  
  10. print(arrayAnyObject01)  
  11.   
  12. // Any任意类型元素的数组(包含方法selector)  
  13. let arrayAnyObject02:Array<Any> = [Grade0ne(chinese: "86", maths"66"), GradeTwo(chinese: "78", english"92"), "DevZhang"30, buttonClick()]  
  14. print(arrayAnyObject02)  
作者:st646889325 发表于2016/10/17 11:24:20 原文链接
阅读:42 评论:0 查看评论

swift中dictionary字典的使用

$
0
0

Swift 字典用来存储无序的相同类型数据的集合,Swift字典会强制检测元素的类型,如果类型不同则会报错。

Swift字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。

和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

Swift字典的key没有类型限制可以是整型或字符串,但必须是唯一的。

如果创建一个字典,并赋值给一个变量,则创建的字典就是可以修改的。这意味着在创建字典后,可以通过添加、删除、修改的方式改变字典里的项目。如果将一个字典赋值给常量,字典就不可修改,并且字典的大小和内容都不可以修改。


[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 创建字典  
  2. // 创建一个特定类型的空字典,格式为:var dict = [KeyType: ValueType]()  
  3. // 创建一个空字典,键的类型为 Int,值的类型为 String 的简单语法:  
  4. var dict01 = [Int: String]()  
  5. print(dict01)  
  6.           
  7. // 创建一个字典的实例:  
  8. var dict02 :[Int:String] = [1:"One", 2:"Two", 3:"Three"]  
  9. print(dict02)  
  10.           
  11. var dict03 = ["name":"DevZhang""job":"iOSDev""company":"VSTECS"]  
  12. print(dict03)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 访问字典  
  2. // 我们可以根据字典的索引来访问数组的元素,语法如下:var value = dict[key]  
  3. let value01 = dict02[1]  
  4. print(value01)  
  5.           
  6. let value02 = dict03["name"]  
  7. print(value02)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 添加数据  
  2. let value03 = dict02.updateValue("Four", forKey4)  //或 dict02[4] = "Four"  
  3. print(value03)  
  4. print(dict02)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 修改字典  
  2. // 方法1 使用 updateValue(forKey:) 增加或更新字典的内容。如果 key 不存在,则添加值,如果存在则修改 key 对应的值。格式为:dict.updateValue(value, forKey:key)方法返回Optional值。  
  3. var value04 = dict02.updateValue("TwoTmp", forKey2)  
  4. print(dict02)  
  5. print(value04)  
  6.    
  7. // 方法2 通过指定的 key 来修改字典的值  
  8. var value05 = dict02[3]  
  9. print(value05)  
  10. value05 = "ThreeTmp" // 修改无效  
  11. print(dict02)  
  12. dict02[3] = "ThreeTmp" // 修改有效  
  13. print(dict02)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 移除 Key-Value 对  
  2. // 1 使用 removeValueForKey() 方法来移除字典 key-value 对。如果 key 存在该方法返回移除的值,如果不存在返回 nil 。  
  3. let valueRemove01 = dict02.removeValueForKey(2)  
  4. print(valueRemove01)  
  5. print(dict02)  
  6.           
  7. // 2 通过指定键的值为 nil 来移除 key-value(键-值)对。  
  8. dict02[1] = nil  
  9. print(dict02)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 遍历字典  
  2. // 1 使用 for-in 循环来遍历某个字典中的键值对。  
  3. for (key, value) in dict03  
  4. {  
  5.  print("字典 key \(key) -  字典 value \(value)")  
  6. }  
  7.           
  8. // 2 使用enumerate()方法来进行字典遍历,返回的是字典的索引及 (key, value) 对  
  9. for (key, value) in dict03.enumerate()  
  10. {  
  11.  print("字典 key \(key) -  字典 (key, value) 对 \(value)")  
  12. }  
  13.   
  14. // 3   
  15. for key in dict03.keys  
  16. {  
  17.  let value = dict03[key]  
  18.  print("key = \(key), value = \(value)")  
  19. }  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 字典转换为数组  
  2. // 提取字典的键值(key-value)对,并转换为独立的数组。  
  3. let dictKeys = [String](dict03.keys)  
  4. for (key) in dictKeys  
  5. {  
  6.  print("\(key)")  
  7. }  
  8.           
  9. let dictValues = [String](dict03.values)  
  10. for (value) in dictValues  
  11. {  
  12.  print("\(value)")  
  13. }  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // count 属性  
  2. let count01 = dict03.count  
  3. print(count01)  
[objc] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // isEmpty 属性  
  2. let empty01 = dict01.isEmpty  
  3. print("dict01 is \(empty01)")  
  4.           
  5. let empty02 = dict03.isEmpty  
  6. print("dict03 is \(empty02)")  

作者:st646889325 发表于2016/10/17 11:24:29 原文链接
阅读:45 评论:0 查看评论

【SpringMVC架构】SpringMVC入门实例,解析工作原理(二)

$
0
0

这篇博文,我们搭建一个简单SpringMVC的环境,使用非注解形式实现一个HelloWorld实例,从简单入手,逐步深入。

环境准备

我们需要有基本的Java环境,下面只是简单的罗列,不再详细的介绍。

jdk1.6以上
eclipse或者myEclipse
tomcat6以上
我们使用SpringMVC的版本是Spring3.2.0,下图是最基本的jar包:
这里写图片描述

在spring的官方API文档中,给出了所有jar包作用的概述,现列举常用的包以及相关作用:

org.springframework.aop-3.2.0.RELEASE.jar :与Aop 编程相关的包
org.springframework.beans-3.2.0.RELEASE.jar :提供了简捷操作bean 的接口
org.springframework.context-3.2.0.RELEASE.jar :构建在beans 包基础上,用来处理资源文件及国际化。
org.springframework.core-3.2.0.RELEASE.jar :spring 核心包
org.springframework.web-3.2.0.RELEASE.jar :web 核心包,提供了web 层接口
org.springframework.web.servlet-3.2.0.RELEASE.jar :web 层的一个具体实现包,DispatcherServlet也位于此包中。

后文全部实例都在spring3.2.0版本中进行,为了方便,建议在搭建环境中导入spring3.2.0 的所有jar 包(所有jar 包位于dist 目录下)。

环境准备好了,下面我们开始动手。

编写HelloWorld 实例

步骤一、建立名为SpringMVC_helloworld 的动态web项目,并选择服务器,并导入上面列出的jar 包。
步骤二、编写web.xml 配置文件,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

简要说明:其实DispatcherServlet就是一个Servlet,也是对Http请求进行转发的核心Servlet 。所有.do的请求将首先被DispatcherServlet处理,而DispatcherServlet 它要作的工作就是对请求进行分发(也即是说把请求转发给具体的Controller )。可以简单地认为,它就是一个总控处理器,但事实上它除了具备总控处理理器对请求进行分发的能力外,还与spring 的IOC 容器完全集成在一起,从而可以更好地使用spring 的其它功能。在这里还需留意< servlet-name> springMVC ,下面步骤三会用到。
3. 步骤三、建立Spring的配置文件,注意上一个步骤中的标签在web.xml中的servlet的名称。DispatcherServlet的初始化后,会在WEB - INF查找一个[servlet-name]-servlet.xml 即springMVC-servlet.xml的文件。它的主要代码如下:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd 
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd 
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.2.xsd 
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-3.2.xsd 
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-3.2.xsd ">

    <bean id="helloControl" class="com.tgb.controller.HelloWorld"></bean>

    <!-- 处理器映射器 将bean的name作为url进行查找 ,需要在配置Handler时指定beanname(就是url) 
    所有的映射器都实现 HandlerMapping接口。
    -->
    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />

    <!--简单url映射  -->
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <!-- 对helloControl进行url映射,url是/helloworld.do -->
                <prop key="/helloworld.do">helloControl</prop>
            </props>
        </property>
    </bean>

    <!-- 处理器适配器 所有处理器适配器都实现 HandlerAdapter接口 -->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />

</beans>

说明: helloworld.do的请求将被名为helloControl的bean 进行转发处理。
4. 步骤四、创建一个SpringMVC的控制类,并处理请求,完成HelloWord.java 的编写,代码如下:

package com.tgb.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class HelloWorld implements Controller{

    @Override
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        ModelAndView mav = new ModelAndView("hello.jsp");
        mav.addObject("message", "Hello World!");
        return mav;
    }
}

说明:ModelAndView 对象是包含视图和业务数据的混合对象,即通过此对象,我们可以知道所返回的相应页面(比如这里返回hello.jsp页面),也可以在相应的页面中获取此对象所包含的业务数据(比如这里message-Hello World!)。
5.步骤五、创建jsp,在当前项目web根目录下编写index.jsp,用于发送请求,hello.jsp用于显示消息,主要代码如下:
用index.jsp里面的超链接发出一个请求到HelloWorldController,并返回到hello.jsp 显示message的信息。

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
    <a href="helloworld.do">SpringMVC,HelloWorld实例</a>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
    世界,你好!
    获取值: ${message }
</body>
</html>

6.步骤六:把项目扔到服务器中,运行服务器,在浏览器中输入http://localhost:8080/SpringMVC_HelloWorld/index.jsp 进行测试。
这里写图片描述
这里写图片描述

简析springmvc 工作原理

启动服务器,根据web.xml的配置加载前端控制器(也称总控制器) DispatcherServlet 。在加载时会完成一系列的初始化动作。
根据servlet的映射请求(上面的HelloWorld实例中针对.do 请求),并参照“控制器配置文件”(即springMVC-servlet.xml 这样的配置文件),把具体的请求分发给特定的后端控制器进行处理(比如上例会分发给HelloWorld 控制器进行处理)
后端控制器调用相应的逻辑层代码,完成处理并返回视图对象( ModelAndView )给前端处理器。
前端控制器根据后端控制器返回的ModelAndView 对象,前端控器器根据视图对象返回具体页面给客户端。
总结

SpringMVC框架的核心是DispatcherServlet,它的作用是将请求分发给不同的后端处理器。Spring的Controller层使用了后端控制器来映射处理器和视图解析器来共同完成Controller层的主要工作。并且spring的Controller层还真正地把业务层处理的数据结果和相应的视图封装成一个对象,即我们后面会经常用到的ModelAndView 对象。

一句话总结springMVC

封装web请求为一个数据对象、调用业务逻辑层来处理数据对象、返回处理数据结果及相应的视图给用户。

作者:huangxingchen123 发表于2016/10/17 11:28:51 原文链接
阅读:42 评论:0 查看评论

iOS开发之Xcode8推出的WKWebView与UIWebView的使用

$
0
0

一、整体介绍

UIWebView自iOS2就有,WKWebView从iOS8才有,毫无疑问WKWebView将逐步取代笨重的UIWebView。通过简单的测试即可发现UIWebView占用过多内存,且内存峰值更是夸张。WKWebView网页加载速度也有提升,但是并不像内存那样提升那么多。下面列举一些其它的优势:

  • 更多的支持HTML5的特性
  • 官方宣称的高达60fps的滚动刷新率以及内置手势
  • Safari相同的JavaScript引擎
  • 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)
  • 另外用的比较多的,增加加载进度属性:estimatedProgress

二、UIWebView使用说明

1 举例:简单的使用

UIWebView使用非常简单,可以分为三步,也是最简单的用法,显示网页

复制代码
- (void)simpleExampleTest {
    // 1.创建webview,并设置大小,"20"为状态栏高度
    UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 
self.view.frame.size.height - 20)];
    // 2.创建请求
    NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cnblogs.com/mddblog/"]];
    // 3.加载网页
    [webView loadRequest:request];

    // 最后将webView添加到界面
    [self.view addSubview:webView];
}
复制代码

2 一些实用函数

  • 加载函数。
- (void)loadRequest:(NSURLRequest *)request;
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName 
baseURL:(NSURL *)baseURL;

UIWebView不仅可以加载HTML页面,还支持pdf、word、txt、各种图片等等的显示。下面以加载mac桌面上的png图片:/Users/coohua/Desktop/bigIcon.png为例

// 1.获取url
NSURL *url = [NSURL fileURLWithPath:@"/Users/coohua/Desktop/bigIcon.png"];
// 2.创建请求
NSURLRequest *request=[NSURLRequest requestWithURL:url];
// 3.加载请求
[self.webView loadRequest:request];
  • 网页导航刷新有关函数
复制代码
// 刷新
- (void)reload;
// 停止加载
- (void)stopLoading;
// 后退函数
- (void)goBack;
// 前进函数
- (void)goForward;
// 是否可以后退
@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
// 是否可以向前
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
// 是否正在加载
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
复制代码

3 代理协议使用:UIWebViewDelegate

一共有四个方法

复制代码
/// 是否允许加载网页,也可获取js要打开的url,通过截取此url可与js交互
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 
navigationType:(UIWebViewNavigationType)navigationType {

    NSString *urlString = [[request URL] absoluteString];
    urlString = [urlString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

    NSArray *urlComps = [urlString componentsSeparatedByString:@"://"];
    NSLog(@"urlString=%@---urlComps=%@",urlString,urlComps);
    return YES;
}
/// 开始加载网页
- (void)webViewDidStartLoad:(UIWebView *)webView {
    NSURLRequest *request = webView.request;
    NSLog(@"webViewDidStartLoad-url=%@--%@",[request URL],[request HTTPBody]);
}
/// 网页加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSURLRequest *request = webView.request;
    NSURL *url = [request URL];
    if ([url.path isEqualToString:@"/normal.html"]) {
        NSLog(@"isEqualToString");
    }
    NSLog(@"webViewDidFinishLoad-url=%@--%@",[request URL],[request HTTPBody]);
    NSLog(@"%@",[self.webView stringByEvaluatingJavaScriptFromString:@"document.title"]);
}
/// 网页加载错误
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    NSURLRequest *request = webView.request;
    NSLog(@"didFailLoadWithError-url=%@--%@",[request URL],[request HTTPBody]);

}
复制代码

4 与js交互

主要有两方面:js执行OC代码、oc调取写好的js代码

  • js执行OC代码:js是不能执行oc代码的,但是可以变相的执行,js可以将要执行的操作封装到网络请求里面,然后oc拦截这个请求,获取url里面的字符串解析即可,这里用到代理协议的- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType函数。
  • oc调取写好的js代码:这里用到UIwebview的一个方法。示例代码一个是网页定位,一个是获取网页title:
复制代码
// 实现自动定位js代码, htmlLocationID为定位的位置(由js开发人员给出),实现自动定位代码,应该在网页加载完成之后再调用
NSString *javascriptStr = [NSString stringWithFormat:@"window.location.href = '#%@'",htmlLocationID];
// webview执行代码
[self.webView stringByEvaluatingJavaScriptFromString:javascriptStr];

// 获取网页的title
NSString *title = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"]
复制代码

三、WKWebView使用说明

1 简单使用

与UIWebview一样,仅需三步:记住导入(#import <WebKit/WebKit.h>)

复制代码
- (void)simpleExampleTest {
    // 1.创建webview,并设置大小,"20"为状态栏高度
    WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 
self.view.frame.size.height - 20)];
    // 2.创建请求
    NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cnblogs.com/mddblog/"]];
    // 3.加载网页
    [webView loadRequest:request];

    // 最后将webView添加到界面
    [self.view addSubview:webView];
}
复制代码

 

 

2 一些实用函数

  • 加载网页函数
    相比UIWebview,WKWebView也支持各种文件格式,并新增了loadFileURL函数,顾名思义加载本地文件。
复制代码
/// 模拟器调试加载mac本地文件
- (void)loadLocalFile {
    // 1.创建webview,并设置大小,"20"为状态栏高度
    WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 
self.view.frame.size.height - 20)];
    // 2.创建url  userName:电脑用户名
    NSURL *url = [NSURL fileURLWithPath:@"/Users/userName/Desktop/bigIcon.png"];
    // 3.加载文件
    [webView loadFileURL:url allowingReadAccessToURL:url];
    // 最后将webView添加到界面
    [self.view addSubview:webView];
}
复制代码

 

/// 其它三个加载函数
- (WKNavigation *)loadRequest:(NSURLRequest *)request;
- (WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
- (WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName 
baseURL:(NSURL *)baseURL;
  • 网页导航刷新相关函数
    和UIWebview几乎一样,不同的是有返回值,WKNavigation(已更新),另外增加了函数reloadFromOrigingoToBackForwardListItem
    • reloadFromOrigin会比较网络数据是否有变化,没有变化则使用缓存,否则从新请求。
    • goToBackForwardListItem:比向前向后更强大,可以跳转到某个指定历史页面
复制代码
@property (nonatomic, readonly) BOOL canGoBack;
@property (nonatomic, readonly) BOOL canGoForward;
- (WKNavigation *)goBack;
- (WKNavigation *)goForward;
- (WKNavigation *)reload;
- (WKNavigation *)reloadFromOrigin; // 增加的函数
- (WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item; // 增加的函数
- (void)stopLoading;
复制代码

 

 
  • 一些常用属性
    • allowsBackForwardNavigationGestures:BOOL类型,是否允许左右划手势导航,默认不允许
    • estimatedProgress:加载进度,取值范围0~1
    • title:页面title
    • .scrollView.scrollEnabled:是否允许上下滚动,默认允许
    • backForwardList:WKBackForwardList类型,访问历史列表,可以通过前进后退按钮访问,或者通过goToBackForwardListItem函数跳到指定页面

3 代理协议使用

一共有三个代理协议:

  • WKNavigationDelegate:最常用,和UIWebViewDelegate功能类似,追踪加载过程,有是否允许加载、开始加载、加载完成、加载失败。下面会对函数做简单的说明,并用数字标出调用的先后次序:1-2-3-4-5

三个是否允许加载函数:

复制代码
/// 接收到服务器跳转请求之后调用 (服务器端redirect),不一定调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation; 
/// 3 在收到服务器的响应头,根据response相关信息,决定是否跳转。decisionHandler必须调用,来决定是否跳转,
//参数WKNavigationActionPolicyCancel取消跳转,WKNavigationActionPolicyAllow允许跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse 
decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
/// 1 在发送请求之前,决定是否跳转 
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
复制代码

 

 

追踪加载过程函数:

复制代码
/// 2 页面开始加载
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
/// 4 开始获取到网页内容时返回
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
/// 5 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
/// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation;

复制代码
  • WKScriptMessageHandler:必须实现的函数,是APP与js交互,提供从网页中收消息的回调方法
/// message: 收到的脚本信息.
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

 

 
  • WKUIDelegate:UI界面相关,原生控件支持,三种提示框:输入、确认、警告。首先将web提示框拦截然后再做处理。
复制代码
/// 创建一个新的WebView
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration 
forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;
/// 输入框
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt 
defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame 
completionHandler:(void (^)(NSString * __nullable result))completionHandler;
/// 确认框
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame 
completionHandler:(void (^)(BOOL result))completionHandler;
/// 警告框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame
 completionHandler:(void (^)(void))completionHandler;
复制代码

四、示例代码

  • 代码可以实现一般网络显示,加载本地文件(pdf、word、txt、图片等等)
  • 搜索框搜索界面,搜索框输入file://则加载本地文件,http://则加载网络内容,如果两者都不是则搜索输入的关键字。
  • 下部网络导航,后退、前进、刷新、用Safari打开链接四个按钮

主界面——简述主页
复制代码
/// 控件高度
#define kSearchBarH  44
#define kBottomViewH 44

/// 屏幕大小尺寸
#define kScreenWidth  [UIScreen mainScreen].bounds.size.width
#define kScreenHeight [UIScreen mainScreen].bounds.size.height

#import "ViewController.h"
#import <WebKit/WebKit.h>


@interface ViewController () <UISearchBarDelegate, WKNavigationDelegate>

@property (nonatomic, strong) UISearchBar *searchBar;
/// 网页控制导航栏
@property (weak, nonatomic) UIView *bottomView;

@property (nonatomic, strong) WKWebView *wkWebView;

@property (weak, nonatomic) UIButton *backBtn;
@property (weak, nonatomic) UIButton *forwardBtn;
@property (weak, nonatomic) UIButton *reloadBtn;
@property (weak, nonatomic) UIButton *browserBtn;

@property (weak, nonatomic) NSString *baseURLString;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
//    [self simpleExampleTest];

    [self addSubViews];
    [self refreshBottomButtonState];

    [self.wkWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cnblogs.com/mddblog/"]]];

}
- (void)simpleExampleTest {
    // 1.创建webview,并设置大小,"20"为状态栏高度
    WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 
self.view.frame.size.height - 20)];
    // 2.创建请求
    NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cnblogs.com/mddblog/"]];
//    // 3.加载网页
    [webView loadRequest:request];
//    [webView loadFileURL:[NSURL fileURLWithPath:@"/Users/userName/Desktop/bigIcon.png"] 
allowingReadAccessToURL:[NSURL fileURLWithPath:@"/Users/userName/Desktop/bigIcon.png"]];
    // 最后将webView添加到界面
    [self.view addSubview:webView];
}
/// 模拟器加载mac本地文件
- (void)loadLocalFile {
    // 1.创建webview,并设置大小,"20"为状态栏高度
    WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 
self.view.frame.size.height - 20)];
    // 2.创建url  userName:电脑用户名
    NSURL *url = [NSURL fileURLWithPath:@"/Users/userName/Desktop/bigIcon.png"];
    // 3.加载文件
    [webView loadFileURL:url allowingReadAccessToURL:url];
    // 最后将webView添加到界面
    [self.view addSubview:webView];
}
- (void)addSubViews {
    [self addBottomViewButtons];

    [self.view addSubview:self.searchBar];

    [self.view addSubview:self.wkWebView];
}

- (void)addBottomViewButtons {
    // 记录按钮个数
    int count = 0;
    // 添加按钮
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setTitle:@"后退" forState:UIControlStateNormal];
    [button setTitleColor:[UIColor colorWithRed:249 / 255.0 green:102 / 255.0 blue:129 / 255.0 alpha:1.0] 
forState:UIControlStateNormal];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [button.titleLabel setFont:[UIFont systemFontOfSize:15]];
    button.tag = ++count;    // 标记按钮
    [button addTarget:self action:@selector(onBottomButtonsClicled:) forControlEvents:UIControlEventTouchUpInside];
    [self.bottomView addSubview:button];
    self.backBtn = button;

    button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setTitle:@"前进" forState:UIControlStateNormal];
    [button setTitleColor:[UIColor colorWithRed:249 / 255.0 green:102 / 255.0 blue:129 / 255.0 alpha:1.0] 
forState:UIControlStateNormal];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [button.titleLabel setFont:[UIFont systemFontOfSize:15]];
    button.tag = ++count;
    [button addTarget:self action:@selector(onBottomButtonsClicled:) forControlEvents:UIControlEventTouchUpInside];
    [self.bottomView addSubview:button];
    self.forwardBtn = button;

    button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setTitle:@"重新加载" forState:UIControlStateNormal];
    [button setTitleColor:[UIColor colorWithRed:249 / 255.0 green:102 / 255.0 blue:129 / 255.0 alpha:1.0] 
forState:UIControlStateNormal];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [button.titleLabel setFont:[UIFont systemFontOfSize:15]];
    button.tag = ++count;
    [button addTarget:self action:@selector(onBottomButtonsClicled:) forControlEvents:UIControlEventTouchUpInside];
    [self.bottomView addSubview:button];
    self.reloadBtn = button;

    button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setTitle:@"Safari" forState:UIControlStateNormal];
    [button setTitleColor:[UIColor colorWithRed:249 / 255.0 green:102 / 255.0 blue:129 / 255.0 alpha:1.0] 
forState:UIControlStateNormal];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted];
    [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [button.titleLabel setFont:[UIFont systemFontOfSize:15]];
    button.tag = ++count;
    [button addTarget:self action:@selector(onBottomButtonsClicled:) forControlEvents:UIControlEventTouchUpInside];
    [self.bottomView addSubview:button];
    self.browserBtn = button;
    // 统一设置frame
    [self setupBottomViewLayout];
}
- (void)setupBottomViewLayout
{
    int count = 4;
    CGFloat btnW = 80;
    CGFloat btnH = 30;

    CGFloat btnY = (self.bottomView.bounds.size.height - btnH) / 2;
    // 按钮间间隙
    CGFloat margin = (self.bottomView.bounds.size.width - btnW * count) / count;

    CGFloat btnX = margin * 0.5;
    self.backBtn.frame = CGRectMake(btnX, btnY, btnW, btnH);

    btnX = self.backBtn.frame.origin.x + btnW + margin;
    self.forwardBtn.frame = CGRectMake(btnX, btnY, btnW, btnH);

    btnX = self.forwardBtn.frame.origin.x + btnW + margin;
    self.reloadBtn.frame = CGRectMake(btnX, btnY, btnW, btnH);

    btnX = self.reloadBtn.frame.origin.x + btnW + margin;
    self.browserBtn.frame = CGRectMake(btnX, btnY, btnW, btnH);
}
/// 刷新按钮是否允许点击
- (void)refreshBottomButtonState {
    if ([self.wkWebView canGoBack]) {
        self.backBtn.enabled = YES;
    } else {
        self.backBtn.enabled = NO;
    }

    if ([self.wkWebView canGoForward]) {
        self.forwardBtn.enabled = YES;
    } else {
        self.forwardBtn.enabled = NO;
    }
}
/// 按钮点击事件
- (void)onBottomButtonsClicled:(UIButton *)sender {
    switch (sender.tag) {
        case 1:
        {
            [self.wkWebView goBack];
            [self refreshBottomButtonState];
        }
            break;
        case 2:
        {
            [self.wkWebView goForward];
            [self refreshBottomButtonState];
        }
            break;
        case 3:
            [self.wkWebView reload];
            break;
        case 4:
            [[UIApplication sharedApplication] openURL:self.wkWebView.URL];
            break;
        default:
            break;
    }
}

#pragma mark - WKWebView WKNavigationDelegate 相关
/// 是否允许加载网页 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

    NSString *urlString = [[navigationAction.request URL] absoluteString];

    urlString = [urlString stringByRemovingPercentEncoding];
    //    NSLog(@"urlString=%@",urlString);
    // 用://截取字符串
    NSArray *urlComps = [urlString componentsSeparatedByString:@"://"];
    if ([urlComps count]) {
        // 获取协议头
        NSString *protocolHead = [urlComps objectAtIndex:0];
        NSLog(@"protocolHead=%@",protocolHead);
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}


#pragma mark - searchBar代理方法
/// 点击搜索按钮
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    // 创建url
    NSURL *url = nil;
    NSString *urlStr = searchBar.text;

    // 如果file://则为打开bundle本地文件,http则为网站,否则只是一般搜索关键字
    if([urlStr hasPrefix:@"file://"]){
        NSRange range = [urlStr rangeOfString:@"file://"];
        NSString *fileName = [urlStr substringFromIndex:range.length];
        url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
        // 如果是模拟器加载电脑上的文件,则用下面的代码
//        url = [NSURL fileURLWithPath:fileName];
    }else if(urlStr.length>0){
        if ([urlStr hasPrefix:@"http://"]) {
            url=[NSURL URLWithString:urlStr];
        } else {
            urlStr=[NSString stringWithFormat:@"http://www.baidu.com/s?wd=%@",urlStr];
        }
        urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
        url=[NSURL URLWithString:urlStr];

    }
    NSURLRequest *request=[NSURLRequest requestWithURL:url];

    // 加载请求页面
    [self.wkWebView loadRequest:request];
}
#pragma mark - 懒加载
- (UIView *)bottomView {
    if (_bottomView == nil) {
        UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, kScreenHeight - kBottomViewH, kScreenWidth, kBottomViewH)];
        view.backgroundColor = [UIColor colorWithRed:230/255.0 green:230/255.0 blue:230/255.0 alpha:1];
        [self.view addSubview:view];
        _bottomView = view;
    }
    return _bottomView;
}
- (UISearchBar *)searchBar {
    if (_searchBar == nil) {
        UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 20, kScreenWidth, kSearchBarH)];
        searchBar.delegate = self;
        searchBar.text = @"http://www.cnblogs.com/mddblog/";
        _searchBar = searchBar;

    }
    return _searchBar;
}

- (WKWebView *)wkWebView {
    if (_wkWebView == nil) {
        WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20 + kSearchBarH, kScreenWidth, 
kScreenHeight - 20 - kSearchBarH - kBottomViewH)];
        webView.navigationDelegate = self;
//                webView.scrollView.scrollEnabled = NO;

        //        webView.backgroundColor = [UIColor colorWithPatternImage:self.image];
        // 允许左右划手势导航,默认允许
        webView.allowsBackForwardNavigationGestures = YES;
        _wkWebView = webView;
    }

    return _wkWebView;
}


@end
复制代码

 

 

五、UIWebView 和 WKWebView的性能分析

iOS 8 推出 WKWebView 以及相关特性,告诉开发者提高多么多么大的性能,实测告诉你WKWebView的性能有多好!

为了验证,我在项目中的分别 使用UIWebView 和 WKWebView 测试,来回加载十多个网页 产生的内存状况如下:

对比发现 WKWebview 这货 在persistent 常驻内存 使用得少多了,想想看,一个app去哪儿省下几十M的内存,再加上用户量分布
iOS 9 60%
iOS 8 30%
iOS 7 6%左右
所以我建议能换WKWebview 就换吧,省下一大块内存减少不必要的异常bug。

 

iOSH5性能监控技术角度分析

性能数据了解

分析移动端H5性能数据,首先我们说说是哪些性能数据。

  • 白屏时间,白屏时间无论安卓还是iOS在加载网页的时候都会存在的问题,也是目前无法解决的;
  • 页面耗时,页面耗时指的是开始加载这个网页到整个页面load完成即渲染完成的时间;
  • 加载链接的一些性能数据,重定向时间,DNS解析时间,TCP链接时间,request请求时间,response响应时间,dom节点解析时间,page渲染时间,同时我们还需要抓取资源时序数据,

什么是资源时序数据呢?每个网页是有很多个资源组成的,有.js、.png、.css、.script等等,我们就需要将这些每个资源链接的耗时拿到,是什么类型的资源,完整链接;对于客户来说有了这些还不够,还需要JS错误,页面的ajax请求。JS错误获取的当然是堆栈信息和错误类型。ajax请求一般是获取三个时间,响应时间,ajax下载时间,ajax回调时间。

 

上面分析的是能够获取到移动端H5的性能数据,这些数据怎么得到就是接下来要讲的了。数据获取是需要js来做的,都知道移动端是通过webView来加载网页的,js里面能通过performance这个接口来从webView内部api获取上面的那些数据,js获取的数据然后发给OC;那JS怎么样才能拿到这些数据呢,这就是最关键的,OC代码如何写才能让JS获取数据。

代码编写

有两种方法可以得到数据,先介绍用NSURLProtocol这个类获取数据。

iOS的UIWebView加载网页的时候会走进NSURLProtocol这个类,所以我们就需要在这个类里面作文章了,我们先用UIWebView加载一个链接,例如百度等等,然后创建一个继承NSURLProtocol的类。


继承NSURLProtocol

NSURLProtocol里面有很多方法,就不一一介绍了,有一个startLoading的方法,我们在这个方法里面用NSURLConnection发送一个请求,设置代理,请求的request就是加载网页的request,因为NSURLProtocol有一个NSURLRequest的属性。

- (instancetype)initWithRequest:(NSURLRequest*)request delegate:(id)delegate startImmediately:(BOOL)startImmediately

这个就是请求的方法。


发送请求

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

通过NSURLConnection设置代理,就需要写出代理方法,在其中一个代理方法里面能获得网页的代码,这就是我们最关键的地方,就是上面这个方法,将data用utf-8转码就会得到代码。


插入js代码的方法

得到网页的代码有什么用呢?熟悉网页端代码的都知道,网页端的代码都是由很多标签组成,我们先找到<head>这个标签,在下面插入一个<script>标签,里面放入js代码,这个js代码就是用来获取上面介绍的性能数据。

- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;

上面会用到很多这个方法,为什么要用这个呢,因为你注入了新的代码,你需要将这个新的网页代码用这个方法加载一下,不然网页会加载不出来的。

最后只需要将MonitorURLProtocol在appDelegate里面注册一下就可以了。


注册创建的类

以上是通过NSURLProtocol这个类注入获取数据的代码,将JS代码注入后就需要JS将数据发送给我们,可以通过交互这些方式,接下来我就介绍一种直白的交互,大多数的交互也是这么做的,只不过封装了,考虑的情况更多,我就只是根据实际情况来做了。

OC与JS交互获取数据

交互都会有一个标识,OC传一个标识给JS,JS通过标识判断,发送给你想要的数据。我首先是通过运行时动态加载的方法将加载链接的三个方法给hook,进入同一个我定义的方法,然后在这个方法里面传标识给JS。


hook

数据获取

将标识和block作为键值对存起来,然后将JS将数据用url的形式发过来,我们取出来,匹配一下对应相应的标识,然后用block回调相应的数据,相应的代码这里就不贴了,最后我会给github上代码的链接。接受url就是用UIWebView的代理方法。

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

{

  [WebViewTracker webView:webView withUrl:request.URL];

  return YES;

}

我们还可以在didFinishLoad里面手动调用JS代码来获取获取数据,和NSURLProtocol结合起来用,各有各的优缺点,一个是在顶部注入代码,一个是在尾部调用代码,都是会丢失一部分数据,所以最好的办法就是结合起来用。

作者:hbblzjy 发表于2016/10/17 11:39:18 原文链接
阅读:41 评论:0 查看评论

Android EventBus 3.0 用法及其原理详解

$
0
0

Android EventBus 3.0 用法及其原理详解

现在的项目中要用到应用程序内各组件间、组件与后台线程间的通信,2个方案可以实现这个功能,一个是利用回调。另一个是用EventBus。因为回调太繁琐,所以决定用EventBus。因为以前用过EventBus,所以直接去github看看有什么新功能,可一看,竟然3.0了,并且和2.0+的用法有很大的不同,所以就研究了下他的源码,记录下来,让大家参考参考,有什么不对的地方,欢迎大家指出。

我现在要介绍的是EventBus 3.0,你可以没有用过EventBus,因为我介绍的会很详细。

EventBus 3.0的使用

首先你要为你的app添加依赖库:
compile 'de.greenrobot:eventbus:3.0.0'

定义一个事件类型:里面的数据根据需求来,也可以是一个空的类。

`public class MainMessage {
    public String msg;
    public MainMessage(String msg) {
    this.msg = msg;
    }
}`

注册:注意,不要忘了在onDestroy里面的代码哦!

`@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //注册
    EventBus.getDefault().register(this);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    //反注册
    EventBus.getDefault().unregister(this);
}`

订阅:在注册了的页面中,声明处理事件的方法

`//主线程中执行
@Subscribe(threadMode = ThreadMode.MAIN,priority = 100)
public void onMainEventBus(MainMessage msg) {
    }
}`

1) ThreadMode总共四个:

NAIN UI主线程

BACKGROUND 后台线程

POSTING 和发布者处在同一个线程

ASYNC 异步线程

2) priority:事件的优先级类似广播的优先级,优先级越高优先获得消息.
3)Sticky事件的使用 :之前说的使用方法, 都是需要先注册(register), 再post,才能接受到事件; 如果你使用postSticky发送事件, 那么可以不需要先注册, 也能接受到事件。

首先,你可能需要声明一个方法

`   //注意,和之前的方法一样,只是多了一个 sticky = true 的属性.
    @Subscribe(threadMode = ThreadMode.MainThread, sticky = true)
    public void onEvent(MainMessage event){
    }`

其次, 你可以在没有register的情况下:(发送事件)

`EventBus.getDefault().postSticky(new MainMessage("With Sticky"));`

之后, 再注册,这样就可以收到刚刚发送的事件了:

`EventBus.getDefault().register(this);//注册之后,马上就能收到刚刚postSticky发送的事件`

发布:在任意地方,调用发送事件
EventBus.getDefault().post(new MainMessage(msg));

现在就ok了,如果你只是用的话就ok了,但是,如果不知道它的原理,我老感觉心里不舒服,用着不放心,接下来就介绍他的原理

首先,我们看看注册register()方法的实现

3.0的只提供了一个register()方法

` public void register(Object subscriber) {
    //首先获得订阅者的class对象
    Class<?> subscriberClass = subscriber.getClass();
    //通过subscriberMethodFinder来找到订阅者订阅了哪些事件.返回一个SubscriberMethod对象的List,SubscriberMethod
    //里包含了这个方法的Method对象,以及将来响应订阅是在哪个线程的ThreadMode,以及订阅的事件类型eventType,以及订阅的优
    //先级priority,以及是否接收粘性sticky事件的boolean值.
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            //订阅
            subscribe(subscriber, subscriberMethod);
        }
    }
}`

这里就是设计模式里我们常用的单例模式了,目的是为了保证getDefault()得到的都是同一个实例。如果不存在实例,就调用了EventBus的构造方法:

` private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();

public EventBus() {
    this(DEFAULT_BUILDER);
}

EventBus(EventBusBuilder builder) {
    //key:订阅的事件,value:订阅这个事件的所有订阅者集合
    //private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
    subscriptionsByEventType = new HashMap<>();
    //key:订阅者对象,value:这个订阅者订阅的事件集合
    //private final Map<Object, List<Class<?>>> typesBySubscriber;
    typesBySubscriber = new HashMap<>();
    //粘性事件 key:粘性事件的class对象, value:事件对象
    //private final Map<Class<?>, Object> stickyEvents;
    stickyEvents = new ConcurrentHashMap<>();
    //事件主线程处理
    mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
    //事件 Background 处理
    backgroundPoster = new BackgroundPoster(this);
    //事件异步线程处理
    asyncPoster = new AsyncPoster(this);
    indexCount = builder.subscriberInfoIndexes != null ? builder.subscriberInfoIndexes.size() : 0;
    //订阅者响应函数信息存储和查找类
    subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,
            builder.strictMethodVerification, builder.ignoreGeneratedIndex);
    logSubscriberExceptions = builder.logSubscriberExceptions;
    logNoSubscriberMessages = builder.logNoSubscriberMessages;
    sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;
    sendNoSubscriberEvent = builder.sendNoSubscriberEvent;
    throwSubscriberException = builder.throwSubscriberException;
    //是否支持事件继承
    eventInheritance = builder.eventInheritance;
    executorService = builder.executorService;
}`

可以看出是通过初始化了一个EventBusBuilder()对象来分别初始化EventBus的一些配置,当我们在写一个需要自定义配置的框架的时候,这种实现方法非常普遍,将配置解耦出去,使我们的代码结构更清晰.

我把每行的代码都写了注释,可以看到register()方法很简洁,我们可以看出通过subscriberMethodFinder.findSubscriberMethods(subscriberClass)方法就能返回一个SubscriberMethod的对象,而SubscriberMethod里包含了所有我们需要的接下来执行subscribe()的信息.所以我们先去看看findSubscriberMethods()是怎么实现的,然后我们再去关注subscribe()。

SubscriberMethodFinder的实现

一句话来描述SubscriberMethodFinder类就是用来查找和缓存订阅者响应函数的信息的类。下面我们就来看findSubscriberMethods()到底是如何实现的:

`   List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    //先从METHOD_CACHE取看是否有缓存,key:保存订阅类的类名,value:保存类中订阅的方法数据,
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    //是否忽略注解器生成的MyEventBusIndex类
    if (ignoreGeneratedIndex) {
        //利用反射来读取订阅类中的订阅方法信息
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
        //从注解器生成的MyEventBusIndex类中获得订阅类的订阅方法信息
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    if (subscriberMethods.isEmpty()) {
        throw new EventBusException("Subscriber " + subscriberClass
                + " and its super classes have no public methods with the @Subscribe annotation");
    } else {
        //保存进METHOD_CACHE缓存
        METHOD_CACHE.put(subscriberClass, subscriberMethods);
        return subscriberMethods;
    }
}`

注释很详细我们就不在多说,由于篇幅原因我们就不在分析findUsingInfo()方法,其无非就是通过查找我们上面所说的MyEventBusIndex类中的信息,来转换成List从而获得订阅类的相关订阅函数的各种信息.有兴趣的可以自己研究看看,下面我们就来看findUsingReflection()方法是如何实现的:

`  private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
    //FindState 用来做订阅方法的校验和保存
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
        //通过反射来获得订阅方法信息
        findUsingReflectionInSingleClass(findState);
        //查找父类的订阅方法
        findState.moveToSuperclass();
    }
    //获取findState中的SubscriberMethod(也就是订阅方法List)并返回
    return getMethodsAndRelease(findState);
}`

这里通过FindState类来做订阅方法的校验和保存,并通过FIND_STATE_POOL静态数组来保存FindState对象,可以使FindState复用,避免重复创建过多的对象.最终是通过findUsingReflectionInSingleClass()来具体获得相关订阅方法的信息的:

`private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
    //通过反射得到方法数组
    try {
        // This is faster than getMethods, especially when subscribers are fat classes like Activities
        methods = findState.clazz.getDeclaredMethods();
    } catch (Throwable th) {
        // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
        methods = findState.clazz.getMethods();
        findState.skipSuperClasses = true;
    }
    //遍历Method
    for (Method method : methods) {
        int modifiers = method.getModifiers();
        if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            //保证必须只有一个事件参数
            if (parameterTypes.length == 1) {
                //得到注解
                Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                if (subscribeAnnotation != null) {
                    Class<?> eventType = parameterTypes[0];
                    //校验是否添加该方法
                    if (findState.checkAdd(method, eventType)) {
                        ThreadMode threadMode = subscribeAnnotation.threadMode();
                        //实例化SubscriberMethod对象并添加
                        findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                    }
                }
            } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                throw new EventBusException("@Subscribe method " + methodName +
                        "must have exactly 1 parameter but has " + parameterTypes.length);
            }
        } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
            String methodName = method.getDeclaringClass().getName() + "." + method.getName();
            throw new EventBusException(methodName +
                    " is a illegal @Subscribe method: must be public, non-static, and non-abstract");
        }
    }
}`

到这里,我们订阅类的所有SubscriberMethod都已经被保存了,最后再通过getMethodsAndRelease()返回List至此,所有关于如何获得订阅类的订阅方法信息即:SubscriberMethod对象就已经完全分析完了.

post()方法原理分析

` public void post(Object event) {
    //得到当前线程的Posting状态.
    PostingThreadState postingState = currentPostingThreadState.get();
    //获取当前线程的事件队列
    List<Object> eventQueue = postingState.eventQueue;
    eventQueue.add(event);

    if (!postingState.isPosting) {
        postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
        postingState.isPosting = true;
        if (postingState.canceled) {
            throw new EventBusException("Internal error. Abort state was not reset");
        }
        try {
            //一直发送
            while (!eventQueue.isEmpty()) {
                //发送单个事件
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}`

首先是通过currentPostingThreadState.get()方法来得到当前线程PostingThreadState的对象。
currentPostingThreadState的实现是一个包含了PostingThreadState的ThreadLocal对象,关于ThreadLocal
张涛的这篇文章解释的很好:ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,
而这段数据是不会与其他线程共享的。其内部原理是通过生成一个它包裹的泛型对象的数组,在不同的线程会有不同的数组索引值,通过这样就可以做到每个线程通过get() 方法获取的时候,取到的只能是自己线程所对应的数据。 所以这里取到的就是每个线程的PostingThreadState状态.接下来我们来看postSingleEvent()方法:

`private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    Class<?> eventClass = event.getClass();
    boolean subscriptionFound = false;
    //是否触发订阅了该事件(eventClass)的父类,以及接口的类的响应方法.
    if (eventInheritance) {
        //查找eventClass类所有的父类以及接口
        List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
        int countTypes = eventTypes.size();
        //循环postSingleEventForEventType
        for (int h = 0; h < countTypes; h++) {
            Class<?> clazz = eventTypes.get(h);
            //只要右边有一个为true,subscriptionFound就为true
            subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
        }
    } else {
        //post单个
        subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
    }
    //如果没发现
    if (!subscriptionFound) {
        if (logNoSubscriberMessages) {
            Log.d(TAG, "No subscribers registered for event " + eventClass);
        }
        if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                eventClass != SubscriberExceptionEvent.class) {
            //发送一个NoSubscriberEvent事件,如果我们需要处理这种状态,接收这个事件就可以了
            post(new NoSubscriberEvent(this, event));
        }
    }
}`

现在发现是在postSingleEventForEventType()方法里去进行事件的分发,代码如下:

` private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    CopyOnWriteArrayList<Subscription> subscriptions;
    //获取订阅了这个事件的Subscription列表.
    synchronized (this) {
        subscriptions = subscriptionsByEventType.get(eventClass);
    }
    if (subscriptions != null && !subscriptions.isEmpty()) {
        for (Subscription subscription : subscriptions) {
            postingState.event = event;
            postingState.subscription = subscription;
            //是否被中断
            boolean aborted = false;
            try {
                //分发给订阅者
                postToSubscription(subscription, event, postingState.isMainThread);
                aborted = postingState.canceled;
            } finally {
                postingState.event = null;
                postingState.subscription = null;
                postingState.canceled = false;
            }
            if (aborted) {
                break;
            }
        }
        return true;
    }
    return false;
}

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case POSTING:
            invokeSubscriber(subscription, event);
            break;
        case MAIN:
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case BACKGROUND:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case ASYNC:
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}`

总结上面的代码就是,首先从subscriptionsByEventType里获得所有订阅了这个事件的Subscription列表,然后在通过postToSubscription()方法来分发事件,在postToSubscription()通过不同的threadMode在不同的线程里invoke()订阅者的方法。

解除注册:unregister()方法分析

` public synchronized void unregister(Object subscriber) {
    //通过typesBySubscriber来取出这个subscriber订阅者订阅的事件类型,
    List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
    if (subscribedTypes != null) {
        //分别解除每个订阅了的事件类型
        for (Class<?> eventType : subscribedTypes) {
            unsubscribeByEventType(subscriber, eventType);
        }
        //从typesBySubscriber移除subscriber
        typesBySubscriber.remove(subscriber);
    } else {
        Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
    }
}`

然后是unsubscribeByEventType()方法的实现:

` private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
    //subscriptionsByEventType里拿出这个事件类型的订阅者列表.
    List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions != null) {
        int size = subscriptions.size();
        //取消订阅
        for (int i = 0; i < size; i++) {
            Subscription subscription = subscriptions.get(i);
            if (subscription.subscriber == subscriber) {
                subscription.active = false;
                subscriptions.remove(i);
                i--;
                size--;
            }
        }
    }
}`

可以看出是从typesBySubscriber和subscriptions里分别移除订阅者以及相关信息.

现在EventBus3.0 的大概解析已经完了,我们知道了原理就可以放心大胆的用了

现在额外的说一下EventBus processor,因为现在还是测试版,所以不推荐使用,知道就好。

EventBus processor使用: EventBus提供了一个EventBusAnnotationProcessor注解处理器来在编译期通过读取@Subscribe()注解并解析,
处理其中所包含的信息,然后生成java类来保存所有订阅者关于订阅的信息,这样就比在运行时使用反射来获得这些订阅者的
信息速度要快.

具体使用:在build.gradle中添加如下配置:

`provided 'de.greenrobot:eventbus-annotation-processor:3.0.0-beta1'`

使用前后,我测了一下,我的项目的代码大概能相差8-9毫秒,这个时间对我来说不是很多,所以我项目中就没有用。

作者:gemgaozhen 发表于2016/10/17 11:48:39 原文链接
阅读:8 评论:0 查看评论

android自定义view之汽车仪表盘增强版

$
0
0

下面是改进后的效果图
这里写图片描述
原文链接:
http://blog.csdn.net/lxk_1993/article/details/51373269
绘图流程总结:
onLayout() 主要是组合排列一些包含的控件(通常是已有控件)进行布局,以及子view布局
onMeasure() 测绘,控件大小测量,子view测绘控件大小(viewGroup)
onDraw() 图形绘制,主要是对画布canvas,画笔paint ;图形绘制,这个会涉及许多API,许多绘图技巧:圆形的绘制,矩形绘制,刻度绘制,文字绘制,bitmap图形绘制,以及复杂的精密图形的绘制(一个摩托车的绘制需要很多小部件,就像PS,图形分层绘制的思想)
前两个都是对view本身或者子view进行整体处理,onDraw() 处理针对的对象是一个view的内容,是局部的处理
当然了如果你需要对自定义的view实现触摸响应,事件分发还需要实现onTouchEvent()
改进点:
1 仪表盘 速度speed字体调整,调大
2 加油启动时 仪表盘亮灯(检测speed处理速度刻度线颜色)
3 对运行速度检测,不同速度表盘文字颜色有不同的变化,用来警示用户是否超速。

SpeedControlView

public class SpeedControlView extends View implements Runnable {

    //画笔
    //mPaint:绘制刻度
    //textPaint 表盘所有文字画笔
    //speedAreaPaint 速度指针扫描弧
    private Paint mPaint, textPaint, speedAreaPaint;
    private Context mContext;
    //屏幕宽高
    private int screenWidth, screenHeight;
    //仪表盘圆的半径
    private float raduis, sRaduis;
    //圆心
    private int pointX, pointY;
    //文字的偏移量
    private float textScale;
    //速度指针变化的位置
    private float linePointerX, linePointerY;
    //速度
    private int speed;
    //速度范围的2个扇形外切矩形
    private RectF speedRectF, speedRectFInner;
    //速度控制模式  1 加速  2 减速  3 手刹
    private int type;
    // 速度文字 绘制的XY坐标
    private int baseX, baseY;
    //屏幕密度
    private float mDensityDpi;
    //设置速度控制模式
    public void setType(int type) {
        this.type = type;
    }
    //开始重绘
    private boolean start = true;

    public void setStart(boolean start) {
        this.start = start;
    }
    // 设置速度 并重绘视图
    public void setSpeed(int speed) {
        this.speed = speed;
        if(speed<120){
            textPaint.setColor(Color.WHITE);
        }
        else if (speed>=120&&speed<=210)
            textPaint.setColor(Color.YELLOW);
        else
            textPaint.setColor(Color.RED);
        //
        postInvalidate();
    }
    public SpeedControlView(Context context) {
        this(context, null);
    }
    public SpeedControlView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public SpeedControlView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        //获取屏幕宽高
//        screenWidth = ((Activity) context).getWindowManager().getDefaultDisplay().getWidth();
//        screenHeight = ((Activity) context).getWindowManager().getDefaultDisplay().getHeight();
        //获取屏幕宽高 和 屏幕密度dpi
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        screenWidth = displayMetrics.widthPixels;
        screenHeight = displayMetrics.heightPixels;
        //像素密度相对于标准320的比例
        mDensityDpi = displayMetrics.densityDpi / 320;
        //开启硬件加速
        //setLayerType(View.LAYER_TYPE_HARDWARE, null);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //设置抗锯齿
        mPaint.setAntiAlias(true);
        //设置画笔样式 实心圆
        mPaint.setStyle(Paint.Style.FILL);
        //设置空心线宽
        mPaint.setStrokeWidth(5 * mDensityDpi);
        //初始化  圆心左边 和 半径
        raduis = screenWidth / 3;
        //圆心位于中心点
        pointX = pointY = screenWidth / 2;
//        pointY = screenHeight / 4;
        //设置抗锯齿
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setAntiAlias(true);
        //设置画笔颜色
        textPaint.setColor(Color.WHITE);
        // 获取字体并设置画笔字体
       // Typeface typeface = Typeface.createFromAsset(mContext.getAssets(), "kt.ttf");
        //为速度文字设置画笔风格
       // textPaint.setTypeface(typeface);
        //设置抗锯齿
        speedAreaPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        speedAreaPaint.setAntiAlias(true);
        //设置画笔样式
        speedAreaPaint.setStyle(Paint.Style.FILL);
        // 设置速度范围扇形的渐变颜色
        //public LinearGradient (float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile);
        Shader mShader = new LinearGradient(pointX - raduis, pointY, pointX + raduis, pointY,
                new int[]{0xFF445EED, 0xFF072AE9, 0xFF0625CE}, null, Shader.TileMode.CLAMP);
        speedAreaPaint.setShader(mShader);
        // 初始化速度范围的2个扇形外切矩形
        speedRectF = new RectF(
                pointX - raduis + 10 * mDensityDpi,
                pointY - raduis + 10 * mDensityDpi,
                pointX + raduis - 10 * mDensityDpi,
                pointY + raduis - 10 * mDensityDpi);
        speedRectFInner = new RectF(
                pointX - raduis / 2,
                pointY - raduis / 2,
                pointX + raduis / 2,
                pointY + raduis / 2);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.BLACK);
        //绘制外层圆
        drawCicle(canvas);
        //绘制速度范围扇形区域
        speedAreaPaint.setColor(0x7E3F51B5);
        drawSpeedArea(canvas);
        //变换画笔颜色 绘制刻度
        mPaint.setColor(0xBF3F6AB5);
        drawScale(canvas);
        //变换画笔颜色 绘制速度标识文字
        textPaint.setTextSize(25 * mDensityDpi);
        mPaint.setColor(Color.WHITE);
        sRaduis = raduis - 50 * mDensityDpi;
        textScale = Math.abs(textPaint.descent() + textPaint.ascent()) / 2;
//        Log.e("textScale", textScale + "");
        for (int i = 0; i < 8; i++) {
            drawText(canvas, 30 * i);
        }
        //绘制中间文字内容
        drawCenter(canvas);

    }
    /**
     * 绘制外层圆
     */
    private void drawCicle(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(0xFF343434);
        canvas.drawCircle(pointX, pointY, raduis, mPaint);
        //外圈2个圆
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(0xBF3F6AB5);
        //设置空心线宽
        mPaint.setStrokeWidth(4 * mDensityDpi);
        canvas.drawCircle(pointX, pointY, raduis, mPaint);
        //设置空心线宽
        mPaint.setStrokeWidth(3 * mDensityDpi);
        canvas.drawCircle(pointX, pointY, raduis - 10 * mDensityDpi, mPaint);
        //内圈2个圆
        mPaint.setStrokeWidth(5 * mDensityDpi);
        mPaint.setColor(0xE73F51B5);
        canvas.drawCircle(pointX, pointY, raduis / 2, mPaint);
        mPaint.setColor(0x7E3F51B5);
        canvas.drawCircle(pointX, pointY, raduis / 2 + 5 * mDensityDpi, mPaint);
        mPaint.setStrokeWidth(3 * mDensityDpi);
    }
    /**
     * 绘制速度区域扇形
     */
    private void drawSpeedArea(Canvas canvas) {
        int degree;
        if (speed < 210) {
            degree = speed * 36 / 30;
        } else {
           // degree = speed * 36 / 30;
            //速度达到一定后表盘指针不变化
            degree = 210 * 36 / 30;
        }
        canvas.drawArc(speedRectF, 144, degree, true, speedAreaPaint);
        // TODO: 2016/5/12
        //不显示中间的内圈的扇形区域
        speedAreaPaint.setColor(0xFF343434);
        mPaint.setColor(0xFF343434);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawArc(speedRectFInner, 144, degree, true, mPaint);
        mPaint.setStyle(Paint.Style.STROKE);
    }
    /**
     * 绘制刻度
     */
    //需要调整刻度线 在此重写,这里使用mPaint
    private void drawScale(Canvas canvas) {
        if (speed>0)
        mPaint.setColor(Color.GREEN);
        for (int i = 0; i < 60; i++) {
            if (i % 6 == 0) {
                canvas.drawLine(pointX - raduis + 10 * mDensityDpi, pointY, pointX - raduis + 50 * mDensityDpi, pointY, mPaint);
            } else {
                canvas.drawLine(pointX - raduis + 10 * mDensityDpi, pointY, pointX - raduis + 30 * mDensityDpi, pointY, mPaint);
            }
            canvas.rotate(6, pointX, pointY);
        }
    }
    /**
     * 绘制速度标识文字
     */
    // 这里是绘制速度文字,使用画笔textPaint
    private void drawText(Canvas canvas, int value) {
        String TEXT = String.valueOf(value);
        switch (value) {
            case 0:
                // 计算Baseline绘制的起点X轴坐标
                baseX = (int) (pointX - sRaduis * Math.cos(Math.PI / 5) + textPaint.measureText(TEXT) / 2 + textScale / 2);
                // 计算Baseline绘制的Y坐标
                baseY = (int) (pointY + sRaduis * Math.sin(Math.PI / 5) + textScale / 2);
                break;
            case 30:
                baseX = (int) (pointX - raduis + 50 * mDensityDpi + textPaint.measureText(TEXT) / 2);
                baseY = (int) (pointY + textScale);
                break;
            case 60:
                baseX = (int) (pointX - sRaduis * Math.cos(Math.PI / 5) + textScale);
                baseY = (int) (pointY - sRaduis * Math.sin(Math.PI / 5) + textScale * 2);
                break;
            case 90:
                baseX = (int) (pointX - sRaduis * Math.cos(2 * Math.PI / 5) - textScale / 2);
                baseY = (int) (pointY - sRaduis * Math.sin(2 * Math.PI / 5) + 2 * textScale);
                break;
            case 120:
                baseX = (int) (pointX + sRaduis * Math.sin(Math.PI / 10) - textPaint.measureText(TEXT) / 2);
                baseY = (int) (pointY - sRaduis * Math.cos(Math.PI / 10) + 2 * textScale);
                break;
            case 150:
                baseX = (int) (pointX + sRaduis * Math.cos(Math.PI / 5) - textPaint.measureText(TEXT) - textScale / 2);
                baseY = (int) (pointY - sRaduis * Math.sin(Math.PI / 5) + textScale * 2);
                break;
            case 180:
                baseX = (int) (pointX + sRaduis - textPaint.measureText(TEXT) - textScale / 2);
                baseY = (int) (pointY + textScale);
                break;
            case 210:
                baseX = (int) (pointX + sRaduis * Math.cos(Math.PI / 5) - textPaint.measureText(TEXT) - textScale / 2);
                baseY = (int) (pointY + sRaduis * Math.sin(Math.PI / 5) - textScale / 2);
                break;
        }
        canvas.drawText(TEXT, baseX, baseY, textPaint);
    }
    /**
     * 绘制中间文字内容
     */
    //这里是最里面实时的速度值,和速度单位的绘制,使用画笔textPaint
    //measureText() 获取速度文本的坐标位置
    private void drawCenter(Canvas canvas) {
        //速度
        textPaint.setTextSize(60 * mDensityDpi);
        float tw = textPaint.measureText(String.valueOf(speed));
        baseX = (int) (pointX - tw / 2);
        baseY = (int) (pointY + Math.abs(textPaint.descent() + textPaint.ascent()) / 4);
        canvas.drawText(String.valueOf(speed), baseX, baseY, textPaint);

        //单位
        textPaint.setTextSize(30 * mDensityDpi);
        tw = textPaint.measureText("Km/h");
        baseX = (int) (pointX - tw / 2);
        baseY = (int) (pointY + raduis / 4 + Math.abs(textPaint.descent() + textPaint.ascent()) / 4);
        canvas.drawText("Km/h", baseX, baseY, textPaint);
    }

    @Override
    public void run() {
        int speedChange;
        //view内部实现一个线程接口,view加载完成 开启线程,这个线程做无限循环,无限的改变绘图参数,并调用view.postInvalied 进行重绘
        //GameLoop  游戏循环模型,无限循环,不断检测用户动作,根据不同动作类型,
        while (start) {
            //循环检测信号量type  这个信号量来源于用户按键动作
            switch (type) {
                case 1://油门
                    speedChange = 2;
                    break;
                case 2://刹车
                    speedChange = -5;
                    break;
                case 3://手刹
                    speed = 0;
                    speedAreaPaint.setColor(0x7E3F51B5);
                    textPaint.setColor(Color.WHITE);
                default:
                    speedChange = -2;
                    break;
            }
            //速度改变模型
            speed += speedChange;

            if (speed < 1) {
                speed = 0;
            }
            try {
                Thread.sleep(50);
                //间接调用 postinvalid
                setSpeed(speed);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}

activity处理部分

    speedControlView = (SpeedControlView) findViewById(R.id.speed_control);
    //实体化
    speedUp = (Button) findViewById(R.id.speed_up);
    speedDown = (Button) findViewById(R.id.speed_down);
    shutDown = (Button) findViewById(R.id.shut_down);
   //设置监听
        speedUp.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //按下的时候加速
                        speedControlView.setType(1);
                        break;
                    case MotionEvent.ACTION_UP:
                        //松开做自然减速
                        speedControlView.setType(0);
                        break;
                }
                return true;
            }
        });
        speedDown.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //按下的时候减速
                        speedControlView.setType(2);
                        break;
                    case MotionEvent.ACTION_UP:
                        //松开做自然减速
                        speedControlView.setType(0);
                        break;
                }
                return true;
            }
        });
        shutDown.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //按下的时候拉手刹
                        speedControlView.setType(3);
                        break;
                    case MotionEvent.ACTION_UP:
                        //松开做自然减速
                        speedControlView.setType(0);
                        break;
                }
                return true;
            }
        });

    }

另外还要在activity的生命周期中添加处理,自定义view注销,速度初始化,状态清零
布局文件

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >
<app.example.com.viewmeasure.view.SpeedControlView
    android:id="@+id/speed_control"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"/>
<Button
    android:id="@+id/speed_up"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="油门"
    android:layout_alignParentBottom="true"
    android:layout_alignParentEnd="true" />
    <Button
        android:id="@+id/speed_down"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="刹车"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true" />
<Button
    android:id="@+id/shut_down"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="手刹"
    android:layout_alignBaseline="@+id/speed_down"
    android:layout_alignBottom="@+id/speed_down"
    android:layout_toStartOf="@+id/speed_down"
    android:layout_marginEnd="41dp" />
</RelativeLayout>

页面布局可以使用android studio提供的拖拽方案,可以快速布局

后期迭代想法

1 仪表盘 背景绘制,提供背景设置接口供用户调用
2 通过传感器,模拟汽车启动,加速,减速,刹车 来实现仪表盘状态的改变
3 当仪表盘检测到速度超过阀值,提示方案:语音,震动器
4 绘图性能优化

作者:u010129985 发表于2016/10/17 12:33:56 原文链接
阅读:11 评论:0 查看评论

Android如何利用ksoap2进行sql server操作实现登陆功能

$
0
0
   最近做了一个小学期的课程设计,一个桌面应用系统,一个android。由于之前做过一个web项目,用的是visual studio,感觉熟练了,所以桌面应用系统就选择了vs,用了wpf实现了桌面应用系统,数据库当然是sql server。但是到了做android的时候,遇到了麻烦,那就是android不能连接sql server,好方。通过各种查资料,发现通过使用第三方包ksoap2连接web server从而实现对sql server的操作。
工具:
 Eclipse、ksoap2 3.0版、visual studio、sql server2008
    首先,第三方包ksoap2的版本一定要选择正确,我最初在网上找的,是2.5.4版本的,写完代码之后一直有问题出现,找了很久才发现是版本的问题,我提供个下载吧,亲测可用。
    这种方法的实现是基于web,所以最近基本的你要学会建一个web,这网上有很多教程,在这里就不写了。这时候你就会想在web到底做了什么,其实只有两件事,1、连接数据库;2、进行数据库的操作。对的,我们把数据库的操作放在了web端,这样我们在android端只需要传递一些参数给web端,然后web端处理参数,再将处理后的信息反馈给android端,很容易理解。
    web端代码如下:
//web.config文件
<connectionStrings>
    <!--数据库连接-->
    <add name="ConnectionString" connectionString="Provider=SQLOLEDB.1;Data Source=.;Initial Catalog=shujujiegou;User Id=sa;Password=admin;"></add>
  </connectionStrings>
//DataBase.cs文件中
public DataSet GetDataSet(string strSql, string tableName)
        {
            DataSet dataSet = new DataSet();
            OleDbConnection conn = new OleDbConnection(ConnectionString);
            OleDbDataAdapter dataAdapter = new OleDbDataAdapter(strSql, conn);
            dataAdapter.Fill(dataSet, tableName);
            return dataSet;                     //返回这个数据集
        }

        public void execsql(string strSql)
        {
            OleDbConnection dbconn = new OleDbConnection(ConnectionString);//数据库连接
            OleDbCommand comm = new OleDbCommand(strSql, dbconn);//定义并初始化命令对象
            dbconn.Open();//打开连接
            comm.ExecuteNonQuery();//执行命令
            dbconn.Close();//关闭连接
        }
//server1.asmx.cs文件中
[WebMethod(Description = "普通用户登录")]
        public bool Selectpeople(string username, string password)
        {
            string strsql;
            string ss = "aaa";
            strsql = "select * from tuser where username ='" + username + "' and password = '" + password + "' and mark = 1";
            DataSet dataSet = new DataSet(); //创建数据据
            dataSet = database.GetDataSet(strsql, "usernamelist"); //将查询的当前用户存入数据集里
            if (dataSet.Tables["usernamelist"].Rows.Count == 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

运行web当然这里还有一些其他的接口,目前就不再介绍了。我们这里只说一下实现返回简单的字符串的功能,返回数据集的下次讲。
这里写图片描述这里写图片描述
这会估计你就已经很清楚了,我们只需要在android端找到你写的web地址,调用selectpeople接口,输入username和password这两个参数,然后android就不需要管了等着就接受web反馈的信息就行了。web端发聩一个bool型字符串,我们判断如果是true就是登陆成功,否则,登陆失败。很简单,复杂的xml解析ksoap2已经帮我们实现,下面解释如何实现上述过程。
android端代码:
1、获得4个重要的参数

// 命名空间      
    String nameSpace = "http://tempuri.org/";
// 调用的方法名称      
    String methodName = "Selectpeople";      
// EndPoint      
    String endPoint = "http://192.168.1.102:8001/Service1.asmx";      
// SOAP Action      
    String soapAction = "http://tempuri.org/Selectpeople";   

这些东西去哪里找呐,当然是在你的web端
这里写图片描述
endpoint是你web打开的地址。
2、ksoap2的使用

public String getRemoteInfo(String nameSpace,  String methodName, String endPoint, String soapAction, String name, String password) {  
         SoapObject rpc = new SoapObject(nameSpace, methodName);  
         rpc.addProperty("username", name);      //你要输入的两个值
         rpc.addProperty("password",password); 

         // 生成调用WebService方法的SOAP请求信息,并指定SOAP的版本  
         SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER10);     
         envelope.bodyOut = rpc;  
         // 设置是否调用的是dotNet开发的WebService  
         envelope.dotNet = true;  
         // 等价于envelope.bodyOut = rpc;  
         envelope.setOutputSoapObject(rpc);  

         HttpTransportSE transport = new HttpTransportSE(endPoint);  
         try {  
             // 调用WebService  
             transport.call(soapAction, envelope);  //执行
         } catch (Exception e) {  
             e.printStackTrace();  
         }  

         // 获取返回的数据  (解析出xml)
         SoapObject object = (SoapObject) envelope.bodyIn;         
         result = object.getPropertyAsString("SelectpeopleResult").toString().trim();
         //这个selectpeople从哪里找请看下图               
         return result;
     }      

这里写图片描述
还有一件很重要的事,那就是android4.0版本之后不支持在主线程中进行这种操作,你要把他放到子线程中才能实现。下面是实现代码。

public class MainActivity extends ActionBarActivity {

    final static String TAG = "MainActivity";
    public EditText username;
    public EditText userpassword;
    private String result; 
    public static String name;

    @Override


    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);                         //调用父类 方法
        requestWindowFeature(Window.FEATURE_NO_TITLE);              //隐藏标题栏
        setContentView(R.layout.activity_main);                     //加载一个布局

        Button button1=(Button) findViewById(R.id.button1);     //声明按钮
        Button button2=(Button) findViewById(R.id.button2);
        button2.setOnClickListener(new OnClickListener() {                  //注册按钮 

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                Intent intent=new Intent(MainActivity.this,Register.class);         //跳转至注册页面
                startActivity(intent);
            }
        });
        username=(EditText) findViewById(R.id.editText1);                   //获取用户名
        userpassword=(EditText) findViewById(R.id.editText2);           //获取密码              
        button1.setOnClickListener(new OnClickListener() {              //登陆按钮点击事件          
            @Override
            public void onClick(View arg0) {
                //result="";
                // TODO Auto-generated method stub
                name=username.getText().toString();                         //获取姓名的值
                String password=userpassword.getText().toString();          //获取密码
                // 命名空间      
                //http://localhost:50109/Service1.asmx
                //http://192.168.1.100:8000/Service1.asmx
                String nameSpace = "http://tempuri.org/";               //获取网站信息及调用方法
                // 调用的方法名称      
                String methodName = "Selectpeople";      
                // EndPoint      
                String endPoint = "http://192.168.1.102:8001/Service1.asmx";      
                // SOAP Action      
                String soapAction = "http://tempuri.org/Selectpeople";    
                // 指定WebService的命名空间和调用的方法名 
                MyThread t=new MyThread(nameSpace,methodName,endPoint,soapAction,//子线程
                        name,password);
                t.start();
                try {
                    t.join();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                if(result.equals("true"))                               //是否在数据库找到对应的值
                {
                    Intent intent=new Intent(MainActivity.this,Main.class);
                    startActivity(intent);
                    finish();
                }else{          
                    Toast.makeText(MainActivity.this,"用户名不存在或者密码错误", Toast.LENGTH_SHORT).show();
                }                           
            }
        });
        }   

 // 通过Runnable接口和Thread类,得到线程返回值(子线程的具体实现)          
     private class MyThread extends Thread
    {

        private String nameSpace;
        private String methodName;
        private String endPoint;
        private String soapAction;
        private String name;
        private String password;

     public MyThread(String nameSpace,  String methodName, 
            String endPoint, String soapAction, String name, String password){  
         this.nameSpace = nameSpace;
         this.methodName = methodName;
         this.endPoint = endPoint;
         this.soapAction = soapAction;
         this.name = name;
         this.password = password;
     }  

        @Override
        public void run()
        {
                result = getRemoteInfo(nameSpace, methodName, endPoint, 
                        soapAction,name,password);
        }

    }


     /**
      * @MethodName : getRemoteInfo
      * @Description    : 调用远程webservice方法
      * @param nameSpace
      * @param methodName
      * @param endPoint
      * @param soapAction
      * @param params
      * @param vals
      * @return
      */
     public String getRemoteInfo(String nameSpace,  String methodName,                                  //连接网站,获取xml
                    String endPoint, String soapAction, String name, 
                    String password) {  
         SoapObject rpc = new SoapObject(nameSpace, methodName);  
         rpc.addProperty("username", name);      
         rpc.addProperty("password",password); 

         // 生成调用WebService方法的SOAP请求信息,并指定SOAP的版本  
         SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER10);     
         envelope.bodyOut = rpc;  
         // 设置是否调用的是dotNet开发的WebService  
         envelope.dotNet = true;  
         // 等价于envelope.bodyOut = rpc;  
         envelope.setOutputSoapObject(rpc);  

         HttpTransportSE transport = new HttpTransportSE(endPoint);  
         try {  
             // 调用WebService  
             transport.call(soapAction, envelope);  //执行
         } catch (Exception e) {  
             e.printStackTrace();  
         }  

         // 获取返回的数据  (解析出xml)
         SoapObject object = (SoapObject) envelope.bodyIn;         
         result = object.getPropertyAsString("SelectpeopleResult").toString().trim();               
         return result;
     }      
}

实现效果:

这里写图片描述这里写图片描述
目前返回简单的字符串就这样,下次补如何返回dataset数据集,就这样(这个csdn写文章时图片怎么变小)。

作者:bug_move 发表于2016/10/17 12:35:21 原文链接
阅读:7 评论:0 查看评论

Android内存管理优化建议

$
0
0

OOM(OutOfMemory)
前面我们提到过使用getMemoryClass()的方法可以得到Dalvik Heap的阈值。简要的获取某个应用的内存占用情况可以参考下面的示例( 关于更多内存查看的知识,可以参考这篇官方教程:Investigating Your RAM Usage )

1)查看内存使用情况
通过命令行查看内存详细占用情况:

通过Android Studio的Memory Monitor查看内存中Dalvik Heap的实时变化

2)发生OOM的条件
关于Native Heap,Dalvik Heap,Pss等内存管理机制比较复杂,这里不展开描述。简单的说,通过不同的内存分配方式(malloc/mmap/JNIEnv/etc)对不同的对象(bitmap,etc)进行操作会因为Android系统版本的差异而产生不同的行为,对Native Heap与Dalvik Heap以及OOM的判断条件都会有所影响。在2.x的系统上,我们常常可以看到Heap Size的total值明显超过了通过getMemoryClass()获取到的阈值而不会发生OOM的情况,那么针对2.x与4.x的Android系统,到底是如何判断会发生OOM呢?

Android 2.x系统 GC LOG中的dalvik allocated + external allocated + 新分配的大小 >= getMemoryClass()值的时候就会发生OOM。 例如,假设有这么一段Dalvik输出的GC LOG:GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms,那么32586+8989+(新分配23975)=65550>64M时,就会发生OOM。

Android 4.x系统 Android 4.x的系统废除了external的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= getMemoryClass()的时候就会发生OOM,如下图所示(虽然图示演示的是art运行环境,但是统计规则还是和dalvik保持一致)

(三)如何避免OOM总结
前面介绍了一些基础的内存管理机制以及OOM的基础知识,那么在实践操作当中,有哪些指导性的规则可以参考呢?归纳下来,可以从四个方面着手,首先是减小对象的内存占用,其次是内存对象的重复利用,然后是避免对象的内存泄露,最后是内存使用策略优化。

减小对象的内存占用
避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象。

1)使用更加轻量的数据结构
例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构,下图演示了HashMap的简要工作原理,相比起Android系统专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。

关于更多ArrayMap/SparseArray的讨论,请参考http://hukai.me/android-performance-patterns-season-3/的前三个段落

2)避免在Android里面使用Enum
Android官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考http://hukai.me/android-performance-patterns-season-3/,所以请避免在Android里面使用到枚举。

3)减小Bitmap对象的内存占用
Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用是很重要的,通常来说有下面2个措施:

inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
4)使用更小的图片
在设计给到资源图片的时候,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用一张更小的图片。尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图的时候就会因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。

内存对象的重复利用
大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码的时候显式的在程序里面去创建对象池,然后处理好复用的实现逻辑,要么就是利用系统框架既有的某些复用特性达到减少对象的重复创建,从而减少内存的分配与回收。

在Android上面最常用的一个缓存算法是LRU(Least Recently Use),简要操作原理如下图所示:

1)复用系统自带的资源
Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异,不符合需求的情况,还是需要应用程序自身内置进去。

2)注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用

3)Bitmap对象的复用
在ListView与GridView等显示大量图片的控件里面需要使用LRU的机制来缓存处理好的Bitmap。

利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率上的提升(3.0以及4.4以后存在一些使用限制上的差异)。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的bitmap会尝试去使用之前那张bitmap在heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。

使用inBitmap需要注意几个限制条件:

在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了。 我们可以创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。如下图所示:

另外提一点:在2.x的系统上,尽管bitmap是分配在native层,但是还是无法避免被计算到OOM的引用计数器里面。这里提示一下,不少应用会通过反射BitmapFactory.Options里面的inNativeAlloc来达到扩大使用内存的目的,但是如果大家都这么做,对系统整体会造成一定的负面影响,建议谨慎采纳。

4)避免在onDraw方法里面执行对象的创建
类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。

5)StringBuilder
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。

避免对象的内存泄露
内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,gc就会更容易被触发,容易出现内存抖动,从而引起性能问题。

最新的LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况,更多关于LeakCanary的介绍,请看这里https://github.com/square/leakcanary(中文使用说明http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/)。另外也可以使用传统的MAT工具查找内存泄露,请参考这里http://android-developers.blogspot.pt/2011/03/memory-analysis-for-android.html(便捷的中文资料http://androidperformance.com/2015/04/11/AndroidMemory-Usage-Of-MAT/

1)注意Activity的泄漏
通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的Activity泄漏:

内部类引用导致Activity的泄漏
最典型的场景是Handler导致的Activity泄漏,如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。

Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意!我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。

2)考虑使用Application Context而不是Activity Context
对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。

3)注意临时Bitmap对象的及时回收
虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回收的。例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。

需要特别留意的是Bitmap类里面提供的createBitmap()方法:

这个函数返回的bitmap有可能和source bitmap是同一个,在回收的时候,需要特别检查source bitmap与return bitmap的引用是否相同,只有在不等的情况下,才能够执行source bitmap的recycle方法。

4)注意监听器的注销
在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。

5)注意缓存容器中的对象泄漏
有时候,我们为了提高对象的复用性把某些对象放到缓存容器中,可是如果这些对象没有及时从容器中清除,也是有可能导致内存泄漏的。例如,针对2.3的系统,如果把drawable添加到缓存容器,因为drawable与View的强应用,很容易导致activity发生泄漏。而从4.0开始,就不存在这个问题。解决这个问题,需要对2.3系统上的缓存drawable做特殊封装,处理引用解绑的问题,避免泄漏的情况。

6)注意WebView的泄漏
Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,看这里WebView causes memory leak - leaks the parent Activity。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

7)注意Cursor对象是否及时关闭
在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。

内存使用策略优化
1)谨慎使用large heap
正如前面提到的,Android设备根据硬件与软件的设置差异而存在不同大小的内存空间,他们为应用程序设置了不同大小的Heap限制阈值。你可以通过调用getMemoryClass()来获取应用的可用Heap大小。在一些特殊的情景下,你可以通过在manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间。然后,你可以通过getLargeMemoryClass()来获取到这个更大的heap size阈值。然而,声明得到更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用large heap。因此请谨慎使用large heap属性。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。

2)综合考虑设备内存阈值与其他因素设计合适的缓存大小
例如,在设计ListView或者GridView的Bitmap LRU缓存的时候,需要考虑的点有:

应用程序剩下了多少可用的内存空间?
有多少图片会被一次呈现到屏幕上?有多少图片需要事先缓存好以便快速滑动时能够立即显示到屏幕?
设备的屏幕大小与密度是多少? 一个xhdpi的设备会比hdpi需要一个更大的Cache来hold住同样数量的图片。
不同的页面针对Bitmap的设计的尺寸与配置是什么,大概会花费多少内存?
页面图片被访问的频率?是否存在其中的一部分比其他的图片具有更高的访问频繁?如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache容器。
3)onLowMemory()与onTrimMemory()
Android用户可以随意在不同的应用之间进行快速切换。为了让background的应用能够迅速的切换到forground,每一个background的应用都会占用一定的内存。Android系统会根据当前的系统的内存使用情况,决定回收部分background的应用内存。如果background的应用从暂停状态直接被恢复到forground,能够获得较快的恢复体验,如果background应用是从Kill的状态进行恢复,相比之下就显得稍微有点慢。

onLowMemory():Android系统提供了一些回调来通知当前应用的内存使用情况,通常来说,当所有的background应用都被kill掉的时候,forground应用会收到onLowMemory()的回调。在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。
onTrimMemory(int):Android系统从4.0开始还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递以下的参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用。下图介绍了各种不同的回调参数:

TRIM_MEMORY_UI_HIDDEN:你的应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键退出应用,导致应用的UI界面完全不可见。这个时候应该释放一些不可见的时候非必须的资源

当程序正在前台运行的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:

TRIM_MEMORY_RUNNING_MODERATE:你的应用正在运行并且不会被列为可杀死的。但是设备此时正运行于低内存状态下,系统开始触发杀死LRU Cache中的Process的机制。
TRIM_MEMORY_RUNNING_LOW:你的应用正在运行且没有被列为可杀死的。但是设备正运行于更低内存的状态下,你应该释放不用的资源用来提升系统性能。
TRIM_MEMORY_RUNNING_CRITICAL:你的应用仍在运行,但是系统已经把LRU Cache中的大多数进程都已经杀死,因此你应该立即释放所有非必须的资源。如果系统不能回收到足够的RAM数量,系统将会清除所有的LRU缓存中的进程,并且开始杀死那些之前被认为不应该杀死的进程,例如那个包含了一个运行态Service的进程。
当应用进程退到后台正在被Cached的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:

TRIM_MEMORY_BACKGROUND: 系统正运行于低内存状态并且你的进程正处于LRU缓存名单中最不容易杀掉的位置。尽管你的应用进程并不是处于被杀掉的高危险状态,系统可能已经开始杀掉LRU缓存中的其他进程了。你应该释放那些容易恢复的资源,以便于你的进程可以保留下来,这样当用户回退到你的应用的时候才能够迅速恢复。
TRIM_MEMORY_MODERATE: 系统正运行于低内存状态并且你的进程已经已经接近LRU名单的中部位置。如果系统开始变得更加内存紧张,你的进程是有可能被杀死的。
TRIM_MEMORY_COMPLETE: 系统正运行于低内存的状态并且你的进程正处于LRU名单中最容易被杀掉的位置。你应该释放任何不影响你的应用恢复状态的资源。

因为onTrimMemory()的回调是在API 14才被加进来的,对于老的版本,你可以使用onLowMemory)回调来进行兼容。onLowMemory相当与TRIM_MEMORY_COMPLETE。
请注意:当系统开始清除LRU缓存中的进程时,虽然它首先按照LRU的顺序来执行操作,但是它同样会考虑进程的内存使用量以及其他因素。占用越少的进程越容易被留下来。
4)资源文件需要选择合适的文件夹进行存放
我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。

5)Try catch某些大内存分配的操作
在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。

6)谨慎使用static对象
因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象。

7)特别留意单例对象中不合理的持有
虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。

8)珍惜Services资源
如果你的应用需要在后台使用service,除非它被触发并执行一个任务,否则其他时候Service都应该是停止状态。另外需要注意当这个service完成任务之后因为停止service失败而引起的内存泄漏。 当你启动一个Service,系统会倾向为了保留这个Service而一直保留Service所在的进程。这使得进程的运行代价很高,因为系统没有办法把Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在运行的service。 建议使用IntentService,它会在处理完交代给它的任务之后尽快结束自己。更多信息,请阅读Running in a Background Service。

9)优化布局层次,减少内存消耗
越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。

10)谨慎使用“抽象”编程
很多时候,开发者会使用抽象类作为”好的编程实践”,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的额外内存开销:他们需要同等量的代码用于可执行,那些代码会被mapping到内存中,因此如果你的抽象没有显著的提升效率,应该尽量避免他们。

11)使用nano protobufs序列化数据
Protocol buffers是由Google为序列化结构数据而设计的,一种语言无关,平台无关,具有良好的扩展性。类似XML,却比XML更加轻量,快速,简单。如果你需要为你的数据实现序列化与协议化,建议使用nano protobufs。关于更多细节,请参考protobuf readme的”Nano version”章节。

12)谨慎使用依赖注入框架
使用类似Guice或者RoboGuice等框架注入代码,在某种程度上可以简化你的代码。下面是使用RoboGuice前后的对比图:

使用RoboGuice之后,代码是简化了不少。然而,那些注入框架会通过扫描你的代码执行许多初始化的操作,这会导致你的代码需要大量的内存空间来mapping代码,而且mapped pages会长时间的被保留在内存中。除非真的很有必要,建议谨慎使用这种技术。

13)谨慎使用多进程
使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著增加内存。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术。

一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的应用可以切分成2个进程:一个用来操作UI,另外一个给后台的Service。

14)使用ProGuard来剔除不需要的代码
ProGuard能够通过移除不需要的代码,重命名类,域与方法等等对代码进行压缩,优化与混淆。使用ProGuard可以使得你的代码更加紧凑,这样能够减少mapping代码所需要的内存空间。

15)谨慎使用第三方libraries
很多开源的library代码都不是为移动网络环境而编写的,如果运用在移动设备上,并不一定适合。即使是针对Android而设计的library,也需要特别谨慎,特别是在你不知道引入的library具体做了什么事情的时候。例如,其中一个library使用的是nano protobufs, 而另外一个使用的是micro protobufs。这样一来,在你的应用里面就有2种protobuf的实现方式。这样类似的冲突还可能发生在输出日志,加载图片,缓存等等模块里面。另外不要为了1个或者2个功能而导入整个library,如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。

16)考虑不同的实现方式来优化内存占用
在某些情况下,设计的某个方案能够快速实现需求,但是这个方案却可能在内存占用上表现的效率不够好。例如:

对于上面这样一个时钟表盘的实现,最简单的就是使用很多张包含指针的表盘图片,使用帧动画实现指针的旋转。但是如果把指针扣出来,单独进行旋转绘制,显然比载入N多张图片占用的内存要少很多。当然这样做,代码复杂度上会有所增加,这里就需要在优化内存占用与实现简易度之间进行权衡了。

写在最后:

设计风格很大程度上会影响到程序的内存与性能,相对来说,如果大量使用类似Material Design的风格,不仅安装包可以变小,还可以减少内存的占用,渲染性能与加载性能都会有一定的提升。
内存优化并不就是说程序占用的内存越少就越好,如果因为想要保持更低的内存占用,而频繁触发执行gc操作,在某种程度上反而会导致应用性能整体有所下降,这里需要综合考虑做一定的权衡。
Android的内存优化涉及的知识面还有很多:内存管理的细节,垃圾回收的工作原理,如何查找内存泄漏等等都可以展开讲很多。OOM是内存优化当中比较突出的一点,尽量减少OOM的概率对内存优化有着很大的意义。
参考资料:

Google I/O 2011: Memory management for Android Apps
Managing Your App’s Memory
Avoiding memory leaks
Android性能优化典范 - 第3季
Android性能优化典范 - 第2季
Android性能优化典范
Android性能优化之内存篇
需要讨论直接加群,扫码直接进入!
这里写图片描述

作者:qq_15950325 发表于2016/10/17 12:42:35 原文链接
阅读:49 评论:0 查看评论

Handler消息传送机制总结

$
0
0


一.线程通信相关知识



    这里的消息传送其实就是不同线程的消息通信。
在Android中子线程是不能直接改变UI界面的,这是Android的运行机制里面规定的,比如你在子线程改变主线程界面的文本(tv.setText(“XXX”)),程序会马上蹦掉。
    所以在子线程做完某些事情后,要改变主页面就要通过数据的通信,让主线程接收到信息后自己改变UI界面。
Android中Handler能满足线程间的通信。

(一)线程通信中相关的类

1.Handler先进先出原则。

2.Looper类用来管理特定线程内对象之间的消息交换(MessageExchange)。

3.Message类用来保存数据。

(二)线程通信的过程

1.Looper: 一个线程可以产生一个Looper对象,由它来管理此线程里的MessageQueue(消息队列)。

2.Handler: 你可以构造Handler对象来与Looper沟通,以便push新消息到MessageQueue里;或者接收Looper从Message Queue取出)所送来的消息。

3. Message Queue(消息队列):用来存放线程放入的消息。

4.线程:UIthread 通常就是main thread,而Android启动程序时会替它建立一个MessageQueue。

(三)理解


看完上面的概念,对于初学者来说,脑袋其实还是一片混乱的。对Handler的消息传送机制还是不懂。其实最好的理解方法还是先学会它的简单使用,在进一步研究它的机制。


这里简单描述一下它的原理:
Handler用到了监听事件和回调方法的思想。


整个原理的大概步骤如下:


1.在类中创建Handler对象,用来接收和处理消息
2.然后再创建一个Loop对象,用来管理MessageQueue
3.MessageQueue来接收和保存子线程发过来的消息
4.上面只是做好接收消息的准备,做好相关准备后,才会让子线程发送消息
5.子线程直接调用Handler对象,通过Handler对象的SendMessage方法来对主线程发送数据
6.消息是保存在MessageQueue对象中的
7.Loop控制MessageQueue传递消息给Handler对象,这里就要注意了,虽然概念上说的是Handler能对子线程的数据进行接收和处理。但实际上它是接收MessageQueue里面的数据,然后进行处理的,MessageQueue里面可以接收很多很多的数据,它们以队列的形式排列,当Handler处理完一个数据后,MessageQueue就会再传递下一个数据给Handler。
8.上面是要重点理解的机制过程,MessageQueue对象内存放很多子线程发来的信息,有序的保存下来,并不做处理。而Handler一次只接收MessageQueue对象传来的一个数据,并进行处理。
9.这是最后一步了,Handler对象对传来的信息进行判断,并作相应的行为。



二.Handler的使用

(一)在子线程改变UI界面


主线程创建Handler对象,等待子线程发来信息,然后做相应的处理。
1.布局文件的简单设计

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="start"
            android:text="开始" />

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="stop"
            android:text="停止" />
    </LinearLayout>

    <TextView
        android:id="@+id/main_tv_showmessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="message" />


</LinearLayout>


2.java代码的设计

package com.example.handlerchangeui;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends Activity {

    // 定义布局里面的控件
    static TextView tv_message;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 初始化TextView
        tv_message = (TextView) findViewById(R.id.main_tv_showmessage);

    }

    // 创建Handler对象,匿名类的方式实现handleMessage方法,这里是子线程
    Handler handler = new Handler() {

        /**
         * 接收Message信息, 只要Handler对象执行了SendMessage方法, 这个方法就会触发
         */
        @Override
        public void handleMessage(Message msg) {

            // 获取Message的what数值
            int index = msg.what;

            // 获取Message里面的复杂数据
            Bundle date = new Bundle();
            date = msg.getData();
            String name = date.getString("name");
            int age = date.getInt("age");
            String sex = date.getString("sex");
            // 这里是主线程,可以直接对页面进行修改
            String line = name + age + sex + "line" + index + "......";
            tv_message.setText(line);
        }

    };

    // 线程是否继续执行的布尔值
    boolean continueRun = true;

    // 创建子线程的对象,匿名类的方式实现run方法
    Runnable runable = new Runnable() {

        @Override
        public void run() {
            int index = 0;
            while (continueRun) {

                index++;
                // 这里的SendEmptyMessage只能发送的是数字
                // handler.sendEmptyMessage(index);

                // 在子线程中利用Handler对象的SendMessage发送复杂的消息
                // 先创建Message对象
                Message msg = Message.obtain();// =Message.obtain();
                                                // 和new Message();是一个意思
                msg.what = index;
                // Message对象保存的数据是Bundle类型的
                Bundle data = new Bundle();
                data.putString("name", "李文志");
                data.putInt("age", 18);
                data.putString("sex", "男");
                // 把数据保存到Message对象中
                msg.setData(data);
                // 使用Handler对象发送消息
                handler.sendMessage(msg);

                // 让线程休眠一秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    };

    // 开始按钮的监听事件
    public void start(View v) {
        // 启动线程
        continueRun = true;
        new Thread(runable).start();
    }

    // 停止按钮的监听事件
    public void stop(View v) {
        // 关闭线程
        continueRun = false;
    }

}


本示例只是简单的实现了Handler的使用,这里也展示了复杂数据的发送和接收。


运行程序之后,如图所示:

Handler1


点击开始按钮后,页面的TextView改变,如图所示:

Handler2

    这里其实可以设计成一个简单的定时器。
    上面的Handler对象是主线程中创建的,所以它可以直接对UI界面进行修改。
    线程中通信如果只是用来发送数值的信息,可以在子线程中使用handler.sendEmptyMessage(what);这里的what是int类型的数值。这时不需要创建Message对象了。


(二)Handler的特殊例子


    主线程给子线程发送一个数字,然后让子线程算出该数字以内的所有质数。然后以吐司的形式显示出来。
    思路:这里在子线程创建Handler对象,主线程调用子线程的Handler对象来给子线程发送信息,子线程接收信息后做相应处理。

1.布局文件的简单设计

<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:orientation="horizontal" >

    <EditText
        android:id="@+id/main_et_prime"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="2" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:onClick="getPrime"
        android:text="计算质数" />

</LinearLayout>


2.java代码的设计

package com.example.handlerforprime;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends Activity {

    // 定义布局控件
    EditText et_prime;
    // 定义一个子线程类 的对象
    PrimeThread thread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 初始化控件
        et_prime = (EditText) findViewById(R.id.main_et_prime);
        // 实例化子线程
        thread = new PrimeThread();
        // 启动子线程
        thread.start();
        // 上面是要先创建handler实例才能进行下面的handler发送信息
        // 所有要让子线程先执行

    }

    // 按钮的监听事件
    public void getPrime(View v) {
        // 获取输入框内的数字
        String prime = et_prime.getText().toString();
        // 判断非空
        if (TextUtils.isEmpty(prime)) {
            Toast.makeText(this, "你还没有数据输入数据", Toast.LENGTH_SHORT).show();
        }
        // 防止非法数据
        try {
            // 获取字符串数值
            int primeNum = Integer.parseInt(prime);
            // 使用Handler对象发送数据
            thread.handler.sendEmptyMessage(primeNum);
        } catch (Exception e) {
            Toast.makeText(this, "你输入的数据不合法", Toast.LENGTH_SHORT).show();
        }

    }

    // 定义一个线程类
    class PrimeThread extends Thread {
        // 定义Handler对象
        public Handler handler;

        // 重写run方法
        // 在线程里面创建Handler对象
        @Override
        public void run() {
            super.run();
            // 创建Loop对象,系统会自动创建MessageQueue
            Looper.prepare();
            // 实例化Handler对象
            handler = new Handler() {
                // 重写handlerMessage方法
                // 只要是通过同一个Handler对象发送的数据肯定会执行这个方法
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    // 接收what值的数据数据
                    int prime = msg.what;
                    // 创建一个集合保存质数
                    List<Integer> list = new ArrayList<Integer>();

                    // 计算0到what之间的质数
                    outer: for (int i = 2; i <= prime; i++) {
                        // 如果i对2到i的平方根的值求余都不等于零,那么这个数是质数
                        // 如果期间有一个值为零,就不是质数
                        for (int j = 2; j <= Math.sqrt(i); j++) {
                            // 这里设置i!=2是为了让2保存到集合中去
                            if (i != 2 && i % j == 0) {
                                // 跳到下一个i的值,如果直接使用continue是跳到下一个j的值;
                                continue outer;
                            }

                        }
                        // 把符合条件的i的值添加到集合中
                        list.add(i);
                    }
                    // 显示结合的元素,就是所有的质数的值
                    Toast.makeText(MainActivity.this, list.toString(),
                            Toast.LENGTH_LONG).show();
                }
            };

            // 让Loop一直进行工作,即让handMessage一直在等待消息
            Looper.loop();
        }
    }

}


程序运行结果,如图所示:

Handler3

    通过上面的俩个例子,应该能简单的理解Handler的使用方法。
对比这两个例子,我们发现示例中使用的Handler对象是同一个,一边是创建者,那么它就一直通过handlerMessage方法来在监听等待消息;另一边是调用者,使用handler.sendMessage(msg);或使用handler.sendEmptyMessage(what)来发送信息。最后创建者接收到信息并进行处理。
    还有一个值得我们注意的是,上面的例子中在子线程创建Handler后,要调用Loop的方法Looper.prepare();和Looper.loop();其中第一个方法是让Loop对象创建,第二个方法是让Loop对象一直处于工作状态。正是因为有第二个方法的执行才能让handlerMessage方法内接收到数据。
    但是在主线程主创建Handler就不需要调用Looper.prepare();和Looper.loop();因为系统已经在主线程加载了这里个方法。


其实上面只是Handler一些比较基础的用法和原理。下面在介绍一下复杂的原理知识。


三.Android消息处理的核心类


android的消息处理有三个核心类:Looper,Handler和Message。其实还有一Message Queue(MQ消息队列),但是MQ被封装到Looper里面了,我们不会直接与MQ打交道,所以它不算是个核心类。

1. 消息类:Message类


     android.os.Message的主要功能是进行消息的封装,同时可以指定消息的操作形式,Message类定义的变量和常用方法如下:

(1)public int what:变量,用于定义此Message属于何种操作

(2)public Object obj:变量,用于定义此Message传递的信息数据,通过它传递信息

(3)public int arg1:变量,传递一些整型数据时使用

(4)public int arg2:变量,传递一些整型数据时使用

(5)public Handler getTarget():普通方法,取得操作此消息的Handler对象。


    在整个消息处理机制中,message又叫task,封装了任务携带的信息和处理该任务的handler。message的用法比较简单,但是有这么几点需要注意:
1)尽管Message有public的默认构造方法,但是你应该通过Message.obtain()来从消息池中获得空消息对象,以节省资源。
2)如果你的message只需要携带简单的int信息,请优先使用Message.arg1和Message.arg2来传递信息,这比用Bundle更省内存
3)擅用message.what来标识信息,以便用不同方式处理message。

2. 消息通道:Looper


在使用Handler处理Message时,需要Looper(通道)来完成。在一个Activity中,系统会自动帮用户启动Looper对象,而在一个用户自定义的类中,则需要用户手工调用Looper类中的方法,然后才可以正常启动Looper对象。Looper的字面意思是“循环者”,它被设计用来使一个普通线程变成Looper线程。所谓Looper线程就是循环工作的线程。在程序开发中(尤其是GUI开发中),我们经常会需要一个线程不断循环,一旦有新任务则执行,执行完继续等待下一个任务,这就是Looper线程。使用Looper类创建Looper线程很简单:

public class LooperThread extends Thread {     
@Override     
    public void run() {//将当前线程初始化为Looper线程        
     Looper.prepare();   // ...其他处理,如实例化handler                 
     // 开始循环处理消息队列       
      Looper.loop();    
      }
  }


    这是在子线程中创建Handler的情况,如果在主线程中创建Handler是不需要调用Looper.prepare(); 和 Looper.loop(); 方法。

关于这两个方法在系统底层做了什么事情:
    Looper.prepare();创建了Loop对象,Loop对象是用来管理MessageQueue对象的,MessageQueue是帮助Handler保存数据的。
    Looper.loop();是在底层保证线程的一直运行状态,只要调用者调用handler.SendMessage(–);方法,创建者就可以接收到数据。

Looper有以下几个要点:
1)每个线程有且只能有一个Looper对象,它是一个ThreadLocal
2)Looper内部有一个消息队列,loop()方法调用后线程开始不断从队列中取出消息执行
3)Looper使一个线程变成Looper线程。
那么,我们如何操作Message Queue上的消息呢?这就是Handler的用处了

3. 消息操作类:Handler类


     Message对象封装了所有的消息,而这些消息的操作需要android.os.Handler类完成。什么是handler?handler起到了处理MQ上的消息的作用(只处理由自己发出的消息,所有Handler都是同一个对象来的),即通知MQ它要执行一个任务(sendMessage),并在loop到自己的时候执行该任务(handleMessage),整个过程是异步的。handler创建时会关联一个looper,默认的构造方法将关联当前线程的looper,不过这也是可以set的。
还有一个要注意的:一个线程可以有多个Handler,但是只能有一个Looper!对应的Handler对象只接收自己对象发送的信息。


创建Handler实例化对象时,可以重写的回调方法:
void handlerMessage(Message msg);

有了handler之后,我们就可以使用Handler发送消息的所有方法:
post(Runnable)
postAtTime(Runnable, long)
postDelayed(Runnable, long)
sendEmptyMessage(int)
sendMessage(Message)
sendMessageAtTime(Message, long)
sendMessageDelayed(Message, long)

    上面就是Handler核心类的详细介绍,如果想要详细理解Handler来,要去理解核心类的系统后台的相关处理。。这里不做详细解释。


    在实际应用中,子线程肯定使用来做耗时的操作的,比如:下载东西,遍历寻找文件,或计算很复杂的运算等等,在结果出来之后就要在主线程中显示出来。这里就需要在主线程中创建Handler对象,当子线程的工作任务完成后,调用Handler对象的方法来给主线程发送数据,主线程接收到数据后,进行简单处理就显示在UI界面上。

四.最后的总结:

(一)Handler类的主要作用:

1.在子线程中发送数据。

2.在主线程中接收数据,处理数据。


(二)Handler创建和使用的简化过程:

1.在主线程创建Handler对象,重写handlerMessage(msg)方法,用来随时接收数据

2.在子线程调用主线程创建的Handler对象,来给主线程发送信息。

3.主线程接收到子线程发送来的信息,进行处理,可以直接显示在UI界面上。


(三)其他的


    对于基本概念我们都是要记住的:UI线程就是我们说的主线程。
    还有就是Handler能在不同线程之间进行数据传递,并不局限于子线程和主线程,也可以是多个线程的数据传递,但是要注意的是,Handler对象只接收自己对象发送的数据。比如说,多个子线程利用主线程创建的Handler对象给主线程发送数据也是可以的,子线程发送的数据都会保存到MessageQueue里面,然后Handler对象对MessageQueue的里面的数据进行逐个的接收和处理。
    上面的两个示例使用的数据传递,尽量不要像上面一样,一般的Message.what不是直接拿来使用的数据,而是用来判断某种行为的数据值,然后创建者做相应的行为,就像Intent数据传递的resultCode请求码的作用是一样的。
    上面就是个人对Handler机制的理解,有些方面说的并不准确,MessageQueue的理解可能比较片面话和个人化,因为它都是系统的底层运转机制,在实际调用中并没有设计。

    Handler机制相对来说也是一个比较复杂的过程,本文中如有误笔也请大家及时纠正。

作者:wenzhi20102321 发表于2016/10/17 12:50:05 原文链接
阅读:14 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>