MyException - 我的异常网
当前位置:我的异常网» Android » Android开发网上的一些主要知识点 [2]

Android开发网上的一些主要知识点 [2]

www.MyException.Cn  网友分享于:2015-08-26  浏览:28次
Android开发网上的一些重要知识点 [2]
11.Android JSON解析示例代码
来自Google官方的有关Android平台的JSON解析示例,如果远程服务器使用了json而不是xml的数据提供,在Android平台上已经内置的org.json包可以很方便的实现手机客户端的解析处理。下面Android123一起分析下这个例子,帮助Android开发者需要有关 HTTP通讯、正则表达式、JSON解析、appWidget开发的一些知识。
public class WordWidget extends AppWidgetProvider { //appWidget
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
        context.startService(new Intent(context, UpdateService.class)); //避免ANR,所以Widget中开了个服务
    }
    public static class UpdateService extends Service {
        @Override
        public void onStart(Intent intent, int startId) {
            // Build the widget update for today
            RemoteViews updateViews = buildUpdate(this);
            ComponentName thisWidget = new ComponentName(this, WordWidget.class);
            AppWidgetManager manager = AppWidgetManager.getInstance(this);
            manager.updateAppWidget(thisWidget, updateViews);
        }
        public RemoteViews buildUpdate(Context context) {
            // Pick out month names from resources
            Resources res = context.getResources();
            String[] monthNames = res.getStringArray(R.array.month_names);
             Time today = new Time();
            today.setToNow();
            String pageName = res.getString(R.string.template_wotd_title,
                    monthNames[today.month], today.monthDay);
            RemoteViews updateViews = null;
            String pageContent = "";
            try {
                SimpleWikiHelper.prepareUserAgent(context);
                pageContent = SimpleWikiHelper.getPageContent(pageName, false);
            } catch (ApiException e) {
                Log.e("WordWidget", "Couldn't contact API", e);
            } catch (ParseException e) {
                Log.e("WordWidget", "Couldn't parse API response", e);
            }
            Pattern pattern = Pattern.compile(SimpleWikiHelper.WORD_OF_DAY_REGEX); //正则表达式处理,有关定义见下面的SimpleWikiHelper类
            Matcher matcher = pattern.matcher(pageContent);
            if (matcher.find()) {
                updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_word);
                String wordTitle = matcher.group(1);
                updateViews.setTextViewText(R.id.word_title, wordTitle);
                updateViews.setTextViewText(R.id.word_type, matcher.group(2));
                updateViews.setTextViewText(R.id.definition, matcher.group(3).trim());
                String definePage = res.getString(R.string.template_define_url,
                        Uri.encode(wordTitle));
                Intent defineIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(definePage)); //这里是打开相应的网页,所以Uri是http的url,action是view即打开web浏览器
                PendingIntent pendingIntent = PendingIntent.getActivity(context,
                        0 /* no requestCode */, defineIntent, 0 /* no flags */);
                updateViews.setOnClickPendingIntent(R.id.widget, pendingIntent); //单击Widget打开Activity
            } else {
                updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_message);
                CharSequence errorMessage = context.getText(R.string.widget_error);
                updateViews.setTextViewText(R.id.message, errorMessage);
            }
            return updateViews;
        }
        @Override
        public IBinder onBind(Intent intent) {
            // We don't need to bind to this service
            return null;
        }
    }
}
  有关网络通讯的实体类,以及一些常量定义如下:
  public class SimpleWikiHelper {
    private static final String TAG = "SimpleWikiHelper";
    public static final String WORD_OF_DAY_REGEX =
            "(?s)\\{\\{wotd\\|(.+?)\\|(.+?)\\|([^#\\|]+).*?\\}\\}";
    private static final String WIKTIONARY_PAGE =
            "http://en.wiktionary.org/w/api.php?action=query&prop=revisions&titles=%s&" +
            "rvprop=content&format=json%s";
    private static final String WIKTIONARY_EXPAND_TEMPLATES =
            "&rvexpandtemplates=true";
    private static final int HTTP_STATUS_OK = 200;
    private static byte[] sBuffer = new byte[512];
    private static String sUserAgent = null;
     public static class ApiException extends Exception {
        public ApiException(String detailMessage, Throwable throwable) {
            super(detailMessage, throwable);
        }
        public ApiException(String detailMessage) {
            super(detailMessage);
        }
    }
    public static class ParseException extends Exception {
        public ParseException(String detailMessage, Throwable throwable) {
            super(detailMessage, throwable);
        }
    }
    public static void prepareUserAgent(Context context) {
        try {
            // Read package name and version number from manifest
            PackageManager manager = context.getPackageManager();
            PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);
            sUserAgent = String.format(context.getString(R.string.template_user_agent),
                    info.packageName, info.versionName);
        } catch(NameNotFoundException e) {
            Log.e(TAG, "Couldn't find package information in PackageManager", e);
        }
    }
    public static String getPageContent(String title, boolean expandTemplates)
            throws ApiException, ParseException {
        String encodedTitle = Uri.encode(title);
        String expandClause = expandTemplates ? WIKTIONARY_EXPAND_TEMPLATES : "";
        String content = getUrlContent(String.format(WIKTIONARY_PAGE, encodedTitle, expandClause));
        try {
            JSONObject response = new JSONObject(content);
            JSONObject query = response.getJSONObject("query");
            JSONObject pages = query.getJSONObject("pages");
            JSONObject page = pages.getJSONObject((String) pages.keys().next());
            JSONArray revisions = page.getJSONArray("revisions");
            JSONObject revision = revisions.getJSONObject(0);
            return revision.getString("*");
        } catch (JSONException e) {
            throw new ParseException("Problem parsing API response", e);
        }
    }
    protected static synchronized String getUrlContent(String url) throws ApiException {
        if (sUserAgent == null) {
            throw new ApiException("User-Agent string must be prepared");
        }
        HttpClient client = new DefaultHttpClient();
        HttpGet request = new HttpGet(url);
        request.setHeader("User-Agent", sUserAgent); //设置客户端标识
        try {
            HttpResponse response = client.execute(request);
            StatusLine status = response.getStatusLine();
            if (status.getStatusCode() != HTTP_STATUS_OK) {
                throw new ApiException("Invalid response from server: " +
                        status.toString());
            }
            HttpEntity entity = response.getEntity();
            InputStream inputStream = entity.getContent(); //获取HTTP返回的数据流
            ByteArrayOutputStream content = new ByteArrayOutputStream();
            int readBytes = 0;
            while ((readBytes = inputStream.read(sBuffer)) != -1) {
                content.write(sBuffer, 0, readBytes); //转化为字节数组流
            }
            return new String(content.toByteArray()); //从字节数组构建String
        } catch (IOException e) {
            throw new ApiException("Problem communicating with API", e);
        }
    }
}
有关整个每日维基的widget例子比较简单,主要是帮助大家积累常用代码,了解Android平台 JSON的处理方式,毕竟很多Server还是Java的。
12.Android中使用定时器TimerTask类介绍
在Android平台中需要反复按周期执行方法可以使用Java上自带的TimerTask类,TimerTask相对于Thread来说对于资源消耗的更低,除了使用Android自带的AlarmManager使用Timer定时器是一种更好的解决方法。 我们需要引入import java.util.Timer; 和 import java.util.TimerTask;
private Timer  mTimer = new Timer(true);
private TimerTask mTimerTask;
    mTimerTask = new TimerTask()
    {
      public void run()
      {
       Log.v("android123","cwj");
      }       
     };
     mTimer.schedule(mTimerTask, 5000,1000);  //在1秒后每5秒执行一次定时器中的方法,比如本文为调用log.v打印输出。
  如果想取消可以调用下面方法,取消定时器的执行
   while(!mTimerTask.cancel());
      mTimer.cancel();
  最后Android123提示大家,如果处理的东西比较耗时还是开个线程比较好,Timer还是会阻塞主线程的执行,更像是一种消息的执行方式。当然比Handler的postDelay等方法更适合处理计划任务。
