【Android进阶】ListView使用“内存双缓存+硬盘缓存”加载网络图片

ListView 加载网络图片是我们经常用到的方式,如果每次滚动ListView就去网络下载图片会非常影响性能(因为网络下载是比较慢的)而且非常耗费流量,所以这里介绍一种使用“内存双缓存+硬盘缓存”的方式来加载图片。

实现的效果如下:

这里使用了滚动时不去网络下载图片,停止时才加载,所以滚动时显示默认的,注意观察


设计思想

内存读取速度 > 文件读取速度 > 从网络获取的速度

基本代码逻辑如下

// 从内存缓存中获取图片  
        Bitmap result = memoryCache.getBitmapFromCache(url);  
        if (result == null) {  
            // 从文件缓存中获取  
            result = fileCache.getImage(url);  
            if (result == null) {  
                // 从网络获取  
                result = ImageGetFromHttp.downloadBitmap(url);  
                if (result != null) {  
                    fileCache.saveBitmap(result, url);  
                    memoryCache.addBitmapToCache(url, result);  
                }  
            } else {  
                // 添加到内存缓存  
                memoryCache.addBitmapToCache(url, result);  
            }  
      }

内存缓存中使用了LruCache,LRU算法请参考:http://blog.csdn.net/luoweifu/article/details/8297084/

我们在内存缓存中再将内存分为两层,强引用缓存和软引用缓存。

