《第一行代码》
#Android 基础知识点总结
----------
##1.adb - android debug bridge
-adb start-server -----开启adb服务
-adb kill-server -----停止adb服务
-adb push 本地路径 手机路径 -----将文件导入手机
-adb pull 手机路径 本地路径 -----导出
-adb logcat -----查看Log
-adb install [-r] [-s] 全路径/xxx.apk
-r 重新安装该程序,保存数据
-s 安装在SD卡内,而不是设备内部存储
-adb uninstall [-k] 全路径/xxx.apk
-k 不删除程序运行所产生的数据和缓存目录
-adb remount -----重新挂载文件系统
-adb reboot -----重启手机
-adb reboot recovery -----重启到Recovery界面
-adb reboot bootloader -----重启到bootloader界面
-adb devices -----列出所有设备名称或IP
-adb -s IP install 全路径-apk
-adb shell -----挂载到Linux空间
-netstat -ano ---查看进程 (协议??本地地址 外部地址??状态???PID)
进入Linux空间后就好玩:
ls ---查看当前路径下的文件
ls -l ---查看文件的具体信息
ps ---查看当前运行进程
netstat -ano ---查看占用端口号的进程
monkey 1000;---猴子测试 测试整个系统;1000代表数量
monkey -p 包名 数量--- 测试某个应用程序运行数量后,会不会爆
cd .. ---返回上级目录
cat xxx.xml ---查看xml文件
sqlite3 xxx.db ---进入后
.help 查看帮助
.tables 查看所有表名
.quit 退出
**可以使用sql语句操作表
##2.UI(脸蛋)
1. 四种基本布局:
#LinearLayout#---线性布局
orientation:vertical竖直, horizontal 水平
gravity:指定子布局位置
layout_gravity:指定当前控件与父布局的相对位置
layout_weight:出去已经分配的具体屏幕大小,将剩余的屏幕大小按权重分配(自己的理解)
#RelativeLayout#---相对布局
初始位置:在屏幕左上角
容易忘记:layout_above="@id/xx"????????????在 xx控件上
layout_below="@id/xx"????????????在 xx控件下
layout_toRightOf="@id/xx"????????在 xx控件右边
layout_alignParentRight="true"????在父窗体右边
layout_centerInParent="true"???在父窗体正中间
#FrameLayout#---帧布局、
特点:一层层覆盖
不好玩,用来被替换的布局,当使用碎片fragment来布局UI的时候,framelayout里面存放一个fragment
#TableLayout#---表格布局
基本不用
子控件 中无法指定宽度,使用 stretchColumns="1";
合并单元格: layout_span="2";
2. 常见控件:
#TextView#
textSize
textColor????#ARGB??a:透明度 r:red g:green b:blue
singleLine???true??单行显示
maxLines?????多行显示
#Button#
四种点击事件:
第一种:通过给控件添加onclick属性,然后进到 activity中 去添加方法
添加方法时,方法的签名也是固定的.(google不推荐使用)
andorid:onclick="xxx"
public void xxx(View v){
}`
第二种:给控件添加id , 然后 在activity中拿到 控件,然后 给控件添加onclick时间的监听器
使用匿名内部类的写法:
`android:id="@+id/xxx"
xxx=(Button) findViewById(R.id.xxx);
xxx.setOnClickListener(new OnClickListener(){
public void onClick(View v){
}
});
第三种:实际上与第二种一样, 只是换成了 内部类的写法.写个类去 实现 Onclick接口
第四种:让activity类去实现 onclicklistner接口最终通过 switch...case去判断到底点击的是哪个控件.
`android:id="@+id/xxx"
xxx=(Button) findViewById(R.id.xxx);
xxx.setOnClickListener(this);
public void onClick(View v){
int id=v.getId();
switch(id){
case R.id.xxx:
break;
default:
break;
}
}
#EditText#
hint
maxLines
inputType
et_content=(EditText) findViewById(R.id.et_content);
String et_content=et_content.getText().toString.trim();
if(TextUtils.isEmpty(et_content)){
Toast.makeText("","",Toast.Length_SHORT).show();
return;
}
#imageView#
src
ImageView.setImageResource(R.drawable.xxx);
好玩的:实现图片轮播
自己见一个xml文件:
/animation-list>`
'ImageView rocketImage=(ImageView) findViewById(R.id.rocket_image);
rocketImage.setBackgroundResource(R.drawable.rocket_thrust);
rocketAnimation=(AnimationDrawable) rocketImage.getBackground()
;
rocketAnimation.start();
#RadioGroup#
eg:
android:id="@+id/rg"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="男"
android:id="@+id/rb_male"
/>
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="女"
android:id="@+id/rb_female"
/>
获取属性值:
if(rg.getCheckedRadioButtonId()==R.id.rb_male){
sex="male";
}else{
sex="female";
}
'
#ProgressBar#
max
progress
重要设置样式:
style="?android:attr/progressBarStyleHorizontal"
#VideoView#??????????播放音频视频,底层是SurfaceView 和 mediaPlayer的结合。
vv=(VideoView) findViewById(R.id.vv);
// 设置要
vv.setVideoPath("/mnt/sdcard/lala.3gp");
//mediaController --- 媒体控制器
MediaController mc=new MediaController(this);
mc.setAnchorView(vv);
vv.setMediaController(mc);???// 成功的将MediaController与vv关联起来
vv.start();
#SurfaceView#
sv=(SurfaceView) findViewById(R.id.sv);
try {
mPlayer=new MediaPlayer();
mPlayer.reset();
mPlayer.setDataSource("/mnt/sdcard/lala.3gp");
mPlayer.prepare();
} catch (Exception e) {
e.printStackTrace();
}
sp=getSharedPreferences("config", 0);
// surfaceHodler --- 界面的持有器,持有者
sv.getHolder().addCallback(new Callback() {
//surface销毁了
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
System.out.println("销毁了 ");
Editor editor=sp.edit();
editor.putInt("position", mPlayer.getCurrentPosition());
editor.commit();
mPlayer.stop();
}
//surface创建了
@Override
public void surfaceCreated(SurfaceHolder holder) {
System.out.println("创建了??");
int position=sp.getInt("position", 0);
mPlayer.setDisplay(sv.getHolder());????//显示画面,必须设置
mPlayer.start();
mPlayer.seekTo(position);??// 直接跳到某个位置, 从这个位置开始播放
}
//surface变化了
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
System.out.println("修改了??");
}
});
#ScollView#??只能有一个子节点
#WebView#
webview=new WebView(this);
//设置WebView属性,能够执行Javascript脚本
webview.getSettings().setJavaScriptEnabled(true);
//加载需要显示的网页
webview.setWebViewClient(new WebViewClient());
webview.loadUrl("http://www.xxxx.com/");
//设置Web视图
setContentView(webview);
#Fragment#??碎片??类似Activity
-1. 静态添加碎片
1.自己布局一个fragment:
android:id="@+id/fm_left"????????//id必须写,不然会报错
android:name="com.example.fragment.LeftFragment"// 添加name属性,包名必须写,此为加载LeftFragment 类
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"/>
2.定义一个LeftFragment 继承 Fragment
public class LeftFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//false 代表 不附着在parent布局中
return inflater.inflate(R.layout.left, container,false);
}
}
3.定义一个R.layout.left布局文件
-2. 动态添加碎片
核心代码
RightFragment rightFragment=new RightFragment();
FragmentManager fragmentManager=getFragmentManager();
FragmentTransaction beginTransaction=fragmentManager.beginTransaction();
beginTransaction.replace(R.id.fl, rightFragment);
beginTransaction.commit();
3. 四种对话框(不能用Application的上下文)
-1.取消对话框
AlertDialog.Builder builder=new AlertDialog.Builder(this);
builder.setTitle("please ");????????????// 下面都对话框都可以设置标题
builder.setMessage("继续撸代码吗?");????????// 同上
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "lu", 0).show();
}
});????????//确定按钮的点击事件
builder.setNegativeButton("No", null);????//取消按钮的点击事件
builder.show();
-2.单选对话框
Builder builder=new AlertDialog.Builder(this);
final String[] items=new String[]{"male","female"};
builder.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {
@Override
//which 是数组的 索引,下同
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "你选的是"+items[which], 0).show();
}
});
builder.show();
-3.多选对话框
AlertDialog.Builder builder=new AlertDialog.Builder(this);
final String[] items=new String[]{"跳楼","上吊","撸代码","滚回家"};
final boolean[] checkedItems=new boolean[]{false,false,false,false};
// fasle 代表默认不被选中
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
Toast.makeText(MainActivity.this, "你选的是"+items[which], 0).show();
checkedItems[which]=isChecked;
}
});
builder.show();
-4.进度条对话框
final ProgressDialog progressDialog=ProgressDialog.show(MainActivity.this, "please wait", "加载loading。。。。。");
new Thread(){
public void run(){
progressDialog.setCancelable(false);//Back键不能取消掉
SystemClock.sleep(2000);
progressDialog.dismiss();????????????//设置 dismiss 退出
}
}.start();
***所有的控件都有的属性:visibility{visible(默认,可见),invisible(不可见占空间),gone(不可见也不占空间)}
4. 自定义控件
开源框架:SmartImageView(看源码)---- SmartImageView.setImageUrl(Url url){}
1 . 继承原生控件
2 . 重写构造方法
3 . 根据业务需求定义方法
5. ListView
-1.作用和方法:用来将数据显示到屏幕上的技术
google按照mvc的三层架构思想设计
m:model--- 模型--- 需要显示的数据
v:view ---视图---- 呈现的界面
c:controller--- 控制器--- Adapter(适配器)
1. lv.setSelection(random1); //该方法可以 ListView 定位到让指定位置(random1),就是让指定位置的条目位于当前界面第一行。非常准确。
2. lv.smoothScrollToPosition(xxx); //该方法可以让 ListView 平滑的滚动到指定位置(xxx),但是非常不准确。
3.??lv1.setOnScrollListener(OnScrollListener); //给ListView 设置监听器
4. //滑动时回调的方法
// scrollState 滚动状态有三种:0 静止,1 手指滑动,2惯性滚动
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) { }
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
-2.BaseAdapter????---是ListAdapter的一个默认实现类
主要需要重写的2个方法:
public int getCount(){????????//总共需要显示数据条数
return list.size();
}
public public View getView(int position, View convertView, ViewGroup parent) {
//三种填充View的方法,本质是一样的
//第一种
View view=View.inflate(MainActivity.this,R,layout.item_main,null);
//第二种
View view=LayoutInflater.from(MainMainActivity.this).inflate(R,layout.item_main,null);
//第三种
LayoutInflater ll=(LayoutInflater)getSystemService("LAYOUT_INFLATER_SERVICE");
View view=ll.inflate(R,layout.item_main,null);
}
-3.ArrayAdapter
listView.setAdapter(new ArrayAdapter(MainActivity.this,R.layout.item,T[] Objects));
最后一个参数也可以是集合。
-4.SimpleAdapter
待完善
-5.优化
(1)//删除之前已经显示的数据 ,然后再次重新加载进来,这样避免重复显示
if(myadapter==null){
myadapter=new MyAdapter();
lv.setAdapter(myadapter);
}else{
//要通知适配器去更新一下数据
myadapter.notifyDataSetChanged();
}
(2)防止OOM(Out Of Memory)异常
View view;
if(convertView==null){
view=View.inflate(MainActivity.this, R.layout.item, null);
}else{
view=convertView;
}
或
if(convertView==null){
convertView=View.inflate(MainActivity.this, R.layout.item, null);
}
(3)faster(用Holder)持有器
static class ViewHolder {
TextView tv;
ImageView iv;
}
View Holder holder;
if (convertView==null) {
convertView=View.inflate(MainActivity.this,R.layout.item,null);
holder=new ViewHolder();
holder.iv=(ImageView) convertView.findViewById(R.id.iv);
holder.tv_title=(TextView) convertView.findViewById(R.id.tv);
convertView.setTag(holder);
}else{
holder=(ViewHolder) convertView.getTag();
}
##3.数据存储和解析
1. 文件存储
-1.基本输入输出流文件
-2.Context提供了两个方法来打开数据文件里的文件IO流,存储路径:/data/data//files
openFileInput("xxx.txt"); //读取
openFileOutput("xxx.txt",Context.MODE_PRIVATE);????//保存
//MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
//MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;
//后面的两种模式在Android4.2被废弃,不安全
//MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
//MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
deleteFile("xxx.txt");????//删除
File getFilesDir():获取该应用程序的数据文件夹得绝对路径
File getCacheDir():缓存区
String[] fileList():返回该应用数据文件夹的全部文件
-3.Sdcard 存储
1、调用Environment的getExternalStorageState()方法判断手机上是否插了sd卡,且应用程序具有读写SD卡的权限,如下代码将返回true
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
2、调用Environment.getExternalStorageDirectory()方法来获取外部存储器,也就是SD卡的目录,或者使用"/mnt/sdcard/"目录
File file=new File(Environment.getExternalStorageDirectory(),filename);
3、使用IO流操作SD卡上的文件
注意点:
手机应该已插入SD卡,对于模拟器而言,可通过mksdcard命令来创建虚拟存储卡
必须在AndroidManifest.xml上配置读写SD卡的权限
2. SharedPreferences存储 --- 单例,一般不会发生并发冲突
1.创建文件:????SharedPreferences sp=context.getSharedPreferences("xxx",int mode);//创建的xxx一定是xml文件,存储在/data/data//shared_prefs,参数mode 和上面一样。
2.存储数据:????Editor edit=sp.edit();
edit.putXxx(String key,Xxx value);
edit.commit(); || edit.apply();
区别: 1. apply没有返回值apply方法不会提示任何失败的提示。而commit返回boolean表明修改是否提交成功
2. apply异步提交数据,commit同步。
3.读取数据: sp.getXxx(String key,XxxDefault);// 如果没有读取到,就返回第二参数。
4.删除数据: sp.remove(String key);//删除key字段
sp.clear();//清空文件
3. SQLite数据库存储
-1.创建表
db.execSQL("create table tablename(_id integer primary key autoincrement,name varchar(30),age Integer)");
-2.Insert
第一种:存在sql注入问题
db.execSQL("insert into tablename(name,age) values('Myth',2)");
第二种:通过占位符? 解决sql注入
db.execSQL("insert into tablename(name,age) values(?,?)",new Object[]{"Myth",2});
第三种:底层还是通过拼接字符串得到sql语句
ContentValues values=new ContentValues();
values.put("name","Myth");
values.put("age",2);
db.insert("tablename",columnNull,values);//第二个参数:sql表不允许插入空,所以就会用null当作该值插入表。
-3.Delete
db.execSQL("delete from tablename where _id=2");
db.execSQL("delete from tablename where _id=?",new Object[]{2});
db.delete("tablename","_id=?",new String[]{String.valueOf(id)});
-4.Update
db.execSQL("update tablename set name='Myth' where age=2");
db.execSQL("update tablename set name=?,age=? where _id=?",new Objecet[]{"Myth",2,2});
ContentValues values=new ContentValues();
values.put("name","Myth");
values.put("age",2);
db.update("tablename",values,"_id=?",new String[]{String.valueOf(id)});
-5.Query
Cursor cursor=db.rawQuery("select * from tablename where _id=2");
Cursor cursor=db.rawQuery("select * from tablename where _id=?",new Object[]{2});
Cursor cursor=db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
if(cursor.moveToFirst){
while(cursor.moveToNext){
}
}
4. SQLite实践
-1.事务操作??--- 同时处理多条数据时,保证数据安全性
db.beginTransaction();
try {
db.setTransactionSuccessful();//执行提交
} finally {
db.endTransaction();
}
-2.数据库升级操作 --- 保证版本更新成功
switch (oldVersion) {
case 1:????????//代表每个版本的操作 该case 是1~2的升级操作
case 2:????????//case 一直到最新版本
default:
}
-3.注意:getWritableDatabase()和getReadableDatabase()方法的区别:
getWriteableDatabase()方法以读写方式打开数据库一旦数据库的磁盘空间满了,数据库就只能读而不能写,倘若使用getWritableDatabase()打开数据库就会出错。
getReadableDatabase()方法先以读写方式打开数据库,如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读的方式打开数据库.
5. XML生成器:XmlSerializer和 XML解析器:XmlPullParser
-1.XmlSerializer:(字符串拼接Xm文件存在语句注入问题,so用XmlSerializer)
XmlSerializer serializer=Xml.newSerializer();
serializer.setOutput(fos,"UTF-8");
serializer.startDocument("UTF-8",true);// "
serializer.startTag(null, "smses");
for (int i=0; i < 50; i++) {
serializer.startTag(null, "sms"); //第一个参数为命名空间
serializer.startTag(null, "body");
serializer.text("我是内容<>" + i);
serializer.endTag(null, "body");
serializer.endTag(null, "sms");
}
serializer.endTag(null, "smses");
serializer.endDocument;
fos.close;
-2.XmlPullParser:
XmlPullParser pullparser=Xml.newPullParser();
pullparser.setInput(fis,"UTF-8");
int eventType=pullparser.next();
while(eventType !=XmlPullParser.END_DOCUMENT){
switch(eventType){
case XmlPullParser.START_TAG:
if ("smses".equals(tagName)) {
list=new ArrayList();
}else if ("sms".equals(tagName)) {
sms=new Sms();
}else if ("body".equals(tagName)) {
sms.setBody(parser.nextText());
}
break;
case XmlPullParser.END_TAG:
if ("sms".equals(tagName)) {
list.add(sms);
}
break;
}
eventType=pullparser.next();
}
##4.Activity
-1.自定义的Activity
-1.创建一个MyActivtiy实现Activity
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
-2.新建一个布局文件 xxx.xml
-3.在onCreate()方法里写 上 setContentView(R.layout.xxx);
-4.最重要的一步:在清单文件中注册一个自己的Activity
android:name="com.example.MyActivity"????//可以缩写.MyActivity
android:label="@string/app_name" >????????//标题栏的内容
-5.非必须的一步:隐藏标题栏 ---在onCreate()方法内写
requestWindowFeature(Window.FEATURE_NO_TITLE);
-2.生命周期
OnCreate()????????创建界面
OnStart()????????不可见---->可见??时候会调用
OnResume()????????获得焦点,此时活动在栈顶,处于运行状态
OnPause()????????失去焦点,不在栈顶但可见,处于暂停状态
OnStop()????????可见---->不可见??时候会调用,处于停止状态
OnDestroy()????????销毁
**注意:要使得Activity不可见,可以再建一个项目同时运行,将其Application的theme属性改成透明状态:
android:theme="@android:style/Theme.Translucent">
-3.活动的四种启动模式(android:launchMode="")
-1.standard:每次启动acitvity组件时, 都会新创建activity 实例,是活动默认的启动模式
-2.singleTop:如果某个activity设置单一顶部模式, 那么当发现当前的activity就在当前任务栈的顶部, 那么就不再新创建当前的activity的实例.例如:系统的短信
-3.singleTask:如果当前任务栈中已经有当前activity 的实例, 那么就将当前activity 的实例直接拿过来用, 用的时候,如果当前activity 不在栈顶,那么将在当前activity 之上的其他的activity的实例 给干掉... 然后再处于栈顶了一般情况下, 当某个activity??启动的时候, 要占用的内存比较大, 而手机上的内存又是有限的, 那么这个时候, 就推荐将这个activity的启动模式设置为单一任务栈模式.例如:系统的浏览器
-4.singleInstance:如果某个activity 的启动模式设置为单一实例模式, 那么系统会为这个activity 单独的去开辟一个任务栈,这个任务栈中,只放这个 activity的实例.这样确保了整个操作系统中,只有一个这个activity的实例了.如果某个activity在整个系统中就只需要有一个实例, 并且永远不会更改, 那么就推荐使用这种模式.例如:系统的电话
-4.使用intent在活动之间传输数据
-1.显示意图:是明显的指定要激活哪个组件 ..一般建议用在应用程序内部 .
Intent intent=new Intent();
//????intent.setClass(this, SecondActivity.class);
//????intent.setClassName("com.itheima.exactintent", "com.itheima.exactintent.SecondActivity");
intent.setClassName(this,??"com.itheima.exactintent.SecondActivity");
startActivity(intent);
-2.隐式意图:是指不明确到底哪个组件可以响应你的意图,你只需要将意图发出去就可以了.当存在满足你的意图的组件时,
这个时候,系统就会将组件给激活起来...一般用在不同应用程序之间 激活组件 ..
Intent intent=new Intent();
//必须和Activity下的intent-filter内容匹配
intent.setAction("com.itheima.nu");
//????intent.addCategory("android.intent.category.DEFAULT");
intent.addCategory(Intent.CATEGORY_DEFAULT);
startActivity(intent);
****1. 每个应用中的组件是可以配置多个Intent-filter,可以配置多个隐式意图去激活这个组件
2. 如果想要激活这个组件, 只需要发送对应的隐式意图就可以了.
-3.intent中setData:接收一个Uri对象,例如:intent.setData(Uri.parse("www.baidu.com"));
与其对应,可以在标签中配置一个子标签。
属性:scheme(http),host(www.baidu.com),port(8080),path(指定端口号后面的部分),mimeType(指定可以处理的数据类型,允许使用通配符)
-4.intent携带数据,实现短信分享:
Intent intent=new Intent();
//短信有很多intent-filter,只需要其中匹配上一个即可
intent.setAction("android.intent.action.SENDTO");
intent.addCategory("android.intent.category.DEFAULT");
intent.addCategory("android.intent.category.BROWSABLE");
intent.setData(Uri.parse("smsto://xxxxx"));
intent.putExtra("sms_body", "xxxxxxxxxx");
startActivity(intent);
//Activity接收数据
Intent intent=getIntent();
String data=intent.getExtraString("sms_body");
-5.带有返回结果的intent
主Activity:
Intent intent2=new Intent();
intent2.setClass(this, xxx.class);
startActivityForResult(intent2, 2);
//接收其他Activity回传的结果数据
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
System.out.println("结果返回到这里了 ....");
if(requestCode==1){
}else if(requestCode==2){
}
super.onActivityResult(requestCode, resultCode, data);
}
xxxActivity:
Intent data=new Intent();
data.putExtra("contact", contact);
setResult(0, data);
finish();????????// 关闭该xxxActivity
-6.其他
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);重新启动Activity
intent.addAction("android.media.action.IMAGE_CAPTURE");启动相机
##5.BroadcastReceiver(买收音机,装电池,调频道)
-1.动态注册广播接收者
(1)监听手机屏幕开关
receiver=new ScreenStateReceiver();//继承BroadcastReceiver
IntentFilter filter=new IntentFilter();
filter.addAction("android.intent.action.SCREEN_OFF");
filter.addAction("android.intent.action.SCREEN_ON");
registerReceiver(receiver, filter);//注册 广播接收者
public void onReceive(Context context, Intent intent) {
String action=intent.getAction();
if("android.intent.action.SCREEN_OFF.equals(action)){
System.out.println("发现用户屏幕关了??");
abortBroadcast();//拦截广播
}else if("android.intent.action.SCREEN_ON".equals(action)){
System.out.println("发现用户屏幕开启 ");
}
}
(2)监听网络变化
receiver=new NetworkChangeReceiver();//继承BroadcastReceiver
IntentFilter filter=new IntentFilter();
filter.addAction("android.intent.action.CONNECTIVITY_CHANGE");
registerReceiver(receiver, filter);//注册 广播接收者
(3)需要手动取消注册
在onDestroy()中调用 unregisterReceiver(receiver);
-2.静态注册一个广播接收者 --- 监听开机启动
//优先级-1000~10000,优先接收广播
-3.发送无序广播
// 定义 intent , intent 设置 必要的信息就可以 了
Intent intent=new Intent();
intent.setAction("com.example.xxx");
// 发送 广播
sendBroadcast(intent);
-4.发送有序广播
// 定义 intent , intent 设置 必要的信息就可以 了
Intent intent=new Intent();
intent.setAction("com.example.xxx");
// 发送 广播
sendOrderBroadcast(intent,null);//第二个参数是一个与权限有关的字符串
##6.Server
-1.Handler异步消息处理机制(IPC 线程通信,避免发生ANR 问题,在Activity中 耗时操作 5s 产生ANR。)
-1.Message:线程间传递消息,
//获得消息对象,两种方式本质一样
Message msg=handler.obtainMessage();
Message msg=Message.obtain();
msg.sendToTarget(); //该msg需要绑定handler,从handler.obtainMessage()获取
-2.Handler:用于发送和处理消息
handler.sendMessage();
handler.sendEmptyMessage();//发送无效消息
private Handler handler=new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case SUCCESS:
Xxx data=(Xxx)msg.obj;
Toast.makeText(MainActivity.this, "成功", 0).show();
break;
case FAILURE:
Toast.makeText(MainActivity.this, "失败", 0).show();
break;
}
}
};
-3.MessageQueue :消息队列,存放将要被处理的消息,每个线程只有一个MessageQueue
-4.Looper:调用loop(),进入一个无线循环中,不断取出MessageQueue中的消息传递到handleMessage()中,每个线程也只有一个Looper对象。
-5.发送消息代码
Message msg=Message.obtain();
msg.what=SUCCESS;????????// what 区分发送的消息类型
msg.obj=data;????????????// obj 携带任意消息
handler.sendMessage(msg);
-2.AsyncTask ---Android已经封装好,方便在子线程中对UI进行操作。(待更新)
-3.进程优先级分类:
-1.前台进程: 就是正在与用户进程交互的引用程序
-2.可视进程: 用户看得见的,但是摸不着的
-3.服务进程: 在服务中运行, 在后台运行着
-4.后台进程: 在后台一直运行着,不是运行在service ,是运行在activity中
-5.空进程:??引用程序已经退出了,没有activity,没有service .
-6.总结:前台进程> 可视进程> 服务进程> 后台进程> 空进程???当系统内存不够用的时候, 就会去尝试回收进程,来重新分配内存. 会按照如上优先级分类杀掉进程.
-4.自定义服务
public class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
}
// 必须清单文件注册
-5.开启服务的生命周期
onCreate()????????????//服务创建时调用
onStartComment()????//每次服务启动时调用,废弃了onStart()
onDestroy()????????????//服务销毁时调用
-1.开启服务
Intent intent=new Intent();
intent.setClass(MainActivity.this,MyService.class);
startService(intent);
-2.停止服务
Intent intent=new Intent();
intent.setClass(MainActivity.this,MyService.class);
stopService(intent);
-6.绑定服务的生命周期
onCreate()????????//创建服务
onBind()?????????//绑定服务
onUnbind()?????????//解除绑定服务
onDestroy()?????//销毁服务
(1)绑定服务时, onStartCommand并不会执行,
(2)绑定服务,服务何时销毁呢, 在应用程序退出的时候就销毁了
(3)绑定服务, 服务不会在后台运行 ---在apps中running 中是看不到的
(4)开启服务, 服务会在后台运行--- 可以在apps中 runnning 中看到
(5)绑定服务, 服务只会创建一次, 如果再次绑定服务, 那么服务是不会重新再次创建的.
(6)如果没有绑定服务, 就直接去解绑服务, 那么会抛异常.so 需要做如下判断,避免异常
if(conn !=null){
unbindService(conn);
conn=null;
mybinder=null;
}
-7.绑定服务实现流程(活动和服务间通信,进程间通信使用 IBinder)
-1.绑定服务
Intent intent=new Intent();
intent.setClass(this, MyService.class);
if(conn==null){
conn=new MyConnection();
}
bindService(intent, new MyConnection(), BIND_AUTO_CREATE);
-2.编写MyBinder 继承 Binder 实现 IService??// IService 接口中是将要调用的方法
private class MyBinder extends Binder implements IService{
@Override
// 服务中对外提供的方法封装在接口中,重写接口中的方法
public void call(String name, String service) {
calle(name, service);
}
//该方法为不对外提供的方法,只对内访问
public void callee(String name, String service) {
System.out.println("呼叫前台");
}
}
-3.向Activity传回MyBinder对象
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
-4.实现MyConnection 实现 ServiceConnection接口
private class MyConnection implements ServiceConnection{
@Override
//当与服务建立联系的时候被回调
public void onServiceConnected(ComponentName name, IBinder service) {
if(mybinder==null){
mybinder=(IService) service;
//????mybinder=IService.Stub.asInterface(service);
}
}
@Override
//当与服务断开联系的时候被回调
public void onServiceDisconnected(ComponentName name) {
mybinder=null;
}
}
-5.调用服务中的方法
mybinder.call("xxxx", "xxxx");
-8.不同Activity之间通信
主要编写.aidl
(1)语法与接口很类似,直接修改接口文件后缀名即可。但是.aidl文件中不能写权限修饰符
(2).aidl文件接受参数和返回值类型是8种基本数据类型,如果是引用数据类型,必须实现Parceable接口
(3)不能修改.aidl文件生成的.java类
-1 Parceable 比 Serializable 性能高
-2 Serializable 使用时会产生大量临时变量,引起频繁的GC
-3 Parceable 不能保证数据的持续性,不能使用数据保存在磁盘上
-9.混合开发服务
(1)开启服务:只能在后台运行
(2)绑定服务:只能调用服务中的方法
想在后台运行,但是同时又想 调用服务中 方法, 那么 就需要混合开启服务了.
以后,如果需要在后台运行, 并且又需要调用服务中的方法时,请严格按照 如下的顺序去 实现程序的逻辑, 否则 就容易出现问题..
混合开启:
开启服务
绑定服务
调用服务中的方法
解除绑定
停止服务
例如:QQ 在退出的时候,选择退出后, 仍然可以接受消息, 就可以使用混合开启服务的方式...
-10.Android提供一个IntentService类:是一个异步的,会自动停止的服务,可以避免ANR问题(服务在主线程中耗时操作 10s 产生ANR 问题)
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
}????????//处理具体的逻辑
}
// 清单文件注册
##7.ContentProvider
-1.自定义内容提供者??---ContentProvider
public class MyContentProvider extends ContentProvider {
private static final int SUCCESS=0;
private static UriMatcher matcher;
//匹配器 -- UriMatcher
//是固定的写法
//这里的no_match是用来指定当匹配不成功时,返回的值
static {
matcher=new UriMatcher(UriMatcher.NO_MATCH);
// com.itheima.xxx 公开的名称, table就是暗号了, SUCCESS指匹配成功后返回的结果
matcher.addURI("com.example.xxx", "table", SUCCESS);
}
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
BankOpenHelper myOpenHelper=new MyOpenHelper(getContext());
SQLiteDatabase db=myOpenHelper.getWritableDatabase();
if (matcher.match(uri)==SUCCESS) {
return db.query("table", projection, selection, selectionArgs, null,
null, sortOrder);
}
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
BankOpenHelper myOpenHelper=new MyOpenHelper(getContext());
SQLiteDatabase db=myOpenHelper.getWritableDatabase();
// TODO Auto-generated method stub
if (matcher.match(uri)==SUCCESS) {
db.insert("table", null, values);
} else {
try {
throw new Exception("暗号错误。");
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
BankOpenHelper myOpenHelper=new MyOpenHelper(getContext());
SQLiteDatabase db=myOpenHelper.getWritableDatabase();
if (matcher.match(uri)==SUCCESS) {
return db.delete("account", selection, selectionArgs);
}
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
BankOpenHelper myOpenHelper=new MyOpenHelper(getContext());
SQLiteDatabase db=myOpenHelper.getWritableDatabase();
if (matcher.match(uri)==SUCCESS) {
return db.update("account", values, selection, selectionArgs);
}
return 0;
}
}
##清单文件注册:
-2. 内容解析者 ---- ContentResolver (类似SQLite操作)
1 insert
ContentResolver resolver=getContentResolver();
Uri url=Uri.parse("content://com.example.xxx/table");
ContentValues values=new ContentValues();
values.put("key", "value");
resolver.insert(url, values);
2.delete
ContentResolver resolver=getContentResolver();
Uri url=Uri.parse("content://com.example.xxx/table");
resolver.delete(url, "key=?", new String[]{"value"});
3.update
ContentResolver resolver=getContentResolver();
Uri url=Uri.parse("content://com.example.xxx/table");
ContentValues values=new ContentValues();
values.put("key1", "value1");
resolver.update(url, values, "key=?", new String[]{"value"});
4.query
ContentResolver resolver=getContentResolver();
Uri url=Uri.parse("content://com.example.xxx/table");
Cursor cursor=resolver.query(url, null, "key=?", new String[]{"value"}, null);
while(cursor.moveToNext()){
}
-3.内容观察者 --- ContentObserver
(1)ContentResolver resolver=getContentResolver();
//注册一个内容观察者
//如果notifyForDescendents参数设为true,假如Uri为content://abc,那么Uri为content://abc/xyz, content://abc/xyz/foo的数据改变时也会触发该监听器,如果参数为false,那么只有content://abc的数据改变时会触发该监听器
resolver.registerContentObserver(uri, boolean notifyForDescendents, new ContentObserver(null) {
// selfChange : 数据的变化是否是来自于自己
@Override
public void onChange(boolean selfChange) {
System.out.println("数据发生变化了 ....");
}
});
(2)在不需要时,需要手动的调用
unregisterContentObserver()去取消注册。
(3)ContentObserver类介绍
构造方法 public void ContentObserver(Handler handler) {}
说明:所有 ContentObserver的派生类都需要调用该构造方法
参数: handler Handler对象。可以是主线程Handler(这时候可以更新UI了),也可以是任何Handler对象。
##8.手机多媒体
-1.通知的使用(NotificationManager)
-1.第一种
//拿到通知管理器
NotificationManager nm=(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification nf=new Notification(R.drawable.ic_launcher,"This is ticker Text ",System.currentTimeMillis());
// 第四个参数为延期意图(能跳转干你想干的事)
nf.setLatestEventInfo(context, "This is content title", "This is content text", null);
//显示通知,第一个参数是每一个通知的id(保证不同)
nm.notify(1,nf);
-2.第二种
//基于Notification是builer模式构建,可以链式编程创建
Notification nf=new Notification.Builder(this)
.setContentTitle("你有消息")????????????//标题
.setContentText("你mama 喊你回家吃饭")????//文本
.setSmallIcon(R.drawable.ic_launcher)????//小图标
.setLargeIcon(BitmapFactory.decodeResource(getResources(),
R.drawable.ic_launcher))?????//大图标
.build();
NotificationManager nm=(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(1,nf);
-3.通过pending(延期意图)拨打电话
Notification nf=new Notification(R.drawable.ic_launcher, "你好一有消息", System.currentTimeMillis());
Intent intent=new Intent();
intent.setAction(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:"+5556));
// pendingIntent 可以getActivity()??getService()??getBroadcast()
PendingIntent pendingIntent=PendingIntent.getActivity(this, 0, intent, 0);
nf.setLatestEventInfo(this, "ai", "huijiachifan", pendingIntent);
//权限不能少,打电话权限
-2.短信的接收和发送(SmsManger)
-1.接收短信:监听 android.provider.Telephony.SMS_RECEIVED的广播
public class MessageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle=intent.getExtras();
//通过密钥"pdus"获取SMS pdu 数组,每一个pdu是一条短信
Object[] objects=(Object[])bundle.get("pdus");
for (Object object : objects) {
//将每一个pdu字节数组转换SmsMessage对象
SmsMessage smsMessage=SmsMessage.createFromPdu((byte[])object);
//获取短信的发送方号码
if(smsMessage.getOriginatingAddress().equals("55666")){
//获取短信
smsMessage.getMessageBody();
}
}
}
}
//接收短信权限
-2.发送短信
SmsManager smsManager=SmsManager.getDefault();
//发送很长很长的短信需要切割短信存到集合中,才能一条一次性发出去
ArrayList divideMessage=smsManager.divideMessage("");
smsManager.sendMultipartTextMessage("phone", null, divideMessage, null, null);
//接收短信权限
-3.监听手机状态(TelephonyManager):开启录音功能(MediaRecoder)
TelephonyManager manager=(TelephonyManager) getSystemService(TELEPHONY_SERVICE);
manager.listen(new myPhoneStateListener(), PhoneStateListener.LISTEN_CALL_STATE);
private class myPhoneStateListener extends PhoneStateListener{
@Override
public void onCallStateChanged(int state, String incomingNumber) {
// TODO Auto-generated method stub
super.onCallStateChanged(state, incomingNumber);
switch (state) {
if(mRecorder !=null){
stopRecording();
}
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
startRecording();
break;
case TelephonyManager.CALL_STATE_RINGING:
break;
default:
break;
}
}
}
MediaRecorder mRecorder;
private void startRecording() {
//下面是模版,可以录音文件的格式有:3gp,mp3。。。
mRecorder=new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mRecorder.setOutputFile("/mnt/sdcard/jianting.3gp");
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
try {
mRecorder.prepare();
} catch (IOException e) {
//????Log.e(LOG_TAG, "prepare() failed");
}
mRecorder.start();
}
private void stopRecording() {
mRecorder.stop();
mRecorder.release();
mRecorder=null;
}
//录音权限,读手机状态权限
-4 播放音视频(MediaPlayer)
setDataSource() // 设置要播放
seekTo()//????从指定位置播放
isPlaying()//当前是否播放
getDuration// 获取音频时长
// 开始播放Button
public void start(View v){
//播放的地址
String dizhi=et_shu.getText().toString().trim();
if(TextUtils.isEmpty(dizhi)){
Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();
return;
}
player=new MediaPlayer();
player.reset();
try {
player.setDataSource(dizhi);
player.prepare();
//异步准备
/*player.prepareAsync();
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// TODO Auto-generated method stub
player.start();
}
});*/
player.start();
} catch (Exception e) {
e.printStackTrace();
}
}
// 暂停Button
public void pause(View v){
if(player!=null && player.isPlaying()){
player.pause();
return;
}
if(player!=null){
player.start();
}
}
// 停止Button
public void stop(View v){
if(player!=null){
player.stop();
player.release();
player=null;
}
}
-5.传感器的使用(SensorManager)
SensorManager sm=(SensorManager) getSystemService(Context.SENSOR_SERVICE);
// Sensor.TYPE_XXX 可以获取
Sensor lightSensor=sm.getDefaultSensor(Sensor.TYPE_XXX);
MySensorEventListener myListenser=new MySensorEventListener();
// 必须注册监听事件,第二个参数 是Senor实例,第三个参数是 传感器输出信息的更新速率
有4个值:SENSOR_DELAY_UI??SENSOR_DELAY_NORMAL??SENSOR_DELAY_GAME??SENSOR_DELAY_FASTEST???越来越快,耗电
sm.registerListener(myListenser, XXXSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
// 不使用的时候取消监听,省电
@Override
protected void onDestroy() {
super.onDestroy();
if(myListenser!=null){
sm.unregisterListener(myListenser);
}
}
private class MySensorEventListener implements SensorEventListener{
// sensor获得周围环境数据发生变化时 ,会被调用
@Override
public void onSensorChanged(SensorEvent event) {
//编写逻辑事件
}
// sensor获得周围环境数据发生变化时,精确度发生变化时会被调用
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
// 获取Sensor.TYPE_ALL
List list=sm.getSensorList(Sensor.TYPE_ALL);
System.out.println("数量 : " + list.size());
for (Sensor sensor : list) {
System.out.println(sensor.getName() +" , 类型 值 : " + sensor.getType());
}
##9.网络编程
-1.HttpURLConnection
1.get请求(拼接URL。。)
//防止乱码??URL编码
String username=URLEncoder.encode(username, "UTF-8");
String password=URLEncoder.encode(password, "UTF-8");
URL url=new URL(path+"?username="+username+"&password="+password);
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
//伪装设置成windows端查看网页
//conn.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586");
//设置连接超时时间
conn.setConnectTimeout(5000);
//获取响应码
int code=conn.getResponseCode();
2.post请求
URL url=new URL(path);
String params="?username="+URLEncoder.encode(username, "UTF-8")+"&password="+URLEncoder.encode(password, "UTF-8");
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
//设置连接超时时间
conn.setConnectTimeout(5000);
//获取响应码
conn.setDoOutput(true);
conn.getOutputStream().write(params.getBytes());
int code=conn.getResponseCode();
-2.HttpClient(Apache) ---android 6.0 废弃
1.get请求
username=URLEncoder.encode(username, "UTF-8");
String path="http://169.254.26.249:8080/qqlogin/servlet/login?username="+username+"&password="+password;
DefaultHttpClient client=new DefaultHttpClient();
HttpGet get=new HttpGet(path);
//执行请求,拿到响应对象
HttpResponse response=client.execute(get);
int code=response.getStatusLine().getStatusCode();
if(code==200){
//拿到服务端响应的输入流
InputStream is=response.getEntity().getContent();
String login=StreamTool.decodeStream(is);
2.post请求
String path="http://169.254.26.249:8080/qqlogin/servlet/login";
DefaultHttpClient client=new DefaultHttpClient();
HttpPost post=new HttpPost(path);
//通过一个NameValuePair集合来存放待提交的数据
List list=new ArrayList();
list.add(new BasicNameValuePair("username", username));
list.add(new BasicNameValuePair("password", password));
//防止乱码
post.setEntity(new UrlEncodedFormEntity(list , "UTF-8"));
//执行请求,拿到响应对象
HttpResponse response=client.execute(post);
int code=response.getStatusLine().getStatusCode();
if(code==200){
InputStream is=response.getEntity().getContent();
String login=StreamTool.decodeStream(is);
-3.AsyncHttpClient---post请求
AsyncHttpClient client=new AsyncHttpClient();
RequestParams params=new RequestParams();
params.add("username", username);
params.add("password", password);
client.post(path, params, new AsyncHttpResponseHandler() {
@Override
//statusCode 是 响应码????????headers响应头???responseBody 响应体
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
Toast.makeText(MainActivity.this,"登录结果为"+new String(responseBody), 0).show();
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
Toast.makeText(MainActivity.this,"登录结果为"+new String(responseBody), 0).show();
}
写在前面的话
一本好的入门书,不应只是简单堆砌知识点,而是以帮助学习者建立知识体系为目标,郭神(郭霖)的《第一行代码》做到了这一点。
从2014年的第1版,到2017年的第2版,这本书得到了Android开发学习者的广泛关注,并收获读者“Android学习第一书”的赞誉。
今年,全新升级的第三行代码终于与大家见面了!最新版的创作和出版都有哪些不为人知的经历?这一版给读者带来了哪些变化?来听郭神讲讲《第一行代码 第3版》背后的故事吧!
原文发表于微信公众号:郭霖(guolin_blog),经作者授权转载。
郭霖:这本《第三行代码》,让大家久等了!
《第一行代码——Android》这本书自2014年出版以来,已经过去了6个年头。期间Android系统版本经历了4.0到10.0系统的巨大升级,开发技术也发生了翻天覆地的变化。在2016年的时候,我曾对书中的内容进行了大幅度的更新,出版了这本书的第2版,也就是所谓的《第二行代码》。而如今,再隔4年之久之后,《第三行代码》终于要以全新的面貌跟大家见面了。
/ 创作 /
不得不说,《第一行代码 第3版》这本书,在出版的过程中经历了太多坎坷,以至于到今天才能跟大家见面。这里先跟所有的读者朋友们说声:对不起,让你们久等了!
早在2017年,Google在I/O大会上宣布Kotlin将成为Android系统的一级开发语言,从此与Java平起平坐。那时我就猜想到,在Android应用层的开发语言将掀起一场大淘汰式的替换。当时图灵也看到了这个新闻,图灵的编辑很快就联系我,问我准不准备写一本Kotlin方面的书?
我认为掌握一门语言是需要时间沉淀的,学语法可能两周就能搞定,但要想体会到精髓,则需要不断通过写代码去磨练和提升。那个时候我不认为我具有写好这样一本书的能力,所以也就没有答应。
长期关注我的朋友应该知道,在这之后的一年多时间里,我把时间基本都放在了GifFun这个开源项目上面。这个项目一开始是使用Java来写的,中途被我切换成了Kotlin。因为我知道,要想熟练掌握一门语言,最好的方式就是不断用它来写代码。我也就借此机会,对Kotlin有了更加深刻的理解。
到了2019年,我和出版社又重新进行了商讨,准备对《第一行代码》再度更新,推出第3版,这次会将全书的代码都改用Kotlin来实现。但问题在于,Kotlin作为一门新兴语言,程序员基数远不可能有Java那样庞大。如果换了语言之后导致大量读者看不懂怎么办?
当时出版社提出了一个方案,让我单独再写一本专门讲解Kotlin语言的书,和《第一行代码》配套使用。不过我认为这个方案会提升《第一行代码 第3版》的阅读门槛,就没有答应。
几经思考之后,我认为想出了一个最佳的写作方案:在一本书里面同时讲解Android和Kotlin这两门技术。这也将会是《第一行代码》中首次引入编程语言的讲解。
但如何安排这两门技术之间的内容顺序是一件非常有挑战的事情。因为Kotlin是Android程序的开发语言,很显然需要先掌握语言才能开发Android程序。可是如果先花小半本书的篇幅讲解Kotlin,然后再开始学习Android开发,这种学习方式一定非常枯燥,因为学编程语言最怕的就是光学不练。
为此,我决定采用一种或许别人从未尝试过的方式,将Kotlin和Android穿插在一起讲解。首先使用一个快速入门章节介绍Kotlin的基础知识,然后利用已掌握的知识开始学习Android开发,这样可以做到立刻上手实践。之后的每一章当中,都会结合相当章节的Android内容再学习一些Kotlin的进阶知识,等学完整本书之后,你就能同时熟练地掌握Android和Kotlin这两门技术了。
这种穿插讲解的方式非常考验我的内容设计能力,因为每章的Android开发中用到的Kotlin知识不能过于超前,不然读者会出现看不懂的情况。同时每章讲解的Kotlin知识又要结合着相应章节的Android知识,这样才能更好地理解该知识点的用法与场景。它们之间的内容是相辅相成的。
为此,在前期制订目录上面我就花了将近一个月的时间,之后编写Kotlin快速入门这一章又花了两个多月的时间。因为我以前从来没有写过编程语言类的书,为了把Kotlin重要的基础知识在这一章里面都体现出来,我花费了很多心思。在编写这一章的过程中,Google I/O 2019大会上正式宣布了Kotlin First的口号,Kotlin已经不再和Java是平起平坐,而是变成一等公民了。虽然这是我意料之中的事情,但是比我预期来得要早,也让我更加坚定了写好这本书的信心。
《第一行代码 第2版》是2016年3月份开始动笔的,同年9月份完稿,12月份出版。《第一行代码 第3版》是2019年3月份开始动笔的,但是9月份却远远无法完稿,因为改动内容和新增内容要远比第2版时大得多。我几乎把所有的业余时间都放在了上面,最终在11月底才勉强全书完稿。
本来是想着好事多磨,好书也不怕晚出版几个月,过完年应该就可以跟大家见面了。没想到,我们恰好碰上了一场从未遇到过的疫情灾难,包括17年前的SARS也没有今天的新冠疫情严重。
这场疫情导致中国各方面都受到了极大的创伤,学校停课,企业停工,大家都只能呆在家里,不出门就是最安全的。而受疫情的影响,出版社、印刷厂都延迟上班,无法复工。我每天在公众号中都会被读者追问,新书到底什么时候可以出版?这个问题我当时真的无法回答,疫情所带来的影响要持续到何时真的没人知道。
后来到了三月份,国内的疫情已经逐渐控制住,多数企业也在慢慢复工复产了,这本书才终于能够得以出版印刷。但是,虽然国内的疫情已经缓和,海外疫情却紧接着爆发了起来,截至到我写本篇文章时也没有任何缓和的迹象。现在仍然不知道这场疫情给全球带来的影响要持续到何时,希望新冠疫情能快点成为过去式,每个人都恢复到正常的生活当中。
经过了如此多磨难才诞生的这本书,期待能给大家送去更多的知识吧。
/ 变化 /
我相信一定会有很多读者朋友都想问一个问题:《第一行代码 第3版》相比于《第一行代码 第2版》具体有哪些变化呢?在这里我就向大家详细地介绍一下主要的变化部分。
首先是编程语言上的改变,本书前两版都是使用Java作为应用程序的编程语言,而第3版使用了Kotlin,这也是目前Google最推荐我们使用的编程语言。
本书的前两版中也没有涉及过语言方面的讲解,默认读者是有Java语言基础的。而第3版中对Kotlin语言进行了非常全面的讲解,不需要读者有任何Kotlin语言的基础。
另外你需要知道,《第一行代码 第3版》是一本升级版的书,而不是一本全新的书,因此书中整体的知识架构仍然和《第一行代码 第2版》是保持一致的。
虽然整体知识架构不变,但还是涉及了一些章节上的变动。
第3版中移除了之前第2版中的11、15两章内容。这两章内容分别讲解的是LBS开发、以及App上架。
LBS开发其实主要讲解的都是百度地图SDK的用法,但是百度地图SDK很可能会频繁更新,而书中的内容却是不能随意改变的,所以我认为这部分内容更应该去百度地图的开发者官网进行学习。
至于App上架主要讲解的是360应用商店的上架过程,由于现在国内的App上架已经变得非常复杂,个人开发者都需要专门去申请软件著作权才能将自己开发的App上架到应用商店,书中的内容已经不再适用,所以也进行了移除。
而第3版中新增了三章内容,分别是第2章、第13章和第16章。
第2章就是之前提到的Kotlin快速入门章节,这一章将会带你迅速上手Kotlin编程,讲解了如变量、函数、逻辑控制、面向对象编程、Lambda编程、空指针检查等Kotlin中最基础、同时也是最核心的知识点。学完了这一章内容之后,你就可以使用Kotlin来编写一些Android程序了。
第13章是专门讲解Jetpack的一个章节。Jetpack是Google于近年推出的一个开发组件工具集,旨在帮助我们编写出更加符合高质量代码规范、更加具有架构设计的应用程序。是的,现在我们已经不能停留在实现功能就万事大吉的层面了,而是要在实现功能的同时,追求更高的代码质量和架构设计。第13章中讲解了ViewModel、Lifecycles、LiveData、Room、WorkManager等内容,基本涵盖了Jetpack架构模块当中最主要的一些组件。
第16章是第3版中新补充的一个实战章节。本书的前两版中都只有一个实战章节,而第3版中会有两个实战章节。不同于过去的App开发实战,第16章会带你一起编写并发布一个开源库。因为我之前也写过几个比较知名的开源项目,并且我在公司的主要工作就是SDK方面的研发,所以其实我很擅长写这部分内容。还有什么事情是比在别人的项目中看到引用了自己的开源库更让人激动的呢?
介绍完了整体章节上的变动,我们再来看一些具体内容上的变化。
为了响应Google的Kotlin First号召,第3版中将全书所有的代码都改成了使用Kotlin语言来进行实现,Android的持续Kotlin化也是未来长期发展的必然趋势。不过,由于新增的第2章只是讲解了一些Kotlin的基础内容,为了能够让你更加全面地掌握Kotlin方方面面的知识,第3版几乎在每一章的最后都加入了一个Kotlin课堂的环节。全书一共有12节Kotlin课堂,全面涵盖了诸如常用技巧、高阶函数、泛型、协程、DSL等等语言层面的知识,绝对足以让你熟练掌握Kotlin这门语言了。
除了在编程语言方面存在大量的变动之外,Android开发方面当然也不会一成不变。自《第一行代码 第2版》出版之后,Android又经历了8 9 10系统的快速迭代。每个新系统版本中都会增加一些崭新的特性,也都需要进行一些开发方面的适配,第3版将这些内容全部涵盖了进去。
另外,近些年来追求架构设计的开发者越来越多,类似MVP、MVVM等架构的使用也越来越广泛,传统的MVC架构逐渐在被抛弃。第3版的第15章中对MVVM架构进行了非常详细的讲解,并且配合着Google提供的Jetpack组件,最终完成了一个MVVM架构的天气预报程序。虽然这个天气预报程序我已经写了3版了,但是这次将会是最具架构性,也最符合高质量编码规范的版本。相信会对你未来开发公司的实际项目带来很大的帮助。
除了上述主要变化之外,还有许多小细节方面的变动这里就不一一列举了,等待你自己阅读的时候发现吧。
/ 设计 /
虽说这是一本升级版的书,可我花费在上面的时间和精力丝毫不亚于写一本新书。所以,不管是在内容方面还是在设计方面,我都希望它和第2版能够有着更加明显的区别。因此,图灵的设计师们这次也辛苦了。
首先是封面的设计,和第2版时一样,这次第3版的封面也是由图灵的设计师做出了几个版本,然后我在公众号上发起了一个投票,让广大读者朋友们共同选出最满意封面,投票结果如下:
最后蓝色封面以压倒性的优势当选,《第一行代码》也终于把红绿蓝这三元色全部凑齐了。第3版的封面正式确定:
然后是书签,第2版中首次在书里附赠了书签,那么第3版当然会把这个好传统继续坚持下去了。书签的设计应该尽可能地展现出一本书的特点,而我认为《第一行代码 第3版》最大的特点就是两部分:Android 10和Kotlin。所以我也让设计师专门为这两个主题设计了两种书签,我们来一起欣赏一下吧:
每本书中都会赠送这两个书签,虽然一本书送两个书签可能有点多余,但我觉得这会给人一种更加完整的感觉。
除此之外,第3版中还首次提供了全书知识架构的思维导图。这是我根据书中主要知识点进行整理,并由本书编辑张霞绘制出来的,共分为Android和Kotlin两张图。
思维导图可以方便你纵览本书Android和Kotlin的宏观图景,帮助你梳理各章的知识要点。
Android的思维导图如下:
Kotlin的思维导图如下:
这两张思维导图都会以彩页的形式装订在书中,方便你随时查看。
最后是给高校老师们的福音,由于本书前两版被大量高校当作教材使用,这次为了便于高校老师和培训机构教学,第3版中专门配备了相应的PPT课件。在书中的前言部分附有所有随书资源的下载地址。
/ 定价 /
我之前发现有部分读者朋友对书的定价方式是有误解的,所以这里先来科普一下。
任何一本书,都是要以成本为依据来定价的,而不是内容。成本指的主要是这本书印刷所需花费的费用,所以页数多的书一定比页数少的书贵,彩色页的书一定比黑白页的书贵。
在我之前的一篇文章中,有些读者留言,认为一本书应该是根据内容的好坏来定价,其实这是不可能的。也许会存在一些好书因为口碑特别好而提高定价,但一定不会存在一本差书因为自我感觉内容比较差而主动降低定价。
由于《第一行代码 第3版》新增了许多Kotlin方面的知识讲解,书的总页数也从第2版时的500多页变成了现在的700多页,所以涨价是在所难免的事情。
一开始本书编辑张霞建议将第3版售价定为99元,我觉得相比于第2版的79元涨幅有点高,问她可不可以降低到89元。她告诉我,700多页的书定价89元有很大概率会被出版社打回,但她仍然以这个价格上报了试试。果不其然,最终这个定价审核没有通过,因为市面上700多页的书定价都至少在100元以上。
最终我也做出了让步,同意将第3版定价为99元,至少控制在了百元以内。不过这仅仅只是书的定价,而通常我们在网上买书都是有折扣的。具体折扣的多少由各网店自己决定,我参与不了,但一般也就是七八十块钱就能买到了。
上一篇:“悲酥清风”这个词出自哪里?
下一篇:STL与泛型编程(第一周笔记)
发表评论