13.Android应用Icon大小在不同分辨率下定义
对于Android平台来说,不同分辨率下Icon的大小设计有着不同的要求,对于目前主流的HDPI即WVGA级别来说,通常hdpi的应用icon大小为72x72,而标准的mdpi即hvga为48x48,对于目前HTC和Motorola推出的一些QVGA的使用了ldpi,图标为32x32,常见的Android图标大小设计规范如下表所示:
Launcher
36 x 36 px
48 x 48 px
72 x 72 px
Menu
36 x 36 px
48 x 48 px
72 x 72 px
Status Bar
24 x 24 px
32 x 32 px
48 x 48 px
Tab
24 x 24 px
32 x 32 px
48 x 48 px
Dialog
24 x 24 px
32 x 32 px
48 x 48 px
List View
24 x 24 px
32 x 32 px
48 x 48 px
  对于android界面设计的安全色,如下表



而对于系统自带默认程序的图标,下面为png的透明格式,直接鼠标右键另存为即可



看看sdk文档上的关于界面图标的详细说明。
14.Android控件美化Shape你会用吗?
如果你对Android系统自带的UI控件感觉不够满意,可以尝试下自定义控件,我们就以Button为例,很早以前Android123就写到过Android Button按钮控件美化方法里面提到了xml的selector构造。当然除了使用drawable这样的图片外今天Android开发网谈下自定义图形shape的方法,对于Button控件Android上支持以下几种属性shape、gradient、stroke、corners等。
  我们就以目前系统的Button的selector为例说下:
          <shape>
            <gradient
                android:startColor="#ff8c00"
                android:endColor="#FFFFFF"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#dcdcdc" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    对于上面,这条shape的定义,分别为渐变,在gradient中startColor属性为开始的颜色,endColor为渐变结束的颜色,下面的angle是角度。接下来是stroke可以理解为边缘,corners为拐角这里radius属性为半径,最后是相对位置属性padding。
对于一个Button完整的定义可以为
  <?xml version="1.0" encoding="utf-8"?>
<selector
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" >
        <shape>
            <gradient
                android:startColor="#ff8c00"
                android:endColor="#FFFFFF"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#dcdcdc" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    </item>
    <item android:state_focused="true" >
        <shape>
            <gradient
                android:startColor="#ffc2b7"
                android:endColor="#ffc2b7"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#dcdcdc" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    </item>
    <item>      
        <shape>
            <gradient
                android:startColor="#ff9d77"
                android:endColor="#ff9d77"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#fad3cf" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    </item>
</selector>
注意Android123提示大家,以上几个item的区别主要是体现在state_pressed按下或state_focused获得焦点时,当当来判断显示什么类型,而没有state_xxx属性的item可以看作是常规状态下。
15. Android开发者应该保持以下特质
Android123推荐新手应该遵循
  1. 深读SDK文档
  2. 深读SDK的APIDemo和Samples
  3. 掌握GIT开源代码
  4. 多了解Android开源项目,学习别人的手法写程序。
16. Android数组排序常见方法
  Android的数组排序方式基本上使用了Sun原生的Java API实现,常用的有Comparator接口实现compare方法和Comparable接口的compareTo方法,我们对于一个数组列表比如ArrayList可以通过这两个接口进行排序和比较,这里Android123给大家一个例子
private final Comparator cwjComparator = new Comparator() {
        private final Collator   collator = Collator.getInstance();
        public final int compare(Object a, Object b) {
            CharSequence  a = ((Item) a).sName;
            CharSequence  b = ((Item) b).sID;
            return collator.compare(a, b);
        }
    };