对强引用和软引用做简单的介绍(具体内容请看:http://blog.csdn.net/u010583599/article/details/51970515

① 强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

② 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

程序介绍

主界面是一个ListView,该ListViewItem布局为

Item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >
    <ImageView
        android:id="@+id/iv_icon" 
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:src="@drawable/ic_launcher"/>
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:paddingLeft="5dp"
        android:orientation="vertical">
        <TextView 
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="15sp"
            android:text="新闻的标题"/>
         <TextView 
            android:id="@+id/tv_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxLines="3"
            android:text="新闻的内容"/>
        
    </LinearLayout>

</LinearLayout>

Activity完成的功能是,请求并解析慕课网的接口的JSon字符串,创建ListView的适配器。

MainActivity.java

/**
 * 主类,访问网络接口获取JSon字符串,并解析字符串,生成对象集合,创建listView适配器
 *
 */
public class MainActivity extends Activity {
	private ListView listView;
	//网络接口来自慕课网,获取一个Json字符串并解析
	private static final String URL = "http://www.imooc.com/api/teacher?type=4&num=30";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (ListView)findViewById(R.id.listview);
        new NewsAsyncTask().execute(URL);
    }
    private List<NewsBean> getJsonData(String url){
    	List<NewsBean> newsBeansList = new ArrayList<NewsBean>();
    	try {
    		//获得json数据并解析Json字符串
			String jsonString = readStream(new URL(url).openStream());
			JSONObject jsonObject;
			NewsBean newsBean;
			jsonObject = new JSONObject(jsonString);
			JSONArray jsonArray = jsonObject.getJSONArray("data");
			for(int i = 0;i< jsonArray.length();i++){
				jsonObject = jsonArray.getJSONObject(i);
				newsBean = new NewsBean();
				newsBean.newsIconUrl = jsonObject.getString("picSmall");
				newsBean.newsTitle = jsonObject.getString("name");
				newsBean.newsContent = jsonObject.getString("description");
				newsBeansList.add(newsBean);
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (JSONException e) {
			e.printStackTrace();
		}
    	return newsBeansList;
    }
    //获取网络输入流数据,返回一个Json字符串
    private String readStream(InputStream is){
    	InputStreamReader isr;
    	String result = "";
    	try{
    		String line = "";
	    	isr = new InputStreamReader(is,"utf-8");
	    	BufferedReader br = new BufferedReader(isr);
	    	while((line = br.readLine()) != null){
	    		result += line;
	    	}
    	}catch(IOException e){
    		e.printStackTrace();
    	}
    	return result;
    }
    /**
     * 使用AsyncTask来访问网络获取JSon数据
     *
     */
    class NewsAsyncTask extends AsyncTask<String , Void, List<NewsBean>>{

		@Override
		protected List<NewsBean> doInBackground(String... params) {
			return getJsonData(params[0]);
		}
    	@Override
    	protected void onPostExecute(List<NewsBean> result) {
    		super.onPostExecute(result);
    		//创建并给listView设置适配器
    		NewsAdapter adapter = new NewsAdapter(getApplicationContext(), result,listView);
    		listView.setAdapter(adapter);
    	}
    }
}

为了解析Json字符串,我们需要创建一个实体类

NewsBean.java

public class NewsBean {
	public String newsIconUrl;
	public String newsTitle;
	public String newsContent;
	public NewsBean(String newsIconUrl,String newsTitle,String newsContent){
		this.newsIconUrl = newsIconUrl;
		this.newsTitle = newsTitle;
		this.newsContent = newsContent;
	}
	public NewsBean(){
		
	}
}

ListView 适配器类中我们完成了图片的下载,并且进行了控制,ListView滑动时不进行任何的下载,停止状态才进行网络下载,并且进行了优化,防止图片加载时错位、重复、闪烁

NewsAdapter.java
public class NewsAdapter extends BaseAdapter implements OnScrollListener{
	private List<NewsBean> mList;
	private LayoutInflater mInflater;
	//图片加载类
	private ImageLoader imageLoader;
	//listView开始下载和结束下载的位置
	private int mStart,mEnd;
	//所有的URL的数组
	public static String[] URLS;
	private boolean mFirstIn;//第一次启动
	public NewsAdapter(Context context,List<NewsBean> mList,ListView listView){
		mInflater = LayoutInflater.from(context);
		this.mList = mList;
		imageLoader = new ImageLoader(listView,context);
		//获取所有的URL并初始化数组
		URLS = new String[mList.size()];
		for(int i = 0;i<mList.size();i++){
			URLS[i] = mList.get(i).newsIconUrl;
		}
		listView.setOnScrollListener(this);
		mFirstIn = true;
	}
	@Override
	public int getCount() {
		return mList.size();
	}

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

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

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder viewHolder = null;
		if(convertView == null){
			viewHolder = new ViewHolder();
			convertView = mInflater.inflate(R.layout.item_layout, null);
			viewHolder.ivIcon = (ImageView)convertView.findViewById(R.id.iv_icon);
			viewHolder.tvTitle = (TextView)convertView.findViewById(R.id.tv_title);
			viewHolder.tvContent = (TextView)convertView.findViewById(R.id.tv_content);
			convertView.setTag(viewHolder);
		}else{
			viewHolder = (ViewHolder) convertView.getTag();
		}
		//设置默认的图片
//		viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);
		
		String url = mList.get(position).newsIconUrl;
		/*因为Itme是重复利用的,ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,
		 * 且滑动过程中该图片加载结束,第2行已不在屏幕内,根据缓存原理,第2行的view可能被第14行复用,这样我
		 * 们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。如果14行的图片也加载结束则会造成闪烁,先显示前一张,再显示后一张
		 * 为了防止图片加载时错位,这里加上tag,把imageView和url标识绑定,在异步显示的位置,判断当前任务的url和item设置的url是否
		 * 相同,只有相同才去加载图片
		 */
		viewHolder.ivIcon.setTag(url);
		//在滚动的时候加载图片,如果缓存中都没有,则使用默认的图片
		imageLoader.showImagesFromCache(viewHolder.ivIcon, url);
		viewHolder.tvTitle.setText(mList.get(position).newsTitle);
		viewHolder.tvContent.setText(mList.get(position).newsContent);
		return convertView;
	}
	class ViewHolder{
		public TextView tvTitle,tvContent;
		public ImageView ivIcon;
	}
	
	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		if(scrollState == SCROLL_STATE_IDLE){
			//当前状态处于停止状态,加载可见项
			imageLoader.loadImages(mStart, mEnd);
		}else{
			//停止任务
			imageLoader.cancelAllTasks();
		}
	}
	/**
	 * 由于我们使用的是滚动状态改变时才去下载图片,但是第一次进入的时候要加载第一屏的图片
	 * listview初始化后会调用onScroll方法,我们在这里去加载第一屏的图片并把第一次进入
	 * 状态位置为false
	 */
	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		//start 为第一个可见的item的位置
		mStart = firstVisibleItem;
		//end 为第一个可见的位置加上可见的item的数量
		mEnd = firstVisibleItem + visibleItemCount;
		if(mFirstIn && visibleItemCount > 0){
			//第一次显示的时候调用,加载图片
			imageLoader.loadImages(mStart, mEnd);
			mFirstIn = false;
		}
	}
}
内存缓存类 ImageMemoryCache.java