我们的ArrayList对象名为mList,则执行排序可以调用方法
Collections.sort(mList, cwjComparator);
17.Android控件TextProgressBar进度条上显文字
Android系统的进度条控件默认的设计的不是很周全,比如没有包含文字的显示,那么如何在Android进度条控件上显示文字呢? 来自Google内部的代码来了解下,主要使用的addView这样的方法通过覆盖一层Chronometer秒表控件来实现,整个代码如下
   public class TextProgressBar extends RelativeLayout implements OnChronometerTickListener {
    public static final String TAG = "TextProgressBar";
    static final int CHRONOMETER_ID = android.R.id.text1;
    static final int PROGRESSBAR_ID = android.R.id.progress;
    Chronometer mChronometer = null;
    ProgressBar mProgressBar = null;
    long mDurationBase = -1;
    int mDuration = -1;
    boolean mChronometerFollow = false;
    int mChronometerGravity = Gravity.NO_GRAVITY;
    public TextProgressBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    public TextProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public TextProgressBar(Context context) {
        super(context);
    }
    //Android开发网提示关键部分在这里
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        int childId = child.getId();
        if (childId == CHRONOMETER_ID && child instanceof Chronometer) {
            mChronometer = (Chronometer) child;
            mChronometer.setOnChronometerTickListener(this);
            // Check if Chronometer should move with with ProgressBar
            mChronometerFollow = (params.width == ViewGroup.LayoutParams.WRAP_CONTENT);
            mChronometerGravity = (mChronometer.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK);
        } else if (childId == PROGRESSBAR_ID && child instanceof ProgressBar) {
            mProgressBar = (ProgressBar) child;
        }
    }
    @android.view.RemotableViewMethod
    public void setDurationBase(long durationBase) {
        mDurationBase = durationBase;
        if (mProgressBar == null || mChronometer == null) {
            throw new RuntimeException("Expecting child ProgressBar with id " +
                    "'android.R.id.progress' and Chronometer id 'android.R.id.text1'");
        }
        // Update the ProgressBar maximum relative to Chronometer base
        mDuration = (int) (durationBase - mChronometer.getBase());
        if (mDuration <= 0) {
            mDuration = 1;
        }
        mProgressBar.setMax(mDuration);
    }
    public void onChronometerTick(Chronometer chronometer) {
        if (mProgressBar == null) {
            throw new RuntimeException(
                "Expecting child ProgressBar with id 'android.R.id.progress'");
        }
        // Stop Chronometer if we're past duration
        long now = SystemClock.elapsedRealtime();
        if (now >= mDurationBase) {
            mChronometer.stop();
        }
        int remaining = (int) (mDurationBase - now);
        mProgressBar.setProgress(mDuration - remaining);
        if (mChronometerFollow) {
            RelativeLayout.LayoutParams params;
            params = (RelativeLayout.LayoutParams) mProgressBar.getLayoutParams();
            int contentWidth = mProgressBar.getWidth() - (params.leftMargin + params.rightMargin);
            int leadingEdge = ((contentWidth * mProgressBar.getProgress()) /
                    mProgressBar.getMax()) + params.leftMargin;
            int adjustLeft = 0;
            int textWidth = mChronometer.getWidth();
            if (mChronometerGravity == Gravity.RIGHT) {
                adjustLeft = -textWidth;
            } else if (mChronometerGravity == Gravity.CENTER_HORIZONTAL) {
                adjustLeft = -(textWidth / 2);
            }
            leadingEdge += adjustLeft;
            int rightLimit = contentWidth - params.rightMargin - textWidth;
            if (leadingEdge < params.leftMargin) {
                leadingEdge = params.leftMargin;
            } else if (leadingEdge > rightLimit) {
                leadingEdge = rightLimit;
            }
            params = (RelativeLayout.LayoutParams) mChronometer.getLayoutParams();
            params.leftMargin = leadingEdge;
            mChronometer.requestLayout();
        }
    }
}
18. Android内存管理-SoftReference的使用
很多时候我们需要考虑Android平台上的内存管理问题,Dalvik VM给每个进程都分配了一定量的可用堆内存,当我们处理一些耗费资源的操作时可能会产生OOM错误(OutOfMemoryError)这样的异常,Android123观察了下国内的类似Market客户端设计,基本上都没有采用很好的内存管理机制和缓存处理。
  如果细心的网友可能发现Android Market客户端载入时,每个列表项的图标是异步刷新显示的,但当我们快速的往下滚动到一定数量比如50个,再往回滚动时可能我们看到了部分App的图标又重新开始加载,当然这一过程可能是从SQLite数据库中缓存的,但是在内存中已经通过类似SoftReference的方式管理内存。
  在Java中内存管理,引用分为四大类,强引用HardReference、弱引用WeakReference、软引用SoftReference和虚引用PhantomReference。它们的区别也很明显,HardReference对象是即使虚拟机内存吃紧抛出OOM也不会导致这一引用的对象被回收,而WeakReference等更适合于一些数量不多,但体积稍微庞大的对象,在这四个引用中,它是最容易被垃圾回收的,而我们对于显示类似Android Market中每个应用的App Icon时可以考虑使用SoftReference来解决内存不至于快速回收,同时当内存短缺面临Java VM崩溃抛出OOM前时,软引用将会强制回收内存,最后的虚引用一般没有实际意义,仅仅观察GC的活动状态,对于测试比较实用同时必须和ReferenceQueue一起使用。
  对于一组数据,我们可以通过HashMap的方式来添加一组SoftReference对象来临时保留一些数据,同时对于需要反复通过网络获取的不经常改变的内容,可以通过本地的文件系统或数据库来存储缓存,希望给国内做App Store这样的客户端一些改进建议。
19. 反射在Android开发中的利弊
由于Android 2.2的推出,很多新的API加入导致很多项目移植需要考虑使用Java的反射机制Reflection来动态调用,动态调用的好处就是不需要使用引用文件,直接通过JDK中声明好的方法直接调用,本身原理基于JVM的,从Java 1.5开始支持,原理上就是根据类名而不实例化对象的情况下,获得对象的方法或属性而直接调用。
  Android开发时反射能帮助我们多少?
  1. 有些网友可能发现Android的SDK比较封闭,很多敏感的方法常规的用户无法编译,我们如果翻看了代码直接在反射中声明动态调用即可。比如很多internal或I开头的AIDL接口均可以通过反射轻松调用。
  2. 反射对于Android123来说更重要的是考虑到应用的兼容性,我们目前主要兼容从Android 1.5到2.2的项目,API Level从3到8可以方便的扩充,调用前我们预留一个标志位声明该API的最低以及最高的API Level为多少可以调用。
  3. 对于调试Java的反射是功臣了,在Logcat中我们可以看到出错的地方肯定有类似java.lang.reflect.XXX的字样,这种自检机制可以帮助我们方便的调试Android应用程序。
  反射的缺点有哪些?
  1. 因为是动态执行的,效率自然没有预编译时引用现有的库效率高,就像平时我们Win32开发时,可以不用h文件,直接通过GetProcAddress一样去动态获取方法的地址。当然效率要根据复杂程度而决定,一般稍微复杂的处理性能损失可能超过20%,对于一些复杂的涉及Java自动类型转换判断,执行时间可能是直接引用的上千倍,所以最终我们调试时必须考虑性能问题。
  2. 因为反射是动态的,所以需要处理很多异常,不然Dalvik崩溃出Force Close的概率会大很多,很简单的一个反射就需要至少3个异常捕获,本身try-catch效率就不是很高,自然进一步影响运行效率,对于Android开发我们必须考虑这些问题。
  3. 反射因为导致代码臃肿,自然稍微复杂的几个方法实用反射将会导致代码可读性和维护性降低,如果很抽象的调用Android开发网强烈不推荐这种方法。
  最后要说的是Reflection并不是Java的专利,微软的.Net也同样支持,同时更多的动态语言如Ruby等均支持这一特性。
20.AsyncTask对比Thread加Handler
很多网友可能发现Android平台很多应用使用的都是AsyncTask,而并非Thread和Handler去更新UI,这里Android123给大家说下他们到底有什么区别,我们平时应该使用哪种解决方案。从Android 1.5开始系统将AsyncTask引入到android.os包中,过去在很早1.1和1.0 SDK时其实官方将其命名为UserTask,其内部是JDK 1.5开始新增的concurrent库,做过J2EE的网友可能明白并发库效率和强大性,比Java原始的Thread更灵活和强大,但对于轻量级的使用更为占用系统资源。Thread是Java早期为实现多线程而设计的,比较简单不支持concurrent中很多特性在同步和线程池类中需要自己去实现很多的东西,对于分布式应用来说更需要自己写调度代码,而为了Android UI的刷新Google引入了Handler和Looper机制,它们均基于消息实现,有事可能消息队列阻塞或其他原因无法准确的使用。
  Android开发网推荐大家使用AsyncTask代替Thread+Handler的方式,不仅调用上更为简单,经过实测更可靠一些,Google在Browser中大量使用了异步任务作为处理耗时的I/O操作,比如下载文件、读写数据库等等,它们在本质上都离不开消息,但是AsyncTask相比Thread加Handler更为可靠,更易于维护,但AsyncTask缺点也是有的比如一旦线程开启即dobackground方法执行后无法给线程发送消息,仅能通过预先设置好的标记来控制逻辑,当然可以通过线程的挂起等待标志位的改变来通讯,对于某些应用Thread和Handler以及Looper可能更灵活。
21. Android Drawable叠加处理方法
大家可能知道Bitmap的叠加处理在Android平台中可以通过Canvas一层一层的画就行了,而Drawable中如何处理呢? 除了使用BitmapDrawable的getBitmap方法将Drawable转换为Bitmap外,今天Android123给大家说下好用简单的LayerDrawable类,LayerDrawable顾名思义就是层图形对象。下面直接用一个简单的代码表示:
    Bitmap bm = BitmapFactory.decodeResource(getResources(),R.drawable.cwj);
    Drawable[] array = new Drawable[3];
     array[0] = new PaintDrawable(Color.BLACK); //黑色
     array[1] = new PaintDrawable(Color.WHITE); //白色
     array[2] = new BitmapDrawable(bm); //位图资源
    LayerDrawable ld = new LayerDrawable(array); //参数为上面的Drawable数组
        ld.setLayerInset(1, 1, 1, 1, 1);  //第一个参数1代表数组的第二个元素,为白色
        ld.setLayerInset(2, 2, 2, 2, 2); //第一个参数2代表数组的第三个元素,为位图资源
    mImageView.setImageDrawable(ld);
  上面的方法中LayerDrawable是关键,Android开发网提示setLayerInset方法原型为public void setLayerInset (int index, int l, int t, int r, int b) 其中第一个参数为层的索引号,后面的四个参数分别为left、top、right和bottom。对于简单的图片合成我们可以将第一和第二层的PaintDrawable换成BitmapDrawable即可实现简单的图片合成。
22. onRetainNonConfigurationInstance和getLastNonConfigurationInstance
很多网友可能知道Android横竖屏切换时会触发onSaveInstanceState,而还原时会产生onRestoreInstanceState,但是Android的Activity类还有一个方法名为onRetainNonConfigurationInstance和getLastNonConfigurationInstance这两个方法。
   我们可以通过  onRetainNonConfigurationInstance 代替 onSaveInstanceState,比如距离2
  @Override
  public Object onRetainNonConfigurationInstance()
{   
       //这里需要保存的内容,在切换时不是bundle了,我们可以直接通过Object来代替
      return obj;
}
在恢复窗口时,我们可以不使用 onRestoreInstanceState,而代替的是 getLastNonConfigurationInstance 方法。我们可以直接在onCreate中使用,比如
  Object obj = getLastNonConfigurationInstance();     最终obj的内容就是上次切换时的内容。
  这里Android123提醒大家,每次Activity横竖屏切换时onCreate方法都会被触发。