/**
 * @author meng.li
 * 内存缓存图片类,这里使用了两层内存缓存
 */
public class ImageMemoryCache {
	/** 
     * 从内存读取数据速度是最快的,为了更大限度使用内存,这里使用了两层缓存。 
     * 强引用缓存不会轻易被回收,用来保存常用数据
     * 不常用的数据转入软引用缓存,不会影响GC的回收。 
     */  
    private static final int SOFT_CACHE_SIZE = 15;  //软引用缓存容量  
    private static LruCache<String, Bitmap> mLruCache;  //硬引用缓存  
    private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache;  //软引用缓存  
                                                                                            
    public ImageMemoryCache(Context context) {  
    	//获取最大可用内存
    	int maxMemory = (int) Runtime.getRuntime().maxMemory();
    	Log.i("mengli","maxMemory = "+ maxMemory);
    	//强引用缓存容量,为系统可用内存的1/4 
    	int cacheSize = maxMemory/4;
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {  
            @Override  
            protected int sizeOf(String key, Bitmap value) {  
                if (value != null)  
                	//每次加入缓存时会调用
    				return value.getByteCount();
                else  
                    return 0;  
            }
            @Override  
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {  
                if (oldValue != null)
                	//LRU算法会把最近使用的元素压入栈顶,所以栈底就是被移除的元素
                    // 强引用缓存容量满的时候,会根据LRU算法把最近最久没有被使用的图片转入此软引用缓存  
                    mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));  
            }  
        };  
        mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(SOFT_CACHE_SIZE, 0.75f, true) {  
//            private static final long serialVersionUID = 6040103833179403725L;  
            @Override  
            protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {  
                if (size() > SOFT_CACHE_SIZE){      
                    return true;    
                }    
                return false;   
            }  
        };  
    }  
                                                                                    
    /** 
     * 从缓存中获取图片 
     */  
    public Bitmap getBitmapFromCache(String url) {  
        Bitmap bitmap;  
        //先从强引用缓存中获取  
        synchronized (mLruCache) {  
            bitmap = mLruCache.get(url);  
            if (bitmap != null) {  
                //如果找到的话,把元素移到LinkedHashMap的最前面,从而保证在LRU算法中最后被删除  
                mLruCache.remove(url);  
                mLruCache.put(url, bitmap);  
                return bitmap;  
            }  
        }  
        //如果强引用缓存中找不到,到软引用缓存中找 
        synchronized (mSoftCache) {   
            SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);  
            if (bitmapReference != null) {  
                bitmap = bitmapReference.get();  
                if (bitmap != null) {  
                    //将图片移回硬缓存  
                    mLruCache.put(url, bitmap);  
                    mSoftCache.remove(url);  
                    return bitmap;  
                } else {
                	//没找到,可能改bigmap已经被回收了,删除url
                    mSoftCache.remove(url);  
                }  
            }  
        }  
        return null;  
    }   
                                                                                    
    /** 
     * 添加图片到缓存 
     */  
    public void addBitmapToCache(String url, Bitmap bitmap) {  
        if (bitmap != null) {  
            synchronized (mLruCache) {  
                mLruCache.put(url, bitmap);  
            }  
        }  
    }  
                                                                                    
    public void clearCache() {  
        mSoftCache.clear();  
    }  
}
文件缓存类 ImageFileCache.java

/**
 * @author meng.li
 *  文件缓存类
 */
public class ImageFileCache {
	//缓存目录名
    private static final String CACHDIR = "ImgeCache";  
    private static final String WHOLESALE_CONV = ".cach";  
                                                              
    private static final int MB = 1024*1024;
    //缓存大小
    private static final int CACHE_SIZE = 10; 
    //剩余最小空间大小
    private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 10;  
                                                                  
    public ImageFileCache() {  
        //清理文件缓存  
        removeCache(getDirectory());  
    }  
                                                                  
    /** 从缓存中获取图片 **/  
    public Bitmap getImage(final String url) {
        final String path = getDirectory() + "/" + getFileNameFromUrl(url);  
        File file = new File(path);  
        if (file.exists()) {  
            Bitmap bmp = BitmapFactory.decodeFile(path);  
            if (bmp == null) {  
                file.delete();  
            } else {
            	//获取时图片时需要更新文件的最后修改时间
                updateFileTime(path);  
                return bmp;  
            }  
        }  
        return null;  
    }
                                                                  
    /** 将图片存入文件缓存 **/  
    public void saveBitmap(Bitmap bm, String url) {  
        if (bm == null) {  
            return;  
        }  
        //判断sdcard上的空间 ,如果不足10M返回
        if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {  
            //SD空间不足  
            return;  
        }  
        String filename = getFileNameFromUrl(url);  
        String dir = getDirectory();  
        File dirFile = new File(dir);  
        if (!dirFile.exists())  
            dirFile.mkdirs();
        //创建文件
        File file = new File(dir +"/" + filename);  
        try {  
            file.createNewFile();  
            OutputStream outStream = new FileOutputStream(file);
            //将图片进行压缩并写入文件,100表示不压缩
            bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream);  
            outStream.flush();  
            outStream.close();  
        } catch (FileNotFoundException e) {  
            Log.w("ImageFileCache", "FileNotFoundException");  
        } catch (IOException e) {  
            Log.w("ImageFileCache", "IOException");  
        }  
    }   
                                                                  
    /** 
     * 计算存储目录下的文件大小, 
     * 当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定 
     * 那么删除40%最近没有被使用的文件 
     */  
    private boolean removeCache(String dirPath) {  
        File dir = new File(dirPath);  
        File[] files = dir.listFiles();  
        if (files == null) {  
            return true;  
        }
        //没有挂载外部存储设备
        if (!android.os.Environment.getExternalStorageState().equals(  
                android.os.Environment.MEDIA_MOUNTED)) {  
            return false;  
        }
                                                              
        int dirSize = 0;  
        for (int i = 0; i < files.length; i++) {
        	//遍历目录下的所有文件,如果包含.cach 则累加size
            if (files[i].getName().contains(WHOLESALE_CONV)) {  
                dirSize += files[i].length();  
            }  
        }
        //如果缓存目录的文件大小 大于规定的缓存大小或者剩余内存不足10M,则删除40%最久未使用的文件
        if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {  
            int removeFactor = (int) ((0.4 * files.length) + 1);
            //对文件按时间排序
            Arrays.sort(files, new FileLastModifSort());
            for (int i = 0; i < removeFactor; i++) {  
                if (files[i].getName().contains(WHOLESALE_CONV)) {  
                    files[i].delete();  
                }  
            }  
        }  
                                                              
        if (freeSpaceOnSd() <= CACHE_SIZE) {  
            return false;  
        }  
                                                                      
        return true;  
    }  
                                                                  
    /** 修改文件的最后修改时间 **/  
    public void updateFileTime(String path) {  
        File file = new File(path);  
        long newModifiedTime = System.currentTimeMillis();  
        file.setLastModified(newModifiedTime);  
    }  
                                                                  
    /** 计算sdcard上的剩余空间 **/  
    private int freeSpaceOnSd() {  
        StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());  
        double sdFreeMB = ((double)stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;  
        return (int) sdFreeMB;  
    }   
                                                                  
    /**从url中获取文件名 **/  
    private String getFileNameFromUrl(String url) {  
        return url.substring(url.lastIndexOf("/")+1)+WHOLESALE_CONV;
    }  
                                                                  
    /** 获得缓存目录 **/  
    private String getDirectory() {  
        String dir = getSDPath() + "/" + CACHDIR;  
        return dir;  
    }  
                                                                  
    /** 取SD卡路径 **/  
    private String getSDPath() {  
        File sdDir = null;  
        boolean sdCardExist = Environment.getExternalStorageState().equals(  
                android.os.Environment.MEDIA_MOUNTED);  //判断sd卡是否存在  
        if (sdCardExist) {  
            sdDir = Environment.getExternalStorageDirectory();  //获取根目录  
        }  
        if (sdDir != null) {  
            return sdDir.toString();  
        } else {  
            return "";  
        }  
    }   
    /** 
     * 根据文件的最后修改时间进行排序,Java中对对象进行排序要实现Comparator 接口,自己实现比较规则
     * 1 表示大于,0表示相等,-1表示小于 
     */  
    private class FileLastModifSort implements Comparator<File> {  
        public int compare(File arg0, File arg1) {  
            if (arg0.lastModified() > arg1.lastModified()) {  
                return 1;  
            } else if (arg0.lastModified() == arg1.lastModified()) {  
                return 0;  
            } else {  
                return -1;  
            }  
        }  
    }  
}  