23. Android中String资源文件的format方法
很多时候我们感性Google在设计Android时遵守了大量MVC架构方式,可以让写公共代码、美工和具体逻辑开发人员独立出来。有关Android的资源文件values/strings.xml中如何实现格式化字符串呢? 这里Android123举个简单的例子,以及最终可能会用到哪些地方。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">cwj_Demo</string>
    <string name="hello">android开发网</string>
</resources>
上面是一段简单的字符串资源文件,没有用到格式化,因为比较简单直接描述了意思,当我们设计一个类似 Delete xxx File ? 的时候,我们可能需要在Java中动态获取 xxx 的名称,所以定义资源时使用格式化可以轻松解决,不需要一堆String去拼接或StringBuffer一个一个append这样的愚蠢方法,看例子
    <string name="alert">Delete %1$s File</string>   这里%1$s代表这是一个字符串型的,如果是整数型可以写为%1$d,类似printf这样的格式化字符串函数,当然如果包含了多个需要格式化的内容,则第二个可以写为%2$s或%2$d了,那么最终在Java中如何调用呢? 看下面的例子:
   例一: 整数型的
  <string name="alert">I am %1$d years old</string>  定义的是这样的
   当然,我们杜绝意外情况,比如冒出个secret这样的string类型的,注意上面是%1$d不是%1$s,所以默认标准的合并成为
   int nAge=23;
   String sAgeFormat = getResources().getString(R.string.alert);
   String sFinalAge = String.format(sAgeFormat, nAge);
    这样执行完后,就组成了 I am 23 years old,是不是很方便啊.  当然了,下面看下String字符串时的情况.
  例二: 字符串型的
  String sName="cwj"
  String sCity="Shanghai"
   资源定义为   <string name="alert2">My name is %1$s , I am form %2$s</string>
   则Java中只需要
  String sInfoFormat = getResources().getString(R.string.alert2);
  String sFinalInfo=String.format(sInfoFormat, sName, sCity);
  我们看到了整个,整个定义类似MFC的CString::Format或Mac OS中的NSLog,但是需要显示类似C#中那样显示的标出参数的数字,比如%1或%n,这里数字代表参数的第n个。本行最终sFinalInfo显示的内容为
  My name is cwj , I am form Shanghai 。当然了你有什么不懂的地方可以来函至 android123@163.com
24. Android工程内嵌资源文件的两种方法
Android软件一般处理大的资源通过sdcard比如在线下载资源到sdcard,而apk中内嵌资源或二进制文件时一般使用下面的两种方法:
  方法一
  res/raw目录下存放,比如cwj.dat一个二进制文件,我们可以读取可以直接  InputStream is=context.getResources().openRawResource(R.raw.cwj);
  方法二
  工程根目录下的assets文件夹中存放,比如assets/cwj.dat 这样我们使用下面的代码
  AssetManager am = context.getAssets(); 
  InputStream is = am.open(cwj.dat); 
  这里Android123提示大家Google的Android系统处理Assert有个bug,在AssertManager中不能处理单个超过1MB的文件,不然会报异常具体数值大家可以测试下传个稍大的文件,我们在两年前的文章中有提到,而第一种raw没这个限制可以放个4MB的Mp3文件没问题。
25. Android自定义View以及layout属性全攻略
对于Android系统的自定义View可能大家都熟悉了,对于自定义View的属性添加,以及Android的Layout的命名空间问题,很多网友还不是很清楚,今天Android123一起再带大家温习一下
  CwjView myView=new CwjView(context);
  如果用于游戏或整个窗体的界面,我们可能直接在onCreate中setContentView(myView); 当然如果是控件,我们可能会需要从Layout的xml中声明,比如
  <cn.com.android123.CwjView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
  />
  当然,我们也可以直接从父类声明比如
  <View class="cn.com.android123.CwjView"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
  />
上面我们仅用了父类View的两个属性,均来自android命名空间,而名称为layout_width或layout_height,我们自定义的控件可能有更多的功能,比如
    <cn.com.android123.CwjView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
  cwj:age="22"
   cwj:university="sjtu"
   cwj:city="shanghai"
   />
我们可以看到上面的三个属性,是我们自定义的。作为标准xml规范,可能还包含了类似 xmlns:android="http://schemas.android.com/apk/res/android"  这样的语句,对于定义完整的View,我们的命名空间为cwj,这里可以写为 xmlns:cwj=http://schemas.android.com/apk/res/cn.com.android123.cwjView 或 xmlns:cwj=http://schemas.android.com/apk/res/android 都可以。
  对于定义的cwj命名空间和age、university以及city的三个属性我们如何定义呢? 在工程的res/values目录中我们新建一个cwj_attr.xml文件,编码方式为utf-8是一个好习惯,内容如下
<?xml version="1.0" encoding="utf-8" ?>
<resources>
  <declare-styleable name="CwjView">
  <attr name="age" format="integer" />
  <attr name="city" format="string" />
  <attr name="university" format="string" />
  </declare-styleable>