通过图片我们可以看到,图片被写入了文件


图片下载类,使用多线程下载图片用了AsyncTask封装类

public class ImageLoader {
	/**
	 * 使用多线程的方式去加载图片
	 */
	private ImageView imageView;
	private String mUrl;
	//内存缓存
	private ImageMemoryCache memoryCache;
	//文件缓存
	private ImageFileCache fileCache;
	private ListView mListView;
	//任务集合,用来处理多个下载线程
	private Set<NewsAsyncTask> mTasks;
	public ImageLoader(ListView listView,Context context){
		mListView = listView;
		mTasks = new HashSet<ImageLoader.NewsAsyncTask>();
		memoryCache = new ImageMemoryCache(context);
		fileCache = new ImageFileCache();
	}
	/**
	 * 用于从一个url获取bitmap
	 */
	public Bitmap getBitmapFromURL(String urlString){
		Bitmap bitmap;
		InputStream is = null;
		try {
			URL url = new URL(urlString);
			HttpURLConnection connection = (HttpURLConnection)url.openConnection();
			is = new BufferedInputStream(connection.getInputStream());
			bitmap = BitmapFactory.decodeStream(is);
			connection.disconnect();
			return bitmap;
		} catch (Exception e) {
			
		}finally{
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
	//在滚动的时候显示缓存的图片,如果没有缓存图片则显示默认的图片
	public void showImagesFromCache(ImageView imageView,String url){
		//从缓存取出图片
		Bitmap result = memoryCache.getBitmapFromCache(url);  
        if (result == null) {
            // 文件缓存中获取  
            result = fileCache.getImage(url);
        }
		if(result == null){
			imageView.setImageResource(R.drawable.ic_launcher);
		}else{
			imageView.setImageBitmap(result);
		}
	}
	//取消加载图片
	public void cancelAllTasks(){
		if(mTasks != null){
			for(NewsAsyncTask task: mTasks){
				task.cancel(false);
			}
		}
	}
	public void loadImages(int start,int end){
		//加载从start到end的图片
		for(int i = start;i<end;i++){
			String url = NewsAdapter.URLS[i];
			//从内存缓存取出图片
			Bitmap bitmap = memoryCache.getBitmapFromCache(url);
			//如果缓存没有,则从文件中读取
			if(bitmap == null){
				//从文件获取图片
				bitmap = fileCache.getImage(url);
				//文件中也为空,则必须从网络下载图片
				if(bitmap == null){
					//使用AsyncTask下载图片,这里会耗费流量
					NewsAsyncTask task = new NewsAsyncTask(url);
					task.execute(url);
					//添加一个任务
					mTasks.add(task);
				}else{
					//文件中获取到了图片,则把图片加入到内存中
					memoryCache.addBitmapToCache(url, bitmap);
				}
			}
			if(bitmap != null){
				//根据url去获取 对应的imageView对象,防止显示混乱
				ImageView imageView = (ImageView) mListView.findViewWithTag(url);
				imageView.setImageBitmap(bitmap);
			}
		}
	}
	private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap>{
		private String mUrl;
		public NewsAsyncTask(String url){
			mUrl = url;
		}
		@Override
		protected Bitmap doInBackground(String... params) {
			//从网络获取图片
			Bitmap bitmap = getBitmapFromURL(params[0]);
			if(bitmap != null){
				//把bitmap加入到缓存
				memoryCache.addBitmapToCache(params[0], bitmap);
				//把bitmap 加入到文件
				fileCache.saveBitmap(bitmap,params[0]);
			}
			return bitmap;
		}
		@Override
		protected void onPostExecute(Bitmap result) {
			super.onPostExecute(result);
			ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
			if(imageView != null && result != null){
				imageView.setImageBitmap(result);
			}
			//下载任务完成则移除这个任务
			mTasks.remove(this);
		}
	}
}

AndroidManifest.xml  注意加上权限

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.newsdemo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="22"
        android:targetSdkVersion="22" />
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.Black.NoTitleBar" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

菜鸟一只,刚踏上追求技术的不归路,记录所学,希望大家指点!

代码下载 :http://download.csdn.net/detail/u010583599/9583583

本文参考:

http://blog.csdn.net/a79412906/article/details/10180583

http://www.imooc.com/learn/406









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章