</resources>
  这里我们可能对format不是很熟悉,目前Android系统内置的格式类型有integer比如ProgressBar的进度值,float比如RatingBar的值可能是3.5颗星,boolean比如ToggleButton的是否勾选,string比如TextView的text属性,当然除了我们常见的基础类型外,Android的属性还有特殊的比如color是用于颜色属性的,可以识别为#FF0000等类型,当然还有dimension的尺寸类型,比如23dip,15px,18sp的长度单位,还有一种特殊的为reference,一般用于引用@+id/cwj @drawable/xxx这样的类型。
  当然什么时候用reference呢? 我们就以定义一个颜色为例子,
  <attr name="red" format="color|reference" />  这里我们用了逻辑或的运算符,定义的红色是颜色类型的,同时可以被引用
  当然,对于我们自定义的类中,我们需要使用一个名为obtainStyledAttributes的方法来获取我们的定义。在我们自定义View的构造方法(Context context, AttributeSet attrs)的重载类型中可以用
  public CwjView(Context context, AttributeSet attrs) {
  super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs,
          R.styleable.cwj_attr);
        mAge = a.getInteger(R.styleable.CwjView_age, 22);
        mCity = a.getString(R.styleable.CwjView_city, "shanghai");
        mUniversity= a.getString(R.styleable.CwjView_university, "sjtu");
        a.recycle(); //Android123提示大家不要忘了回收资源
}
这样类的全局成员变量 mAge、mCity就获取了我们需要的内容,当然根据layout中的数值我们自定义的CwjView需要动态的处理一些数据的情况,可以使用AttributeSet类的getAttributeResourceValue方法获取。
public CwjView(Context context, AttributeSet attrs)
{
  super(context, attrs);
  resId = attrs.getAttributeResourceValue("cn.com.android123.CwjView", "age", 100); 
  resId = attrs.getAttributeResourceValue("cn.com.android123.CwjView", "city", "shanghai");
  //resID就可以任意使用了
}
以上两种方法中,参数的最后一个数值为默认的,如果您有不明白的地方可以来函到 android123@163.com 我们会在第一时间回复。
26. 自定义Android主题风格theme.xml方法
在Android中可以通过自定义主题风格方式来实现个性化以及复用,首先我们创建theme.xml主题文件,保存位置为工程的res/values/theme.xml ,这里我们可以可以为主题起一个名称,比如CWJ,这里去除了xml的文件头<?xml version="1.0" encoding="utf-8"?>这行,我们在工程中只需在androidmanifest.xml文件的Activity节点中加入android:theme="@style/Theme.CWJ" 属性,则这个Activity就使用了这种主题风格,整个xml的关键代码如下:
<resources>
    <style name="Theme.CWJ" parent="android:Theme">
        <item name="android:windowBackground">@drawable/android123</item>
    </style>
</resources>
  其中上面的代码中,我们定义设置全局android:windowBackground即背景值为/res/drawable中的android123图片为背景,更多的属性定义可以参考view的layout xml属性设置,比如我们设置所有字体颜色、大体大小和样式,可以在style节点中加入
  <item name="android:textColor">#fff</item>
  <item name="android:textSize">14sp</item>
  <item name="android:textStyle">bold</item>
当然我们可以将上面的android123的图片改进下,使用一个xml文件替代,比如使用bitmap对象,则/res/drawable/android123.xml的完整代码变为
  <?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
     android:src="@drawable/cwj_image"
     android:tileMode="repeat" />
  这里我们使用了一个bitmap对象来解析cwj_image图片,当然这里可以识别各种类型的图片,其中android:tileMode是bitmap的内部属性,其中tileMode设置为repeat代表重复,这样可以节省bitmap资源,比如我们的背景是一层楼,那么全屏可以显示同样的为5层效果,而图片仅是一层大小,对于资源利用相对更高。
  当然bitmap的属性tileMode的值为repeat外还有其他的值比如clamp、mirror,这些值并没有在SDK中并没有找到定义,通过上次Android开发网的 Android自定义View以及layout属性全攻略 一文,我们可以联想到bitmap属于android.graphics.Bitmap 包,由于是android框架,所以下载git的base包,找到该类,类的实例化时android123已经在 Android自定义View以及layout属性全攻略 说的很清楚,所以我们定位到res\values中找到attr.xml有关bitmap的定义即可,有关bitmap的更多属性如  antialias、filter和dither都可以找到使用。
27. android调试工具monkey压力测试实战
很多Android开发者可能因为没有充分测试自己的软件造成很容易出现FC(Force Close)的问题,这里我们可以通过使用Android固件中自带的monkey工具来做软件的压力测试,monkey工具可以模拟各种按键,触屏,轨迹球、activity等事件,这里Android123提示大家说白了monkey就是一个小猴子随机狂玩你的android软件,看看会不会产生异常。
  具体的使用我们通过Android SDK给我们的adb调试桥链接设备或模拟器,进入Linux Shell状态,当然我们可以输入adb shell获取设备的shell,也可以直接通过adb命令执行,比如说adb shell monkey来查看monkey工具中的参数说明,如图:



  我们要测试的apk文件要在android设备中已经安装,当然模拟器中也可以测试的。执行adb shell monkey -p cn.com.android123.cwj -v 100 我们执行这句的中包含了p参数,这里代表已安装软件的packageName,而v代表查看monkey生成的详细随机事件名,最后的数字100为我们测试的随机事件数量为100.有关更多的测试方法,请查看上图中的参数,整个测试比较简单单很有效,不妨试试。
28. 自定义View
有关Android的自定义View的框架今天我们一起讨论下,对于常规的游戏,我们在View中需要处理以下几种问题: 1.控制事件 2.刷新View 3. 绘制View
  1. 对于控制事件今天我们只处理按键事件onKeyDown,以后的文章中将会讲到屏幕触控的具体处理onTouchEvent以及Sensor重力感应等方法。
  2. 刷新view的方法这里主要有invalidate(int l, int t, int r, int b) 刷新局部,四个参数分别为左、上、右、下。整个view刷新 invalidate(),刷新一个矩形区域 invalidate(Rect dirty) ,刷新一个特性Drawable, invalidateDrawable(Drawable drawable) ,执行invalidate类的方法将会设置view为无效,最终导致onDraw方法被重新调用。由于今天的view比较简单,Android123提示大家如果在线程中刷新,除了使用handler方式外,可以在Thread中直接使用postInvalidate方法来实现。
  3. 绘制View主要是onDraw()中通过形参canvas来处理,相关的绘制主要有drawRect、drawLine、drawPath等等。view方法内部还重写了很多接口,其回调方法可以帮助我们判断出view的位置和大小,比如onMeasure(int, int) Called to determine the size requirements for this view and all of its children.  、onLayout(boolean, int, int, int, int) Called when this view should assign a size and position to all of its children 和onSizeChanged(int, int, int, int) Called when the size of this view has changed. 具体的作用,大家可以用Logcat获取当view变化时每个形参的变动。
  下面cwjView是我们为今后游戏设计的一个简单自定义View框架,我们可以看到在Android平台自定义view还是很简单的,同时Java支持多继承可以帮助我们不断的完善复杂的问题。
public class cwjView extends View {
    public cwjView(Context context) {
      super(context);
      setFocusable(true); //允许获得焦点
      setFocusableInTouchMode(true); //获取焦点时允许触控
   }
   @Override
   protected Parcelable onSaveInstanceState() {  //处理窗口保存事件
      Parcelable pSaved = super.onSaveInstanceState();
      Bundle bundle = new Bundle();
     //dosomething
      return bundle;
   }
   @Override
   protected void onRestoreInstanceState(Parcelable state) {  //处理窗口还原事件
      Bundle bundle = (Bundle) state;
     //dosomething
     super.onRestoreInstanceState(bundle.getParcelable("cwj"));
      return;
   }
       @Override
   protected void onSizeChanged(int w, int h, int oldw, int oldh) //处理窗口大小变化事件
   {
      super.onSizeChanged(w, h, oldw, oldh);
   }
   @Override
   protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) 
   {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec); //如果不让父类处理记住调用setMeasuredDimension
   }
   @Override
   protected void onLayout (boolean changed, int left, int top, int right, int bottom)
   {
    super.onLayout (changed,left,top, ight,bottom) ;
   }
   @Override
   protected void onDraw(Canvas canvas) {
      Paint bg = new Paint();
      bg.setColor(Color.Red);
      canvas.drawRect(0, 0, getWidth()/2, getHeight()/2, bg); //将view的左上角四分之一填充为红色 
   }
   @Override
   public boolean onTouchEvent(MotionEvent event) {
         return super.onTouchEvent(event); //让父类处理屏幕触控事件
   }
   @Override
   public boolean onKeyDown(int keyCode, KeyEvent event) { //处理按键事件,响应的轨迹球事件为 public boolean onTrackballEvent (MotionEvent event)
      switch (keyCode) {
      case KeyEvent.KEYCODE_DPAD_UP:
         break;
      case KeyEvent.KEYCODE_DPAD_DOWN:
         break;
      case KeyEvent.KEYCODE_DPAD_LEFT:
         break;
      case KeyEvent.KEYCODE_DPAD_RIGHT:
         break;
      case KeyEvent.KEYCODE_DPAD_CENTER: //处理中键按下
         break;
      default:
         return super.onKeyDown(keyCode, event);
      }
      return true;
   }
}
  上面我们可以看到onMeasure使用的是父类的处理方法,如果我们需要解决自定义View的大小,可以尝试下面的方法
   @Override
   protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) 
   {
      height = View.MeasureSpec.getSize(heightMeasureSpec);
      width = View.MeasureSpec.getSize(widthMeasureSpec);
      setMeasuredDimension(width,height);   //这里面是原始的大小,需要重新计算可以修改本行
     //dosomething
   }
29. Canvas和Paint实例
昨天我们在Android游戏开发之旅三 View详解中提到了onDraw方法,有关详细的实现我们今天主要说下Android的Canvas和Paint对象的使用实例。
  Canvas类主要实现了屏幕的绘制过程,其中包含了很多实用的方法,比如绘制一条路径、区域、贴图、画点、画线、渲染文本,下面是Canvas类常用的方法,当然Android开发网提示大家很多方法有不同的重载版本,参数更灵活。
  void drawRect(RectF rect, Paint paint) //绘制区域,参数一为RectF一个区域
  void drawPath(Path path, Paint paint) //绘制一个路径,参数一为Path路径对象
  void  drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)   //贴图,参数一就是我们常规的Bitmap对象,参数二是源区域(Android123提示这里是bitmap),参数三是目标区域(应该在canvas的位置和大小),参数四是Paint画刷对象,因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。
  void  drawLine(float startX, float startY, float stopX, float stopY, Paint paint)  //画线,参数一起始点的x轴位置,参数二起始点的y轴位置,参数三终点的x轴水平位置,参数四y轴垂直位置,最后一个参数为Paint画刷对象。
  void  drawPoint(float x, float y, Paint paint) //画点,参数一水平x轴,参数二垂直y轴,第三个参数为Paint对象。
  void drawText(String text, float x, float y, Paint paint)  //渲染文本,Canvas类除了上面的还可以描绘文字,参数一是String类型的文本,参数二x轴,参数三y轴,参数四是Paint对象。
  void  drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) //在路径上绘制文本,相对于上面第二个参数是Path路径对象
  从上面来看我们可以看出Canvas绘制类比较简单同时很灵活,实现一般的方法通常没有问题,同时可以叠加的处理设计出一些效果,不过细心的网友可能发现最后一个参数均为Paint对象。如果我们把Canvas当做绘画师来看,那么Paint就是我们绘画的工具,比如画笔、画刷、颜料等等。
  Paint类常用方法:
void  setARGB(int a, int r, int g, int b)  设置Paint对象颜色,参数一为alpha透明通道
void  setAlpha(int a)  设置alpha不透明度,范围为0~255
void  setAntiAlias(boolean aa)  //是否抗锯齿
void  setColor(int color)  //设置颜色,这里Android内部定义的有Color类包含了一些常见颜色定义
.
void  setFakeBoldText(boolean fakeBoldText)  //设置伪粗体文本
void  setLinearText(boolean linearText)  //设置线性文本
PathEffect  setPathEffect(PathEffect effect)  //设置路径效果
Rasterizer  setRasterizer(Rasterizer rasterizer) //设置光栅化
Shader  setShader(Shader shader)  //设置阴影
void  setTextAlign(Paint.Align align)  //设置文本对齐
void  setTextScaleX(float scaleX)  //设置文本缩放倍数,1.0f为原始
void  setTextSize(float textSize)  //设置字体大小
Typeface  setTypeface(Typeface typeface)  //设置字体,Typeface包含了字体的类型,粗细,还有倾斜、颜色等。
void  setUnderlineText(boolean underlineText)  //设置下划线
最终Canvas和Paint在onDraw中直接使用
@Override
   protected void onDraw(Canvas canvas) {
    Paint paintRed=new Paint();
    paintRed.setColor(Color.Red);
    canvas.drawPoint(11,3,paintRed); //在坐标11,3上画一个红点
  }
  下一次Android123将会具体讲到强大的Path路径,和字体Typeface相关的使用。
30. View类详解
在Android游戏开发之旅二中我们讲到了View和SurfaceView的区别,今天Android123从View类开始着重的介绍Android图形显示基类的相关方法和注意点。
  自定义View的常用方法:
onFinishInflate() 当View中所有的子控件均被映射成xml后触发
onMeasure(int, int) 确定所有子元素的大小
onLayout(boolean, int, int, int, int) 当View分配所有的子元素的大小和位置时触发
onSizeChanged(int, int, int, int) 当view的大小发生变化时触发
onDraw(Canvas) view渲染内容的细节
onKeyDown(int, KeyEvent) 有按键按下后触发
onKeyUp(int, KeyEvent) 有按键按下后弹起时触发
onTrackballEvent(MotionEvent) 轨迹球事件
onTouchEvent(MotionEvent) 触屏事件
onFocusChanged(boolean, int, Rect) 当View获取或失去焦点时触发
onWindowFocusChanged(boolean) 当窗口包含的view获取或失去焦点时触发
onAttachedToWindow() 当view被附着到一个窗口时触发
onDetachedFromWindow() 当view离开附着的窗口时触发,Android123提示该方法和  onAttachedToWindow() 是相反的。
onWindowVisibilityChanged(int) 当窗口中包含的可见的view发生变化时触发
  以上是View实现的一些基本接口的回调方法,一般我们需要处理画布的显示时,重写onDraw(Canvas)用的的是最多的:
  @Override
   protected void onDraw(Canvas canvas) {
    //这里我们直接使用canvas对象处理当前的画布,比如说使用Paint来选择要填充的颜色
   Paint paintBackground = new Paint();
   paintBackground.setColor(getResources().getColor(R.color.xxx));  //从Res中找到名为xxx的color颜色定义
   canvas.drawRect(0, 0, getWidth(), getHeight(), paintBackground); //设置当前画布的背景颜色为paintBackground中定义的颜色,以0,0作为为起点,以当前画布的宽度和高度为重点即整块画布来填充。 
   具体的请查看Android123未来讲到的Canvas和Paint,在Canvas中我们可以实现画路径,图形,区域,线。而Paint作为绘画方式的对象可以设置颜色,大小,甚至字体的类型等等。
}
当然还有就是处理窗口还原状态问题(一般用于横竖屏切换),除了在Activity中可以调用外,开发游戏时我们尽量在View中使用类似
@Override
   protected Parcelable onSaveInstanceState() {
      Parcelable p = super.onSaveInstanceState();
      Bundle bundle = new Bundle();
      bundle.putInt("x", pX);
      bundle.putInt("y", pY);
      bundle.putParcelable("android123_state", p);
      return bundle;
   }
   @Override
   protected void onRestoreInstanceState(Parcelable state) {
      Bundle bundle = (Bundle) state;
      dosomething(bundle.getInt("x"), bundle.getInt("y")); //获取刚才存储的x和y信息
      super.onRestoreInstanceState(bundle.getParcelable("android123_state"));
      return;
   }
  在View中如果需要强制调用绘制方法onDraw,可以使用invalidate()方法,它有很多重载版本,同时在线程中的postInvailidate()方法将在Android游戏开发之旅六中的 自定义View完整篇讲到。

文章评论

中美印日四国程序员比较
中美印日四国程序员比较
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
鲜为人知的编程真相
鲜为人知的编程真相
Java程序员必看电影
Java程序员必看电影
程序员必看的十大电影
程序员必看的十大电影
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
程序员和编码员之间的区别
程序员和编码员之间的区别
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
编程语言是女人
编程语言是女人
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
 程序员的样子
程序员的样子
为什么程序员都是夜猫子
为什么程序员都是夜猫子
程序员的鄙视链
程序员的鄙视链
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
每天工作4小时的程序员
每天工作4小时的程序员
程序员应该关注的一些事儿
程序员应该关注的一些事儿
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
10个调试和排错的小建议
10个调试和排错的小建议
我是如何打败拖延症的
我是如何打败拖延症的
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
程序员都该阅读的书
程序员都该阅读的书
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
一个程序员的时间管理
一个程序员的时间管理
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
代码女神横空出世
代码女神横空出世
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
漫画:程序员的工作
漫画:程序员的工作
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
那些争议最大的编程观点
那些争议最大的编程观点
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
老程序员的下场
老程序员的下场
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
总结2014中国互联网十大段子
总结2014中国互联网十大段子
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有